Commit 5b5ca491268f1ce8db9cb84b4687e773587b567e

Authored by 刘宏哲
1 parent 37deec52

feat(app): bug fixed

Showing 35 changed files with 284 additions and 92 deletions
1 import 'package:doublefeel_flutter/app/routes/app_pages.dart'; 1 import 'package:doublefeel_flutter/app/routes/app_pages.dart';
  2 +import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
2 import 'package:doublefeel_flutter/core/theme/app_colors.dart'; 3 import 'package:doublefeel_flutter/core/theme/app_colors.dart';
3 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 4 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
4 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 5 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
5 -  
6 import 'package:flutter/material.dart'; 6 import 'package:flutter/material.dart';
7 import 'package:get/get.dart'; 7 import 'package:get/get.dart';
8 8
@@ -69,10 +69,33 @@ class HelpView extends GetView<HelpController> { @@ -69,10 +69,33 @@ class HelpView extends GetView<HelpController> {
69 } 69 }
70 70
71 List<({String title, List<String> body})> _buildFaqItems( 71 List<({String title, List<String> body})> _buildFaqItems(
72 - BuildContext context,  
73 - ) { 72 + BuildContext context) {
74 final l10n = context.l10n; 73 final l10n = context.l10n;
75 74
  75 + if (isOhos()) {
  76 + return [
  77 + (
  78 + title: "APP没有数据怎么办",
  79 + body: [
  80 + "1、 确定手表系统在 5.1.0.32SP3 及以上,手表型号需为 WATCH GT 5 系列(含 Pro)/ WATCH 4 系列以上,手环从华为手环 10 及以上。",
  81 + "2、确认设备是否处于省电模式、低电量状态或手表佩戴未贴紧,以上情况会影响手表数据采集。",
  82 + l10n.helpNoDataReasonFooter,
  83 + ],
  84 + ),
  85 + (
  86 + title: l10n.todayFaqLinkHrvRealtimeUpdate,
  87 + body: [
  88 + "在智能手表的默认设置下,HRV 每1小时更新一次。",
  89 + "为了解决 HRV 更新间隔较长的问题,DoubleFeel 设计了实时压力功能:",
  90 + "实时压力每6分钟更新一次(好友状态更新依赖 华为健康 同步,受系统机制影响可能存在短暂延迟。若好友经常使用DoubleFeel,健康状态会更新得更加及时)",
  91 + l10n.todayRealtimeStressScenarioTimely,
  92 + l10n.todayRealtimeStressScenarioConsistentTrend,
  93 + l10n.todayRealtimeStressScenarioSummary,
  94 + ],
  95 + )
  96 + ];
  97 + }
  98 +
76 return [ 99 return [
77 ( 100 (
78 title: l10n.todayFaqLinkNoData, 101 title: l10n.todayFaqLinkNoData,
@@ -141,7 +164,7 @@ class _FaqCard extends StatelessWidget { @@ -141,7 +164,7 @@ class _FaqCard extends StatelessWidget {
141 @override 164 @override
142 Widget build(BuildContext context) { 165 Widget build(BuildContext context) {
143 return Container( 166 return Container(
144 - height: 257, 167 + height: 149,
145 padding: EdgeInsets.fromLTRB(20, 20, 20, 9), 168 padding: EdgeInsets.fromLTRB(20, 20, 20, 9),
146 decoration: BoxDecoration( 169 decoration: BoxDecoration(
147 color: Colors.white, 170 color: Colors.white,
@@ -305,26 +305,26 @@ class TodayController extends GetxController with WidgetsBindingObserver { @@ -305,26 +305,26 @@ class TodayController extends GetxController with WidgetsBindingObserver {
305 } 305 }
306 } 306 }
307 307
308 - Future<void> _checkHealthAuthStatus( 308 + Future<bool> _checkHealthAuthStatus(
309 {bool isPullToRefresh = false, 309 {bool isPullToRefresh = false,
310 bool shouldCaculateAndUpload = false}) async { 310 bool shouldCaculateAndUpload = false}) async {
311 final result = await _hostApi.checkHealthAppAuthorization(); 311 final result = await _hostApi.checkHealthAppAuthorization();
312 _currentStatus = result.status; 312 _currentStatus = result.status;
313 _refreshHealthDataAuthCardStatus(status: _currentStatus); 313 _refreshHealthDataAuthCardStatus(status: _currentStatus);
314 if (_currentStatus != 1) { 314 if (_currentStatus != 1) {
315 - return; 315 + return false;
316 } 316 }
317 if (!shouldCaculateAndUpload) { 317 if (!shouldCaculateAndUpload) {
318 - return; 318 + return false;
319 } 319 }
320 - _performCaculateAndUpload(isPullToRefresh: isPullToRefresh); 320 + return _performCaculateAndUpload(isPullToRefresh: isPullToRefresh);
321 } 321 }
322 322
323 - _performCaculateAndUpload({ 323 + Future<bool> _performCaculateAndUpload({
324 bool isPullToRefresh = false, 324 bool isPullToRefresh = false,
325 }) { 325 }) {
326 _performHealthDataUpload(); 326 _performHealthDataUpload();
327 - _calculateHealthDataWithoutLoading( 327 + return _calculateHealthDataWithoutLoading(
328 uploaded: () async { 328 uploaded: () async {
329 _refreshHealthDataAuthCardStatus(status: 1); 329 _refreshHealthDataAuthCardStatus(status: 1);
330 }, 330 },
@@ -520,13 +520,21 @@ class TodayController extends GetxController with WidgetsBindingObserver { @@ -520,13 +520,21 @@ class TodayController extends GetxController with WidgetsBindingObserver {
520 firstSelectableDay.value = firstTime; 520 firstSelectableDay.value = firstTime;
521 } else { 521 } else {
522 try { 522 try {
523 - await _checkHealthAuthStatus( 523 + final didCalculateAndReload = await _checkHealthAuthStatus(
524 isPullToRefresh: isPullToRefresh, 524 isPullToRefresh: isPullToRefresh,
525 shouldCaculateAndUpload: shouldCaculateAndUpload); 525 shouldCaculateAndUpload: shouldCaculateAndUpload);
526 ta.setSuperProperties( 526 ta.setSuperProperties(
527 {'health_data_permission': _currentStatus.clamp(0, 1)}); 527 {'health_data_permission': _currentStatus.clamp(0, 1)});
528 - unawaited(loadDataForDate(selectedDate.value,  
529 - isPullToRefresh: isPullToRefresh)); 528 + // When a pull-to-refresh asks OHOS to recalculate health data,
  529 + // _calculateHealthDataWithoutLoading already reloads the page after
  530 + // the calculation. Starting another load here caused a second visible
  531 + // refresh shortly after the first one completed.
  532 + if (!didCalculateAndReload) {
  533 + await loadDataForDate(
  534 + selectedDate.value,
  535 + isPullToRefresh: isPullToRefresh,
  536 + );
  537 + }
530 } catch (error, stackTrace) { 538 } catch (error, stackTrace) {
531 AppLogger.e( 539 AppLogger.e(
532 'TodayController.checkHealthDataAuthCardVisible failed', 540 'TodayController.checkHealthDataAuthCardVisible failed',
@@ -556,25 +564,25 @@ class TodayController extends GetxController with WidgetsBindingObserver { @@ -556,25 +564,25 @@ class TodayController extends GetxController with WidgetsBindingObserver {
556 _refreshHealthDataAuthCardStatus(status: result.status); 564 _refreshHealthDataAuthCardStatus(status: result.status);
557 } 565 }
558 566
559 - void _calculateHealthDataWithoutLoading( 567 + Future<bool> _calculateHealthDataWithoutLoading(
560 {Future<void> Function()? uploaded, 568 {Future<void> Function()? uploaded,
561 bool isPullToRefresh = false, 569 bool isPullToRefresh = false,
562 - bool refreshAfterComplete = false}) {  
563 - unawaited(  
564 - (() async {  
565 - try {  
566 - await _healthRawDataCoreService.startCoreCaculate();  
567 - await loadDataForDate(selectedDate.value,  
568 - isPullToRefresh: isPullToRefresh);  
569 - await uploaded?.call();  
570 - if (refreshAfterComplete) {  
571 - await loadDataForDate(selectedDate.value);  
572 - }  
573 - } catch (error, stackTrace) {  
574 - AppLogger.e('Apple Health calculate failed', error, stackTrace);  
575 - }  
576 - })(),  
577 - ); 570 + bool refreshAfterComplete = false}) async {
  571 + try {
  572 + await _healthRawDataCoreService.startCoreCaculate();
  573 + await loadDataForDate(
  574 + selectedDate.value,
  575 + isPullToRefresh: isPullToRefresh,
  576 + );
  577 + await uploaded?.call();
  578 + if (refreshAfterComplete) {
  579 + await loadDataForDate(selectedDate.value);
  580 + }
  581 + return true;
  582 + } catch (error, stackTrace) {
  583 + AppLogger.e('Apple Health calculate failed', error, stackTrace);
  584 + return false;
  585 + }
578 } 586 }
579 587
580 void _performHealthDataUpload() { 588 void _performHealthDataUpload() {
@@ -925,7 +933,7 @@ class TodayController extends GetxController with WidgetsBindingObserver { @@ -925,7 +933,7 @@ class TodayController extends GetxController with WidgetsBindingObserver {
925 DateUtils.dateOnly(selectedDate.value), 933 DateUtils.dateOnly(selectedDate.value),
926 DateUtils.dateOnly(DateTime.now()), 934 DateUtils.dateOnly(DateTime.now()),
927 ); 935 );
928 - checkHealthDataAuthCardVisible( 936 + await checkHealthDataAuthCardVisible(
929 isPullToRefresh: isPullToRefresh, 937 isPullToRefresh: isPullToRefresh,
930 shouldCaculateAndUpload: isOhos() && isSelectedToday, 938 shouldCaculateAndUpload: isOhos() && isSelectedToday,
931 ); 939 );
@@ -3,20 +3,17 @@ import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart'; @@ -3,20 +3,17 @@ import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart';
3 import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; 3 import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart';
4 import 'package:doublefeel_flutter/app/routes/app_pages.dart'; 4 import 'package:doublefeel_flutter/app/routes/app_pages.dart';
5 import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; 5 import 'package:doublefeel_flutter/app/utils/dialog_utils.dart';
6 -import 'package:doublefeel_flutter/app/widget/marquee_text.dart';  
7 - 6 +import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
8 import 'package:doublefeel_flutter/core/network/api/user_api.dart'; 7 import 'package:doublefeel_flutter/core/network/api/user_api.dart';
9 import 'package:doublefeel_flutter/core/result/app_result.dart'; 8 import 'package:doublefeel_flutter/core/result/app_result.dart';
10 import 'package:doublefeel_flutter/core/services/loading_service.dart'; 9 import 'package:doublefeel_flutter/core/services/loading_service.dart';
11 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 10 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
12 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 11 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
13 import 'package:doublefeel_flutter/core/util/app_toast.dart'; 12 import 'package:doublefeel_flutter/core/util/app_toast.dart';
14 -  
15 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 13 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
16 import 'package:doublefeel_flutter/data/models/user/user_models.dart'; 14 import 'package:doublefeel_flutter/data/models/user/user_models.dart';
17 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 15 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
18 import 'package:flutter/material.dart'; 16 import 'package:flutter/material.dart';
19 -  
20 import 'package:get/get.dart'; 17 import 'package:get/get.dart';
21 18
22 class AccountSettingView extends GetView<MyController> { 19 class AccountSettingView extends GetView<MyController> {
@@ -496,7 +493,7 @@ class _DeleteAccountBottomSheet extends StatelessWidget { @@ -496,7 +493,7 @@ class _DeleteAccountBottomSheet extends StatelessWidget {
496 height: 12, 493 height: 12,
497 ), 494 ),
498 Text( 495 Text(
499 - context.l10n.deleteAccountWarningNote1, 496 + _getWarningNote1(context),
500 style: TextStyle( 497 style: TextStyle(
501 color: context.colors.textSecondary, 498 color: context.colors.textSecondary,
502 fontSize: 12, 499 fontSize: 12,
@@ -507,7 +504,7 @@ class _DeleteAccountBottomSheet extends StatelessWidget { @@ -507,7 +504,7 @@ class _DeleteAccountBottomSheet extends StatelessWidget {
507 height: 12, 504 height: 12,
508 ), 505 ),
509 Text( 506 Text(
510 - context.l10n.deleteAccountWarningNote2, 507 + _getWarningNote2(context),
511 style: TextStyle( 508 style: TextStyle(
512 color: context.colors.textSecondary, 509 color: context.colors.textSecondary,
513 fontSize: 12, 510 fontSize: 12,
@@ -570,4 +567,18 @@ class _DeleteAccountBottomSheet extends StatelessWidget { @@ -570,4 +567,18 @@ class _DeleteAccountBottomSheet extends StatelessWidget {
570 ), 567 ),
571 ); 568 );
572 } 569 }
  570 +
  571 + String _getWarningNote1(BuildContext context) {
  572 + if (isOhos()) {
  573 + return "注1:你的健康数据会保存在华为健康,我们不会删除华为健康中的数据。";
  574 + }
  575 + return context.l10n.deleteAccountWarningNote1;
  576 + }
  577 +
  578 + String _getWarningNote2(BuildContext context) {
  579 + if (isOhos()) {
  580 + return "注2:删除账号不会影响你在支付宝的自动续费状态,如果需要取消自动续费,请在支付宝中手动取消。";
  581 + }
  582 + return context.l10n.deleteAccountWarningNote2;
  583 + }
573 } 584 }
@@ -150,7 +150,7 @@ class StressStatusExplanationBottomSheet extends StatelessWidget { @@ -150,7 +150,7 @@ class StressStatusExplanationBottomSheet extends StatelessWidget {
150 const SizedBox(height: 12), 150 const SizedBox(height: 12),
151 isOhos() 151 isOhos()
152 ? _InfoCard( 152 ? _InfoCard(
153 - title: '手机 App 与智能表显示的压力状态有什么区别?', 153 + title: '手机 App 与智能表显示的压力状态有什么区别?',
154 children: [ 154 children: [
155 _InfoText( 155 _InfoText(
156 '手机 App 首页显示的是当天的综合压力状态,会综合分析 HRV、静息心率与整日身体趋势。'), 156 '手机 App 首页显示的是当天的综合压力状态,会综合分析 HRV、静息心率与整日身体趋势。'),
@@ -46,7 +46,9 @@ class TodayHealthDataAuthCard extends StatelessWidget { @@ -46,7 +46,9 @@ class TodayHealthDataAuthCard extends StatelessWidget {
46 ), 46 ),
47 const SizedBox(height: 5), 47 const SizedBox(height: 5),
48 Text( 48 Text(
49 - l10n.todayHealthDataAuthDescription, 49 + isOhos()
  50 + ? 'DoubleFeel 需要访问你的华为健康数据,以提供压力提醒、实时压力统计和健康建议。\n如果尚未授权,请点击下方完成授权;如果已经授权,首次同步大量健康数据可能需要等待几分钟,请稍后再试。'
  51 + : l10n.todayHealthDataAuthDescription,
50 style: TextStyle( 52 style: TextStyle(
51 color: colors.textSecondary, 53 color: colors.textSecondary,
52 fontSize: 13, 54 fontSize: 13,
1 import 'dart:async'; 1 import 'dart:async';
2 2
  3 +import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
3 import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; 4 import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
4 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 5 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
5 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 6 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
@@ -9,7 +10,6 @@ import 'package:fl_chart/fl_chart.dart'; @@ -9,7 +10,6 @@ import 'package:fl_chart/fl_chart.dart';
9 import 'package:flutter/material.dart'; 10 import 'package:flutter/material.dart';
10 import 'package:get/get.dart'; 11 import 'package:get/get.dart';
11 import 'package:intl/intl.dart'; 12 import 'package:intl/intl.dart';
12 -import 'package:lottie/lottie.dart';  
13 13
14 import '../../controllers/today_controller.dart'; 14 import '../../controllers/today_controller.dart';
15 import 'hrv_principle_explanation_bottom_sheet.dart'; 15 import 'hrv_principle_explanation_bottom_sheet.dart';
@@ -399,6 +399,7 @@ class TodayHrvChartCard extends StatelessWidget { @@ -399,6 +399,7 @@ class TodayHrvChartCard extends StatelessWidget {
399 399
400 List<TextSpan>? _getTooltip( 400 List<TextSpan>? _getTooltip(
401 BarChartRodData rod, int x, int? state, bool? isWorkout) { 401 BarChartRodData rod, int x, int? state, bool? isWorkout) {
  402 + final useOhosIcon = isOhos();
402 return [ 403 return [
403 TextSpan( 404 TextSpan(
404 text: isWorkout == true ? '◉ ' : '○ ', 405 text: isWorkout == true ? '◉ ' : '○ ',
@@ -432,14 +433,16 @@ class TodayHrvChartCard extends StatelessWidget { @@ -432,14 +433,16 @@ class TodayHrvChartCard extends StatelessWidget {
432 ), 433 ),
433 ), 434 ),
434 TextSpan( 435 TextSpan(
435 - text: '♥ ',  
436 - style: const TextStyle(  
437 - color: Color(0xFFB0B0B6),  
438 - fontSize: 14,  
439 - fontWeight: FontWeight.bold, 436 + text:
  437 + useOhosIcon ? String.fromCharCode(Icons.favorite.codePoint) : '♥ ',
  438 + style: TextStyle(
440 inherit: false, 439 inherit: false,
441 fontFamilyFallback: null, 440 fontFamilyFallback: null,
442 - fontFamily: '', 441 + fontFamily: useOhosIcon ? Icons.favorite.fontFamily : '',
  442 + package: useOhosIcon ? Icons.favorite.fontPackage : null,
  443 + color: const Color(0xFFB0B0B6),
  444 + fontSize: 14,
  445 + fontWeight: FontWeight.bold,
443 ), 446 ),
444 ), 447 ),
445 TextSpan( 448 TextSpan(
@@ -454,6 +457,7 @@ class TodayHrvChartCard extends StatelessWidget { @@ -454,6 +457,7 @@ class TodayHrvChartCard extends StatelessWidget {
454 } 457 }
455 458
456 List<TextSpan>? _getTooltipChildren(LineBarSpot flSpot, int? state) { 459 List<TextSpan>? _getTooltipChildren(LineBarSpot flSpot, int? state) {
  460 + final useOhosIcon = isOhos();
457 return [ 461 return [
458 TextSpan( 462 TextSpan(
459 text: '○ ', 463 text: '○ ',
@@ -482,12 +486,14 @@ class TodayHrvChartCard extends StatelessWidget { @@ -482,12 +486,14 @@ class TodayHrvChartCard extends StatelessWidget {
482 ), 486 ),
483 ), 487 ),
484 TextSpan( 488 TextSpan(
485 - text: '♥ ',  
486 - style: const TextStyle( 489 + text:
  490 + useOhosIcon ? String.fromCharCode(Icons.favorite.codePoint) : '♥ ',
  491 + style: TextStyle(
487 inherit: false, 492 inherit: false,
488 fontFamilyFallback: null, 493 fontFamilyFallback: null,
489 - fontFamily: '',  
490 - color: Color(0xFFB0B0B6), 494 + fontFamily: useOhosIcon ? Icons.favorite.fontFamily : '',
  495 + package: useOhosIcon ? Icons.favorite.fontPackage : null,
  496 + color: const Color(0xFFB0B0B6),
491 fontSize: 14, 497 fontSize: 14,
492 fontWeight: FontWeight.bold, 498 fontWeight: FontWeight.bold,
493 ), 499 ),
@@ -962,10 +968,13 @@ class _StressBarChartState extends State<_StressBarChart> { @@ -962,10 +968,13 @@ class _StressBarChartState extends State<_StressBarChart> {
962 final centerOfBar = 968 final centerOfBar =
963 _stressTouchedGroupIndex! * stepWidth + (barWidth / 2); 969 _stressTouchedGroupIndex! * stepWidth + (barWidth / 2);
964 customIndicator = Positioned( 970 customIndicator = Positioned(
965 - left: centerOfBar - 1.0, // 2px 宽度,居中 971 + left: centerOfBar - 1.0,
  972 + // 2px 宽度,居中
966 width: 2.0, 973 width: 2.0,
967 - top: 12.0, // 向上延展以连接 tooltip 底部  
968 - bottom: chartHeight - gridBottom, // 修正:bottom 指的是距离 Stack 底部的像素值 974 + top: 12.0,
  975 + // 向上延展以连接 tooltip 底部
  976 + bottom: chartHeight - gridBottom,
  977 + // 修正:bottom 指的是距离 Stack 底部的像素值
969 child: IgnorePointer( 978 child: IgnorePointer(
970 child: ColoredBox( 979 child: ColoredBox(
971 color: context.colors.textTertiary, 980 color: context.colors.textTertiary,
@@ -1144,9 +1153,10 @@ class _StressBarChartState extends State<_StressBarChart> { @@ -1144,9 +1153,10 @@ class _StressBarChartState extends State<_StressBarChart> {
1144 tooltipBorder: BorderSide.none, 1153 tooltipBorder: BorderSide.none,
1145 maxContentWidth: 400, 1154 maxContentWidth: 400,
1146 fitInsideHorizontally: true, 1155 fitInsideHorizontally: true,
1147 - fitInsideVertically:  
1148 - false, // 允许 tooltip 超出 BarChart 顶边界(绘制在 18px 的 top margin 区域内)  
1149 - tooltipMargin: 6, // 调整为和折线图一样的 6 像素空隙 1156 + fitInsideVertically: false,
  1157 + // 允许 tooltip 超出 BarChart 顶边界(绘制在 18px 的 top margin 区域内)
  1158 + tooltipMargin: 6,
  1159 + // 调整为和折线图一样的 6 像素空隙
1150 getTooltipColor: (touchedSpot) => const Color(0xFFF3F3F3), 1160 getTooltipColor: (touchedSpot) => const Color(0xFFF3F3F3),
1151 tooltipPadding: const EdgeInsets.only( 1161 tooltipPadding: const EdgeInsets.only(
1152 left: 12, 1162 left: 12,
1 -import 'package:cached_network_image/cached_network_image.dart';  
2 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 1 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
3 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 2 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
4 import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'; 3 import 'package:doublefeel_flutter/data/models/friend/friend_models.dart';
@@ -40,7 +39,7 @@ class TrendFriendSelectBottomSheet extends StatelessWidget { @@ -40,7 +39,7 @@ class TrendFriendSelectBottomSheet extends StatelessWidget {
40 children: [ 39 children: [
41 Center( 40 Center(
42 child: Text( 41 child: Text(
43 - 'Choose a Loved One', 42 + context.l10n.friendsSelect,
44 style: TextStyle( 43 style: TextStyle(
45 color: context.colors.textPrimary, 44 color: context.colors.textPrimary,
46 fontSize: 16, 45 fontSize: 16,
1 import 'dart:async'; 1 import 'dart:async';
2 import 'dart:convert'; 2 import 'dart:convert';
  3 +
3 import 'package:crypto/crypto.dart'; 4 import 'package:crypto/crypto.dart';
4 -import 'package:flutter_timezone/flutter_timezone.dart';  
5 import 'package:doublefeel_flutter/app/modules/login/views/email_login_new_password_view.dart'; 5 import 'package:doublefeel_flutter/app/modules/login/views/email_login_new_password_view.dart';
6 import 'package:doublefeel_flutter/app/modules/login/views/email_login_view.dart'; 6 import 'package:doublefeel_flutter/app/modules/login/views/email_login_view.dart';
  7 +import 'package:doublefeel_flutter/app/routes/app_pages.dart';
7 import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; 8 import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
8 import 'package:doublefeel_flutter/core/constants/app_const.dart'; 9 import 'package:doublefeel_flutter/core/constants/app_const.dart';
9 import 'package:doublefeel_flutter/core/logging/app_logger.dart'; 10 import 'package:doublefeel_flutter/core/logging/app_logger.dart';
10 -import 'package:doublefeel_flutter/core/services/app_config_service.dart';  
11 -import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';  
12 -import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';  
13 -import 'package:doublefeel_flutter/l10n/gen/app_localizations.dart';  
14 -import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';  
15 -import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';  
16 -import 'package:flutter/services.dart';  
17 -import 'package:flutter/widgets.dart';  
18 -import 'package:get/get.dart';  
19 -import 'package:doublefeel_flutter/core/util/app_toast.dart';  
20 import 'package:doublefeel_flutter/core/network/api/user_api.dart'; 11 import 'package:doublefeel_flutter/core/network/api/user_api.dart';
21 import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; 12 import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
  13 +import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
  14 +import 'package:doublefeel_flutter/core/result/app_result.dart';
  15 +import 'package:doublefeel_flutter/core/services/app_config_service.dart';
  16 +import 'package:doublefeel_flutter/core/services/loading_service.dart';
  17 +import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
22 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 18 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
  19 +import 'package:doublefeel_flutter/core/util/app_toast.dart';
23 import 'package:doublefeel_flutter/data/local/local_storage.dart'; 20 import 'package:doublefeel_flutter/data/local/local_storage.dart';
24 import 'package:doublefeel_flutter/data/local/user_account_storage.dart'; 21 import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
25 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 22 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
  23 +import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
26 import 'package:doublefeel_flutter/data/models/user/user_models.dart'; 24 import 'package:doublefeel_flutter/data/models/user/user_models.dart';
27 import 'package:doublefeel_flutter/data/models/vip/vip_info.dart'; 25 import 'package:doublefeel_flutter/data/models/vip/vip_info.dart';
28 -import 'package:doublefeel_flutter/core/result/app_result.dart';  
29 -import 'package:doublefeel_flutter/app/routes/app_pages.dart';  
30 -import 'package:doublefeel_flutter/core/services/loading_service.dart'; 26 +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
31 import 'package:doublefeel_flutter/pigeon/wechat_api.g.dart'; 27 import 'package:doublefeel_flutter/pigeon/wechat_api.g.dart';
  28 +import 'package:flutter/services.dart';
  29 +import 'package:flutter/widgets.dart';
  30 +import 'package:flutter_timezone/flutter_timezone.dart';
  31 +import 'package:get/get.dart';
32 32
33 class LoginController extends GetxController { 33 class LoginController extends GetxController {
34 final UserApi _userApi = Get.find<UserApi>(); 34 final UserApi _userApi = Get.find<UserApi>();
@@ -81,6 +81,7 @@ class LoginController extends GetxController { @@ -81,6 +81,7 @@ class LoginController extends GetxController {
81 bool get isEmailValid => 81 bool get isEmailValid =>
82 _emailRegex.hasMatch(emailInput.value.trim()) && 82 _emailRegex.hasMatch(emailInput.value.trim()) &&
83 emailInput.value.isNotEmpty; 83 emailInput.value.isNotEmpty;
  84 +
84 bool get isPasswordValid => 85 bool get isPasswordValid =>
85 _passwordRegex.hasMatch(passwordInput.value) && 86 _passwordRegex.hasMatch(passwordInput.value) &&
86 passwordInput.value.isNotEmpty; 87 passwordInput.value.isNotEmpty;
@@ -205,7 +206,16 @@ class LoginController extends GetxController { @@ -205,7 +206,16 @@ class LoginController extends GetxController {
205 if (code.isEmpty) return; 206 if (code.isEmpty) return;
206 await _loginWithWechat(code); 207 await _loginWithWechat(code);
207 } catch (e) { 208 } catch (e) {
208 - AppToast.show(e.toString()); 209 + if (e is PlatformException) {
  210 + var msg = e.message ?? "";
  211 + if (msg.isNotEmpty) {
  212 + AppToast.show(msg);
  213 + } else {
  214 + AppToast.show("微信登录失败");
  215 + }
  216 + } else {
  217 + AppToast.show(e.toString());
  218 + }
209 } 219 }
210 } 220 }
211 221
@@ -556,7 +566,7 @@ class LoginController extends GetxController { @@ -556,7 +566,7 @@ class LoginController extends GetxController {
556 } else if (isWechat) { 566 } else if (isWechat) {
557 registerData = { 567 registerData = {
558 'login_type': 'wechat', 568 'login_type': 'wechat',
559 - 'code': wechatCode, 569 + 'wechat_code': wechatCode,
560 }; 570 };
561 } else { 571 } else {
562 registerData = { 572 registerData = {
@@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/modules/home/views/tabs/my_tab.dart'; @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/modules/home/views/tabs/my_tab.dart';
2 import 'package:doublefeel_flutter/app/modules/membership_offer/controllers/membership_detail_controller.dart'; 2 import 'package:doublefeel_flutter/app/modules/membership_offer/controllers/membership_detail_controller.dart';
3 import 'package:doublefeel_flutter/app/modules/purchase/widgets/purchase_colors.dart'; 3 import 'package:doublefeel_flutter/app/modules/purchase/widgets/purchase_colors.dart';
4 import 'package:doublefeel_flutter/app/modules/purchase/widgets/refund_explanation_bottom_sheet.dart'; 4 import 'package:doublefeel_flutter/app/modules/purchase/widgets/refund_explanation_bottom_sheet.dart';
  5 +import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
5 import 'package:doublefeel_flutter/app/widget/premium_benefit_list_view.dart'; 6 import 'package:doublefeel_flutter/app/widget/premium_benefit_list_view.dart';
6 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 7 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
7 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 8 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
@@ -249,10 +250,11 @@ class MembershipDetailView extends GetView<MembershipDetailController> { @@ -249,10 +250,11 @@ class MembershipDetailView extends GetView<MembershipDetailController> {
249 ], 250 ],
250 ), 251 ),
251 ), 252 ),
252 - if (vipInfo?.vipType == 1 ||  
253 - vipInfo?.vipType == 2 ||  
254 - vipInfo?.vipType == 3 ||  
255 - vipInfo?.vipType == 4) 253 + if (!isOhos() &&
  254 + (vipInfo?.vipType == 1 ||
  255 + vipInfo?.vipType == 2 ||
  256 + vipInfo?.vipType == 3 ||
  257 + vipInfo?.vipType == 4))
256 GestureDetector( 258 GestureDetector(
257 onTap: () { 259 onTap: () {
258 controller.manageSubscription(); 260 controller.manageSubscription();
@@ -365,6 +367,49 @@ class _LegalNotes extends StatelessWidget { @@ -365,6 +367,49 @@ class _LegalNotes extends StatelessWidget {
365 367
366 @override 368 @override
367 Widget build(BuildContext context) { 369 Widget build(BuildContext context) {
  370 + if (isOhos()) {
  371 + return Padding(
  372 + padding: const EdgeInsets.symmetric(horizontal: 16),
  373 + child: Column(
  374 + crossAxisAlignment: CrossAxisAlignment.start,
  375 + children: [
  376 + Text(
  377 + context.l10n.purchaseNotesTitle,
  378 + style: const TextStyle(
  379 + color: Colors.black,
  380 + fontSize: 18,
  381 + fontWeight: FontWeight.w600,
  382 + height: 1.2,
  383 + ),
  384 + ),
  385 + const SizedBox(height: 24),
  386 + const _LegalText(
  387 + prefix: '1. ',
  388 + text: '确认支付成功后,会员权益将立即生效。',
  389 + ),
  390 + const SizedBox(height: 12),
  391 + const _LegalText(
  392 + prefix: '2. ',
  393 + text: 'DoubleFeel Pro 会员属于虚拟服务商品,购买后除法律法规另有规定外,通常不支持退款。',
  394 + ),
  395 + const SizedBox(height: 12),
  396 + const _LegalText(
  397 + prefix: '3. ',
  398 + text: '如支付成功后会员未生效,请尝试重新登录账号。',
  399 + ),
  400 + const SizedBox(height: 12),
  401 + _LegalText(
  402 + prefix: '4. ',
  403 + text: '如果有更多其它问题请',
  404 + linkText: context.l10n.purchaseLinkContactUs,
  405 + linkSuffix: '。',
  406 + onLinkTap: controller.openContactUs,
  407 + ),
  408 + ],
  409 + ),
  410 + );
  411 + }
  412 +
368 return Padding( 413 return Padding(
369 padding: const EdgeInsets.symmetric(horizontal: 16), 414 padding: const EdgeInsets.symmetric(horizontal: 16),
370 child: Column( 415 child: Column(
1 import 'package:doublefeel_flutter/app/utils/assets_helper.dart'; 1 import 'package:doublefeel_flutter/app/utils/assets_helper.dart';
  2 +import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
2 import 'package:doublefeel_flutter/app/widget/payment_confirmation_bottom_sheet.dart'; 3 import 'package:doublefeel_flutter/app/widget/payment_confirmation_bottom_sheet.dart';
3 import 'package:doublefeel_flutter/app/widget/premium_benefit_list_view.dart'; 4 import 'package:doublefeel_flutter/app/widget/premium_benefit_list_view.dart';
4 import 'package:doublefeel_flutter/core/util/size_extensions.dart'; 5 import 'package:doublefeel_flutter/core/util/size_extensions.dart';
@@ -265,6 +266,48 @@ class _LegalNotes extends StatelessWidget { @@ -265,6 +266,48 @@ class _LegalNotes extends StatelessWidget {
265 266
266 @override 267 @override
267 Widget build(BuildContext context) { 268 Widget build(BuildContext context) {
  269 + if (isOhos()) {
  270 + return Padding(
  271 + padding: const EdgeInsets.symmetric(horizontal: 16),
  272 + child: Column(
  273 + crossAxisAlignment: CrossAxisAlignment.start,
  274 + children: [
  275 + Text(
  276 + context.l10n.purchaseNotesTitle,
  277 + style: const TextStyle(
  278 + color: Colors.black,
  279 + fontSize: 18,
  280 + fontWeight: FontWeight.w600,
  281 + height: 1.2,
  282 + ),
  283 + ),
  284 + const SizedBox(height: 24),
  285 + const _LegalText(
  286 + prefix: '1. ',
  287 + text: '确认支付成功后,会员权益将立即生效。',
  288 + ),
  289 + const SizedBox(height: 12),
  290 + const _LegalText(
  291 + prefix: '2. ',
  292 + text: 'DoubleFeel Pro 会员属于虚拟服务商品,购买后除法律法规另有规定外,通常不支持退款。',
  293 + ),
  294 + const SizedBox(height: 12),
  295 + const _LegalText(
  296 + prefix: '3. ',
  297 + text: '如支付成功后会员未生效,请尝试重新登录账号。',
  298 + ),
  299 + const SizedBox(height: 12),
  300 + _LegalText(
  301 + prefix: '4. ',
  302 + text: '如果有更多其它问题请',
  303 + linkText: context.l10n.purchaseLinkContactUs,
  304 + linkSuffix: '。',
  305 + onLinkTap: controller.openContactUs,
  306 + ),
  307 + ],
  308 + ),
  309 + );
  310 + }
268 return Padding( 311 return Padding(
269 padding: const EdgeInsets.symmetric(horizontal: 16), 312 padding: const EdgeInsets.symmetric(horizontal: 16),
270 child: Column( 313 child: Column(
@@ -2,10 +2,8 @@ import 'dart:io'; @@ -2,10 +2,8 @@ import 'dart:io';
2 2
3 bool isOhos() { 3 bool isOhos() {
4 return Platform.isOhos; 4 return Platform.isOhos;
5 - // return true;  
6 } 5 }
7 6
8 bool isIOS() { 7 bool isIOS() {
9 return Platform.isIOS; 8 return Platform.isIOS;
10 - // return false;  
11 } 9 }
@@ -3,6 +3,8 @@ @@ -3,6 +3,8 @@
3 "analyzerMode":"BEFORE_RELEASE_CHECK_MODE", 3 "analyzerMode":"BEFORE_RELEASE_CHECK_MODE",
4 "autoCompile":true, 4 "autoCompile":true,
5 "autoTest":true, 5 "autoTest":true,
  6 + "cardCode":"auto_before_release_check",
  7 + "cardEnName":"Auto Release Experience Compliance Check",
6 "cardId":200, 8 "cardId":200,
7 "cardName":"自动上架体验规范体检", 9 "cardName":"自动上架体验规范体检",
8 "coldStartCount":0, 10 "coldStartCount":0,
@@ -3,6 +3,8 @@ @@ -3,6 +3,8 @@
3 "analyzerMode":"BEFORE_RELEASE_CHECK_MODE", 3 "analyzerMode":"BEFORE_RELEASE_CHECK_MODE",
4 "autoCompile":true, 4 "autoCompile":true,
5 "autoTest":true, 5 "autoTest":true,
  6 + "cardCode":"auto_before_release_check",
  7 + "cardEnName":"Auto Release Experience Compliance Check",
6 "cardId":300, 8 "cardId":300,
7 "cardName":"自动上架体验规范体检", 9 "cardName":"自动上架体验规范体检",
8 "coldStartCount":0, 10 "coldStartCount":0,
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 "vendor": "DiDi", 4 "vendor": "DiDi",
5 "versionCode": 100, 5 "versionCode": 100,
6 "versionName": "2.7.0", 6 "versionName": "2.7.0",
7 - "icon": "$media:app_icon", 7 + "icon": "$media:layered_image",
8 "label": "$string:app_name" 8 "label": "$string:app_name"
9 } 9 }
10 } 10 }
  1 +{
  2 + "layered-image":
  3 + {
  4 + "background" : "$media:background",
  5 + "foreground" : "$media:foreground"
  6 + }
  7 +}
@@ -93,6 +93,9 @@ export default class EntryAbility extends FlutterAbility implements MethodCallHa @@ -93,6 +93,9 @@ export default class EntryAbility extends FlutterAbility implements MethodCallHa
93 onForeground(): void { 93 onForeground(): void {
94 super.onForeground() 94 super.onForeground()
95 NativePigeonRegistrar.setAppForeground(true) 95 NativePigeonRegistrar.setAppForeground(true)
  96 + // A normal return from WeChat has no SendAuthResp callback. Release the
  97 + // pending authorization request so Flutter can remove its loading overlay.
  98 + this.weChatLoginBridge?.cancelPendingAuthorization()
96 } 99 }
97 100
98 onBackground(): void { 101 onBackground(): void {
@@ -34,24 +34,24 @@ export class WeChatLoginBridge extends WeChatHostApi implements WXApiEventHandle @@ -34,24 +34,24 @@ export class WeChatLoginBridge extends WeChatHostApi implements WXApiEventHandle
34 if (WECHAT_APP_ID.length === 0) { 34 if (WECHAT_APP_ID.length === 0) {
35 result.error(new FlutterError( 35 result.error(new FlutterError(
36 'wechat_app_id_missing', 36 'wechat_app_id_missing',
37 - 'WeChatError',  
38 'Set WECHAT_APP_ID in WeChatLoginBridge.ets first.', 37 'Set WECHAT_APP_ID in WeChatLoginBridge.ets first.',
  38 + 'WeChatError',
39 )); 39 ));
40 return; 40 return;
41 } 41 }
42 if (this.pendingResult !== null) { 42 if (this.pendingResult !== null) {
43 result.error(new FlutterError( 43 result.error(new FlutterError(
44 'wechat_authorization_in_progress', 44 'wechat_authorization_in_progress',
45 - 'WeChatError',  
46 'WeChat authorization is already in progress.', 45 'WeChat authorization is already in progress.',
  46 + 'WeChatError',
47 )); 47 ));
48 return; 48 return;
49 } 49 }
50 if (!this.wxApi.isWXAppInstalled()) { 50 if (!this.wxApi.isWXAppInstalled()) {
51 result.error(new FlutterError( 51 result.error(new FlutterError(
52 'wechat_not_installed', 52 'wechat_not_installed',
53 - 'WeChatError',  
54 'WeChat is not installed.', 53 'WeChat is not installed.',
  54 + 'WeChatError',
55 )); 55 ));
56 return; 56 return;
57 } 57 }
@@ -80,6 +80,21 @@ export class WeChatLoginBridge extends WeChatHostApi implements WXApiEventHandle @@ -80,6 +80,21 @@ export class WeChatLoginBridge extends WeChatHostApi implements WXApiEventHandle
80 this.wxApi.handleWant(want, this); 80 this.wxApi.handleWant(want, this);
81 } 81 }
82 82
  83 + /**
  84 + * WeChat does not send an authorization response when the user simply
  85 + * returns to our app. Complete the pending Pigeon call in that case, so the
  86 + * Flutter loading overlay is always dismissed.
  87 + */
  88 + cancelPendingAuthorization(): void {
  89 + if (this.pendingResult === null) {
  90 + return;
  91 + }
  92 + this.finishWithError(
  93 + 'wechat_authorization_cancelled',
  94 + 'WeChat authorization was cancelled.',
  95 + );
  96 + }
  97 +
83 onReq(_request: BaseReq): void {} 98 onReq(_request: BaseReq): void {}
84 99
85 onResp(response: BaseResp): void { 100 onResp(response: BaseResp): void {
@@ -114,7 +129,7 @@ export class WeChatLoginBridge extends WeChatHostApi implements WXApiEventHandle @@ -114,7 +129,7 @@ export class WeChatLoginBridge extends WeChatHostApi implements WXApiEventHandle
114 const result = this.pendingResult; 129 const result = this.pendingResult;
115 this.pendingResult = null; 130 this.pendingResult = null;
116 if (result !== null) { 131 if (result !== null) {
117 - result.error(new FlutterError(code, 'WeChatError', message)); 132 + result.error(new FlutterError(code, message, 'WeChatError'));
118 } 133 }
119 } 134 }
120 } 135 }
@@ -22,9 +22,9 @@ @@ -22,9 +22,9 @@
22 "name": "EntryAbility", 22 "name": "EntryAbility",
23 "srcEntry": "./ets/entryability/EntryAbility.ets", 23 "srcEntry": "./ets/entryability/EntryAbility.ets",
24 "description": "$string:EntryAbility_desc", 24 "description": "$string:EntryAbility_desc",
25 - "icon": "$media:icon", 25 + "icon": "$media:layered_image",
26 "label": "$string:EntryAbility_label", 26 "label": "$string:EntryAbility_label",
27 - "startWindowIcon": "$media:icon", 27 + "startWindowIcon": "$media:startIcon",
28 "startWindowBackground": "$color:start_window_background", 28 "startWindowBackground": "$color:start_window_background",
29 "exported": true, 29 "exported": true,
30 "skills": [ 30 "skills": [
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 "color": [ 2 "color": [
3 { 3 {
4 "name": "start_window_background", 4 "name": "start_window_background",
5 - "value": "#FFFFFF" 5 + "value": "#EDE5FF"
6 } 6 }
7 ] 7 ]
8 } 8 }
  1 +{
  2 + "layered-image":
  3 + {
  4 + "background" : "$media:background",
  5 + "foreground" : "$media:foreground"
  6 + }
  7 +}
@@ -16,10 +16,10 @@ @@ -16,10 +16,10 @@
16 "name": "TestAbility", 16 "name": "TestAbility",
17 "srcEntry": "./ets/testability/TestAbility.ets", 17 "srcEntry": "./ets/testability/TestAbility.ets",
18 "description": "$string:TestAbility_desc", 18 "description": "$string:TestAbility_desc",
19 - "icon": "$media:icon", 19 + "icon": "$media:layered_image",
20 "label": "$string:TestAbility_label", 20 "label": "$string:TestAbility_label",
21 "exported": true, 21 "exported": true,
22 - "startWindowIcon": "$media:icon", 22 + "startWindowIcon": "$media:startIcon",
23 "startWindowBackground": "$color:start_window_background", 23 "startWindowBackground": "$color:start_window_background",
24 "skills": [ 24 "skills": [
25 { 25 {
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 "color": [ 2 "color": [
3 { 3 {
4 "name": "start_window_background", 4 "name": "start_window_background",
5 - "value": "#FFFFFF" 5 + "value": "#EDE5FF"
6 } 6 }
7 ] 7 ]
8 } 8 }
  1 +{
  2 + "layered-image":
  3 + {
  4 + "background" : "$media:background",
  5 + "foreground" : "$media:foreground"
  6 + }
  7 +}