Commit a70e879151295edfad673d628dda080b0f50dade

Authored by 刘宏哲
1 parent ad199966

feat(app): change payment logic

  1 +import 'dart:async';
  2 +
1 import 'package:doublefeel_flutter/app/modules/login/views/email_login_contact_us_bottom_sheet.dart'; 3 import 'package:doublefeel_flutter/app/modules/login/views/email_login_contact_us_bottom_sheet.dart';
  4 +import 'package:doublefeel_flutter/app/modules/membership_offer/widgets/cancel_subscription_dialog.dart';
  5 +import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
  6 +import 'package:doublefeel_flutter/core/error/app_error.dart';
  7 +import 'package:doublefeel_flutter/core/error/http_error_handling_policy.dart';
2 import 'package:doublefeel_flutter/core/logging/app_logger.dart'; 8 import 'package:doublefeel_flutter/core/logging/app_logger.dart';
3 import 'package:doublefeel_flutter/core/network/api/pay_api.dart'; 9 import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
4 import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; 10 import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
  11 +import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
5 import 'package:doublefeel_flutter/core/result/app_result.dart'; 12 import 'package:doublefeel_flutter/core/result/app_result.dart';
6 import 'package:doublefeel_flutter/core/util/app_toast.dart'; 13 import 'package:doublefeel_flutter/core/util/app_toast.dart';
7 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 14 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
8 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; 15 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
9 -import 'package:doublefeel_flutter/data/models/pay/apple_pay_order_response.dart';  
10 -import 'package:doublefeel_flutter/data/models/pay/pay_models.dart';  
11 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 16 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
12 -import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';  
13 import 'package:flutter/material.dart'; 17 import 'package:flutter/material.dart';
14 import 'package:get/get.dart'; 18 import 'package:get/get.dart';
15 19
@@ -27,8 +31,10 @@ class MembershipDetailController extends GetxController { @@ -27,8 +31,10 @@ class MembershipDetailController extends GetxController {
27 31
28 final isRestoring = false.obs; 32 final isRestoring = false.obs;
29 final scrollController = ScrollController(); 33 final scrollController = ScrollController();
  34 + final subscriptionAgreementNo = RxnString();
30 35
31 final isUnlocking = false.obs; 36 final isUnlocking = false.obs;
  37 +
32 Future<void> unlock() async { 38 Future<void> unlock() async {
33 // isUnlocking.value = true; 39 // isUnlocking.value = true;
34 // try { 40 // try {
@@ -139,7 +145,10 @@ class MembershipDetailController extends GetxController { @@ -139,7 +145,10 @@ class MembershipDetailController extends GetxController {
139 @override 145 @override
140 void onInit() { 146 void onInit() {
141 super.onInit(); 147 super.onInit();
142 - _refreshVipInfo(); 148 + if (isOhos()) {
  149 + unawaited(_loadSubscription());
  150 + }
  151 + unawaited(_refreshVipInfo());
143 } 152 }
144 153
145 @override 154 @override
@@ -180,6 +189,56 @@ class MembershipDetailController extends GetxController { @@ -180,6 +189,56 @@ class MembershipDetailController extends GetxController {
180 feedbackCompat(pageName: 'Subscription page'); 189 feedbackCompat(pageName: 'Subscription page');
181 } 190 }
182 191
  192 + void openSupportEmail() {
  193 + final userId = _userPreferences.preferences.value.meUserInfo?.id ?? 0;
  194 + sendFeedbackEmail(
  195 + userId: userId.toString(),
  196 + pageName: 'Subscription page',
  197 + );
  198 + }
  199 +
  200 + void showCancelSubscriptionDialog() {
  201 + if (subscriptionAgreementNo.value == null) return;
  202 + Get.dialog<void>(
  203 + CancelSubscriptionDialog(onConfirm: cancelSubscription),
  204 + barrierDismissible: true,
  205 + barrierColor: Colors.black.withValues(alpha: 0.7),
  206 + );
  207 + }
  208 +
  209 + Future<void> _loadSubscription() async {
  210 + final result = await _payApi.getSubscriptionList();
  211 + if (result case AppSuccess(data: final response)) {
  212 + final agreementNo =
  213 + response.productList?.firstOrNull?.agreementNo?.trim();
  214 + subscriptionAgreementNo.value =
  215 + agreementNo == null || agreementNo.isEmpty ? null : agreementNo;
  216 + }
  217 + }
  218 +
  219 + Future<bool> cancelSubscription() async {
  220 + final agreementNo = subscriptionAgreementNo.value;
  221 + if (agreementNo == null || agreementNo.isEmpty) return false;
  222 +
  223 + final result = await _payApi.cancelSubscription(
  224 + agreementNo,
  225 + errorHandlingPolicy:
  226 + const HttpErrorHandlingPolicy(showBusinessMessage: false),
  227 + );
  228 + if (result case AppSuccess()) {
  229 + subscriptionAgreementNo.value = null;
  230 + AppToast.show('取消成功');
  231 + return true;
  232 + }
  233 + if (result case AppFailure(error: final error)) {
  234 + final message = error.displayMessage;
  235 + if (!error.isAccessTokenInvalid && message.isNotEmpty) {
  236 + AppToast.show(message);
  237 + }
  238 + }
  239 + return false;
  240 + }
  241 +
183 Future<void> manageSubscription() async { 242 Future<void> manageSubscription() async {
184 try { 243 try {
185 await _platformHostApi 244 await _platformHostApi
1 -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'; 1 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';  
4 import 'package:doublefeel_flutter/app/modules/purchase/widgets/refund_explanation_bottom_sheet.dart'; 2 import 'package:doublefeel_flutter/app/modules/purchase/widgets/refund_explanation_bottom_sheet.dart';
5 import 'package:doublefeel_flutter/app/utils/platform_compact.dart'; 3 import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
6 import 'package:doublefeel_flutter/app/widget/premium_benefit_list_view.dart'; 4 import 'package:doublefeel_flutter/app/widget/premium_benefit_list_view.dart';
  5 +import 'package:doublefeel_flutter/app/widget/purchase_legal_notes.dart';
7 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 6 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
8 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 7 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
9 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; 8 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
10 -import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';  
11 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart' as context; 9 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart' as context;
12 -import 'package:flutter/gestures.dart'; 10 +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
13 import 'package:flutter/material.dart'; 11 import 'package:flutter/material.dart';
14 import 'package:get/get.dart'; 12 import 'package:get/get.dart';
15 import 'package:intl/intl.dart'; 13 import 'package:intl/intl.dart';
@@ -250,6 +248,36 @@ class MembershipDetailView extends GetView<MembershipDetailController> { @@ -250,6 +248,36 @@ class MembershipDetailView extends GetView<MembershipDetailController> {
250 ], 248 ],
251 ), 249 ),
252 ), 250 ),
  251 + if (isOhos() &&
  252 + controller.subscriptionAgreementNo.value !=
  253 + null)
  254 + GestureDetector(
  255 + onTap:
  256 + controller.showCancelSubscriptionDialog,
  257 + behavior: HitTestBehavior.opaque,
  258 + child: SizedBox(
  259 + height: 56,
  260 + child: Row(
  261 + children: [
  262 + Text(
  263 + '取消自动续费',
  264 + textAlign: TextAlign.center,
  265 + style: TextStyle(
  266 + color: context.colors.textPrimary,
  267 + fontSize: 14,
  268 + fontWeight: FontWeight.w400,
  269 + ),
  270 + ),
  271 + Spacer(),
  272 + Image.asset(
  273 + 'assets/images/common/ic_more_gray.png',
  274 + width: 16,
  275 + height: 16,
  276 + ),
  277 + ],
  278 + ),
  279 + ),
  280 + ),
253 if (!isOhos() && 281 if (!isOhos() &&
254 (vipInfo?.vipType == 1 || 282 (vipInfo?.vipType == 1 ||
255 vipInfo?.vipType == 2 || 283 vipInfo?.vipType == 2 ||
@@ -292,7 +320,15 @@ class MembershipDetailView extends GetView<MembershipDetailController> { @@ -292,7 +320,15 @@ class MembershipDetailView extends GetView<MembershipDetailController> {
292 SizedBox( 320 SizedBox(
293 height: 28, 321 height: 28,
294 ), 322 ),
295 - _LegalNotes(controller: controller) 323 + isOhos()
  324 + ? OhosPurchaseLegalNotes(
  325 + onContactUsTap: controller.openContactUs,
  326 + onSupportEmailTap: controller.openSupportEmail,
  327 + )
  328 + : IosPurchaseLegalNotes(
  329 + onRefundExplanationTap: showRefundExplanationBottomSheet,
  330 + onContactUsTap: controller.openContactUs,
  331 + )
296 ], 332 ],
297 ), 333 ),
298 ), 334 ),
@@ -334,7 +370,7 @@ class MembershipDetailView extends GetView<MembershipDetailController> { @@ -334,7 +370,7 @@ class MembershipDetailView extends GetView<MembershipDetailController> {
334 370
335 final dateStr = DateFormat.yMMMd(locale).format(date); 371 final dateStr = DateFormat.yMMMd(locale).format(date);
336 372
337 - return l10n.membershipValidUntilDate(dateStr); 373 + return isOhos() ? dateStr : l10n.membershipValidUntilDate(dateStr);
338 } 374 }
339 375
340 String _vipType(UserPreferencesVipInfo? vipInfo) { 376 String _vipType(UserPreferencesVipInfo? vipInfo) {
@@ -359,191 +395,3 @@ class MembershipDetailView extends GetView<MembershipDetailController> { @@ -359,191 +395,3 @@ class MembershipDetailView extends GetView<MembershipDetailController> {
359 } 395 }
360 } 396 }
361 } 397 }
362 -  
363 -class _LegalNotes extends StatelessWidget {  
364 - const _LegalNotes({required this.controller});  
365 -  
366 - final MembershipDetailController controller;  
367 -  
368 - @override  
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 -  
413 - return Padding(  
414 - padding: const EdgeInsets.symmetric(horizontal: 16),  
415 - child: Column(  
416 - crossAxisAlignment: CrossAxisAlignment.start,  
417 - children: [  
418 - Text(  
419 - context.l10n.purchaseNotesTitle,  
420 - style: const TextStyle(  
421 - color: Colors.black,  
422 - fontSize: 18,  
423 - fontWeight: FontWeight.w600,  
424 - height: 1.2,  
425 - ),  
426 - ),  
427 - const SizedBox(height: 24),  
428 - _LegalText(  
429 - prefix: '1. ',  
430 - text: context.l10n.purchaseNoteSubscription,  
431 - linkText: context.l10n.purchaseLinkLearnMore,  
432 - linkSuffix: '。',  
433 - onLinkTap: showRefundExplanationBottomSheet,  
434 - ),  
435 - const SizedBox(height: 12),  
436 - /*  
437 - _LegalText(  
438 - prefix: '2. ',  
439 - text: context.l10n.purchaseNoteRestore,  
440 - linkText: context.l10n.purchaseRestore,  
441 - linkSuffix: '。',  
442 - onLinkTap: controller.restorePurchase,  
443 - ),  
444 - const SizedBox(height: 12),  
445 - */  
446 - _LegalText(  
447 - prefix: '2. ',  
448 - text: context.l10n.purchaseNoteContact,  
449 - linkText: context.l10n.purchaseLinkContactUs,  
450 - linkSuffix: '。',  
451 - onLinkTap: controller.openContactUs,  
452 - ),  
453 - ],  
454 - ),  
455 - );  
456 - }  
457 -}  
458 -  
459 -class _LegalText extends StatefulWidget {  
460 - const _LegalText({  
461 - required this.prefix,  
462 - required this.text,  
463 - this.linkText,  
464 - this.linkSuffix,  
465 - this.onLinkTap,  
466 - });  
467 -  
468 - final String prefix;  
469 - final String text;  
470 - final String? linkText;  
471 - final String? linkSuffix;  
472 - final VoidCallback? onLinkTap;  
473 -  
474 - @override  
475 - State<_LegalText> createState() => _LegalTextState();  
476 -}  
477 -  
478 -class _LegalTextState extends State<_LegalText> {  
479 - late final TapGestureRecognizer _linkRecognizer;  
480 -  
481 - @override  
482 - void initState() {  
483 - super.initState();  
484 - _linkRecognizer = TapGestureRecognizer()..onTap = widget.onLinkTap;  
485 - }  
486 -  
487 - @override  
488 - void didUpdateWidget(covariant _LegalText oldWidget) {  
489 - super.didUpdateWidget(oldWidget);  
490 - _linkRecognizer.onTap = widget.onLinkTap;  
491 - }  
492 -  
493 - @override  
494 - void dispose() {  
495 - _linkRecognizer.dispose();  
496 - super.dispose();  
497 - }  
498 -  
499 - static const _baseStyle = TextStyle(  
500 - color: PurchaseColors.subtitle,  
501 - fontSize: 12,  
502 - height: 1.4,  
503 - );  
504 -  
505 - static const _linkStyle = TextStyle(  
506 - color: Color(0xFF845EEE),  
507 - fontWeight: FontWeight.w500,  
508 - decoration: TextDecoration.underline,  
509 - decorationColor: Color(0xFF845EEE),  
510 - );  
511 -  
512 - TextSpan _buildTextSpan() => TextSpan(  
513 - style: _baseStyle,  
514 - children: [  
515 - TextSpan(text: widget.text),  
516 - if (widget.linkText case final linkText?)  
517 - TextSpan(  
518 - text: linkText,  
519 - style: _linkStyle,  
520 - recognizer: widget.onLinkTap == null ? null : _linkRecognizer,  
521 - ),  
522 - if (widget.linkSuffix case final linkSuffix?)  
523 - TextSpan(text: linkSuffix),  
524 - ],  
525 - );  
526 -  
527 - @override  
528 - Widget build(BuildContext context) {  
529 - return Row(  
530 - crossAxisAlignment: CrossAxisAlignment.start,  
531 - children: [  
532 - Text(  
533 - widget.prefix,  
534 - style: const TextStyle(  
535 - color: PurchaseColors.subtitle,  
536 - fontSize: 12,  
537 - height: 1.4,  
538 - ),  
539 - ),  
540 - Expanded(  
541 - child: Text.rich(  
542 - _buildTextSpan(),  
543 - textScaler: MediaQuery.textScalerOf(context),  
544 - ),  
545 - ),  
546 - ],  
547 - );  
548 - }  
549 -}  
  1 +import 'package:doublefeel_flutter/core/util/size_extensions.dart';
  2 +import 'package:flutter/material.dart';
  3 +import 'package:get/get.dart';
  4 +
  5 +/// Confirmation dialog for cancelling an OHOS auto-renewing subscription.
  6 +class CancelSubscriptionDialog extends StatefulWidget {
  7 + const CancelSubscriptionDialog({
  8 + super.key,
  9 + required this.onConfirm,
  10 + });
  11 +
  12 + final Future<bool> Function() onConfirm;
  13 +
  14 + @override
  15 + State<CancelSubscriptionDialog> createState() =>
  16 + _CancelSubscriptionDialogState();
  17 +}
  18 +
  19 +class _CancelSubscriptionDialogState extends State<CancelSubscriptionDialog> {
  20 + bool _isSubmitting = false;
  21 +
  22 + @override
  23 + Widget build(BuildContext context) {
  24 + return PopScope(
  25 + canPop: !_isSubmitting,
  26 + child: Dialog(
  27 + backgroundColor: Colors.transparent,
  28 + insetPadding: EdgeInsets.zero,
  29 + child: Container(
  30 + width: 300.dp,
  31 + padding: EdgeInsets.fromLTRB(24.dp, 24.dp, 24.dp, 20.dp),
  32 + decoration: BoxDecoration(
  33 + color: Colors.white,
  34 + borderRadius: BorderRadius.circular(24.dp),
  35 + ),
  36 + child: Column(
  37 + mainAxisSize: MainAxisSize.min,
  38 + children: [
  39 + Image.asset(
  40 + 'assets/images/common/ic_warning.png',
  41 + width: 60.dp,
  42 + height: 60.dp,
  43 + ),
  44 + SizedBox(height: 20.dp),
  45 + Column(
  46 + children: [
  47 + SizedBox(
  48 + width: 220.dp,
  49 + child: const Text(
  50 + '确认取消自动续费?',
  51 + textAlign: TextAlign.center,
  52 + style: TextStyle(
  53 + color: Color(0xFF141414),
  54 + fontSize: 18,
  55 + fontWeight: FontWeight.w600,
  56 + height: 1.2,
  57 + ),
  58 + ),
  59 + ),
  60 + SizedBox(height: 16.dp),
  61 + const Text(
  62 + '取消后,DoubleFeel Pro 会员权益仍可使用至当前订阅周期结束,之后将不再自动续费或扣款。',
  63 + textAlign: TextAlign.center,
  64 + style: TextStyle(
  65 + color: Color(0xFF6E6D80),
  66 + fontSize: 15,
  67 + fontWeight: FontWeight.w400,
  68 + height: 1.35,
  69 + ),
  70 + ),
  71 + ],
  72 + ),
  73 + SizedBox(height: 20.dp),
  74 + _DialogButton(
  75 + label: '确定',
  76 + backgroundColor: const Color(0xFF845EEE),
  77 + foregroundColor: Colors.white,
  78 + loading: _isSubmitting,
  79 + onTap: _isSubmitting ? null : _confirm,
  80 + ),
  81 + SizedBox(height: 4.dp),
  82 + _DialogButton(
  83 + label: '取消',
  84 + foregroundColor: const Color(0xFF6E6D80),
  85 + onTap: _isSubmitting ? null : Get.back,
  86 + ),
  87 + ],
  88 + ),
  89 + ),
  90 + ),
  91 + );
  92 + }
  93 +
  94 + Future<void> _confirm() async {
  95 + if (_isSubmitting) return;
  96 +
  97 + setState(() => _isSubmitting = true);
  98 + try {
  99 + final shouldClose = await widget.onConfirm();
  100 + if (shouldClose && mounted && Get.isDialogOpen == true) {
  101 + Get.back<void>();
  102 + }
  103 + } finally {
  104 + if (mounted) setState(() => _isSubmitting = false);
  105 + }
  106 + }
  107 +}
  108 +
  109 +class _DialogButton extends StatelessWidget {
  110 + const _DialogButton({
  111 + required this.label,
  112 + required this.foregroundColor,
  113 + required this.onTap,
  114 + this.backgroundColor,
  115 + this.loading = false,
  116 + });
  117 +
  118 + final String label;
  119 + final Color foregroundColor;
  120 + final Color? backgroundColor;
  121 + final VoidCallback? onTap;
  122 + final bool loading;
  123 +
  124 + @override
  125 + Widget build(BuildContext context) {
  126 + return GestureDetector(
  127 + onTap: onTap,
  128 + behavior: HitTestBehavior.opaque,
  129 + child: Container(
  130 + width: 220.dp,
  131 + height: 48.dp,
  132 + alignment: Alignment.center,
  133 + decoration: BoxDecoration(
  134 + color: backgroundColor,
  135 + borderRadius: BorderRadius.circular(24.dp),
  136 + ),
  137 + child: loading
  138 + ? SizedBox(
  139 + width: 18.dp,
  140 + height: 18.dp,
  141 + child: const CircularProgressIndicator(
  142 + color: Colors.white,
  143 + strokeWidth: 2,
  144 + ),
  145 + )
  146 + : Text(
  147 + label,
  148 + style: TextStyle(
  149 + color: foregroundColor,
  150 + fontSize: 14,
  151 + fontWeight: FontWeight.w500,
  152 + height: 1.2,
  153 + ),
  154 + ),
  155 + ),
  156 + );
  157 + }
  158 +}
@@ -484,6 +484,15 @@ class PurchaseController extends GetxController { @@ -484,6 +484,15 @@ class PurchaseController extends GetxController {
484 feedbackCompat(pageName: 'Subscription page'); 484 feedbackCompat(pageName: 'Subscription page');
485 } 485 }
486 486
  487 + void openSupportEmail() {
  488 + _trackClickVipCenter('邮件联系');
  489 + final userId = _userPreferences.preferences.value.meUserInfo?.id ?? 0;
  490 + sendFeedbackEmail(
  491 + userId: userId.toString(),
  492 + pageName: 'Subscription page',
  493 + );
  494 + }
  495 +
487 void openUserTerms() { 496 void openUserTerms() {
488 String url = environmentConfig.region.value == AppRegion.china 497 String url = environmentConfig.region.value == AppRegion.china
489 ? AppConst.userTerms 498 ? AppConst.userTerms
@@ -504,6 +513,14 @@ class PurchaseController extends GetxController { @@ -504,6 +513,14 @@ class PurchaseController extends GetxController {
504 ); 513 );
505 } 514 }
506 515
  516 + void openAutoRenewalAgreement() {
  517 + _trackClickVipCenter('自动续费协议');
  518 + Get.toNamed(
  519 + AppRoutes.webview,
  520 + parameters: {'url': AppConst.autoRenewalAgreement},
  521 + );
  522 + }
  523 +
507 Map<String, dynamic> get _baseAnalyticsProperties => 524 Map<String, dynamic> get _baseAnalyticsProperties =>
508 {'channel_type': channelType}; 525 {'channel_type': channelType};
509 526
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';  
3 import 'package:doublefeel_flutter/app/widget/payment_confirmation_bottom_sheet.dart'; 2 import 'package:doublefeel_flutter/app/widget/payment_confirmation_bottom_sheet.dart';
  3 +import 'package:doublefeel_flutter/app/widget/purchase_legal_notes.dart';
4 import 'package:doublefeel_flutter/app/widget/premium_benefit_list_view.dart'; 4 import 'package:doublefeel_flutter/app/widget/premium_benefit_list_view.dart';
5 import 'package:doublefeel_flutter/core/util/size_extensions.dart'; 5 import 'package:doublefeel_flutter/core/util/size_extensions.dart';
6 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 6 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
7 import 'package:doublefeel_flutter/r.dart'; 7 import 'package:doublefeel_flutter/r.dart';
8 -import 'package:flutter/gestures.dart';  
9 import 'package:flutter/material.dart'; 8 import 'package:flutter/material.dart';
10 import 'package:flutter/services.dart'; 9 import 'package:flutter/services.dart';
11 import 'package:get/get.dart'; 10 import 'package:get/get.dart';
@@ -68,7 +67,16 @@ class PurchaseView extends GetView<PurchaseController> { @@ -68,7 +67,16 @@ class PurchaseView extends GetView<PurchaseController> {
68 const SizedBox(height: 14), 67 const SizedBox(height: 14),
69 const PremiumBenefitListView(), 68 const PremiumBenefitListView(),
70 const SizedBox(height: 24), 69 const SizedBox(height: 24),
71 - _LegalNotes(controller: controller), 70 + controller.isOhos
  71 + ? OhosPurchaseLegalNotes(
  72 + onContactUsTap: controller.openContactUs,
  73 + onSupportEmailTap: controller.openSupportEmail,
  74 + )
  75 + : IosPurchaseLegalNotes(
  76 + onRefundExplanationTap:
  77 + controller.openRefundExplanation,
  78 + onContactUsTap: controller.openContactUs,
  79 + ),
72 ], 80 ],
73 ), 81 ),
74 ), 82 ),
@@ -112,6 +120,9 @@ class PurchaseView extends GetView<PurchaseController> { @@ -112,6 +120,9 @@ class PurchaseView extends GetView<PurchaseController> {
112 }, 120 },
113 onTermsTap: controller.openUserTerms, 121 onTermsTap: controller.openUserTerms,
114 onPrivacyTap: controller.openPrivacyPolicy, 122 onPrivacyTap: controller.openPrivacyPolicy,
  123 + showAutoRenewalAgreement: controller.isOhos,
  124 + onAutoRenewalAgreementTap:
  125 + controller.openAutoRenewalAgreement,
115 ), 126 ),
116 ), 127 ),
117 ), 128 ),
@@ -258,190 +269,3 @@ class _PurchaseNavigationBar extends StatelessWidget { @@ -258,190 +269,3 @@ class _PurchaseNavigationBar extends StatelessWidget {
258 ); 269 );
259 } 270 }
260 } 271 }
261 -  
262 -class _LegalNotes extends StatelessWidget {  
263 - const _LegalNotes({required this.controller});  
264 -  
265 - final PurchaseController controller;  
266 -  
267 - @override  
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 - }  
311 - return Padding(  
312 - padding: const EdgeInsets.symmetric(horizontal: 16),  
313 - child: Column(  
314 - crossAxisAlignment: CrossAxisAlignment.start,  
315 - children: [  
316 - Text(  
317 - context.l10n.purchaseNotesTitle,  
318 - style: const TextStyle(  
319 - color: Colors.black,  
320 - fontSize: 18,  
321 - fontWeight: FontWeight.w600,  
322 - height: 1.2,  
323 - ),  
324 - ),  
325 - const SizedBox(height: 24),  
326 - _LegalText(  
327 - prefix: '1. ',  
328 - text: context.l10n.purchaseNoteSubscription,  
329 - linkText: context.l10n.purchaseLinkLearnMore,  
330 - linkSuffix: '。',  
331 - onLinkTap: controller.openRefundExplanation,  
332 - ),  
333 - const SizedBox(height: 12),  
334 - /*  
335 - _LegalText(  
336 - prefix: '3. ',  
337 - text: context.l10n.purchaseNoteRestore,  
338 - linkText: context.l10n.purchaseRestore,  
339 - linkSuffix: '。',  
340 - onLinkTap: controller.restorePurchase,  
341 - ),  
342 - const SizedBox(height: 12),  
343 - */  
344 - _LegalText(  
345 - prefix: '2. ',  
346 - text: context.l10n.purchaseNoteContact,  
347 - linkText: context.l10n.purchaseLinkContactUs,  
348 - linkSuffix: '。',  
349 - onLinkTap: controller.openContactUs,  
350 - ),  
351 - ],  
352 - ),  
353 - );  
354 - }  
355 -}  
356 -  
357 -class _LegalText extends StatefulWidget {  
358 - const _LegalText({  
359 - required this.prefix,  
360 - required this.text,  
361 - this.linkText,  
362 - this.linkSuffix,  
363 - this.onLinkTap,  
364 - });  
365 -  
366 - final String prefix;  
367 - final String text;  
368 - final String? linkText;  
369 - final String? linkSuffix;  
370 - final VoidCallback? onLinkTap;  
371 -  
372 - @override  
373 - State<_LegalText> createState() => _LegalTextState();  
374 -}  
375 -  
376 -class _LegalTextState extends State<_LegalText> {  
377 - late final TapGestureRecognizer _linkRecognizer;  
378 -  
379 - @override  
380 - void initState() {  
381 - super.initState();  
382 - _linkRecognizer = TapGestureRecognizer()..onTap = widget.onLinkTap;  
383 - }  
384 -  
385 - @override  
386 - void didUpdateWidget(covariant _LegalText oldWidget) {  
387 - super.didUpdateWidget(oldWidget);  
388 - _linkRecognizer.onTap = widget.onLinkTap;  
389 - }  
390 -  
391 - @override  
392 - void dispose() {  
393 - _linkRecognizer.dispose();  
394 - super.dispose();  
395 - }  
396 -  
397 - static const _baseStyle = TextStyle(  
398 - color: PurchaseColors.subtitle,  
399 - fontSize: 12,  
400 - height: 1.4,  
401 - );  
402 -  
403 - static const _linkStyle = TextStyle(  
404 - color: Color(0xFF845EEE),  
405 - fontWeight: FontWeight.w500,  
406 - decoration: TextDecoration.underline,  
407 - decorationColor: Color(0xFF845EEE),  
408 - );  
409 -  
410 - TextSpan _buildTextSpan() => TextSpan(  
411 - style: _baseStyle,  
412 - children: [  
413 - TextSpan(text: widget.text),  
414 - if (widget.linkText case final linkText?)  
415 - TextSpan(  
416 - text: linkText,  
417 - style: _linkStyle,  
418 - recognizer: widget.onLinkTap == null ? null : _linkRecognizer,  
419 - ),  
420 - if (widget.linkSuffix case final linkSuffix?)  
421 - TextSpan(text: linkSuffix),  
422 - ],  
423 - );  
424 -  
425 - @override  
426 - Widget build(BuildContext context) {  
427 - return Row(  
428 - crossAxisAlignment: CrossAxisAlignment.start,  
429 - children: [  
430 - Text(  
431 - widget.prefix,  
432 - style: const TextStyle(  
433 - color: PurchaseColors.subtitle,  
434 - fontSize: 12,  
435 - height: 1.4,  
436 - ),  
437 - ),  
438 - Expanded(  
439 - child: Text.rich(  
440 - _buildTextSpan(),  
441 - textScaler: MediaQuery.textScalerOf(context),  
442 - ),  
443 - ),  
444 - ],  
445 - );  
446 - }  
447 -}  
@@ -13,6 +13,8 @@ class PurchaseBottomBar extends StatelessWidget { @@ -13,6 +13,8 @@ class PurchaseBottomBar extends StatelessWidget {
13 required this.onPressed, 13 required this.onPressed,
14 this.onTermsTap, 14 this.onTermsTap,
15 this.onPrivacyTap, 15 this.onPrivacyTap,
  16 + this.showAutoRenewalAgreement = false,
  17 + this.onAutoRenewalAgreementTap,
16 }); 18 });
17 19
18 final String label; 20 final String label;
@@ -20,6 +22,8 @@ class PurchaseBottomBar extends StatelessWidget { @@ -20,6 +22,8 @@ class PurchaseBottomBar extends StatelessWidget {
20 final VoidCallback onPressed; 22 final VoidCallback onPressed;
21 final VoidCallback? onTermsTap; 23 final VoidCallback? onTermsTap;
22 final VoidCallback? onPrivacyTap; 24 final VoidCallback? onPrivacyTap;
  25 + final bool showAutoRenewalAgreement;
  26 + final VoidCallback? onAutoRenewalAgreementTap;
23 27
24 @override 28 @override
25 Widget build(BuildContext context) { 29 Widget build(BuildContext context) {
@@ -103,6 +107,22 @@ class PurchaseBottomBar extends StatelessWidget { @@ -103,6 +107,22 @@ class PurchaseBottomBar extends StatelessWidget {
103 label: context.l10n.purchasePrivacyPolicy, 107 label: context.l10n.purchasePrivacyPolicy,
104 onTap: onPrivacyTap, 108 onTap: onPrivacyTap,
105 ), 109 ),
  110 + if (showAutoRenewalAgreement) ...[
  111 + const Padding(
  112 + padding: EdgeInsets.symmetric(horizontal: 8),
  113 + child: Text(
  114 + '|',
  115 + style: TextStyle(
  116 + color: PurchaseColors.subtitle,
  117 + fontSize: 12,
  118 + ),
  119 + ),
  120 + ),
  121 + _BottomLink(
  122 + label: '自动续费协议',
  123 + onTap: onAutoRenewalAgreementTap,
  124 + ),
  125 + ],
106 ], 126 ],
107 ), 127 ),
108 const SizedBox(height: 18), 128 const SizedBox(height: 18),
  1 +import 'package:doublefeel_flutter/app/modules/purchase/widgets/purchase_colors.dart';
  2 +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
  3 +import 'package:flutter/gestures.dart';
  4 +import 'package:flutter/material.dart';
  5 +
  6 +/// OHOS subscription terms shown on the purchase and membership pages.
  7 +class OhosPurchaseLegalNotes extends StatelessWidget {
  8 + const OhosPurchaseLegalNotes({
  9 + super.key,
  10 + required this.onContactUsTap,
  11 + required this.onSupportEmailTap,
  12 + });
  13 +
  14 + final VoidCallback onContactUsTap;
  15 + final VoidCallback onSupportEmailTap;
  16 +
  17 + @override
  18 + Widget build(BuildContext context) {
  19 + return Padding(
  20 + padding: const EdgeInsets.symmetric(horizontal: 16),
  21 + child: Column(
  22 + crossAxisAlignment: CrossAxisAlignment.start,
  23 + children: [
  24 + Text(
  25 + context.l10n.purchaseNotesTitle,
  26 + style: const TextStyle(
  27 + color: Colors.black,
  28 + fontSize: 18,
  29 + fontWeight: FontWeight.w600,
  30 + height: 1.2,
  31 + ),
  32 + ),
  33 + const SizedBox(height: 24),
  34 + const _LegalText(
  35 + prefix: '1. ',
  36 + spans: [
  37 + _LegalTextSpan(
  38 + text: '本服务为自动续费订阅服务,¥68/年或¥16/月,到期后将按所选订阅周期自动续费并扣款。',
  39 + ),
  40 + ],
  41 + ),
  42 + const SizedBox(height: 12),
  43 + const _LegalText(
  44 + prefix: '2. ',
  45 + spans: [
  46 + _LegalTextSpan(
  47 + text:
  48 + '如需取消自动续费,请前往支付宝 > 我的 > 设置 > 支付设置 > 免密支付/自动扣款 > 关闭对应服务。取消后,当前会员权益可使用至本周期结束,下个周期将不再扣费。',
  49 + ),
  50 + ],
  51 + ),
  52 + const SizedBox(height: 12),
  53 + const _LegalText(
  54 + prefix: '3. ',
  55 + spans: [
  56 + _LegalTextSpan(
  57 + text: '确认支付成功后,会员权益立即生效。如支付成功后会员未生效,请尝试重新登录账号。',
  58 + ),
  59 + ],
  60 + ),
  61 + const SizedBox(height: 12),
  62 + const _LegalText(
  63 + prefix: '4. ',
  64 + spans: [
  65 + _LegalTextSpan(
  66 + text: 'DoubleFeel Pro 属于虚拟服务商品,购买后除法律法规另有规定外,通常不支持退款。',
  67 + ),
  68 + ],
  69 + ),
  70 + const SizedBox(height: 12),
  71 + _LegalText(
  72 + prefix: '5. ',
  73 + spans: const [
  74 + _LegalTextSpan(text: '如有其他问题,请'),
  75 + _LegalTextSpan(text: '联系我们', isLink: true),
  76 + _LegalTextSpan(text: ',或发送邮件至'),
  77 + _LegalTextSpan(text: 'support@doublefeel.cn', isLink: true),
  78 + _LegalTextSpan(text: '。'),
  79 + ],
  80 + onLinkTaps: [onContactUsTap, onSupportEmailTap],
  81 + ),
  82 + ],
  83 + ),
  84 + );
  85 + }
  86 +}
  87 +
  88 +/// iOS subscription terms shown on the purchase and membership pages.
  89 +class IosPurchaseLegalNotes extends StatelessWidget {
  90 + const IosPurchaseLegalNotes({
  91 + required this.onRefundExplanationTap,
  92 + required this.onContactUsTap,
  93 + });
  94 +
  95 + final VoidCallback onRefundExplanationTap;
  96 + final VoidCallback onContactUsTap;
  97 +
  98 + @override
  99 + Widget build(BuildContext context) {
  100 + return Padding(
  101 + padding: const EdgeInsets.symmetric(horizontal: 16),
  102 + child: Column(
  103 + crossAxisAlignment: CrossAxisAlignment.start,
  104 + children: [
  105 + Text(
  106 + context.l10n.purchaseNotesTitle,
  107 + style: const TextStyle(
  108 + color: Colors.black,
  109 + fontSize: 18,
  110 + fontWeight: FontWeight.w600,
  111 + height: 1.2,
  112 + ),
  113 + ),
  114 + const SizedBox(height: 24),
  115 + _LegalText(
  116 + prefix: '1. ',
  117 + spans: [
  118 + _LegalTextSpan(text: context.l10n.purchaseNoteSubscription),
  119 + _LegalTextSpan(
  120 + text: context.l10n.purchaseLinkLearnMore,
  121 + isLink: true,
  122 + ),
  123 + const _LegalTextSpan(text: '。'),
  124 + ],
  125 + onLinkTaps: [onRefundExplanationTap],
  126 + ),
  127 + const SizedBox(height: 12),
  128 + _LegalText(
  129 + prefix: '2. ',
  130 + spans: [
  131 + _LegalTextSpan(text: context.l10n.purchaseNoteContact),
  132 + _LegalTextSpan(
  133 + text: context.l10n.purchaseLinkContactUs,
  134 + isLink: true,
  135 + ),
  136 + const _LegalTextSpan(text: '。'),
  137 + ],
  138 + onLinkTaps: [onContactUsTap],
  139 + ),
  140 + ],
  141 + ),
  142 + );
  143 + }
  144 +}
  145 +
  146 +class _LegalTextSpan {
  147 + const _LegalTextSpan({required this.text, this.isLink = false});
  148 +
  149 + final String text;
  150 + final bool isLink;
  151 +}
  152 +
  153 +class _LegalText extends StatefulWidget {
  154 + const _LegalText({
  155 + required this.prefix,
  156 + required this.spans,
  157 + this.onLinkTaps = const [],
  158 + });
  159 +
  160 + final String prefix;
  161 + final List<_LegalTextSpan> spans;
  162 + final List<VoidCallback> onLinkTaps;
  163 +
  164 + @override
  165 + State<_LegalText> createState() => _LegalTextState();
  166 +}
  167 +
  168 +class _LegalTextState extends State<_LegalText> {
  169 + late List<TapGestureRecognizer> _linkRecognizers;
  170 +
  171 + @override
  172 + void initState() {
  173 + super.initState();
  174 + _linkRecognizers = _createRecognizers();
  175 + }
  176 +
  177 + @override
  178 + void didUpdateWidget(covariant _LegalText oldWidget) {
  179 + super.didUpdateWidget(oldWidget);
  180 + for (var index = 0; index < _linkRecognizers.length; index++) {
  181 + _linkRecognizers[index].onTap = widget.onLinkTaps[index];
  182 + }
  183 + }
  184 +
  185 + List<TapGestureRecognizer> _createRecognizers() => [
  186 + for (final onTap in widget.onLinkTaps)
  187 + TapGestureRecognizer()..onTap = onTap,
  188 + ];
  189 +
  190 + @override
  191 + void dispose() {
  192 + for (final recognizer in _linkRecognizers) {
  193 + recognizer.dispose();
  194 + }
  195 + super.dispose();
  196 + }
  197 +
  198 + static const _baseStyle = TextStyle(
  199 + color: PurchaseColors.subtitle,
  200 + fontSize: 12,
  201 + height: 1.4,
  202 + );
  203 +
  204 + static const _linkStyle = TextStyle(
  205 + color: Color(0xFF845EEE),
  206 + fontWeight: FontWeight.w500,
  207 + decoration: TextDecoration.underline,
  208 + decorationColor: Color(0xFF845EEE),
  209 + );
  210 +
  211 + @override
  212 + Widget build(BuildContext context) {
  213 + var linkIndex = 0;
  214 + return Row(
  215 + crossAxisAlignment: CrossAxisAlignment.start,
  216 + children: [
  217 + Text(widget.prefix, style: _baseStyle),
  218 + Expanded(
  219 + child: Text.rich(
  220 + TextSpan(
  221 + style: _baseStyle,
  222 + children: [
  223 + for (final span in widget.spans)
  224 + TextSpan(
  225 + text: span.text,
  226 + style: span.isLink ? _linkStyle : null,
  227 + recognizer:
  228 + span.isLink ? _linkRecognizers[linkIndex++] : null,
  229 + ),
  230 + ],
  231 + ),
  232 + textScaler: MediaQuery.textScalerOf(context),
  233 + ),
  234 + ),
  235 + ],
  236 + );
  237 + }
  238 +}
@@ -28,6 +28,8 @@ abstract final class AppConst { @@ -28,6 +28,8 @@ abstract final class AppConst {
28 'https://cdn.doublefeel.cn/doublefeel/protocol/DoubleFeel%E7%94%A8%E6%88%B7%E5%8D%8F%E8%AE%AE.html'; 28 'https://cdn.doublefeel.cn/doublefeel/protocol/DoubleFeel%E7%94%A8%E6%88%B7%E5%8D%8F%E8%AE%AE.html';
29 static const String privacyPolicy = 29 static const String privacyPolicy =
30 'https://cdn.doublefeel.cn/doublefeel/protocol/DoubleFeel%E9%9A%90%E7%A7%81%E5%8D%8F%E8%AE%AE.html'; 30 'https://cdn.doublefeel.cn/doublefeel/protocol/DoubleFeel%E9%9A%90%E7%A7%81%E5%8D%8F%E8%AE%AE.html';
  31 + static const String autoRenewalAgreement =
  32 + 'http://cdn.doublefeel.cn/doublefeel/protocol/DoubleFeel_HarmonyOS_Auto_Renewal_Agreement.html';
31 static const String userTermsGlobal = 33 static const String userTermsGlobal =
32 'https://cdn-oversea.doublefeel.cn/doublefeel/protocol/DoubleFeel_Terms_of_Service.html'; 34 'https://cdn-oversea.doublefeel.cn/doublefeel/protocol/DoubleFeel_Terms_of_Service.html';
33 static const String privacyPolicyGlobal = 35 static const String privacyPolicyGlobal =
@@ -29,8 +29,9 @@ class AppErrorHandler { @@ -29,8 +29,9 @@ class AppErrorHandler {
29 _handleAccessTokenInvalid(accessToken); 29 _handleAccessTokenInvalid(accessToken);
30 return; 30 return;
31 } 31 }
32 - if (businessCode == null ||  
33 - !policy.excludeErrorCodeList.contains(businessCode)) { 32 + if (policy.showBusinessMessage &&
  33 + (businessCode == null ||
  34 + !policy.excludeErrorCodeList.contains(businessCode))) {
34 if (businessMessage != null) { 35 if (businessMessage != null) {
35 _showMessage(businessMessage); 36 _showMessage(businessMessage);
36 } 37 }
1 -/// Controls global HTTP error side effects (toast, logout, etc.). 1 +/// Controls global HTTP error side effects (business-error toast, logout, etc.).
2 /// 2 ///
3 -/// Pass an instance to [safeCall] to enable toast / logout behaviour. 3 +/// Pass an instance to [safeCall] to enable logout handling; business-error
  4 +/// messages can be suppressed while keeping authentication handling active.
4 /// Omit (pass null) to handle errors silently at the call site. 5 /// Omit (pass null) to handle errors silently at the call site.
5 class HttpErrorHandlingPolicy { 6 class HttpErrorHandlingPolicy {
6 - const HttpErrorHandlingPolicy({this.excludeErrorCodeList = const {}}); 7 + const HttpErrorHandlingPolicy({
  8 + this.excludeErrorCodeList = const {},
  9 + this.showBusinessMessage = true,
  10 + });
7 11
8 final Set<int> excludeErrorCodeList; 12 final Set<int> excludeErrorCodeList;
  13 + final bool showBusinessMessage;
9 14
10 /// Default: show errors, no exclusions. 15 /// Default: show errors, no exclusions.
11 static const HttpErrorHandlingPolicy defaultPolicy = 16 static const HttpErrorHandlingPolicy defaultPolicy =
@@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/utils/platform_compact.dart'; @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
2 import 'package:doublefeel_flutter/data/models/pay/apple_pay_order_response.dart'; 2 import 'package:doublefeel_flutter/data/models/pay/apple_pay_order_response.dart';
3 3
4 import '../../../data/models/pay/pay_models.dart'; 4 import '../../../data/models/pay/pay_models.dart';
  5 +import '../../error/http_error_handling_policy.dart';
5 import '../../result/app_result.dart'; 6 import '../../result/app_result.dart';
6 import '../../result/safe_call.dart'; 7 import '../../result/safe_call.dart';
7 import '../api_paths.dart'; 8 import '../api_paths.dart';
@@ -107,7 +108,11 @@ class PayApi { @@ -107,7 +108,11 @@ class PayApi {
107 ); 108 );
108 } 109 }
109 110
110 - Future<AppResult<void>> cancelSubscription(String agreementNo) { 111 + Future<AppResult<void>> cancelSubscription(
  112 + String agreementNo, {
  113 + HttpErrorHandlingPolicy? errorHandlingPolicy =
  114 + HttpErrorHandlingPolicy.defaultPolicy,
  115 + }) {
111 return safeCall( 116 return safeCall(
112 call: () async { 117 call: () async {
113 await _dioClient.dio.post( 118 await _dioClient.dio.post(
@@ -115,6 +120,7 @@ class PayApi { @@ -115,6 +120,7 @@ class PayApi {
115 data: SubscriptionCancelRequest(agreementNo: agreementNo).toJson(), 120 data: SubscriptionCancelRequest(agreementNo: agreementNo).toJson(),
116 ); 121 );
117 }, 122 },
  123 + errorHandlingPolicy: errorHandlingPolicy,
118 ); 124 );
119 } 125 }
120 } 126 }
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 "app": { 2 "app": {
3 "bundleName": "com.doublefeel.hmapp", 3 "bundleName": "com.doublefeel.hmapp",
4 "vendor": "DiDi", 4 "vendor": "DiDi",
5 - "versionCode": 101, 5 + "versionCode": 103,
6 "versionName": "2.7.0", 6 "versionName": "2.7.0",
7 "icon": "$media:layered_image", 7 "icon": "$media:layered_image",
8 "label": "$string:app_name" 8 "label": "$string:app_name"
@@ -2,7 +2,7 @@ name: doublefeel_flutter @@ -2,7 +2,7 @@ name: doublefeel_flutter
2 description: "A new Flutter project." 2 description: "A new Flutter project."
3 publish_to: 'none' 3 publish_to: 'none'
4 4
5 -version: 2.7.0+101 5 +version: 2.7.0+103
6 6
7 environment: 7 environment:
8 sdk: ^3.6.2 8 sdk: ^3.6.2