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
0228ae8 1
import "dart:async";
0228ae8 2
import "dart:convert";
0228ae8 3
0228ae8 4
import "package:flutter/foundation.dart";
0228ae8 5
import "package:flutter/material.dart";
0228ae8 6
import "package:flutter/scheduler.dart";
0228ae8 7
import "package:http/http.dart" as http;
0228ae8 8
0228ae8 9
/// Sends (or otherwise records) a single error report. Implementations
0228ae8 10
/// should be best-effort — a failed report must never throw back into the
0228ae8 11
/// error handler that triggered it.
0228ae8 12
typedef ErrorReportSender =
0228ae8 13
    Future<void> Function(String message, StackTrace? stack);
0228ae8 14
0228ae8 15
/// Global crash/error reporting: wires [FlutterError.onError] (framework
0228ae8 16
/// errors) and [PlatformDispatcher.onError] (uncaught async/platform
0228ae8 17
/// errors), shows a user-consent dialog before reporting in release builds,
0228ae8 18
/// and just logs to the console in debug builds.
0228ae8 19
class ErrorReporting {
0228ae8 20
  ErrorReporting._();
0228ae8 21
0228ae8 22
  static GlobalKey<NavigatorState>? _navigatorKey;
0228ae8 23
  static ErrorReportSender? _onReport;
0228ae8 24
  static Set<String> _ignoredSubstrings = const {};
0228ae8 25
0228ae8 26
  /// Call once at app startup, after `runApp`'s `navigatorKey` exists.
0228ae8 27
  static void install({
0228ae8 28
    required GlobalKey<NavigatorState> navigatorKey,
0228ae8 29
    required ErrorReportSender onReport,
0228ae8 30
    Set<String> ignoredSubstrings = const {},
0228ae8 31
  }) {
0228ae8 32
    _navigatorKey = navigatorKey;
0228ae8 33
    _onReport = onReport;
0228ae8 34
    _ignoredSubstrings = ignoredSubstrings;
0228ae8 35
0228ae8 36
    FlutterError.onError = (errorDetails) {
0228ae8 37
      FlutterError.presentError(errorDetails);
0228ae8 38
      _handle(errorDetails.exception.toString(), errorDetails.stack);
0228ae8 39
    };
0228ae8 40
0228ae8 41
    PlatformDispatcher.instance.onError = (error, stack) {
0228ae8 42
      _handle(error.toString(), stack);
0228ae8 43
      return true;
0228ae8 44
    };
0228ae8 45
  }
0228ae8 46
0228ae8 47
  /// Manually reports [message] through the same sender configured via
0228ae8 48
  /// [install], bypassing the consent dialog — for call sites that already
0228ae8 49
  /// caught and handled a specific error and just want it recorded (e.g.
0228ae8 50
  /// only-bible-app's audio-playback failure handler).
0228ae8 51
  static Future<void> report(String message, StackTrace? stack) async {
0228ae8 52
    if (kDebugMode) {
0228ae8 53
      debugPrint("Error: $message");
0228ae8 54
      debugPrint("$stack");
0228ae8 55
      return;
0228ae8 56
    }
0228ae8 57
    await _onReport?.call(message, stack);
0228ae8 58
  }
0228ae8 59
0228ae8 60
  static void _handle(String message, StackTrace? stack) {
0228ae8 61
    if (kDebugMode) {
0228ae8 62
      debugPrint("Error: $message");
0228ae8 63
      debugPrint("$stack");
0228ae8 64
      return;
0228ae8 65
    }
0228ae8 66
    if (_ignoredSubstrings.any(message.contains)) {
0228ae8 67
      // Known framework noise: still recorded for visibility, just not
0228ae8 68
      // worth interrupting the user with a dialog they can't act on.
0228ae8 69
      unawaited(_onReport?.call(message, stack));
0228ae8 70
      return;
0228ae8 71
    }
0228ae8 72
    SchedulerBinding.instance.addPostFrameCallback((_) {
0228ae8 73
      final context = _navigatorKey?.currentContext;
0228ae8 74
      if (context == null || !context.mounted) return;
0228ae8 75
      _showReportDialog(context, message, stack);
0228ae8 76
    });
0228ae8 77
  }
0228ae8 78
0228ae8 79
  static void _showReportDialog(
0228ae8 80
    BuildContext context,
0228ae8 81
    String message,
0228ae8 82
    StackTrace? stack,
0228ae8 83
  ) {
0228ae8 84
    showDialog<void>(
0228ae8 85
      context: context,
0228ae8 86
      barrierColor: Colors.black54,
0228ae8 87
      builder: (dialogContext) => AlertDialog(
0228ae8 88
        title: const Text("Alert"),
0228ae8 89
        content: const Text(
0228ae8 90
          "An error has occurred. Do you want to report this error to us?",
0228ae8 91
        ),
0228ae8 92
        actionsAlignment: MainAxisAlignment.end,
0228ae8 93
        actionsOverflowButtonSpacing: 8.0,
0228ae8 94
        actions: [
0228ae8 95
          TextButton(
0228ae8 96
            onPressed: () {
0228ae8 97
              Navigator.of(dialogContext).pop();
0228ae8 98
              unawaited(_onReport?.call(message, stack));
0228ae8 99
            },
0228ae8 100
            child: const Text("Yes"),
0228ae8 101
          ),
0228ae8 102
          TextButton(
0228ae8 103
            onPressed: () => Navigator.of(dialogContext).pop(),
0228ae8 104
            child: const Text("No"),
0228ae8 105
          ),
0228ae8 106
        ],
0228ae8 107
      ),
0228ae8 108
    );
0228ae8 109
  }
0228ae8 110
}
0228ae8 111
0228ae8 112
/// Builds an [ErrorReportSender] that POSTs to the shared `error-reporter`
0228ae8 113
/// Cloudflare Worker (see workers/error-reporter/), which emails the report
0228ae8 114
/// via Cloudflare Email Sending. [sharedSecret], if provided, is sent as
0228ae8 115
/// `X-Report-Secret` — this is NOT a real security boundary (it's embedded
0228ae8 116
/// client-side, same as any app secret), it only filters out generic bots
0228ae8 117
/// blindly probing the endpoint. Real abuse resistance is the Worker's own
0228ae8 118
/// rate limiter.
0228ae8 119
ErrorReportSender httpErrorReportSender({
0228ae8 120
  required Uri endpoint,
0228ae8 121
  required String appName,
0228ae8 122
  String? sharedSecret,
0228ae8 123
  String? appVersion,
0228ae8 124
  String? buildNumber,
0228ae8 125
}) {
0228ae8 126
  return (message, stack) async {
0228ae8 127
    try {
0228ae8 128
      await http.post(
0228ae8 129
        endpoint,
0228ae8 130
        headers: {
0228ae8 131
          "Content-Type": "application/json",
0228ae8 132
          if (sharedSecret != null) "X-Report-Secret": sharedSecret,
0228ae8 133
        },
0228ae8 134
        body: jsonEncode({
0228ae8 135
          "app": appName,
0228ae8 136
          "message": message,
0228ae8 137
          "stack": stack?.toString(),
0228ae8 138
          "platform": defaultTargetPlatform.name,
0228ae8 139
          "appVersion": appVersion,
0228ae8 140
          "buildNumber": buildNumber,
0228ae8 141
        }),
0228ae8 142
      );
0228ae8 143
    } catch (_) {
0228ae8 144
      // Best-effort — a failed report must not cascade into another error.
0228ae8 145
    }
0228ae8 146
  };
0228ae8 147
}