forked from eatvio/eatvio_app
feat(push_notifications): issue-660, fix: issue-908
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/push/hms_client_error.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
/// Rules for which errors are sent to crash reporting.
|
||||
@@ -15,6 +16,7 @@ class ErrorReportPolicy {
|
||||
if (error == null) return false;
|
||||
if (error is String) return false;
|
||||
if (error is SocketException) return false;
|
||||
if (isHmsClientUnavailable(error)) return false;
|
||||
|
||||
if (error is DioException) {
|
||||
return _shouldReportDio(error);
|
||||
|
||||
@@ -4,14 +4,13 @@ import 'dart:io';
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/notifications/notification_permission_requester.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:eatvio_app/core/push/hms_client_error.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/services.dart' show MethodChannel, PlatformException;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:eatvio_app/core/error_reporting/error_reporter.dart';
|
||||
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
|
||||
|
||||
@@ -27,20 +26,8 @@ class FirebaseApi {
|
||||
_notificationPermissionRequester = notificationPermissionRequester ?? NotificationPermissionRequester();
|
||||
|
||||
Future<String?> getToken() async {
|
||||
String? token;
|
||||
|
||||
if (await _isHuaweiDevice()) {
|
||||
token = await _getHuaweiPushTokenNative().timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => null,
|
||||
);
|
||||
} else {
|
||||
token = await _firebaseMessaging.getToken().timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => null,
|
||||
);
|
||||
}
|
||||
return token;
|
||||
final credentials = await _resolvePushCredentials();
|
||||
return credentials.token;
|
||||
}
|
||||
|
||||
Future<void> _requestNotificationPermission() async {
|
||||
@@ -58,12 +45,15 @@ class FirebaseApi {
|
||||
try {
|
||||
await _requestNotificationPermission();
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
final credentials = await _resolvePushCredentials();
|
||||
final token = credentials.token;
|
||||
if (token == null || token.isEmpty) return;
|
||||
await sl<Dio>().post(
|
||||
'/api/anonymous_token',
|
||||
data: {
|
||||
'platform': Platform.isIOS ? 'ios' : (await _isHuaweiDevice()) ? 'huawei' : 'android',
|
||||
'platform': credentials.platform,
|
||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||
'token': await getToken(),
|
||||
'token': token,
|
||||
},
|
||||
).timeout(
|
||||
const Duration(seconds: 10),
|
||||
@@ -83,13 +73,15 @@ class FirebaseApi {
|
||||
try {
|
||||
await _requestNotificationPermission();
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
final isHuawei = Platform.isAndroid ? await _isHuaweiDevice() : false;
|
||||
final credentials = await _resolvePushCredentials();
|
||||
final token = credentials.token;
|
||||
if (token == null || token.isEmpty) return;
|
||||
await sl<Dio>().post(
|
||||
'/api/profile/notification/token/register',
|
||||
data: {
|
||||
'platform': Platform.isIOS ? 'ios' : (isHuawei ? 'huawei' : 'android'),
|
||||
'platform': credentials.platform,
|
||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||
'token': await getToken(),
|
||||
'token': token,
|
||||
},
|
||||
).timeout(
|
||||
const Duration(seconds: 10),
|
||||
@@ -124,13 +116,13 @@ class FirebaseApi {
|
||||
Future<void> unregisterPushToken() async {
|
||||
if (!await TokenStorage.haveTokenInStorage()) return;
|
||||
try {
|
||||
final isHuawei = Platform.isAndroid ? await _isHuaweiDevice() : false;
|
||||
final platform = Platform.isIOS ? 'ios' : (isHuawei ? 'huawei' : 'android');
|
||||
final token = (await getToken()) ?? '';
|
||||
final credentials = await _resolvePushCredentials();
|
||||
final token = credentials.token ?? '';
|
||||
if (token.isEmpty) return;
|
||||
await sl<Dio>().post(
|
||||
'/api/profile/notification/token/unregister',
|
||||
data: {
|
||||
'platform': platform,
|
||||
'platform': credentials.platform,
|
||||
'token': token,
|
||||
},
|
||||
).timeout(
|
||||
@@ -190,30 +182,74 @@ class FirebaseApi {
|
||||
}
|
||||
}
|
||||
|
||||
Future<({String? token, String platform})> _resolvePushCredentials() async {
|
||||
if (Platform.isIOS) {
|
||||
return (
|
||||
token: await _getFcmToken(),
|
||||
platform: pushPlatform(isIos: true, huaweiFamily: false, hmsAvailable: false, hasHmsToken: false),
|
||||
);
|
||||
}
|
||||
|
||||
final huaweiFamily = await _isHuaweiDevice();
|
||||
if (huaweiFamily && await _isHmsAvailable()) {
|
||||
final token = await _getHuaweiPushTokenNative().timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => null,
|
||||
);
|
||||
if (token != null && token.isNotEmpty) {
|
||||
return (
|
||||
token: token,
|
||||
platform: pushPlatform(isIos: false, huaweiFamily: true, hmsAvailable: true, hasHmsToken: true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
token: await _getFcmToken(),
|
||||
platform: pushPlatform(isIos: false, huaweiFamily: huaweiFamily, hmsAvailable: false, hasHmsToken: false),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> _getFcmToken() {
|
||||
return _firebaseMessaging.getToken().timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => null,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _isHuaweiDevice() async {
|
||||
if (!Platform.isAndroid) return false;
|
||||
try {
|
||||
final info = await DeviceInfoPlugin().androidInfo;
|
||||
final brand = info.brand.toLowerCase();
|
||||
final manufacturer = info.manufacturer.toLowerCase();
|
||||
return brand.contains('huawei') || brand.contains('honor') || manufacturer.contains('huawei') || manufacturer.contains('honor');
|
||||
return isHuaweiFamilyBrand(brand: info.brand, manufacturer: info.manufacturer);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _isHmsAvailable() async {
|
||||
try {
|
||||
const channel = MethodChannel('com.eatvioapp/hms_push');
|
||||
final available = await channel.invokeMethod<bool>('isHmsAvailable').timeout(const Duration(seconds: 5));
|
||||
return available ?? false;
|
||||
} catch (e, st) {
|
||||
log('HMS availability check failed: $e', stackTrace: st);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _getHuaweiPushTokenNative() async {
|
||||
try {
|
||||
const channel = MethodChannel('com.eatvioapp/hms_push');
|
||||
final String? token = await channel.invokeMethod<String>('getHuaweiToken');
|
||||
return token;
|
||||
} on PlatformException catch (e, st) {
|
||||
showErrorToast('$e $st');
|
||||
log(e.toString(), stackTrace: st);
|
||||
await _errorReporter.report(e, st, origin: 'FirebaseApi');
|
||||
if (!isHmsClientUnavailable(e)) {
|
||||
await _errorReporter.report(e, st, origin: 'FirebaseApi');
|
||||
}
|
||||
return null;
|
||||
} catch (e, st) {
|
||||
showErrorToast('$e $st');
|
||||
log(e.toString(), stackTrace: st);
|
||||
await _errorReporter.report(e, st, origin: 'FirebaseApi');
|
||||
return null;
|
||||
|
||||
36
lib/core/push/hms_client_error.dart
Normal file
36
lib/core/push/hms_client_error.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
bool isHuaweiFamilyBrand({
|
||||
required String brand,
|
||||
required String manufacturer,
|
||||
}) {
|
||||
final normalizedBrand = brand.toLowerCase();
|
||||
final normalizedManufacturer = manufacturer.toLowerCase();
|
||||
return normalizedBrand.contains('huawei') ||
|
||||
normalizedBrand.contains('honor') ||
|
||||
normalizedManufacturer.contains('huawei') ||
|
||||
normalizedManufacturer.contains('honor');
|
||||
}
|
||||
|
||||
/// HMS Core is missing or rejects this app.
|
||||
///
|
||||
/// Honor devices with Google services return `907135003: client api invalid`.
|
||||
/// That is a device capability, not an app crash: the caller should use FCM.
|
||||
bool isHmsClientUnavailable(Object error) {
|
||||
if (error is! PlatformException) return false;
|
||||
final message = '${error.message ?? ''} ${error.details ?? ''}'.toLowerCase();
|
||||
return message.contains('907135003') ||
|
||||
message.contains('client api invalid');
|
||||
}
|
||||
|
||||
/// `huawei` only when an HMS token was actually issued. Otherwise Android uses FCM.
|
||||
String pushPlatform({
|
||||
required bool isIos,
|
||||
required bool huaweiFamily,
|
||||
required bool hmsAvailable,
|
||||
required bool hasHmsToken,
|
||||
}) {
|
||||
if (isIos) return 'ios';
|
||||
if (huaweiFamily && hmsAvailable && hasHmsToken) return 'huawei';
|
||||
return 'android';
|
||||
}
|
||||
11
lib/core/utils/url_utils.dart
Normal file
11
lib/core/utils/url_utils.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
/// Joins an API media path with [baseUrl].
|
||||
///
|
||||
/// Uploaded photos come back as relative paths (`/storage/...`).
|
||||
/// Default avatars are already absolute (`https://ui-avatars...`) and must stay as-is.
|
||||
String joinBaseUrl(String baseUrl, String path) {
|
||||
final normalized = path.toLowerCase();
|
||||
if (normalized.startsWith('https://') || normalized.startsWith('http://')) {
|
||||
return path;
|
||||
}
|
||||
return '$baseUrl$path';
|
||||
}
|
||||
Reference in New Issue
Block a user