add sentry

This commit is contained in:
Phuoc Nguyen
2025-12-11 13:44:26 +07:00
parent f130820131
commit e3632d4445
9 changed files with 445 additions and 126 deletions

View File

@@ -161,6 +161,8 @@ PODS:
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- objective_c (0.0.1):
- Flutter
- onesignal_flutter (5.3.4):
- Flutter
- OneSignalXCFramework (= 5.2.14)
@@ -212,6 +214,8 @@ PODS:
- OneSignalXCFramework/OneSignalOutcomes
- open_file_ios (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
@@ -219,6 +223,11 @@ PODS:
- SDWebImage (5.21.4):
- SDWebImage/Core (= 5.21.4)
- SDWebImage/Core (5.21.4)
- Sentry/HybridSDK (8.56.2)
- sentry_flutter (9.8.0):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.56.2)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -242,10 +251,13 @@ DEPENDENCIES:
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- objective_c (from `.symlinks/plugins/objective_c/ios`)
- onesignal_flutter (from `.symlinks/plugins/onesignal_flutter/ios`)
- OneSignalXCFramework (= 5.2.14)
- open_file_ios (from `.symlinks/plugins/open_file_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
@@ -269,6 +281,7 @@ SPEC REPOS:
- OneSignalXCFramework
- PromisesObjC
- SDWebImage
- Sentry
- SwiftyGif
EXTERNAL SOURCES:
@@ -292,12 +305,18 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/integration_test/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/darwin"
objective_c:
:path: ".symlinks/plugins/objective_c/ios"
onesignal_flutter:
:path: ".symlinks/plugins/onesignal_flutter/ios"
open_file_ios:
:path: ".symlinks/plugins/open_file_ios/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@@ -331,12 +350,16 @@ SPEC CHECKSUMS:
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
objective_c: 77e887b5ba1827970907e10e832eec1683f3431d
onesignal_flutter: f69ff09eeaf41cea4b7a841de5a61e79e7fd9a5a
OneSignalXCFramework: 7112f3e89563e41ebc23fe807788f11985ac541c
open_file_ios: 461db5853723763573e140de3193656f91990d9e
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SDWebImage: d0184764be51240d49c761c37f53dd017e1ccaaf
Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7
sentry_flutter: f074f75557daea0e1dd9607381a05cc0e3e456fe
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d

View File

@@ -56,21 +56,10 @@ class AuthInterceptor extends Interceptor {
// Get session data from secure storage
final sid = await _secureStorage.read(key: 'frappe_sid');
final csrfToken = await _secureStorage.read(key: 'frappe_csrf_token');
final fullName = await _secureStorage.read(key: 'frappe_full_name');
final userId = await _secureStorage.read(key: 'frappe_user_id');
if (sid != null && csrfToken != null) {
// Build cookie header with all required fields
final cookieHeader = [
'sid=$sid',
'full_name=${fullName ?? "User"}',
'system_user=no',
'user_id=${userId != null ? Uri.encodeComponent(userId) : ApiConstants.frappePublicUserId}',
'user_image=',
].join('; ');
// Add Frappe session headers
options.headers['Cookie'] = cookieHeader;
// Only send sid in Cookie header - other fields are not needed
options.headers['Cookie'] = 'sid=$sid';
options.headers['X-Frappe-CSRF-Token'] = csrfToken;
}

View File

@@ -447,6 +447,7 @@ Future<CacheOptions> cacheOptions(Ref ref) async {
Future<Dio> dio(Ref ref) async {
final dio = Dio();
// Base configuration
dio
..options = BaseOptions(
@@ -466,7 +467,7 @@ Future<Dio> dio(Ref ref) async {
},
)
// Add interceptors in order
// 1. Custom Curl interceptor (first to log cURL commands)
// 1. Custom Curl interceptor (logs cURL commands)
// Uses debugPrint and developer.log for better visibility
..interceptors.add(CustomCurlLoggerInterceptor())
// 2. Logging interceptor

View File

@@ -89,13 +89,8 @@ class FrappeAuthService {
const url = '${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeLogin}';
// Build cookie header
// Get stored session - only need sid and csrf_token
final storedSession = await getStoredSession();
final cookieHeader = _buildCookieHeader(
sid: storedSession!['sid']!,
fullName: storedSession['fullName']!,
userId: storedSession['userId']!,
);
final response = await _dio.post<Map<String, dynamic>>(
url,
@@ -109,7 +104,7 @@ class FrappeAuthService {
},
options: Options(
headers: {
'Cookie': cookieHeader,
'Cookie': 'sid=${storedSession!['sid']!}',
'X-Frappe-Csrf-Token': storedSession['csrfToken']!,
'Content-Type': 'application/json',
},
@@ -203,31 +198,13 @@ class FrappeAuthService {
return sid != null && csrfToken != null;
}
/// Build cookie header string
String _buildCookieHeader({
required String sid,
required String fullName,
required String userId,
}) {
return [
'sid=$sid',
'full_name=$fullName',
'system_user=no',
'user_id=${Uri.encodeComponent(userId)}',
'user_image=',
].join('; ');
}
/// Get headers for Frappe API requests
/// Only sends sid in Cookie - other fields are not needed
Future<Map<String, String>> getHeaders() async {
final session = await ensureSession();
return {
'Cookie': _buildCookieHeader(
sid: session['sid']!,
fullName: session['fullName']!,
userId: session['userId']!,
),
'Cookie': 'sid=${session['sid']!}',
'X-Frappe-Csrf-Token': session['csrfToken']!,
'Content-Type': 'application/json',
};

View File

@@ -4,12 +4,16 @@ import 'package:onesignal_flutter/onesignal_flutter.dart';
/// OneSignal service for managing push notifications and external user ID.
///
/// This service handles:
/// - Initializing OneSignal SDK
/// - Setting external user ID after login (using phone number)
/// - Restoring external user ID on app startup
/// - Clearing external user ID on logout
///
/// Usage:
/// ```dart
/// // Initialize in main.dart
/// await OneSignalService.init(appId: 'your-app-id');
///
/// // After successful login
/// await OneSignalService.login(phoneNumber);
///
@@ -19,6 +23,78 @@ import 'package:onesignal_flutter/onesignal_flutter.dart';
class OneSignalService {
OneSignalService._();
/// OneSignal App ID - Replace with your actual App ID from OneSignal dashboard
static const String _defaultAppId = '778ca22d-c719-4ec8-86cb-a6b911166066';
/// Initialize OneSignal SDK
///
/// Must be called before using any other OneSignal methods.
/// Sets up push subscription observers and requests notification permission.
///
/// [appId] - Optional App ID override (uses default if not provided)
/// [requestPermission] - Whether to request notification permission (default: true)
static Future<void> init({
String? appId,
bool requestPermission = true,
}) async {
try {
// Set debug log level (verbose in debug, none in release)
OneSignal.Debug.setLogLevel(kDebugMode ? OSLogLevel.verbose : OSLogLevel.none);
// Initialize with App ID
OneSignal.initialize(appId ?? _defaultAppId);
debugPrint('🔔 OneSignal initialized');
// Add push subscription observer to track subscription state changes
OneSignal.User.pushSubscription.addObserver((state) {
debugPrint('🔔 Push subscription state changed:');
debugPrint(' Previous - optedIn: ${state.previous.optedIn}, id: ${state.previous.id}');
debugPrint(' Current - optedIn: ${state.current.optedIn}, id: ${state.current.id}');
debugPrint(' Subscription ID: ${state.current.id}');
debugPrint(' Push Token: ${state.current.token}');
if (state.current.id != null) {
debugPrint('🔔 ✅ Device successfully subscribed!');
}
});
// Add notification permission observer
OneSignal.Notifications.addPermissionObserver((isGranted) {
debugPrint('🔔 Notification permission changed: $isGranted');
});
// Request permission if enabled
if (requestPermission) {
final accepted = await OneSignal.Notifications.requestPermission(true);
debugPrint('🔔 Permission request result: $accepted');
}
// Give OneSignal SDK time to complete initialization and server registration
await Future<void>.delayed(const Duration(seconds: 2));
// Log current subscription status
_logSubscriptionStatus();
} catch (e) {
debugPrint('🔔 OneSignal error: Failed to initialize - $e');
}
}
/// Log current subscription status for debugging
static void _logSubscriptionStatus() {
final optedIn = OneSignal.User.pushSubscription.optedIn;
final id = OneSignal.User.pushSubscription.id;
final token = OneSignal.User.pushSubscription.token;
debugPrint('🔔 Current subscription status:');
debugPrint(' Opted In: $optedIn');
debugPrint(' Subscription ID: $id');
debugPrint(' Push Token: $token');
if (id == null) {
debugPrint('🔔 ⚠️ Subscription ID is null - check device connectivity and OneSignal app ID');
}
}
/// Login user to OneSignal by setting external user ID.
///
/// This associates the device with the user's phone number,

View File

@@ -0,0 +1,250 @@
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
/// Sentry service for error tracking and performance monitoring.
///
/// This service handles:
/// - Initializing Sentry SDK
/// - Capturing exceptions and errors
/// - Capturing custom messages
/// - Setting user context after login
/// - Performance monitoring
///
/// Usage:
/// ```dart
/// // Initialize in main.dart
/// await SentryService.init(
/// dsn: 'your-sentry-dsn',
/// appRunner: () => runApp(MyApp()),
/// );
///
/// // Capture exception
/// SentryService.captureException(error, stackTrace: stackTrace);
///
/// // Capture message
/// SentryService.captureMessage('User performed action X');
///
/// // Set user context after login
/// SentryService.setUser(userId: '123', email: 'user@example.com');
/// ```
class SentryService {
SentryService._();
/// Sentry DSN - Replace with your actual DSN from Sentry dashboard
static const String _dsn = 'https://2c5893508a29e5ea750b64d5ee31aeef@o4509632266436608.ingest.us.sentry.io/4510464530972672';
/// Initialize Sentry SDK
///
/// Must be called before runApp() in main.dart.
/// Wraps the app with Sentry error boundary.
///
/// [dsn] - Optional DSN override (uses default if not provided)
/// [appRunner] - The function that runs the app (typically runApp(MyApp()))
/// [environment] - Environment name (e.g., 'development', 'production')
static Future<void> init({
String? dsn,
required Future<void> Function() appRunner,
String? environment,
}) async {
// Get package info for release version
final packageInfo = await PackageInfo.fromPlatform();
final release = 'partner@${packageInfo.version}+${packageInfo.buildNumber}';
await SentryFlutter.init(
(options) {
options
..dsn = dsn ?? _dsn
// Release version: worker@1.0.1+29
..release = release
// Environment configuration
..environment = environment ?? (kReleaseMode ? 'production' : 'development')
// Performance monitoring
..tracesSampleRate = kReleaseMode ? 0.2 : 1.0 // 20% in prod, 100% in dev
..profilesSampleRate = kReleaseMode ? 0.2 : 1.0
// Enable automatic instrumentation
..enableAutoPerformanceTracing = true
// Capture failed requests
..captureFailedRequests = true
// Debug mode settings
..debug = kDebugMode
// Add app-specific tags
..beforeSend = (event, hint) {
// Filter out certain errors if needed
// Return null to drop the event
return event;
};
},
appRunner: appRunner,
);
debugPrint('🔴 Sentry initialized (release: $release, enabled: ${!kDebugMode})');
}
/// Capture an exception with optional stack trace
///
/// [exception] - The exception to capture
/// [stackTrace] - Optional stack trace
/// [hint] - Optional hint with additional context
static Future<void> captureException(
dynamic exception, {
StackTrace? stackTrace,
Hint? hint,
}) async {
try {
await Sentry.captureException(
exception,
stackTrace: stackTrace,
hint: hint,
);
debugPrint('🔴 Sentry: Exception captured - ${exception.runtimeType}');
} catch (e) {
debugPrint('🔴 Sentry error: Failed to capture exception - $e');
}
}
/// Capture a custom message
///
/// [message] - The message to capture
/// [level] - Severity level (default: info)
/// [params] - Optional parameters to include
static Future<void> captureMessage(
String message, {
SentryLevel level = SentryLevel.info,
Map<String, dynamic>? params,
}) async {
try {
await Sentry.captureMessage(
message,
level: level,
withScope: params != null
? (scope) {
params.forEach((key, value) {
scope.setExtra(key, value);
});
}
: null,
);
debugPrint('🔴 Sentry: Message captured - $message');
} catch (e) {
debugPrint('🔴 Sentry error: Failed to capture message - $e');
}
}
/// Set user context for error tracking
///
/// Call this after successful login to associate errors with users.
///
/// [userId] - User's unique identifier
/// [email] - User's email (optional)
/// [username] - User's display name (optional)
/// [extras] - Additional user data (optional)
static Future<void> setUser({
required String userId,
String? email,
String? username,
Map<String, dynamic>? extras,
}) async {
try {
await Sentry.configureScope((scope) {
scope.setUser(SentryUser(
id: userId,
email: email,
username: username,
data: extras,
));
});
debugPrint('🔴 Sentry: User set - $userId');
} catch (e) {
debugPrint('🔴 Sentry error: Failed to set user - $e');
}
}
/// Clear user context on logout
static Future<void> clearUser() async {
try {
await Sentry.configureScope((scope) {
scope.setUser(null);
});
debugPrint('🔴 Sentry: User cleared');
} catch (e) {
debugPrint('🔴 Sentry error: Failed to clear user - $e');
}
}
/// Add a breadcrumb for tracking user actions
///
/// Breadcrumbs are used to track the sequence of events leading to an error.
///
/// [message] - Description of the action
/// [category] - Category of the breadcrumb (e.g., 'navigation', 'ui.click')
/// [data] - Additional data (optional)
static Future<void> addBreadcrumb({
required String message,
String? category,
Map<String, dynamic>? data,
SentryLevel level = SentryLevel.info,
}) async {
try {
await Sentry.addBreadcrumb(Breadcrumb(
message: message,
category: category,
data: data,
level: level,
timestamp: DateTime.now(),
));
} catch (e) {
debugPrint('🔴 Sentry error: Failed to add breadcrumb - $e');
}
}
/// Set a tag for filtering in Sentry dashboard
///
/// [key] - Tag name
/// [value] - Tag value
static Future<void> setTag(String key, String value) async {
try {
await Sentry.configureScope((scope) {
scope.setTag(key, value);
});
} catch (e) {
debugPrint('🔴 Sentry error: Failed to set tag - $e');
}
}
/// Set extra context data
///
/// [key] - Context key
/// [value] - Context value (will be serialized)
static Future<void> setExtra(String key, dynamic value) async {
try {
await Sentry.configureScope((scope) {
scope.setExtra(key, value);
});
} catch (e) {
debugPrint('🔴 Sentry error: Failed to set extra - $e');
}
}
/// Start a performance transaction
///
/// [name] - Transaction name
/// [operation] - Operation type (e.g., 'http.client', 'ui.load')
///
/// Returns the transaction to be finished later.
static ISentrySpan? startTransaction(String name, String operation) {
try {
return Sentry.startTransaction(name, operation);
} catch (e) {
debugPrint('🔴 Sentry error: Failed to start transaction - $e');
return null;
}
}
}

View File

@@ -1,17 +1,17 @@
import 'dart:async';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:onesignal_flutter/onesignal_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:worker/app.dart';
import 'package:worker/core/database/app_settings_box.dart';
import 'package:worker/core/database/hive_initializer.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:worker/core/services/onesignal_service.dart';
import 'package:worker/core/services/sentry_service.dart';
import 'package:worker/firebase_options.dart';
/// Main entry point of the Worker Mobile App
///
@@ -45,72 +45,35 @@ void main() async {
/// Initialize all app dependencies with comprehensive error handling
Future<void> _initializeApp() async {
// Set up error handlers before anything else
// Initialize Sentry first to capture any initialization errors
// Sentry wraps the app runner to capture errors during startup
await SentryService.init(
appRunner: () async {
// Set up error handlers
_setupErrorHandlers();
try {
// Initialize core dependencies in parallel for faster startup
await Future.wait([_initializeHive(), _initializeSharedPreferences()]);
// Initialize OneSignal with verbose logging for debugging
OneSignal.Debug.setLogLevel(OSLogLevel.verbose);
// Initialize with your OneSignal App ID
OneSignal.initialize("778ca22d-c719-4ec8-86cb-a6b911166066");
debugPrint('🔔 OneSignal initialized');
// Add observer BEFORE requesting permission to catch the subscription event
OneSignal.User.pushSubscription.addObserver((state) {
debugPrint('🔔 Push subscription state changed:');
debugPrint(' Previous - optedIn: ${state.previous.optedIn}, id: ${state.previous.id}');
debugPrint(' Current - optedIn: ${state.current.optedIn}, id: ${state.current.id}');
debugPrint(' Subscription ID: ${state.current.id}');
debugPrint(' Push Token: ${state.current.token}');
// Save subscription info when available
if (state.current.id != null) {
debugPrint('🔔 ✅ Device successfully subscribed!');
}
});
// Add notification permission observer
OneSignal.Notifications.addPermissionObserver((isGranted) {
debugPrint('🔔 Notification permission changed: $isGranted');
});
// Request permission - TRUE to show the native permission prompt
final accepted = await OneSignal.Notifications.requestPermission(true);
debugPrint('🔔 Permission request result: $accepted');
// Give OneSignal SDK time to complete initialization and server registration
// This is necessary because the subscription happens asynchronously
await Future.delayed(const Duration(seconds: 2));
// Check current subscription status after initialization completes
final optedIn = OneSignal.User.pushSubscription.optedIn;
final id = OneSignal.User.pushSubscription.id;
final token = OneSignal.User.pushSubscription.token;
debugPrint('🔔 Current subscription status (after delay):');
debugPrint(' Opted In: $optedIn');
debugPrint(' Subscription ID: $id');
debugPrint(' Push Token: $token');
if (id == null) {
debugPrint('🔔 ⚠️ Subscription ID is still null - check device connectivity and OneSignal app ID');
}
// Initialize OneSignal push notifications
await OneSignalService.init();
// Run the app with Riverpod ProviderScope
runApp(const ProviderScope(child: WorkerApp()));
} catch (error, stackTrace) {
// Critical initialization error - show error screen
// Critical initialization error - capture and show error screen
debugPrint('Failed to initialize app: $error');
debugPrint('StackTrace: $stackTrace');
// Report to Sentry
await SentryService.captureException(error, stackTrace: stackTrace);
// Run minimal error app
runApp(_buildErrorApp(error, stackTrace));
}
},
);
}
/// Initialize Hive database
@@ -164,7 +127,8 @@ Future<void> _initializeSharedPreferences() async {
/// Set up global error handlers
///
/// Captures and logs all Flutter framework errors and uncaught exceptions
/// Captures and logs all Flutter framework errors and uncaught exceptions.
/// Reports errors to Sentry for crash analytics.
void _setupErrorHandlers() {
// Handle Flutter framework errors
FlutterError.onError = (FlutterErrorDetails details) {
@@ -176,8 +140,11 @@ void _setupErrorHandlers() {
debugPrint('StackTrace: ${details.stack}');
}
// In production, you would send to crash analytics service
// Example: FirebaseCrashlytics.instance.recordFlutterError(details);
// Report to Sentry
SentryService.captureException(
details.exception,
stackTrace: details.stack,
);
};
// Handle errors outside of Flutter framework
@@ -187,27 +154,11 @@ void _setupErrorHandlers() {
debugPrint('StackTrace: $stackTrace');
}
// In production, you would send to crash analytics service
// Example: FirebaseCrashlytics.instance.recordError(error, stackTrace);
// Report to Sentry
SentryService.captureException(error, stackTrace: stackTrace);
return true; // Return true to indicate error was handled
};
// Handle zone errors (async errors not caught by Flutter)
runZonedGuarded(
() {
// App will run in this zone
},
(error, stackTrace) {
if (kDebugMode) {
debugPrint('Zone Error: $error');
debugPrint('StackTrace: $stackTrace');
}
// In production, you would send to crash analytics service
// Example: FirebaseCrashlytics.instance.recordError(error, stackTrace);
},
);
}
/// Build minimal error app when initialization fails

View File

@@ -892,6 +892,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
jni:
dependency: transitive
description:
name: jni
sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1
url: "https://pub.dev"
source: hosted
version: "0.14.2"
js:
dependency: transitive
description:
@@ -1044,6 +1052,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.2"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: "64e35e1e2e79da4e83f2ace3bf4e5437cef523f46c7db2eba9a1419c49573790"
url: "https://pub.dev"
source: hosted
version: "8.0.0"
octo_image:
dependency: transitive
description:
@@ -1132,6 +1148,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
url: "https://pub.dev"
source: hosted
version: "8.3.1"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
path:
dependency: transitive
description:
@@ -1324,6 +1356,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.28.0"
sentry:
dependency: transitive
description:
name: sentry
sha256: "10a0bc25f5f21468e3beeae44e561825aaa02cdc6829438e73b9b64658ff88d9"
url: "https://pub.dev"
source: hosted
version: "9.8.0"
sentry_flutter:
dependency: "direct main"
description:
name: sentry_flutter
sha256: aafbf41c63c98a30b17bdbf3313424d5102db62b08735c44bff810f277e786a5
url: "https://pub.dev"
source: hosted
version: "9.8.0"
share_plus:
dependency: "direct main"
description:

View File

@@ -73,6 +73,7 @@ dependencies:
# Utilities
package_info_plus: ^8.0.0
intl: ^0.20.0
share_plus: ^12.0.1
image_picker: ^1.1.2
@@ -86,6 +87,9 @@ dependencies:
onesignal_flutter: ^5.3.4
# Error Tracking
sentry_flutter: ^9.8.0
# Navigation
go_router: ^14.6.2