Commit 60a95593cb9bf728df8ff64dc797a7264445fb76

Authored by 刘宏哲
1 parent d13cd4c8

feat(app): bug fixed

@@ -36,6 +36,10 @@ class HealthKitHostApiImpl( @@ -36,6 +36,10 @@ class HealthKitHostApiImpl(
36 36
37 } 37 }
38 38
  39 + override fun cancelAuthorizations(callback: (Result<Boolean>) -> Unit) {
  40 + callback(Result.success(false))
  41 + }
  42 +
39 override fun syncHealthDataToCloud(callback: (Result<Boolean>) -> Unit) { 43 override fun syncHealthDataToCloud(callback: (Result<Boolean>) -> Unit) {
40 callback(Result.success(false)) 44 callback(Result.success(false))
41 } 45 }
@@ -240,6 +240,8 @@ interface HealthKitHostApi { @@ -240,6 +240,8 @@ interface HealthKitHostApi {
240 /** Opens Huawei Health client authorization UI. Returns whether user granted. */ 240 /** Opens Huawei Health client authorization UI. Returns whether user granted. */
241 fun checkHealthAppAuthorization(callback: (Result<HealthAuthorization>) -> Unit) 241 fun checkHealthAppAuthorization(callback: (Result<HealthAuthorization>) -> Unit)
242 fun requestHealthClientAuthorization(callback: (Result<Boolean>) -> Unit) 242 fun requestHealthClientAuthorization(callback: (Result<Boolean>) -> Unit)
  243 + /** Cancels all Huawei Health authorizations on OHOS. */
  244 + fun cancelAuthorizations(callback: (Result<Boolean>) -> Unit)
243 /** 245 /**
244 * Requests Huawei Health to upload the user's latest data to its cloud. 246 * Requests Huawei Health to upload the user's latest data to its cloud.
245 * 247 *
@@ -298,6 +300,24 @@ interface HealthKitHostApi { @@ -298,6 +300,24 @@ interface HealthKitHostApi {
298 } 300 }
299 } 301 }
300 run { 302 run {
  303 + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelAuthorizations$separatedMessageChannelSuffix", codec)
  304 + if (api != null) {
  305 + channel.setMessageHandler { _, reply ->
  306 + api.cancelAuthorizations{ result: Result<Boolean> ->
  307 + val error = result.exceptionOrNull()
  308 + if (error != null) {
  309 + reply.reply(HealthKitApiPigeonUtils.wrapError(error))
  310 + } else {
  311 + val data = result.getOrNull()
  312 + reply.reply(HealthKitApiPigeonUtils.wrapResult(data))
  313 + }
  314 + }
  315 + }
  316 + } else {
  317 + channel.setMessageHandler(null)
  318 + }
  319 + }
  320 + run {
301 val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.syncHealthDataToCloud$separatedMessageChannelSuffix", codec) 321 val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.syncHealthDataToCloud$separatedMessageChannelSuffix", codec)
302 if (api != null) { 322 if (api != null) {
303 channel.setMessageHandler { _, reply -> 323 channel.setMessageHandler { _, reply ->
@@ -290,6 +290,8 @@ protocol HealthKitHostApi { @@ -290,6 +290,8 @@ protocol HealthKitHostApi {
290 /// Opens Huawei Health client authorization UI. Returns whether user granted. 290 /// Opens Huawei Health client authorization UI. Returns whether user granted.
291 func checkHealthAppAuthorization(completion: @escaping (Result<HealthAuthorization, Error>) -> Void) 291 func checkHealthAppAuthorization(completion: @escaping (Result<HealthAuthorization, Error>) -> Void)
292 func requestHealthClientAuthorization(completion: @escaping (Result<Bool, Error>) -> Void) 292 func requestHealthClientAuthorization(completion: @escaping (Result<Bool, Error>) -> Void)
  293 + /// Cancels all Huawei Health authorizations on OHOS.
  294 + func cancelAuthorizations(completion: @escaping (Result<Bool, Error>) -> Void)
293 /// Requests Huawei Health to upload the user's latest data to its cloud. 295 /// Requests Huawei Health to upload the user's latest data to its cloud.
294 /// 296 ///
295 /// This only triggers the Health app's configured device-to-cloud sync; it 297 /// This only triggers the Health app's configured device-to-cloud sync; it
@@ -338,6 +340,22 @@ class HealthKitHostApiSetup { @@ -338,6 +340,22 @@ class HealthKitHostApiSetup {
338 } else { 340 } else {
339 requestHealthClientAuthorizationChannel.setMessageHandler(nil) 341 requestHealthClientAuthorizationChannel.setMessageHandler(nil)
340 } 342 }
  343 + /// Cancels all Huawei Health authorizations on OHOS.
  344 + let cancelAuthorizationsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelAuthorizations\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
  345 + if let api = api {
  346 + cancelAuthorizationsChannel.setMessageHandler { _, reply in
  347 + api.cancelAuthorizations { result in
  348 + switch result {
  349 + case .success(let res):
  350 + reply(wrapResult(res))
  351 + case .failure(let error):
  352 + reply(wrapError(error))
  353 + }
  354 + }
  355 + }
  356 + } else {
  357 + cancelAuthorizationsChannel.setMessageHandler(nil)
  358 + }
341 /// Requests Huawei Health to upload the user's latest data to its cloud. 359 /// Requests Huawei Health to upload the user's latest data to its cloud.
342 /// 360 ///
343 /// This only triggers the Health app's configured device-to-cloud sync; it 361 /// This only triggers the Health app's configured device-to-cloud sync; it
@@ -52,6 +52,10 @@ final class HealthKitHostApiImpl: HealthKitHostApi { @@ -52,6 +52,10 @@ final class HealthKitHostApiImpl: HealthKitHostApi {
52 } 52 }
53 } 53 }
54 54
  55 + func cancelAuthorizations(completion: @escaping (Result<Bool, Error>) -> Void) {
  56 + completion(.success(false))
  57 + }
  58 +
55 func syncHealthDataToCloud(completion: @escaping (Result<Bool, Error>) -> Void) { 59 func syncHealthDataToCloud(completion: @escaping (Result<Bool, Error>) -> Void) {
56 // Huawei Health manual cloud synchronization is HarmonyOS-specific. 60 // Huawei Health manual cloud synchronization is HarmonyOS-specific.
57 completion(.success(false)) 61 completion(.success(false))
1 import 'dart:async'; 1 import 'dart:async';
2 2
  3 +import 'package:doublefeel_flutter/app/actions/dialog_action.dart';
3 import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_controller.dart'; 4 import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_controller.dart';
4 import 'package:doublefeel_flutter/app/modules/home/controllers/home_controller.dart'; 5 import 'package:doublefeel_flutter/app/modules/home/controllers/home_controller.dart';
5 import 'package:doublefeel_flutter/app/modules/home/controllers/trend/trend_controller.dart'; 6 import 'package:doublefeel_flutter/app/modules/home/controllers/trend/trend_controller.dart';
@@ -12,8 +13,10 @@ import 'package:doublefeel_flutter/app/routes/app_pages.dart'; @@ -12,8 +13,10 @@ import 'package:doublefeel_flutter/app/routes/app_pages.dart';
12 import 'package:doublefeel_flutter/app/utils/platform_compact.dart'; 13 import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
13 import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; 14 import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
14 import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; 15 import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
  16 +import 'package:doublefeel_flutter/core/error/app_error.dart';
15 import 'package:doublefeel_flutter/core/logging/app_logger.dart'; 17 import 'package:doublefeel_flutter/core/logging/app_logger.dart';
16 import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; 18 import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
  19 +import 'package:doublefeel_flutter/core/network/api/harmony_api.dart';
17 import 'package:doublefeel_flutter/core/network/api/pay_api.dart'; 20 import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
18 import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; 21 import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
19 import 'package:doublefeel_flutter/core/result/app_result.dart'; 22 import 'package:doublefeel_flutter/core/result/app_result.dart';
@@ -101,6 +104,7 @@ class TodayController extends GetxController with WidgetsBindingObserver { @@ -101,6 +104,7 @@ class TodayController extends GetxController with WidgetsBindingObserver {
101 final stressSubtitle = ''.obs; 104 final stressSubtitle = ''.obs;
102 bool _isFirstLoad = true; 105 bool _isFirstLoad = true;
103 bool _isHandlingHealthAuthorizationTap = false; 106 bool _isHandlingHealthAuthorizationTap = false;
  107 + bool _isUnbindingHuaweiHealth = false;
104 108
105 // ── HRV 表盘引导 Banner ─────────────────── 109 // ── HRV 表盘引导 Banner ───────────────────
106 final showHrvAdBanner = false.obs; 110 final showHrvAdBanner = false.obs;
@@ -336,11 +340,39 @@ class TodayController extends GetxController with WidgetsBindingObserver { @@ -336,11 +340,39 @@ class TodayController extends GetxController with WidgetsBindingObserver {
336 } 340 }
337 341
338 Future<void> _showHuaweiHealthRegistrationDialog() async { 342 Future<void> _showHuaweiHealthRegistrationDialog() async {
339 - final confirmed = await HuaweiHealthRegistrationDialog.show();  
340 - if (confirmed) { 343 + final action = await HuaweiHealthRegistrationDialog.show();
  344 + if (action == DialogAction.confirm) {
341 await _platformHostApi.nativeHandleUrl( 345 await _platformHostApi.nativeHandleUrl(
342 'huaweischeme://healthapp/home/main', 346 'huaweischeme://healthapp/home/main',
343 ); 347 );
  348 + } else if (action == DialogAction.cancel) {
  349 + await _unbindHuaweiHealth();
  350 + }
  351 + }
  352 +
  353 + Future<void> _unbindHuaweiHealth() async {
  354 + if (_isUnbindingHuaweiHealth) {
  355 + return;
  356 + }
  357 + _isUnbindingHuaweiHealth = true;
  358 + try {
  359 + final result = await Get.find<HarmonyApi>().deletePrivacyRecords();
  360 + switch (result) {
  361 + case AppSuccess():
  362 + AppToast.show('已解除华为健康绑定');
  363 + await _checkHealthAuthStatus();
  364 + case AppFailure(:final error):
  365 + // 登录失效及服务端错误文案已由 safeCall 的全局处理器处理。
  366 + if (error is AppCancelledError ||
  367 + error.isAccessTokenInvalid ||
  368 + (error is AppHttpError &&
  369 + (error.businessMessage?.isNotEmpty ?? false))) {
  370 + return;
  371 + }
  372 + AppToast.show('解除绑定失败,请稍后重试');
  373 + }
  374 + } finally {
  375 + _isUnbindingHuaweiHealth = false;
344 } 376 }
345 } 377 }
346 378
@@ -456,6 +488,7 @@ class TodayController extends GetxController with WidgetsBindingObserver { @@ -456,6 +488,7 @@ class TodayController extends GetxController with WidgetsBindingObserver {
456 return; 488 return;
457 } 489 }
458 await Get.to(() => NoHealthDataPage( 490 await Get.to(() => NoHealthDataPage(
  491 + onUnbind: _unbindHuaweiHealth,
459 onRefresh: () { 492 onRefresh: () {
460 refreshTab(); 493 refreshTab();
461 Future.delayed(const Duration(milliseconds: 800), () { 494 Future.delayed(const Duration(milliseconds: 800), () {
@@ -8,49 +8,47 @@ import 'package:flutter/material.dart'; @@ -8,49 +8,47 @@ import 'package:flutter/material.dart';
8 8
9 /// Registration guidance shown before leaving the app for Huawei Health. 9 /// Registration guidance shown before leaving the app for Huawei Health.
10 class HuaweiHealthRegistrationDialog extends StatelessWidget { 10 class HuaweiHealthRegistrationDialog extends StatelessWidget {
11 - const HuaweiHealthRegistrationDialog({super.key, required this.onConfirm}); 11 + const HuaweiHealthRegistrationDialog({super.key, required this.onAction});
12 12
13 - final VoidCallback onConfirm; 13 + final ValueChanged<ActionResult<DialogAction, DialogMetaData>> onAction;
14 14
15 static const _data = DialogMetaData( 15 static const _data = DialogMetaData(
16 iconAsset: null, 16 iconAsset: null,
17 title: '该账号暂未注册华为运动健康', 17 title: '该账号暂未注册华为运动健康',
18 message: '请注册后再进行授权', 18 message: '请注册后再进行授权',
19 - confirmText: '好的', 19 + confirmText: '去注册',
  20 + cancelText: '解除绑定该账号',
20 isCancelable: false, 21 isCancelable: false,
21 ); 22 );
22 23
23 - static Future<bool> show() async { 24 + static Future<DialogAction> show() async {
24 final result = await DialogUtils.showDialog<DialogAction, DialogMetaData>( 25 final result = await DialogUtils.showDialog<DialogAction, DialogMetaData>(
25 (data, callback) => HuaweiHealthRegistrationDialog( 26 (data, callback) => HuaweiHealthRegistrationDialog(
26 - onConfirm: () => callback(const ActionResult(  
27 - action: DialogAction.confirm,  
28 - data: _data,  
29 - )), 27 + onAction: callback,
30 ), 28 ),
31 _data, 29 _data,
32 barrierDismissible: false, 30 barrierDismissible: false,
33 dismissAction: DialogAction.dismiss, 31 dismissAction: DialogAction.dismiss,
34 ); 32 );
35 - return result.action == DialogAction.confirm; 33 + return result.action ?? DialogAction.dismiss;
36 } 34 }
37 35
38 static const _titleStyle = TextStyle( 36 static const _titleStyle = TextStyle(
39 color: Color(0xFF141414), 37 color: Color(0xFF141414),
40 fontSize: 18, 38 fontSize: 18,
41 fontWeight: FontWeight.w600, 39 fontWeight: FontWeight.w600,
42 - height: 1.2, 40 + height: 1.4,
43 ); 41 );
44 static const _messageStyle = TextStyle( 42 static const _messageStyle = TextStyle(
45 color: Color(0xFF6E6D80), 43 color: Color(0xFF6E6D80),
46 fontSize: 15, 44 fontSize: 15,
47 - height: 1.2, 45 + height: 1.4,
48 ); 46 );
49 47
50 @override 48 @override
51 Widget build(BuildContext context) => CommonDialogView( 49 Widget build(BuildContext context) => CommonDialogView(
52 dialogMetaData: _data, 50 dialogMetaData: _data,
53 - onAction: (_) => onConfirm(), 51 + onAction: onAction,
54 contentPadding: EdgeInsets.fromLTRB(24.dp, 24.dp, 24.dp, 20.dp), 52 contentPadding: EdgeInsets.fromLTRB(24.dp, 24.dp, 24.dp, 20.dp),
55 titleWidth: 210.dp, 53 titleWidth: 210.dp,
56 titleSpacing: 16.dp, 54 titleSpacing: 16.dp,
@@ -58,5 +56,7 @@ class HuaweiHealthRegistrationDialog extends StatelessWidget { @@ -58,5 +56,7 @@ class HuaweiHealthRegistrationDialog extends StatelessWidget {
58 titleStyle: _titleStyle, 56 titleStyle: _titleStyle,
59 messageStyle: _messageStyle, 57 messageStyle: _messageStyle,
60 confirmFontWeight: FontWeight.w500, 58 confirmFontWeight: FontWeight.w500,
  59 + cancelFontWeight: FontWeight.w500,
  60 + cancelTextColor: const Color(0xFF6E6D80),
61 ); 61 );
62 } 62 }
@@ -14,10 +14,12 @@ class NoHealthDataPage extends StatelessWidget { @@ -14,10 +14,12 @@ class NoHealthDataPage extends StatelessWidget {
14 super.key, 14 super.key,
15 this.onRefresh, 15 this.onRefresh,
16 this.onHelp, 16 this.onHelp,
  17 + this.onUnbind,
17 }); 18 });
18 19
19 final VoidCallback? onRefresh; 20 final VoidCallback? onRefresh;
20 final VoidCallback? onHelp; 21 final VoidCallback? onHelp;
  22 + final VoidCallback? onUnbind;
21 23
22 @override 24 @override
23 Widget build(BuildContext context) { 25 Widget build(BuildContext context) {
@@ -43,7 +45,7 @@ class NoHealthDataPage extends StatelessWidget { @@ -43,7 +45,7 @@ class NoHealthDataPage extends StatelessWidget {
43 20 + MediaQuery.paddingOf(context).bottom, 45 20 + MediaQuery.paddingOf(context).bottom,
44 ), 46 ),
45 children: [ 47 children: [
46 - const _PageHeading(), 48 + _PageHeading(onUnbind: onUnbind),
47 const SizedBox(height: 24), 49 const SizedBox(height: 24),
48 _InfoCard( 50 _InfoCard(
49 path: isOhos() 51 path: isOhos()
@@ -150,7 +152,9 @@ class _TopBar extends StatelessWidget { @@ -150,7 +152,9 @@ class _TopBar extends StatelessWidget {
150 } 152 }
151 153
152 class _PageHeading extends StatelessWidget { 154 class _PageHeading extends StatelessWidget {
153 - const _PageHeading(); 155 + const _PageHeading({this.onUnbind});
  156 +
  157 + final VoidCallback? onUnbind;
154 158
155 @override 159 @override
156 Widget build(BuildContext context) { 160 Widget build(BuildContext context) {
@@ -185,6 +189,41 @@ class _PageHeading extends StatelessWidget { @@ -185,6 +189,41 @@ class _PageHeading extends StatelessWidget {
185 ), 189 ),
186 ), 190 ),
187 ), 191 ),
  192 + if (isOhos()) ...[
  193 + const SizedBox(height: 8),
  194 + Padding(
  195 + padding: const EdgeInsets.symmetric(horizontal: 8),
  196 + child: Wrap(
  197 + alignment: WrapAlignment.center,
  198 + children: [
  199 + Text(
  200 + '如需解除绑定,点此',
  201 + style: TextStyle(
  202 + color: context.colors.textSecondary,
  203 + fontSize: 12,
  204 + fontWeight: FontWeight.w400,
  205 + height: 17 / 12,
  206 + ),
  207 + ),
  208 + GestureDetector(
  209 + behavior: HitTestBehavior.opaque,
  210 + onTap: onUnbind,
  211 + child: Text(
  212 + '解除华为健康绑定',
  213 + style: TextStyle(
  214 + color: context.colors.primary,
  215 + fontSize: 12,
  216 + fontWeight: FontWeight.w400,
  217 + height: 17 / 12,
  218 + decoration: TextDecoration.underline,
  219 + decorationColor: context.colors.primary,
  220 + ),
  221 + ),
  222 + ),
  223 + ],
  224 + ),
  225 + ),
  226 + ],
188 ], 227 ],
189 ); 228 );
190 } 229 }
@@ -20,6 +20,8 @@ class CommonDialogView extends StatelessWidget { @@ -20,6 +20,8 @@ class CommonDialogView extends StatelessWidget {
20 this.titleStyle, 20 this.titleStyle,
21 this.messageStyle, 21 this.messageStyle,
22 this.confirmFontWeight = FontWeight.w600, 22 this.confirmFontWeight = FontWeight.w600,
  23 + this.cancelFontWeight = FontWeight.w400,
  24 + this.cancelTextColor = AppColors.textSecondary,
23 }); 25 });
24 26
25 final DialogMetaData dialogMetaData; 27 final DialogMetaData dialogMetaData;
@@ -32,6 +34,8 @@ class CommonDialogView extends StatelessWidget { @@ -32,6 +34,8 @@ class CommonDialogView extends StatelessWidget {
32 final TextStyle? titleStyle; 34 final TextStyle? titleStyle;
33 final TextStyle? messageStyle; 35 final TextStyle? messageStyle;
34 final FontWeight confirmFontWeight; 36 final FontWeight confirmFontWeight;
  37 + final FontWeight cancelFontWeight;
  38 + final Color cancelTextColor;
35 39
36 @override 40 @override
37 Widget build(BuildContext context) { 41 Widget build(BuildContext context) {
@@ -119,8 +123,8 @@ class CommonDialogView extends StatelessWidget { @@ -119,8 +123,8 @@ class CommonDialogView extends StatelessWidget {
119 SizedBox(height: 4.h), 123 SizedBox(height: 4.h),
120 _DialogButton( 124 _DialogButton(
121 text: dialogMetaData.cancelText, 125 text: dialogMetaData.cancelText,
122 - textColor: AppColors.textSecondary,  
123 - fontWeight: FontWeight.w400, 126 + textColor: cancelTextColor,
  127 + fontWeight: cancelFontWeight,
124 onTap: () { 128 onTap: () {
125 onAction?.call( 129 onAction?.call(
126 ActionResult( 130 ActionResult(
@@ -42,6 +42,14 @@ class HarmonyApi { @@ -42,6 +42,14 @@ class HarmonyApi {
42 ); 42 );
43 } 43 }
44 44
  45 + Future<AppResult<void>> deletePrivacyRecords() {
  46 + return safeCall(
  47 + call: () async {
  48 + await _dioClient.dio.delete(ApiPaths.hmPrivacyRecords);
  49 + },
  50 + );
  51 + }
  52 +
45 /// startDate: 开始日期20260801 53 /// startDate: 开始日期20260801
46 /// endDate: 截止日期20260801,和开始日期最大相隔31天 54 /// endDate: 截止日期20260801,和开始日期最大相隔31天
47 Future<AppResult<HmHealthData>> getHealthData( 55 Future<AppResult<HmHealthData>> getHealthData(
@@ -295,6 +295,13 @@ class AppHealthKitHostApi extends _AppPigeonApiFacade { @@ -295,6 +295,13 @@ class AppHealthKitHostApi extends _AppPigeonApiFacade {
295 }; 295 };
296 }); 296 });
297 297
  298 + /// Cancels all Huawei Health authorizations on HarmonyOS.
  299 + Future<bool> cancelAuthorizations() => dispatch(
  300 + ios: () async => false,
  301 + android: () async => false,
  302 + ohos: () => _api.cancelAuthorizations(),
  303 + );
  304 +
298 /// Requests Huawei Health to synchronize its local data to the cloud. 305 /// Requests Huawei Health to synchronize its local data to the cloud.
299 /// 306 ///
300 /// Available on HarmonyOS only. A `true` result means the request was 307 /// Available on HarmonyOS only. A `true` result means the request was
@@ -298,6 +298,35 @@ class HealthKitHostApi { @@ -298,6 +298,35 @@ class HealthKitHostApi {
298 } 298 }
299 } 299 }
300 300
  301 + /// Cancels all Huawei Health authorizations on OHOS.
  302 + Future<bool> cancelAuthorizations() async {
  303 + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelAuthorizations$pigeonVar_messageChannelSuffix';
  304 + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
  305 + pigeonVar_channelName,
  306 + pigeonChannelCodec,
  307 + binaryMessenger: pigeonVar_binaryMessenger,
  308 + );
  309 + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
  310 + final List<Object?>? pigeonVar_replyList =
  311 + await pigeonVar_sendFuture as List<Object?>?;
  312 + if (pigeonVar_replyList == null) {
  313 + throw _createConnectionError(pigeonVar_channelName);
  314 + } else if (pigeonVar_replyList.length > 1) {
  315 + throw PlatformException(
  316 + code: pigeonVar_replyList[0]! as String,
  317 + message: pigeonVar_replyList[1] as String?,
  318 + details: pigeonVar_replyList[2],
  319 + );
  320 + } else if (pigeonVar_replyList[0] == null) {
  321 + throw PlatformException(
  322 + code: 'null-error',
  323 + message: 'Host platform returned null value for non-null return value.',
  324 + );
  325 + } else {
  326 + return (pigeonVar_replyList[0] as bool?)!;
  327 + }
  328 + }
  329 +
301 /// Requests Huawei Health to upload the user's latest data to its cloud. 330 /// Requests Huawei Health to upload the user's latest data to its cloud.
302 /// 331 ///
303 /// This only triggers the Health app's configured device-to-cloud sync; it 332 /// This only triggers the Health app's configured device-to-cloud sync; it
@@ -92,6 +92,16 @@ export class HealthKitHostApiImpl extends HealthKitHostApi { @@ -92,6 +92,16 @@ export class HealthKitHostApiImpl extends HealthKitHostApi {
92 }); 92 });
93 } 93 }
94 94
  95 + cancelAuthorizations(result: Result<boolean>): void {
  96 + this.ensureInitialized()
  97 + .then(() => healthStore.cancelAuthorizations())
  98 + .then(() => result.success(true))
  99 + .catch((error: BusinessError) => {
  100 + console.error(`cancelAuthorizations failed: ${error.code}, ${error.message}`);
  101 + result.success(false);
  102 + });
  103 + }
  104 +
95 readActivityGoal(result: Result<HealthActivityGoal | undefined>): void { 105 readActivityGoal(result: Result<HealthActivityGoal | undefined>): void {
96 this.ensureInitialized() 106 this.ensureInitialized()
97 .then(() => healthService.workout.readActivityReport()) 107 .then(() => healthService.workout.readActivityReport())
@@ -327,6 +327,8 @@ export abstract class HealthKitHostApi { @@ -327,6 +327,8 @@ export abstract class HealthKitHostApi {
327 abstract checkHealthAppAuthorization(result: Result<HealthAuthorization>): void; 327 abstract checkHealthAppAuthorization(result: Result<HealthAuthorization>): void;
328 328
329 abstract requestHealthClientAuthorization(result: Result<boolean>): void; 329 abstract requestHealthClientAuthorization(result: Result<boolean>): void;
  330 + /* Cancels all Huawei Health authorizations on OHOS.*/
  331 + abstract cancelAuthorizations(result: Result<boolean>): void;
330 /* 332 /*
331 * Requests Huawei Health to upload the user's latest data to its cloud. 333 * Requests Huawei Health to upload the user's latest data to its cloud.
332 * 334 *
@@ -402,6 +404,33 @@ export abstract class HealthKitHostApi { @@ -402,6 +404,33 @@ export abstract class HealthKitHostApi {
402 { 404 {
403 let channel: BasicMessageChannel<Object> = 405 let channel: BasicMessageChannel<Object> =
404 new BasicMessageChannel( 406 new BasicMessageChannel(
  407 + binaryMessenger, 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelAuthorizations' + separatedMessageChannelSuffix, HealthKitHostApi.getCodec());
  408 + if (api != null) {
  409 + channel.setMessageHandler({
  410 + onMessage(message: Object, reply: Reply<Object>) {
  411 + class ResultImp implements Result<boolean>{
  412 + success(result: boolean): void {
  413 + let res: Array<Object | null> = [];
  414 + res.push(result);
  415 + reply.reply(res);
  416 + }
  417 +
  418 + error(error: Error): void {
  419 + let wrappedError: Array<Object | null> = wrapError(error);
  420 + reply.reply(wrappedError);
  421 + }
  422 + }
  423 + let resultCallback: Result<boolean> = new ResultImp();
  424 +
  425 + api!.cancelAuthorizations(resultCallback);
  426 + } });
  427 + } else {
  428 + channel.setMessageHandler(null);
  429 + }
  430 + }
  431 + {
  432 + let channel: BasicMessageChannel<Object> =
  433 + new BasicMessageChannel(
405 binaryMessenger, 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.syncHealthDataToCloud' + separatedMessageChannelSuffix, HealthKitHostApi.getCodec()); 434 binaryMessenger, 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.syncHealthDataToCloud' + separatedMessageChannelSuffix, HealthKitHostApi.getCodec());
406 if (api != null) { 435 if (api != null) {
407 channel.setMessageHandler({ 436 channel.setMessageHandler({
@@ -68,6 +68,10 @@ abstract class HealthKitHostApi { @@ -68,6 +68,10 @@ abstract class HealthKitHostApi {
68 @async 68 @async
69 bool requestHealthClientAuthorization(); 69 bool requestHealthClientAuthorization();
70 70
  71 + /// Cancels all Huawei Health authorizations on OHOS.
  72 + @async
  73 + bool cancelAuthorizations();
  74 +
71 /// Requests Huawei Health to upload the user's latest data to its cloud. 75 /// Requests Huawei Health to upload the user's latest data to its cloud.
72 /// 76 ///
73 /// This only triggers the Health app's configured device-to-cloud sync; it 77 /// This only triggers the Health app's configured device-to-cloud sync; it
  1 +import 'package:doublefeel_flutter/app/actions/dialog_action.dart';
1 import 'dart:async'; 2 import 'dart:async';
2 3
3 import 'package:doublefeel_flutter/core/error/app_error.dart'; 4 import 'package:doublefeel_flutter/core/error/app_error.dart';
@@ -31,7 +32,7 @@ void main() { @@ -31,7 +32,7 @@ void main() {
31 32
32 testWidgets('registration dialog waits for confirmation', (tester) async { 33 testWidgets('registration dialog waits for confirmation', (tester) async {
33 addTearDown(Get.reset); 34 addTearDown(Get.reset);
34 - bool? confirmed; 35 + DialogAction? confirmed;
35 await tester.pumpWidget(GetMaterialApp( 36 await tester.pumpWidget(GetMaterialApp(
36 home: Scaffold( 37 home: Scaffold(
37 body: TextButton( 38 body: TextButton(
@@ -49,11 +50,33 @@ void main() { @@ -49,11 +50,33 @@ void main() {
49 await tester.tapAt(const Offset(10, 10)); 50 await tester.tapAt(const Offset(10, 10));
50 await tester.pumpAndSettle(); 51 await tester.pumpAndSettle();
51 expect(confirmed, isNull); 52 expect(confirmed, isNull);
52 - expect(find.text('好的'), findsOneWidget);  
53 - await tester.tap(find.text('好的')); 53 + expect(find.text('去注册'), findsOneWidget);
  54 + await tester.tap(find.text('去注册'));
54 await tester.pumpAndSettle(); 55 await tester.pumpAndSettle();
55 - expect(confirmed, isTrue);  
56 - expect(find.text('好的'), findsNothing); 56 + expect(confirmed, DialogAction.confirm);
  57 + expect(find.text('去注册'), findsNothing);
  58 + expect(tester.takeException(), isNull);
  59 + });
  60 +
  61 + testWidgets('unbind returns a distinct action without registering',
  62 + (tester) async {
  63 + addTearDown(Get.reset);
  64 + DialogAction? action;
  65 + await tester.pumpWidget(GetMaterialApp(
  66 + home: Scaffold(
  67 + body: TextButton(
  68 + onPressed: () async {
  69 + action = await HuaweiHealthRegistrationDialog.show();
  70 + },
  71 + child: const Text('show'),
  72 + )),
  73 + ));
  74 + await tester.tap(find.text('show'));
  75 + await tester.pumpAndSettle();
  76 + await tester.tap(find.text('解除绑定该账号'));
  77 + await tester.pumpAndSettle();
  78 + expect(action, DialogAction.cancel);
  79 + expect(find.text('去注册'), findsNothing);
57 expect(tester.takeException(), isNull); 80 expect(tester.takeException(), isNull);
58 }); 81 });
59 82
@@ -145,7 +168,7 @@ void main() { @@ -145,7 +168,7 @@ void main() {
145 } 168 }
146 expect(openedUrls, isEmpty); 169 expect(openedUrls, isEmpty);
147 // The facade reports the result without displaying UI or opening apps. 170 // The facade reports the result without displaying UI or opening apps.
148 - expect(find.text('好的'), findsNothing); 171 + expect(find.text('去注册'), findsNothing);
149 expect( 172 expect(
150 authorization, 173 authorization,
151 switch (error) { 174 switch (error) {
1 import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; 1 import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
2 import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart' as native; 2 import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart' as native;
3 import 'package:flutter_test/flutter_test.dart'; 3 import 'package:flutter_test/flutter_test.dart';
  4 +import 'package:flutter/services.dart';
4 5
5 class _HealthApi extends native.HealthKitHostApi { 6 class _HealthApi extends native.HealthKitHostApi {
6 _HealthApi(this.status, {this.granted = false}); 7 _HealthApi(this.status, {this.granted = false});
@@ -17,6 +18,36 @@ class _HealthApi extends native.HealthKitHostApi { @@ -17,6 +18,36 @@ class _HealthApi extends native.HealthKitHostApi {
17 } 18 }
18 19
19 void main() { 20 void main() {
  21 + TestWidgetsFlutterBinding.ensureInitialized();
  22 + test('cancel authorization dispatches to OHOS and preserves failures',
  23 + () async {
  24 + const channel = BasicMessageChannel<Object?>(
  25 + 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelAuthorizations',
  26 + StandardMessageCodec(),
  27 + );
  28 + final messenger =
  29 + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
  30 + var calls = 0;
  31 + var succeeds = true;
  32 + messenger.setMockDecodedMessageHandler<Object?>(channel, (_) async {
  33 + calls++;
  34 + return [succeeds];
  35 + });
  36 + addTearDown(
  37 + () => messenger.setMockDecodedMessageHandler<Object?>(channel, null));
  38 + final api = AppHealthKitHostApi(appPlatform: AppPigeonPlatform.ohos);
  39 + expect(await api.cancelAuthorizations(), isTrue);
  40 + succeeds = false;
  41 + expect(await api.cancelAuthorizations(), isFalse);
  42 + for (final platform in [AppPigeonPlatform.ios, AppPigeonPlatform.android]) {
  43 + expect(
  44 + await AppHealthKitHostApi(appPlatform: platform)
  45 + .cancelAuthorizations(),
  46 + isFalse);
  47 + }
  48 + expect(calls, 2);
  49 + });
  50 +
20 test('wire values map explicitly and preserve Apple Health states', () { 51 test('wire values map explicitly and preserve Apple Health states', () {
21 const expected = { 52 const expected = {
22 -1: HealthAuthorizationStatus.unavailable, 53 -1: HealthAuthorizationStatus.unavailable,