diff --git a/ios/Podfile.lock b/ios/Podfile.lock index abf2160..b1b5f76 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,6 +2,8 @@ PODS: - connectivity_plus (0.0.1): - Flutter - FlutterMacOS + - device_info (0.0.1): + - Flutter - device_info_plus (0.0.1): - Flutter - DKImagePickerController/Core (4.3.9): @@ -124,6 +126,8 @@ PODS: - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter + - platform_device_id (0.0.1): + - Flutter - PromisesObjC (2.4.0) - SDWebImage (5.19.2): - SDWebImage/Core (= 5.19.2) @@ -148,6 +152,7 @@ PODS: DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) + - device_info (from `.symlinks/plugins/device_info/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) @@ -161,6 +166,7 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - platform_device_id (from `.symlinks/plugins/platform_device_id/ios`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - sqflite (from `.symlinks/plugins/sqflite/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -188,6 +194,8 @@ SPEC REPOS: EXTERNAL SOURCES: connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/darwin" + device_info: + :path: ".symlinks/plugins/device_info/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" file_picker: @@ -214,6 +222,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + platform_device_id: + :path: ".symlinks/plugins/platform_device_id/ios" sentry_flutter: :path: ".symlinks/plugins/sentry_flutter/ios" sqflite: @@ -227,6 +237,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db + device_info: d7d233b645a32c40dfdc212de5cf646ca482f175 device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 @@ -250,6 +261,7 @@ SPEC CHECKSUMS: package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + platform_device_id: 81b3e2993881f87d0c82ef151dc274df4869aef5 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a Sentry: 51b056d96914a741f63eca774d118678b1eb05a1 diff --git a/lib/providers/account.dart b/lib/providers/account.dart index 5c4f03f..39c3423 100644 --- a/lib/providers/account.dart +++ b/lib/providers/account.dart @@ -173,11 +173,12 @@ class AccountProvider extends GetxController { if (!await auth.isAuthorized) throw Exception('unauthorized'); final deviceUuid = await PlatformDeviceId.getDeviceId; - final token = await FirebaseMessaging.instance.setAutoInitEnabled(true); + final token = await FirebaseMessaging.instance.getToken(); + // TODO On iOS/macOS, using getAPNSToken() instead. final client = auth.configureClient(service: 'passport'); - final resp = await client.post('/api/notifications/subtribe', { + final resp = await client.post('/api/notifications/subscribe', { 'provider': 'firebase', 'device_token': token, 'device_id': deviceUuid, diff --git a/lib/screens/account.dart b/lib/screens/account.dart index 847edb3..4fe6467 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -6,6 +6,7 @@ import 'package:solian/router.dart'; import 'package:solian/screens/auth/signin.dart'; import 'package:solian/screens/auth/signup.dart'; import 'package:solian/widgets/account/account_heading.dart'; +import 'package:solian/widgets/account/push_notify_register_dialog.dart'; class AccountScreen extends StatefulWidget { const AccountScreen({super.key}); @@ -101,6 +102,31 @@ class _AccountScreenState extends State { setState(() {}); }, ), + const Divider(thickness: 0.3, height: 0.3) + .paddingSymmetric(vertical: 16), + Wrap( + spacing: 4, + children: [ + InkWell( + child: Text( + 'pushNotifyRegisterAction'.tr, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.85), + ), + ), + onTap: () { + showDialog( + context: context, + builder: (context) => + const PushNotifyRegisterDialog(), + ); + }, + ) + ], + ) ], ); }, diff --git a/lib/screens/auth/signin.dart b/lib/screens/auth/signin.dart index ad31ee7..46c6f05 100644 --- a/lib/screens/auth/signin.dart +++ b/lib/screens/auth/signin.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:solian/exts.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/services.dart'; +import 'package:solian/widgets/account/push_notify_register_dialog.dart'; import 'package:url_launcher/url_launcher_string.dart'; class SignInPopup extends StatefulWidget { @@ -23,6 +24,11 @@ class _SignInPopupState extends State { final password = _passwordController.value.text; if (username.isEmpty || password.isEmpty) return; provider.signin(context, username, password).then((_) async { + await showDialog( + context: context, + builder: (context) => const PushNotifyRegisterDialog(), + ); + Navigator.pop(context, true); }).catchError((e) { List messages = e.toString().split('\n'); diff --git a/lib/translations.dart b/lib/translations.dart index 5e888bb..16f017f 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -193,6 +193,11 @@ class SolianMessages extends Translations { 'badgeSolsynthStaff': 'Solsynth Staff', 'badgeSolarOriginalCitizen': 'Solar Network Natives', 'badgeGreatCommunityContributor': 'Great Community Contributor', + 'pushNotifyRegisterAction': 'Enable Push Notifications', + 'pushNotifyRegister': 'Register Push Notification Device', + 'pushNotifyRegisterCaption': + 'Activating push notifications allows you to get our latest notifications even when the app is completely closed. We use Apple\'s official push service on iOS/macOS devices; other devices provide push notifications through Google Firebase. To register a device for push notifications, you may need to connect to Google\'s servers and install the Google Framework on your device.', + 'pushNotifyRegisterDone': 'Push Notifications has been activated.', }, 'zh_CN': { 'hide': '隐藏', @@ -372,6 +377,11 @@ class SolianMessages extends Translations { 'badgeSolsynthStaff': 'Solsynth 工作人员', 'badgeSolarOriginalCitizen': 'Solar Network 原住民', 'badgeGreatCommunityContributor': '优秀社区贡献者', + 'pushNotifyRegisterAction': '激活推送通知', + 'pushNotifyRegister': '注册推送通知设备', + 'pushNotifyRegisterCaption': + '激活推送通知便可以让你在应用程序完全关闭的时候仍然获取到我们最新的通知。在 iOS/macOS 设备上我们使用 Apple 官方的推送服务;其他设备则通过 Google Firebase 提供推送通知。要注册推送通知设备,您可能需要连接到 Google 的服务器(在中国大陆不可用)并在您的设备上安装 Google Framework。', + 'pushNotifyRegisterDone': '推送通知已成功激活', } }; } diff --git a/lib/widgets/account/push_notify_register_dialog.dart b/lib/widgets/account/push_notify_register_dialog.dart new file mode 100644 index 0000000..4b23f39 --- /dev/null +++ b/lib/widgets/account/push_notify_register_dialog.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:solian/exts.dart'; +import 'package:solian/providers/account.dart'; + +class PushNotifyRegisterDialog extends StatefulWidget { + const PushNotifyRegisterDialog({super.key}); + + @override + State createState() => + _PushNotifyRegisterDialogState(); +} + +class _PushNotifyRegisterDialogState extends State { + bool _isBusy = false; + + void performAction() async { + setState(() => _isBusy = true); + + try { + await Get.find().registerPushNotifications(); + context.showSnackbar('pushNotifyRegisterDone'.tr); + Navigator.pop(context); + } catch (e) { + context.showErrorDialog(e); + } + + setState(() => _isBusy = false); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('pushNotifyRegister'.tr), + content: Text('pushNotifyRegisterCaption'.tr), + actions: [ + TextButton( + onPressed: _isBusy ? null : () => Navigator.pop(context), + child: Text('cancel'.tr), + ), + TextButton( + onPressed: _isBusy ? null : performAction, + child: Text('confirm'.tr), + ), + ], + ); + } +}