feat: align shared app foundations

This commit is contained in:
jingyun committed 2026-08-13 17:47:01 +08:00
1 parent bd6bb0ffce
commit c5653a3699
21 files changed
+894 -66

No files matched your search

@@ -0,0 +1,155 @@
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../common/services/device_header_service.dart';
import '../../auth/data/auth_models.dart';
class FeedbackEmailService {
const FeedbackEmailService._();
static const recipient = 'business@yqzhice.top';
static const productName = '金值';
static Future<void> send(AuthState? authState) async {
final email = await _buildEmail(authState);
final uri = composeMailtoUri(
recipient: email.recipient,
subject: email.subject,
body: email.body,
);
try {
final launched = await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
if (!launched) throw const FeedbackEmailUnavailableException();
} catch (_) {
throw const FeedbackEmailUnavailableException();
}
}
@visibleForTesting
static Uri composeMailtoUri({
required String recipient,
required String subject,
required String body,
}) => Uri(
scheme: 'mailto',
path: recipient,
query: _encodeQueryParameters({'subject': subject, 'body': body}),
);
static String _encodeQueryParameters(
Map<String, String> parameters,
) => parameters.entries
.map(
(entry) =>
'${Uri.encodeComponent(entry.key)}=${Uri.encodeComponent(entry.value)}',
)
.join('&');
static Future<_FeedbackEmail> _buildEmail(AuthState? authState) async {
final packageInfo = await PackageInfo.fromPlatform();
final device = await _loadDeviceInfo();
try {
await DeviceHeaderService.to.ensureInitialized();
} catch (_) {}
final headers = DeviceHeaderService.to.commonHeaders();
final loggedIn = authState is LoggedInAuthState ? authState : null;
final version = '${packageInfo.version}+${packageInfo.buildNumber}';
final diagnostics = [
'App version: $version',
'Channel: ${headers['User-Channel'] ?? ''}',
'OS version: ${device.osVersion}',
'Device manufacturer: ${device.manufacturer}',
'Device model: ${device.model}',
'Login type: ${loggedIn == null ? 'guest' : 'phone'}',
'Phone: ${loggedIn?.user.phone ?? ''}',
].join('\n');
return _FeedbackEmail(
recipient: recipient,
subject:
'$productName反馈[$version]-[${device.manufacturer}]-[${device.osVersion}]',
body: [
'$productName团队你好,',
'请描述你的问题,可附上截图或视频:',
'',
'',
'----',
diagnostics,
].join('\n'),
);
}
static Future<_FeedbackDeviceInfo> _loadDeviceInfo() async {
final plugin = DeviceInfoPlugin();
try {
if (kIsWeb) {
final info = await plugin.webBrowserInfo;
return _FeedbackDeviceInfo(
osVersion: info.platform ?? 'web',
manufacturer: info.browserName.name,
model: info.userAgent ?? '',
);
}
if (defaultTargetPlatform == TargetPlatform.android) {
final info = await plugin.androidInfo;
return _FeedbackDeviceInfo(
osVersion: info.version.release,
manufacturer: info.manufacturer,
model: info.model,
);
}
if (defaultTargetPlatform == TargetPlatform.iOS) {
final info = await plugin.iosInfo;
return _FeedbackDeviceInfo(
osVersion: info.systemVersion,
manufacturer: 'Apple',
model: info.utsname.machine,
);
}
return const _FeedbackDeviceInfo(
osVersion: '',
manufacturer: '',
model: '',
);
} catch (_) {
return const _FeedbackDeviceInfo(
osVersion: '',
manufacturer: '',
model: '',
);
}
}
}
class FeedbackEmailUnavailableException implements Exception {
const FeedbackEmailUnavailableException();
}
class _FeedbackEmail {
const _FeedbackEmail({
required this.recipient,
required this.subject,
required this.body,
});
final String recipient;
final String subject;
final String body;
}
class _FeedbackDeviceInfo {
const _FeedbackDeviceInfo({
required this.osVersion,
required this.manufacturer,
required this.model,
});
final String osVersion;
final String manufacturer;
final String model;
}