Commit a70e879151295edfad673d628dda080b0f50dade

Authored by 刘宏哲
1 parent ad199966

feat(app): change payment logic

import 'dart:async';
import 'package:doublefeel_flutter/app/modules/login/views/email_login_contact_us_bottom_sheet.dart';
import 'package:doublefeel_flutter/app/modules/membership_offer/widgets/cancel_subscription_dialog.dart';
import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
import 'package:doublefeel_flutter/core/error/app_error.dart';
import 'package:doublefeel_flutter/core/error/http_error_handling_policy.dart';
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
import 'package:doublefeel_flutter/data/models/pay/apple_pay_order_response.dart';
import 'package:doublefeel_flutter/data/models/pay/pay_models.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
... ... @@ -27,8 +31,10 @@ class MembershipDetailController extends GetxController {
final isRestoring = false.obs;
final scrollController = ScrollController();
final subscriptionAgreementNo = RxnString();
final isUnlocking = false.obs;
Future<void> unlock() async {
// isUnlocking.value = true;
// try {
... ... @@ -139,7 +145,10 @@ class MembershipDetailController extends GetxController {
@override
void onInit() {
super.onInit();
_refreshVipInfo();
if (isOhos()) {
unawaited(_loadSubscription());
}
unawaited(_refreshVipInfo());
}
@override
... ... @@ -180,6 +189,56 @@ class MembershipDetailController extends GetxController {
feedbackCompat(pageName: 'Subscription page');
}
void openSupportEmail() {
final userId = _userPreferences.preferences.value.meUserInfo?.id ?? 0;
sendFeedbackEmail(
userId: userId.toString(),
pageName: 'Subscription page',
);
}
void showCancelSubscriptionDialog() {
if (subscriptionAgreementNo.value == null) return;
Get.dialog<void>(
CancelSubscriptionDialog(onConfirm: cancelSubscription),
barrierDismissible: true,
barrierColor: Colors.black.withValues(alpha: 0.7),
);
}
Future<void> _loadSubscription() async {
final result = await _payApi.getSubscriptionList();
if (result case AppSuccess(data: final response)) {
final agreementNo =
response.productList?.firstOrNull?.agreementNo?.trim();
subscriptionAgreementNo.value =
agreementNo == null || agreementNo.isEmpty ? null : agreementNo;
}
}
Future<bool> cancelSubscription() async {
final agreementNo = subscriptionAgreementNo.value;
if (agreementNo == null || agreementNo.isEmpty) return false;
final result = await _payApi.cancelSubscription(
agreementNo,
errorHandlingPolicy:
const HttpErrorHandlingPolicy(showBusinessMessage: false),
);
if (result case AppSuccess()) {
subscriptionAgreementNo.value = null;
AppToast.show('取消成功');
return true;
}
if (result case AppFailure(error: final error)) {
final message = error.displayMessage;
if (!error.isAccessTokenInvalid && message.isNotEmpty) {
AppToast.show(message);
}
}
return false;
}
Future<void> manageSubscription() async {
try {
await _platformHostApi
... ...
import 'package:doublefeel_flutter/app/modules/home/views/tabs/my_tab.dart';
import 'package:doublefeel_flutter/app/modules/membership_offer/controllers/membership_detail_controller.dart';
import 'package:doublefeel_flutter/app/modules/purchase/widgets/purchase_colors.dart';
import 'package:doublefeel_flutter/app/modules/purchase/widgets/refund_explanation_bottom_sheet.dart';
import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
import 'package:doublefeel_flutter/app/widget/premium_benefit_list_view.dart';
import 'package:doublefeel_flutter/app/widget/purchase_legal_notes.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart' as context;
import 'package:flutter/gestures.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
... ... @@ -250,6 +248,36 @@ class MembershipDetailView extends GetView<MembershipDetailController> {
],
),
),
if (isOhos() &&
controller.subscriptionAgreementNo.value !=
null)
GestureDetector(
onTap:
controller.showCancelSubscriptionDialog,
behavior: HitTestBehavior.opaque,
child: SizedBox(
height: 56,
child: Row(
children: [
Text(
'取消自动续费',
textAlign: TextAlign.center,
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
Spacer(),
Image.asset(
'assets/images/common/ic_more_gray.png',
width: 16,
height: 16,
),
],
),
),
),
if (!isOhos() &&
(vipInfo?.vipType == 1 ||
vipInfo?.vipType == 2 ||
... ... @@ -292,7 +320,15 @@ class MembershipDetailView extends GetView<MembershipDetailController> {
SizedBox(
height: 28,
),
_LegalNotes(controller: controller)
isOhos()
? OhosPurchaseLegalNotes(
onContactUsTap: controller.openContactUs,
onSupportEmailTap: controller.openSupportEmail,
)
: IosPurchaseLegalNotes(
onRefundExplanationTap: showRefundExplanationBottomSheet,
onContactUsTap: controller.openContactUs,
)
],
),
),
... ... @@ -334,7 +370,7 @@ class MembershipDetailView extends GetView<MembershipDetailController> {
final dateStr = DateFormat.yMMMd(locale).format(date);
return l10n.membershipValidUntilDate(dateStr);
return isOhos() ? dateStr : l10n.membershipValidUntilDate(dateStr);
}
String _vipType(UserPreferencesVipInfo? vipInfo) {
... ... @@ -359,191 +395,3 @@ class MembershipDetailView extends GetView<MembershipDetailController> {
}
}
}
class _LegalNotes extends StatelessWidget {
const _LegalNotes({required this.controller});
final MembershipDetailController controller;
@override
Widget build(BuildContext context) {
if (isOhos()) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.purchaseNotesTitle,
style: const TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.w600,
height: 1.2,
),
),
const SizedBox(height: 24),
const _LegalText(
prefix: '1. ',
text: '确认支付成功后,会员权益将立即生效。',
),
const SizedBox(height: 12),
const _LegalText(
prefix: '2. ',
text: 'DoubleFeel Pro 会员属于虚拟服务商品,购买后除法律法规另有规定外,通常不支持退款。',
),
const SizedBox(height: 12),
const _LegalText(
prefix: '3. ',
text: '如支付成功后会员未生效,请尝试重新登录账号。',
),
const SizedBox(height: 12),
_LegalText(
prefix: '4. ',
text: '如果有更多其它问题请',
linkText: context.l10n.purchaseLinkContactUs,
linkSuffix: '。',
onLinkTap: controller.openContactUs,
),
],
),
);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.purchaseNotesTitle,
style: const TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.w600,
height: 1.2,
),
),
const SizedBox(height: 24),
_LegalText(
prefix: '1. ',
text: context.l10n.purchaseNoteSubscription,
linkText: context.l10n.purchaseLinkLearnMore,
linkSuffix: '。',
onLinkTap: showRefundExplanationBottomSheet,
),
const SizedBox(height: 12),
/*
_LegalText(
prefix: '2. ',
text: context.l10n.purchaseNoteRestore,
linkText: context.l10n.purchaseRestore,
linkSuffix: '。',
onLinkTap: controller.restorePurchase,
),
const SizedBox(height: 12),
*/
_LegalText(
prefix: '2. ',
text: context.l10n.purchaseNoteContact,
linkText: context.l10n.purchaseLinkContactUs,
linkSuffix: '。',
onLinkTap: controller.openContactUs,
),
],
),
);
}
}
class _LegalText extends StatefulWidget {
const _LegalText({
required this.prefix,
required this.text,
this.linkText,
this.linkSuffix,
this.onLinkTap,
});
final String prefix;
final String text;
final String? linkText;
final String? linkSuffix;
final VoidCallback? onLinkTap;
@override
State<_LegalText> createState() => _LegalTextState();
}
class _LegalTextState extends State<_LegalText> {
late final TapGestureRecognizer _linkRecognizer;
@override
void initState() {
super.initState();
_linkRecognizer = TapGestureRecognizer()..onTap = widget.onLinkTap;
}
@override
void didUpdateWidget(covariant _LegalText oldWidget) {
super.didUpdateWidget(oldWidget);
_linkRecognizer.onTap = widget.onLinkTap;
}
@override
void dispose() {
_linkRecognizer.dispose();
super.dispose();
}
static const _baseStyle = TextStyle(
color: PurchaseColors.subtitle,
fontSize: 12,
height: 1.4,
);
static const _linkStyle = TextStyle(
color: Color(0xFF845EEE),
fontWeight: FontWeight.w500,
decoration: TextDecoration.underline,
decorationColor: Color(0xFF845EEE),
);
TextSpan _buildTextSpan() => TextSpan(
style: _baseStyle,
children: [
TextSpan(text: widget.text),
if (widget.linkText case final linkText?)
TextSpan(
text: linkText,
style: _linkStyle,
recognizer: widget.onLinkTap == null ? null : _linkRecognizer,
),
if (widget.linkSuffix case final linkSuffix?)
TextSpan(text: linkSuffix),
],
);
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.prefix,
style: const TextStyle(
color: PurchaseColors.subtitle,
fontSize: 12,
height: 1.4,
),
),
Expanded(
child: Text.rich(
_buildTextSpan(),
textScaler: MediaQuery.textScalerOf(context),
),
),
],
);
}
}
... ...
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
/// Confirmation dialog for cancelling an OHOS auto-renewing subscription.
class CancelSubscriptionDialog extends StatefulWidget {
const CancelSubscriptionDialog({
super.key,
required this.onConfirm,
});
final Future<bool> Function() onConfirm;
@override
State<CancelSubscriptionDialog> createState() =>
_CancelSubscriptionDialogState();
}
class _CancelSubscriptionDialogState extends State<CancelSubscriptionDialog> {
bool _isSubmitting = false;
@override
Widget build(BuildContext context) {
return PopScope(
canPop: !_isSubmitting,
child: Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.zero,
child: Container(
width: 300.dp,
padding: EdgeInsets.fromLTRB(24.dp, 24.dp, 24.dp, 20.dp),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24.dp),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/images/common/ic_warning.png',
width: 60.dp,
height: 60.dp,
),
SizedBox(height: 20.dp),
Column(
children: [
SizedBox(
width: 220.dp,
child: const Text(
'确认取消自动续费?',
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFF141414),
fontSize: 18,
fontWeight: FontWeight.w600,
height: 1.2,
),
),
),
SizedBox(height: 16.dp),
const Text(
'取消后,DoubleFeel Pro 会员权益仍可使用至当前订阅周期结束,之后将不再自动续费或扣款。',
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFF6E6D80),
fontSize: 15,
fontWeight: FontWeight.w400,
height: 1.35,
),
),
],
),
SizedBox(height: 20.dp),
_DialogButton(
label: '确定',
backgroundColor: const Color(0xFF845EEE),
foregroundColor: Colors.white,
loading: _isSubmitting,
onTap: _isSubmitting ? null : _confirm,
),
SizedBox(height: 4.dp),
_DialogButton(
label: '取消',
foregroundColor: const Color(0xFF6E6D80),
onTap: _isSubmitting ? null : Get.back,
),
],
),
),
),
);
}
Future<void> _confirm() async {
if (_isSubmitting) return;
setState(() => _isSubmitting = true);
try {
final shouldClose = await widget.onConfirm();
if (shouldClose && mounted && Get.isDialogOpen == true) {
Get.back<void>();
}
} finally {
if (mounted) setState(() => _isSubmitting = false);
}
}
}
class _DialogButton extends StatelessWidget {
const _DialogButton({
required this.label,
required this.foregroundColor,
required this.onTap,
this.backgroundColor,
this.loading = false,
});
final String label;
final Color foregroundColor;
final Color? backgroundColor;
final VoidCallback? onTap;
final bool loading;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Container(
width: 220.dp,
height: 48.dp,
alignment: Alignment.center,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(24.dp),
),
child: loading
? SizedBox(
width: 18.dp,
height: 18.dp,
child: const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Text(
label,
style: TextStyle(
color: foregroundColor,
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.2,
),
),
),
);
}
}
... ...
... ... @@ -484,6 +484,15 @@ class PurchaseController extends GetxController {
feedbackCompat(pageName: 'Subscription page');
}
void openSupportEmail() {
_trackClickVipCenter('邮件联系');
final userId = _userPreferences.preferences.value.meUserInfo?.id ?? 0;
sendFeedbackEmail(
userId: userId.toString(),
pageName: 'Subscription page',
);
}
void openUserTerms() {
String url = environmentConfig.region.value == AppRegion.china
? AppConst.userTerms
... ... @@ -504,6 +513,14 @@ class PurchaseController extends GetxController {
);
}
void openAutoRenewalAgreement() {
_trackClickVipCenter('自动续费协议');
Get.toNamed(
AppRoutes.webview,
parameters: {'url': AppConst.autoRenewalAgreement},
);
}
Map<String, dynamic> get _baseAnalyticsProperties =>
{'channel_type': channelType};
... ...
import 'package:doublefeel_flutter/app/utils/assets_helper.dart';
import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
import 'package:doublefeel_flutter/app/widget/payment_confirmation_bottom_sheet.dart';
import 'package:doublefeel_flutter/app/widget/purchase_legal_notes.dart';
import 'package:doublefeel_flutter/app/widget/premium_benefit_list_view.dart';
import 'package:doublefeel_flutter/core/util/size_extensions.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:doublefeel_flutter/r.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
... ... @@ -68,7 +67,16 @@ class PurchaseView extends GetView<PurchaseController> {
const SizedBox(height: 14),
const PremiumBenefitListView(),
const SizedBox(height: 24),
_LegalNotes(controller: controller),
controller.isOhos
? OhosPurchaseLegalNotes(
onContactUsTap: controller.openContactUs,
onSupportEmailTap: controller.openSupportEmail,
)
: IosPurchaseLegalNotes(
onRefundExplanationTap:
controller.openRefundExplanation,
onContactUsTap: controller.openContactUs,
),
],
),
),
... ... @@ -112,6 +120,9 @@ class PurchaseView extends GetView<PurchaseController> {
},
onTermsTap: controller.openUserTerms,
onPrivacyTap: controller.openPrivacyPolicy,
showAutoRenewalAgreement: controller.isOhos,
onAutoRenewalAgreementTap:
controller.openAutoRenewalAgreement,
),
),
),
... ... @@ -258,190 +269,3 @@ class _PurchaseNavigationBar extends StatelessWidget {
);
}
}
class _LegalNotes extends StatelessWidget {
const _LegalNotes({required this.controller});
final PurchaseController controller;
@override
Widget build(BuildContext context) {
if (isOhos()) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.purchaseNotesTitle,
style: const TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.w600,
height: 1.2,
),
),
const SizedBox(height: 24),
const _LegalText(
prefix: '1. ',
text: '确认支付成功后,会员权益将立即生效。',
),
const SizedBox(height: 12),
const _LegalText(
prefix: '2. ',
text: 'DoubleFeel Pro 会员属于虚拟服务商品,购买后除法律法规另有规定外,通常不支持退款。',
),
const SizedBox(height: 12),
const _LegalText(
prefix: '3. ',
text: '如支付成功后会员未生效,请尝试重新登录账号。',
),
const SizedBox(height: 12),
_LegalText(
prefix: '4. ',
text: '如果有更多其它问题请',
linkText: context.l10n.purchaseLinkContactUs,
linkSuffix: '。',
onLinkTap: controller.openContactUs,
),
],
),
);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.purchaseNotesTitle,
style: const TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.w600,
height: 1.2,
),
),
const SizedBox(height: 24),
_LegalText(
prefix: '1. ',
text: context.l10n.purchaseNoteSubscription,
linkText: context.l10n.purchaseLinkLearnMore,
linkSuffix: '。',
onLinkTap: controller.openRefundExplanation,
),
const SizedBox(height: 12),
/*
_LegalText(
prefix: '3. ',
text: context.l10n.purchaseNoteRestore,
linkText: context.l10n.purchaseRestore,
linkSuffix: '。',
onLinkTap: controller.restorePurchase,
),
const SizedBox(height: 12),
*/
_LegalText(
prefix: '2. ',
text: context.l10n.purchaseNoteContact,
linkText: context.l10n.purchaseLinkContactUs,
linkSuffix: '。',
onLinkTap: controller.openContactUs,
),
],
),
);
}
}
class _LegalText extends StatefulWidget {
const _LegalText({
required this.prefix,
required this.text,
this.linkText,
this.linkSuffix,
this.onLinkTap,
});
final String prefix;
final String text;
final String? linkText;
final String? linkSuffix;
final VoidCallback? onLinkTap;
@override
State<_LegalText> createState() => _LegalTextState();
}
class _LegalTextState extends State<_LegalText> {
late final TapGestureRecognizer _linkRecognizer;
@override
void initState() {
super.initState();
_linkRecognizer = TapGestureRecognizer()..onTap = widget.onLinkTap;
}
@override
void didUpdateWidget(covariant _LegalText oldWidget) {
super.didUpdateWidget(oldWidget);
_linkRecognizer.onTap = widget.onLinkTap;
}
@override
void dispose() {
_linkRecognizer.dispose();
super.dispose();
}
static const _baseStyle = TextStyle(
color: PurchaseColors.subtitle,
fontSize: 12,
height: 1.4,
);
static const _linkStyle = TextStyle(
color: Color(0xFF845EEE),
fontWeight: FontWeight.w500,
decoration: TextDecoration.underline,
decorationColor: Color(0xFF845EEE),
);
TextSpan _buildTextSpan() => TextSpan(
style: _baseStyle,
children: [
TextSpan(text: widget.text),
if (widget.linkText case final linkText?)
TextSpan(
text: linkText,
style: _linkStyle,
recognizer: widget.onLinkTap == null ? null : _linkRecognizer,
),
if (widget.linkSuffix case final linkSuffix?)
TextSpan(text: linkSuffix),
],
);
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.prefix,
style: const TextStyle(
color: PurchaseColors.subtitle,
fontSize: 12,
height: 1.4,
),
),
Expanded(
child: Text.rich(
_buildTextSpan(),
textScaler: MediaQuery.textScalerOf(context),
),
),
],
);
}
}
... ...
... ... @@ -13,6 +13,8 @@ class PurchaseBottomBar extends StatelessWidget {
required this.onPressed,
this.onTermsTap,
this.onPrivacyTap,
this.showAutoRenewalAgreement = false,
this.onAutoRenewalAgreementTap,
});
final String label;
... ... @@ -20,6 +22,8 @@ class PurchaseBottomBar extends StatelessWidget {
final VoidCallback onPressed;
final VoidCallback? onTermsTap;
final VoidCallback? onPrivacyTap;
final bool showAutoRenewalAgreement;
final VoidCallback? onAutoRenewalAgreementTap;
@override
Widget build(BuildContext context) {
... ... @@ -103,6 +107,22 @@ class PurchaseBottomBar extends StatelessWidget {
label: context.l10n.purchasePrivacyPolicy,
onTap: onPrivacyTap,
),
if (showAutoRenewalAgreement) ...[
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text(
'|',
style: TextStyle(
color: PurchaseColors.subtitle,
fontSize: 12,
),
),
),
_BottomLink(
label: '自动续费协议',
onTap: onAutoRenewalAgreementTap,
),
],
],
),
const SizedBox(height: 18),
... ...
import 'package:doublefeel_flutter/app/modules/purchase/widgets/purchase_colors.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
/// OHOS subscription terms shown on the purchase and membership pages.
class OhosPurchaseLegalNotes extends StatelessWidget {
const OhosPurchaseLegalNotes({
super.key,
required this.onContactUsTap,
required this.onSupportEmailTap,
});
final VoidCallback onContactUsTap;
final VoidCallback onSupportEmailTap;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.purchaseNotesTitle,
style: const TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.w600,
height: 1.2,
),
),
const SizedBox(height: 24),
const _LegalText(
prefix: '1. ',
spans: [
_LegalTextSpan(
text: '本服务为自动续费订阅服务,¥68/年或¥16/月,到期后将按所选订阅周期自动续费并扣款。',
),
],
),
const SizedBox(height: 12),
const _LegalText(
prefix: '2. ',
spans: [
_LegalTextSpan(
text:
'如需取消自动续费,请前往支付宝 > 我的 > 设置 > 支付设置 > 免密支付/自动扣款 > 关闭对应服务。取消后,当前会员权益可使用至本周期结束,下个周期将不再扣费。',
),
],
),
const SizedBox(height: 12),
const _LegalText(
prefix: '3. ',
spans: [
_LegalTextSpan(
text: '确认支付成功后,会员权益立即生效。如支付成功后会员未生效,请尝试重新登录账号。',
),
],
),
const SizedBox(height: 12),
const _LegalText(
prefix: '4. ',
spans: [
_LegalTextSpan(
text: 'DoubleFeel Pro 属于虚拟服务商品,购买后除法律法规另有规定外,通常不支持退款。',
),
],
),
const SizedBox(height: 12),
_LegalText(
prefix: '5. ',
spans: const [
_LegalTextSpan(text: '如有其他问题,请'),
_LegalTextSpan(text: '联系我们', isLink: true),
_LegalTextSpan(text: ',或发送邮件至'),
_LegalTextSpan(text: 'support@doublefeel.cn', isLink: true),
_LegalTextSpan(text: '。'),
],
onLinkTaps: [onContactUsTap, onSupportEmailTap],
),
],
),
);
}
}
/// iOS subscription terms shown on the purchase and membership pages.
class IosPurchaseLegalNotes extends StatelessWidget {
const IosPurchaseLegalNotes({
required this.onRefundExplanationTap,
required this.onContactUsTap,
});
final VoidCallback onRefundExplanationTap;
final VoidCallback onContactUsTap;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.purchaseNotesTitle,
style: const TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.w600,
height: 1.2,
),
),
const SizedBox(height: 24),
_LegalText(
prefix: '1. ',
spans: [
_LegalTextSpan(text: context.l10n.purchaseNoteSubscription),
_LegalTextSpan(
text: context.l10n.purchaseLinkLearnMore,
isLink: true,
),
const _LegalTextSpan(text: '。'),
],
onLinkTaps: [onRefundExplanationTap],
),
const SizedBox(height: 12),
_LegalText(
prefix: '2. ',
spans: [
_LegalTextSpan(text: context.l10n.purchaseNoteContact),
_LegalTextSpan(
text: context.l10n.purchaseLinkContactUs,
isLink: true,
),
const _LegalTextSpan(text: '。'),
],
onLinkTaps: [onContactUsTap],
),
],
),
);
}
}
class _LegalTextSpan {
const _LegalTextSpan({required this.text, this.isLink = false});
final String text;
final bool isLink;
}
class _LegalText extends StatefulWidget {
const _LegalText({
required this.prefix,
required this.spans,
this.onLinkTaps = const [],
});
final String prefix;
final List<_LegalTextSpan> spans;
final List<VoidCallback> onLinkTaps;
@override
State<_LegalText> createState() => _LegalTextState();
}
class _LegalTextState extends State<_LegalText> {
late List<TapGestureRecognizer> _linkRecognizers;
@override
void initState() {
super.initState();
_linkRecognizers = _createRecognizers();
}
@override
void didUpdateWidget(covariant _LegalText oldWidget) {
super.didUpdateWidget(oldWidget);
for (var index = 0; index < _linkRecognizers.length; index++) {
_linkRecognizers[index].onTap = widget.onLinkTaps[index];
}
}
List<TapGestureRecognizer> _createRecognizers() => [
for (final onTap in widget.onLinkTaps)
TapGestureRecognizer()..onTap = onTap,
];
@override
void dispose() {
for (final recognizer in _linkRecognizers) {
recognizer.dispose();
}
super.dispose();
}
static const _baseStyle = TextStyle(
color: PurchaseColors.subtitle,
fontSize: 12,
height: 1.4,
);
static const _linkStyle = TextStyle(
color: Color(0xFF845EEE),
fontWeight: FontWeight.w500,
decoration: TextDecoration.underline,
decorationColor: Color(0xFF845EEE),
);
@override
Widget build(BuildContext context) {
var linkIndex = 0;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.prefix, style: _baseStyle),
Expanded(
child: Text.rich(
TextSpan(
style: _baseStyle,
children: [
for (final span in widget.spans)
TextSpan(
text: span.text,
style: span.isLink ? _linkStyle : null,
recognizer:
span.isLink ? _linkRecognizers[linkIndex++] : null,
),
],
),
textScaler: MediaQuery.textScalerOf(context),
),
),
],
);
}
}
... ...
... ... @@ -28,6 +28,8 @@ abstract final class AppConst {
'https://cdn.doublefeel.cn/doublefeel/protocol/DoubleFeel%E7%94%A8%E6%88%B7%E5%8D%8F%E8%AE%AE.html';
static const String privacyPolicy =
'https://cdn.doublefeel.cn/doublefeel/protocol/DoubleFeel%E9%9A%90%E7%A7%81%E5%8D%8F%E8%AE%AE.html';
static const String autoRenewalAgreement =
'http://cdn.doublefeel.cn/doublefeel/protocol/DoubleFeel_HarmonyOS_Auto_Renewal_Agreement.html';
static const String userTermsGlobal =
'https://cdn-oversea.doublefeel.cn/doublefeel/protocol/DoubleFeel_Terms_of_Service.html';
static const String privacyPolicyGlobal =
... ...
... ... @@ -29,8 +29,9 @@ class AppErrorHandler {
_handleAccessTokenInvalid(accessToken);
return;
}
if (businessCode == null ||
!policy.excludeErrorCodeList.contains(businessCode)) {
if (policy.showBusinessMessage &&
(businessCode == null ||
!policy.excludeErrorCodeList.contains(businessCode))) {
if (businessMessage != null) {
_showMessage(businessMessage);
}
... ...
/// Controls global HTTP error side effects (toast, logout, etc.).
/// Controls global HTTP error side effects (business-error toast, logout, etc.).
///
/// Pass an instance to [safeCall] to enable toast / logout behaviour.
/// Pass an instance to [safeCall] to enable logout handling; business-error
/// messages can be suppressed while keeping authentication handling active.
/// Omit (pass null) to handle errors silently at the call site.
class HttpErrorHandlingPolicy {
const HttpErrorHandlingPolicy({this.excludeErrorCodeList = const {}});
const HttpErrorHandlingPolicy({
this.excludeErrorCodeList = const {},
this.showBusinessMessage = true,
});
final Set<int> excludeErrorCodeList;
final bool showBusinessMessage;
/// Default: show errors, no exclusions.
static const HttpErrorHandlingPolicy defaultPolicy =
... ...
... ... @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
import 'package:doublefeel_flutter/data/models/pay/apple_pay_order_response.dart';
import '../../../data/models/pay/pay_models.dart';
import '../../error/http_error_handling_policy.dart';
import '../../result/app_result.dart';
import '../../result/safe_call.dart';
import '../api_paths.dart';
... ... @@ -107,7 +108,11 @@ class PayApi {
);
}
Future<AppResult<void>> cancelSubscription(String agreementNo) {
Future<AppResult<void>> cancelSubscription(
String agreementNo, {
HttpErrorHandlingPolicy? errorHandlingPolicy =
HttpErrorHandlingPolicy.defaultPolicy,
}) {
return safeCall(
call: () async {
await _dioClient.dio.post(
... ... @@ -115,6 +120,7 @@ class PayApi {
data: SubscriptionCancelRequest(agreementNo: agreementNo).toJson(),
);
},
errorHandlingPolicy: errorHandlingPolicy,
);
}
}
... ...
... ... @@ -2,7 +2,7 @@
"app": {
"bundleName": "com.doublefeel.hmapp",
"vendor": "DiDi",
"versionCode": 101,
"versionCode": 103,
"versionName": "2.7.0",
"icon": "$media:layered_image",
"label": "$string:app_name"
... ...
... ... @@ -2,7 +2,7 @@ name: doublefeel_flutter
description: "A new Flutter project."
publish_to: 'none'
version: 2.7.0+101
version: 2.7.0+103
environment:
sdk: ^3.6.2
... ...