Commit 7f4473c5c59e9c7054109a272e0834e03094b789

Authored by 常守达
1 parent ffa69d85

feat(ui): 鸿蒙下单掉用

@@ -33,6 +33,8 @@ class SubmitFeedbackController extends GetxController { @@ -33,6 +33,8 @@ class SubmitFeedbackController extends GetxController {
33 }); 33 });
34 } 34 }
35 35
  36 + static const maxVideoSizeBytes = 200 * 1024 * 1024;
  37 +
36 Future<void> pickImages() async { 38 Future<void> pickImages() async {
37 final remainingCount = maxImageCount - selectedImages.length; 39 final remainingCount = maxImageCount - selectedImages.length;
38 if (remainingCount <= 0) { 40 if (remainingCount <= 0) {
@@ -53,7 +55,22 @@ class SubmitFeedbackController extends GetxController { @@ -53,7 +55,22 @@ class SubmitFeedbackController extends GetxController {
53 55
54 if (images.isEmpty) return; 56 if (images.isEmpty) return;
55 57
56 - selectedImages.addAll(images.take(remainingCount)); 58 + final validImages = <XFile>[];
  59 + var hasOversizedFile = false;
  60 + for (final file in images) {
  61 + final size = await file.length();
  62 + if (size > maxVideoSizeBytes) {
  63 + hasOversizedFile = true;
  64 + continue;
  65 + }
  66 +
  67 + validImages.add(file);
  68 + }
  69 + if (hasOversizedFile) {
  70 + AppToast.show('单个文件大小不能超过 200MB');
  71 + }
  72 +
  73 + selectedImages.addAll(validImages);
57 } 74 }
58 75
59 void removeImage(XFile image) { 76 void removeImage(XFile image) {
@@ -9,6 +9,7 @@ import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data @@ -9,6 +9,7 @@ import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data
9 import 'package:doublefeel_flutter/app/modules/login/views/email_login_contact_us_bottom_sheet.dart'; 9 import 'package:doublefeel_flutter/app/modules/login/views/email_login_contact_us_bottom_sheet.dart';
10 import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart'; 10 import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart';
11 import 'package:doublefeel_flutter/app/routes/app_pages.dart'; 11 import 'package:doublefeel_flutter/app/routes/app_pages.dart';
  12 +import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
12 import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; 13 import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
13 import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; 14 import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
14 import 'package:doublefeel_flutter/core/logging/app_logger.dart'; 15 import 'package:doublefeel_flutter/core/logging/app_logger.dart';
@@ -737,9 +738,11 @@ class TodayController extends GetxController with WidgetsBindingObserver { @@ -737,9 +738,11 @@ class TodayController extends GetxController with WidgetsBindingObserver {
737 final result = await _payApi.getProductList(); 738 final result = await _payApi.getProductList();
738 if (result is! AppSuccess<PayProductListResponse>) return; 739 if (result is! AppSuccess<PayProductListResponse>) return;
739 740
740 - final products = (result.data.productList ?? const <PayProduct>[])  
741 - .where((product) => _nonEmpty(product.appleId) != null)  
742 - .toList(); 741 + final products = isIOS()
  742 + ? (result.data.productList ?? const <PayProduct>[])
  743 + .where((product) => _nonEmpty(product.appleId) != null)
  744 + .toList()
  745 + : (result.data.productList ?? const <PayProduct>[]);
743 yearlyProduct.value = 746 yearlyProduct.value =
744 products.firstWhereOrNull((p) => p.content?.isDiscountOffer == 1); 747 products.firstWhereOrNull((p) => p.content?.isDiscountOffer == 1);
745 // AppLogger.e(yearlyProduct); 748 // AppLogger.e(yearlyProduct);
@@ -774,26 +777,42 @@ class TodayController extends GetxController with WidgetsBindingObserver { @@ -774,26 +777,42 @@ class TodayController extends GetxController with WidgetsBindingObserver {
774 } 777 }
775 778
776 String originDisplayPrice() { 779 String originDisplayPrice() {
777 - final appleInfo = yearlyProductAppleInfo.value;  
778 - if (appleInfo == null) return '';  
779 - final actual =  
780 - appleInfo.price > 0 ? appleInfo.price : appleInfo.originPrice;  
781 - final inflated = actual / 100.0 * 1.2;  
782 - // 保留两位小数,单位与 currencyCode 一致  
783 - return '${appleInfo.currencyCode}${inflated.toStringAsFixed(2)}'; 780 + if (isIOS()) {
  781 + final appleInfo = yearlyProductAppleInfo.value;
  782 + if (appleInfo == null) return '';
  783 + final actual =
  784 + appleInfo.price > 0 ? appleInfo.price : appleInfo.originPrice;
  785 + final inflated = actual / 100.0 * 1.2;
  786 + // 保留两位小数,单位与 currencyCode 一致
  787 + return '${appleInfo.currencyCode}${inflated.toStringAsFixed(2)}';
  788 + } else if (isOhos()) {
  789 + final actual = yearlyProduct.value?.price ?? 0;
  790 + final inflated = actual / 100.0 * 1.2;
  791 + // 保留两位小数,单位与 currencyCode 一致
  792 + return ${inflated.toStringAsFixed(2)}';
  793 + } else {
  794 + return '';
  795 + }
784 } 796 }
785 797
786 String displayPrice() { 798 String displayPrice() {
787 - final appleInfo = yearlyProductAppleInfo.value;  
788 - if (appleInfo == null) return '';  
789 - final price = appleInfo.price;  
790 - if (price > 0) {  
791 - final priceDescription = appleInfo.priceDescription.trim();  
792 - if (priceDescription.isNotEmpty) {  
793 - return priceDescription; 799 + if (isIOS()) {
  800 + final appleInfo = yearlyProductAppleInfo.value;
  801 + if (appleInfo == null) return '';
  802 + final price = appleInfo.price;
  803 + if (price > 0) {
  804 + final priceDescription = appleInfo.priceDescription.trim();
  805 + if (priceDescription.isNotEmpty) {
  806 + return priceDescription;
  807 + }
794 } 808 }
  809 + return appleInfo.originPriceDescription;
  810 + } else if (isOhos()) {
  811 + final actual = (yearlyProduct.value?.price ?? 0) / 100.0;
  812 + return ${actual.toStringAsFixed(2)}';
  813 + } else {
  814 + return '';
795 } 815 }
796 - return appleInfo.originPriceDescription;  
797 } 816 }
798 817
799 @override 818 @override
@@ -9,6 +9,7 @@ import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_mo @@ -9,6 +9,7 @@ import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_mo
9 import 'package:doublefeel_flutter/app/routes/app_pages.dart'; 9 import 'package:doublefeel_flutter/app/routes/app_pages.dart';
10 import 'package:doublefeel_flutter/app/utils/assets_helper.dart'; 10 import 'package:doublefeel_flutter/app/utils/assets_helper.dart';
11 import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; 11 import 'package:doublefeel_flutter/app/utils/dialog_utils.dart';
  12 +import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
12 import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; 13 import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
13 import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; 14 import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
14 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 15 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
@@ -57,12 +58,14 @@ class MyTab extends GetView<MyController> { @@ -57,12 +58,14 @@ class MyTab extends GetView<MyController> {
57 ? _ProCard(vipInfo: vipInfo) 58 ? _ProCard(vipInfo: vipInfo)
58 : _UnlockPremiumCard(controller), 59 : _UnlockPremiumCard(controller),
59 const SizedBox(height: 12), 60 const SizedBox(height: 12),
60 - _WatchThemeCard(  
61 - themes: controller.watchThemeItems,  
62 - isLoading: controller.isLoadingWatchThemes.value,  
63 - onTap: () => controller.goWatchThemePage(),  
64 - ),  
65 - const SizedBox(height: 12), 61 + if (isIOS()) ...[
  62 + _WatchThemeCard(
  63 + themes: controller.watchThemeItems,
  64 + isLoading: controller.isLoadingWatchThemes.value,
  65 + onTap: () => controller.goWatchThemePage(),
  66 + ),
  67 + const SizedBox(height: 12)
  68 + ],
66 _SettingsRow( 69 _SettingsRow(
67 title: context.l10n.accountInformation, 70 title: context.l10n.accountInformation,
68 onTap: () => Get.to( 71 onTap: () => Get.to(
@@ -10,7 +10,8 @@ import 'today_faq_bottom_sheet.dart'; @@ -10,7 +10,8 @@ import 'today_faq_bottom_sheet.dart';
10 void showHrvPrincipleExplanationBottomSheet(BuildContext context) { 10 void showHrvPrincipleExplanationBottomSheet(BuildContext context) {
11 final screenHeight = MediaQuery.of(context).size.height; 11 final screenHeight = MediaQuery.of(context).size.height;
12 final statusBarHeight = MediaQuery.of(context).padding.top; 12 final statusBarHeight = MediaQuery.of(context).padding.top;
13 - final maxChildSize = (screenHeight - statusBarHeight) / screenHeight; 13 + final maxChildSize =
  14 + isOhos() ? 0.8 : (screenHeight - statusBarHeight) / screenHeight;
14 Get.bottomSheet( 15 Get.bottomSheet(
15 DraggableScrollableSheet( 16 DraggableScrollableSheet(
16 maxChildSize: maxChildSize, 17 maxChildSize: maxChildSize,
1 import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart'; 1 import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart';
  2 +import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
2 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 3 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
3 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 4 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
4 import 'package:flutter/material.dart'; 5 import 'package:flutter/material.dart';
@@ -95,9 +96,11 @@ class PremiumCard extends StatelessWidget { @@ -95,9 +96,11 @@ class PremiumCard extends StatelessWidget {
95 ], 96 ],
96 ), 97 ),
97 const Spacer(), 98 const Spacer(),
98 - Obx(() => controller.yearlyProductAppleInfo.value !=  
99 - null &&  
100 - controller.yearlyProduct.value != null 99 + Obx(() => (isIOS()
  100 + ? controller.yearlyProductAppleInfo.value !=
  101 + null &&
  102 + controller.yearlyProduct.value != null
  103 + : controller.yearlyProduct.value != null)
101 ? Row( 104 ? Row(
102 children: [ 105 children: [
103 Text( 106 Text(
@@ -110,7 +113,8 @@ class PremiumCard extends StatelessWidget { @@ -110,7 +113,8 @@ class PremiumCard extends StatelessWidget {
110 ), 113 ),
111 const SizedBox(width: 6), 114 const SizedBox(width: 6),
112 Text( 115 Text(
113 - controller.displayPrice(), 116 + l10n.originalPrice(
  117 + controller.displayPrice()),
114 style: TextStyle( 118 style: TextStyle(
115 fontSize: 12, 119 fontSize: 12,
116 fontWeight: FontWeight.w500, 120 fontWeight: FontWeight.w500,
1 import 'dart:async'; 1 import 'dart:async';
2 import 'package:doublefeel_flutter/app/routes/app_pages.dart'; 2 import 'package:doublefeel_flutter/app/routes/app_pages.dart';
3 import 'package:doublefeel_flutter/app/utils/assets_helper.dart'; 3 import 'package:doublefeel_flutter/app/utils/assets_helper.dart';
  4 +import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
4 import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; 5 import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
5 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 6 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
6 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 7 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
@@ -287,7 +288,7 @@ class TodayAdCarousel extends StatelessWidget { @@ -287,7 +288,7 @@ class TodayAdCarousel extends StatelessWidget {
287 ), 288 ),
288 ); 289 );
289 } 290 }
290 - if (showHrv) { 291 + if (isIOS() && showHrv) {
291 activeBanners.add( 292 activeBanners.add(
292 TodayHrvAdBanner( 293 TodayHrvAdBanner(
293 controller: controller, 294 controller: controller,
@@ -30,25 +30,25 @@ class MembershipDetailController extends GetxController { @@ -30,25 +30,25 @@ class MembershipDetailController extends GetxController {
30 30
31 final isUnlocking = false.obs; 31 final isUnlocking = false.obs;
32 Future<void> unlock() async { 32 Future<void> unlock() async {
33 - isUnlocking.value = true;  
34 - try {  
35 - final orderResult = await _payApi.createOrderByApple(123);  
36 - if (orderResult is! AppSuccess<ApplePayOrderResponse>) return;  
37 -  
38 - final orderUuid = orderResult.data.orderInfo?.orderUuid?.trim();  
39 - if (orderUuid == null || orderUuid.isEmpty) {  
40 - AppToast.show(l10n.purchaseOrderInfoUnavailable);  
41 - return;  
42 - }  
43 -  
44 - final paymentResult = await _performApplePayment(  
45 - productId: 'appleProductId',  
46 - orderUuid: orderUuid,  
47 - );  
48 - await _handleApplePaymentResult(paymentResult);  
49 - } finally {  
50 - isUnlocking.value = false;  
51 - } 33 + // isUnlocking.value = true;
  34 + // try {
  35 + // final orderResult = await _payApi.createOrderByApple(123);
  36 + // if (orderResult is! AppSuccess<ApplePayOrderResponse>) return;
  37 +
  38 + // final orderUuid = orderResult.data.orderInfo?.orderUuid?.trim();
  39 + // if (orderUuid == null || orderUuid.isEmpty) {
  40 + // AppToast.show(l10n.purchaseOrderInfoUnavailable);
  41 + // return;
  42 + // }
  43 +
  44 + // final paymentResult = await _performApplePayment(
  45 + // productId: 'appleProductId',
  46 + // orderUuid: orderUuid,
  47 + // );
  48 + // await _handleApplePaymentResult(paymentResult);
  49 + // } finally {
  50 + // isUnlocking.value = false;
  51 + // }
52 } 52 }
53 53
54 Future<void> _handleApplePaymentResult( 54 Future<void> _handleApplePaymentResult(
  1 +import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
1 import 'package:doublefeel_flutter/core/network/api/pay_api.dart'; 2 import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
2 import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; 3 import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
3 import 'package:doublefeel_flutter/core/result/app_result.dart'; 4 import 'package:doublefeel_flutter/core/result/app_result.dart';
@@ -22,41 +23,84 @@ class MembershipOfferController extends GetxController { @@ -22,41 +23,84 @@ class MembershipOfferController extends GetxController {
22 final UserPreferencesStorage _userPreferences = 23 final UserPreferencesStorage _userPreferences =
23 Get.find<UserPreferencesStorage>(); 24 Get.find<UserPreferencesStorage>();
24 final AppPlatformHostApi _platformHostApi = AppPlatformHostApi(); 25 final AppPlatformHostApi _platformHostApi = AppPlatformHostApi();
  26 + final AlipayHostApi _alipayHostApi = AlipayHostApi();
25 27
26 final isUnlocking = false.obs; 28 final isUnlocking = false.obs;
27 Future<void> unlock() async { 29 Future<void> unlock() async {
28 if (isUnlocking.value) return; 30 if (isUnlocking.value) return;
29 31
30 final productId = yearlyProduct.value?.id; 32 final productId = yearlyProduct.value?.id;
31 - final appleProductId = yearlyProduct.value?.appleId;  
32 - if (productId == null || appleProductId == null || appleProductId.isEmpty) {  
33 - AppToast.show(l10n.purchaseProductInfoUnavailable);  
34 - return;  
35 - }  
36 - isUnlocking.value = true; 33 +
37 try { 34 try {
38 - final orderResult = await _payApi.createOrderByApple(productId);  
39 - if (orderResult is! AppSuccess<ApplePayOrderResponse>) {  
40 - if (isFromOnboard) {  
41 - Get.offAllNamed(AppRoutes.home, arguments: {'isFromOnboard': true}); 35 + if (isOhos()) {
  36 + if (productId == null) {
  37 + AppToast.show(l10n.purchaseProductInfoUnavailable);
  38 + return;
  39 + }
  40 + isUnlocking.value = true;
  41 + final orderResult = await _payApi.createCustomOrder(
  42 + productId: productId,
  43 + paymentChannel: PaymentChannel.alipay.value,
  44 + productChannel: ProductChannel.huawei.value,
  45 + );
  46 + if (orderResult is! AppSuccess<ApplePayOrderResponse>) {
  47 + if (isFromOnboard) {
  48 + Get.offAllNamed(AppRoutes.home, arguments: {'isFromOnboard': true});
  49 + }
  50 + return;
42 } 51 }
43 - return;  
44 - }  
45 52
46 - final orderUuid = orderResult.data.orderInfo?.orderUuid?.trim();  
47 - if (orderUuid == null || orderUuid.isEmpty) {  
48 - AppToast.show(l10n.purchaseOrderInfoUnavailable);  
49 - if (isFromOnboard) {  
50 - Get.offAllNamed(AppRoutes.home, arguments: {'isFromOnboard': true}); 53 + final prepayData = orderResult.data.prepayData?.trim();
  54 + if (prepayData == null || prepayData.isEmpty) {
  55 + AppToast.show(l10n.purchaseOrderInfoUnavailable);
  56 + if (isFromOnboard) {
  57 + Get.offAllNamed(AppRoutes.home, arguments: {'isFromOnboard': true});
  58 + }
  59 + return;
51 } 60 }
52 - return;  
53 - }  
54 61
55 - final paymentResult = await _performApplePayment(  
56 - productId: appleProductId,  
57 - orderUuid: orderUuid,  
58 - );  
59 - await _handleApplePaymentResult(paymentResult); 62 + //todo 鸿蒙Alipay prepayData
  63 + final result = await _alipayHostApi.launchAliPay(prepayData);
  64 + if (result == AliPayResultCode.success) {
  65 + _trackSuccessPayOrder();
  66 + await _completeSuccessfulPurchase();
  67 + } else if (result == AliPayResultCode.error) {
  68 + if (isFromOnboard) {
  69 + Get.offAllNamed(AppRoutes.home, arguments: {'isFromOnboard': true});
  70 + }
  71 + }
  72 + } else if (isIOS()) {
  73 + final appleProductId = yearlyProduct.value?.appleId;
  74 + if (productId == null ||
  75 + appleProductId == null ||
  76 + appleProductId.isEmpty) {
  77 + AppToast.show(l10n.purchaseProductInfoUnavailable);
  78 + return;
  79 + }
  80 + isUnlocking.value = true;
  81 + final orderResult = await _payApi.createOrderByApple(productId);
  82 + if (orderResult is! AppSuccess<ApplePayOrderResponse>) {
  83 + if (isFromOnboard) {
  84 + Get.offAllNamed(AppRoutes.home, arguments: {'isFromOnboard': true});
  85 + }
  86 + return;
  87 + }
  88 +
  89 + final orderUuid = orderResult.data.orderInfo?.orderUuid?.trim();
  90 + if (orderUuid == null || orderUuid.isEmpty) {
  91 + AppToast.show(l10n.purchaseOrderInfoUnavailable);
  92 + if (isFromOnboard) {
  93 + Get.offAllNamed(AppRoutes.home, arguments: {'isFromOnboard': true});
  94 + }
  95 + return;
  96 + }
  97 +
  98 + final paymentResult = await _performApplePayment(
  99 + productId: appleProductId,
  100 + orderUuid: orderUuid,
  101 + );
  102 + await _handleApplePaymentResult(paymentResult);
  103 + }
60 } finally { 104 } finally {
61 isUnlocking.value = false; 105 isUnlocking.value = false;
62 } 106 }
@@ -87,13 +131,16 @@ class MembershipOfferController extends GetxController { @@ -87,13 +131,16 @@ class MembershipOfferController extends GetxController {
87 } 131 }
88 trackMap['product_type'] = yearlyProduct.value?.appleId; 132 trackMap['product_type'] = yearlyProduct.value?.appleId;
89 trackMap['price'] = yearlyProduct.value?.price; 133 trackMap['price'] = yearlyProduct.value?.price;
90 -  
91 - final appleInfo = yearlyProductAppleInfo.value;  
92 - trackMap['currency_code'] = appleInfo?.currencyCode ?? '';  
93 -  
94 - var actualPrice = appleInfo?.price ?? 0;  
95 - trackMap['actual_price'] =  
96 - actualPrice > 0 ? actualPrice : (appleInfo?.originPrice ?? 0); 134 + if (isOhos()) {
  135 + trackMap['currency_code'] = 'CN';
  136 + trackMap['actual_price'] = yearlyProduct.value?.price;
  137 + } else if (isIOS()) {
  138 + final appleInfo = yearlyProductAppleInfo.value;
  139 + trackMap['currency_code'] = appleInfo?.currencyCode ?? '';
  140 + var actualPrice = appleInfo?.price ?? 0;
  141 + trackMap['actual_price'] =
  142 + actualPrice > 0 ? actualPrice : (appleInfo?.originPrice ?? 0);
  143 + }
97 144
98 ta.track( 145 ta.track(
99 'success_doublefeel_pay_order', 146 'success_doublefeel_pay_order',
@@ -192,19 +239,24 @@ class MembershipOfferController extends GetxController { @@ -192,19 +239,24 @@ class MembershipOfferController extends GetxController {
192 final result = await _payApi.getProductList(); 239 final result = await _payApi.getProductList();
193 if (result is! AppSuccess<PayProductListResponse>) return; 240 if (result is! AppSuccess<PayProductListResponse>) return;
194 241
195 - final products = (result.data.productList ?? const <PayProduct>[])  
196 - .where((product) => _nonEmpty(product.appleId) != null)  
197 - .toList();  
198 - yearlyProduct.value =  
199 - products.firstWhereOrNull((p) => p.content?.isDiscountOffer == 1);  
200 -  
201 - if (yearlyProduct.value != null) {  
202 - final appleProductId = _nonEmpty(yearlyProduct.value!.appleId);  
203 - if (appleProductId == null) return;  
204 -  
205 - yearlyProductAppleInfo.value = await _requestAppleProductInfo(  
206 - productId: appleProductId,  
207 - baseUnit: yearlyProduct.value!.content?.baseUnit() ?? 1); 242 + if (isIOS()) {
  243 + final products = (result.data.productList ?? const <PayProduct>[])
  244 + .where((product) => _nonEmpty(product.appleId) != null)
  245 + .toList();
  246 + yearlyProduct.value =
  247 + products.firstWhereOrNull((p) => p.content?.isDiscountOffer == 1);
  248 + if (yearlyProduct.value != null) {
  249 + final appleProductId = _nonEmpty(yearlyProduct.value!.appleId);
  250 + if (appleProductId == null) return;
  251 +
  252 + yearlyProductAppleInfo.value = await _requestAppleProductInfo(
  253 + productId: appleProductId,
  254 + baseUnit: yearlyProduct.value!.content?.baseUnit() ?? 1);
  255 + }
  256 + } else if (isOhos()) {
  257 + final products = (result.data.productList ?? const <PayProduct>[]);
  258 + yearlyProduct.value =
  259 + products.firstWhereOrNull((p) => p.content?.isDiscountOffer == 1);
208 } 260 }
209 } 261 }
210 262
1 import 'package:doublefeel_flutter/app/modules/membership_offer/controllers/membership_offer_controller.dart'; 1 import 'package:doublefeel_flutter/app/modules/membership_offer/controllers/membership_offer_controller.dart';
2 import 'package:doublefeel_flutter/app/routes/app_pages.dart'; 2 import 'package:doublefeel_flutter/app/routes/app_pages.dart';
  3 +import 'package:doublefeel_flutter/app/utils/platform_compact.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/data/models/pay/pay_models.dart'; 5 import 'package:doublefeel_flutter/data/models/pay/pay_models.dart';
5 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 6 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
@@ -78,51 +79,97 @@ class MembershipOfferView extends GetView<MembershipOfferController> { @@ -78,51 +79,97 @@ class MembershipOfferView extends GetView<MembershipOfferController> {
78 'assets/images/user_onboarding/ic_membership_box.png'), 79 'assets/images/user_onboarding/ic_membership_box.png'),
79 const SizedBox(height: 4), 80 const SizedBox(height: 4),
80 Obx(() { 81 Obx(() {
81 - final yearInfo =  
82 - controller.yearlyProductAppleInfo.value;  
83 final yearProduct = controller.yearlyProduct.value; 82 final yearProduct = controller.yearlyProduct.value;
84 -  
85 - return yearInfo == null || yearProduct == null  
86 - ? const SizedBox(  
87 - child: CircularProgressIndicator())  
88 - : Column(  
89 - children: [  
90 - Text(  
91 - context.l10n.originalPricePerYear(  
92 - _originDisplayPrice(yearInfo)),  
93 - textAlign: TextAlign.center,  
94 - style: const TextStyle(  
95 - color: Color(0xFF78787D),  
96 - fontSize: 16,  
97 - decoration:  
98 - TextDecoration.lineThrough,  
99 - decorationColor: Color(0xFF78787D),  
100 - letterSpacing: 0, 83 + if (isOhos()) {
  84 + return (yearProduct == null)
  85 + ? const SizedBox(
  86 + child: CircularProgressIndicator())
  87 + : Column(
  88 + children: [
  89 + Text(
  90 + context.l10n.originalPricePerYear(
  91 + _originDisplayPriceOhos(
  92 + yearProduct)),
  93 + textAlign: TextAlign.center,
  94 + style: const TextStyle(
  95 + color: Color(0xFF78787D),
  96 + fontSize: 16,
  97 + decoration:
  98 + TextDecoration.lineThrough,
  99 + decorationColor: Color(0xFF78787D),
  100 + letterSpacing: 0,
  101 + ),
  102 + ),
  103 + const SizedBox(height: 6),
  104 + Text(
  105 + _displayPriceOhos(yearProduct),
  106 + textAlign: TextAlign.center,
  107 + style: TextStyle(
  108 + color: context.colors.primary,
  109 + fontSize: 28,
  110 + fontWeight: FontWeight.w600,
  111 + letterSpacing: 0,
  112 + ),
  113 + ),
  114 + const SizedBox(height: 6),
  115 + Text(
  116 + _planSubtitleOhos(yearProduct),
  117 + textAlign: TextAlign.center,
  118 + style: const TextStyle(
  119 + color: Color(0xFF78787D),
  120 + fontSize: 12,
  121 + letterSpacing: 0,
  122 + ),
101 ), 123 ),
102 - ),  
103 - const SizedBox(height: 6),  
104 - Text(  
105 - _displayPrice(yearInfo),  
106 - textAlign: TextAlign.center,  
107 - style: TextStyle(  
108 - color: context.colors.primary,  
109 - fontSize: 28,  
110 - fontWeight: FontWeight.w600,  
111 - letterSpacing: 0, 124 + ],
  125 + );
  126 + } else if (isIOS()) {
  127 + final yearInfo =
  128 + controller.yearlyProductAppleInfo.value;
  129 + return (yearInfo == null || yearProduct == null)
  130 + ? const SizedBox(
  131 + child: CircularProgressIndicator())
  132 + : Column(
  133 + children: [
  134 + Text(
  135 + context.l10n.originalPricePerYear(
  136 + _originDisplayPrice(yearInfo)),
  137 + textAlign: TextAlign.center,
  138 + style: const TextStyle(
  139 + color: Color(0xFF78787D),
  140 + fontSize: 16,
  141 + decoration:
  142 + TextDecoration.lineThrough,
  143 + decorationColor: Color(0xFF78787D),
  144 + letterSpacing: 0,
  145 + ),
112 ), 146 ),
113 - ),  
114 - const SizedBox(height: 6),  
115 - Text(  
116 - _planSubtitle(yearProduct, yearInfo),  
117 - textAlign: TextAlign.center,  
118 - style: const TextStyle(  
119 - color: Color(0xFF78787D),  
120 - fontSize: 12,  
121 - letterSpacing: 0, 147 + const SizedBox(height: 6),
  148 + Text(
  149 + _displayPrice(yearInfo),
  150 + textAlign: TextAlign.center,
  151 + style: TextStyle(
  152 + color: context.colors.primary,
  153 + fontSize: 28,
  154 + fontWeight: FontWeight.w600,
  155 + letterSpacing: 0,
  156 + ),
122 ), 157 ),
123 - ),  
124 - ],  
125 - ); 158 + const SizedBox(height: 6),
  159 + Text(
  160 + _planSubtitle(yearProduct, yearInfo),
  161 + textAlign: TextAlign.center,
  162 + style: const TextStyle(
  163 + color: Color(0xFF78787D),
  164 + fontSize: 12,
  165 + letterSpacing: 0,
  166 + ),
  167 + ),
  168 + ],
  169 + );
  170 + } else {
  171 + return SizedBox();
  172 + }
126 }), 173 }),
127 ], 174 ],
128 ), 175 ),
@@ -186,6 +233,22 @@ class MembershipOfferView extends GetView<MembershipOfferController> { @@ -186,6 +233,22 @@ class MembershipOfferView extends GetView<MembershipOfferController> {
186 return appleInfo.originPriceDescription; 233 return appleInfo.originPriceDescription;
187 } 234 }
188 235
  236 + String _originDisplayPriceOhos(PayProduct product) {
  237 + final inflated = (product.price ?? 0) / 100.0 * 1.2;
  238 + return ${inflated.toStringAsFixed(2)}';
  239 + }
  240 +
  241 + String _displayPriceOhos(PayProduct product) {
  242 + final actual = (product.price ?? 0) / 100.0;
  243 + return ${actual.toStringAsFixed(2)}';
  244 + }
  245 +
  246 + String _planSubtitleOhos(PayProduct product) {
  247 + final monthly = (product.price ?? 0) / 100.0 / 12;
  248 +
  249 + return l10n.purchaseMonthlyUnitPrice(monthly.toStringAsFixed(2));
  250 + }
  251 +
189 String? _nonEmpty(String? value) { 252 String? _nonEmpty(String? value) {
190 final trimmed = value?.trim(); 253 final trimmed = value?.trim();
191 if (trimmed == null || trimmed.isEmpty) return null; 254 if (trimmed == null || trimmed.isEmpty) return null;
  1 +import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
1 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';
2 3
3 import '../../result/app_result.dart'; 4 import '../../result/app_result.dart';
@@ -17,8 +18,12 @@ class PayApi { @@ -17,8 +18,12 @@ class PayApi {
17 final response = await _dioClient.dio.get( 18 final response = await _dioClient.dio.get(
18 ApiPaths.paymentProducts, 19 ApiPaths.paymentProducts,
19 queryParameters: { 20 queryParameters: {
20 - 'product_type': productType ?? 1,  
21 - 'product_channel': 2, 21 + 'product_type': productType ?? ProductType.vip.value,
  22 + 'product_channel': isOhos()
  23 + ? ProductChannel.huawei.value
  24 + : (isIOS()
  25 + ? ProductChannel.apple.value
  26 + : ProductChannel.self.value),
22 }, 27 },
23 ); 28 );
24 return PayProductListResponse.fromJson( 29 return PayProductListResponse.fromJson(
@@ -35,8 +40,8 @@ class PayApi { @@ -35,8 +40,8 @@ class PayApi {
35 ApiPaths.paymentOrder, 40 ApiPaths.paymentOrder,
36 data: CreatePayOrderRequest( 41 data: CreatePayOrderRequest(
37 productId: productId, 42 productId: productId,
38 - productChannel: 2,  
39 - paymentChannel: 3, 43 + productChannel: ProductChannel.apple.value,
  44 + paymentChannel: PaymentChannel.apple.value,
40 ).toJson(), 45 ).toJson(),
41 ); 46 );
42 return ApplePayOrderResponse.fromJson( 47 return ApplePayOrderResponse.fromJson(
@@ -66,6 +71,28 @@ class PayApi { @@ -66,6 +71,28 @@ class PayApi {
66 ); 71 );
67 } 72 }
68 73
  74 + Future<AppResult<ApplePayOrderResponse>> createCustomOrder({
  75 + required int productId,
  76 + required int paymentChannel,
  77 + required int productChannel,
  78 + }) {
  79 + return safeCall(
  80 + call: () async {
  81 + final response = await _dioClient.dio.post(
  82 + ApiPaths.paymentOrder,
  83 + data: CreatePayOrderRequest(
  84 + productId: productId,
  85 + paymentChannel: paymentChannel,
  86 + productChannel: productChannel,
  87 + ).toJson(),
  88 + );
  89 + return ApplePayOrderResponse.fromJson(
  90 + response.data as Map<String, dynamic>,
  91 + );
  92 + },
  93 + );
  94 + }
  95 +
69 Future<AppResult<SubscriptionProductListResponse>> getSubscriptionList() { 96 Future<AppResult<SubscriptionProductListResponse>> getSubscriptionList() {
70 return safeCall( 97 return safeCall(
71 call: () async { 98 call: () async {
@@ -89,3 +116,44 @@ class PayApi { @@ -89,3 +116,44 @@ class PayApi {
89 ); 116 );
90 } 117 }
91 } 118 }
  119 +
  120 +enum ProductType {
  121 + /// 会员 (1)
  122 + vip(1),
  123 +
  124 + /// 订阅会员 (10)
  125 + subscriptionVip(10);
  126 +
  127 + const ProductType(this.value);
  128 + final int value;
  129 +}
  130 +
  131 +/// 商品渠道
  132 +enum ProductChannel {
  133 + /// 自营 (1)
  134 + self(1),
  135 +
  136 + /// 苹果 (2)
  137 + apple(2),
  138 +
  139 + /// 华为 (3)
  140 + huawei(3);
  141 +
  142 + const ProductChannel(this.value);
  143 + final int value;
  144 +}
  145 +
  146 +/// 支付渠道
  147 +enum PaymentChannel {
  148 + /// 微信 (1)
  149 + wechat(1),
  150 +
  151 + /// 支付宝 (2)
  152 + alipay(2),
  153 +
  154 + /// 苹果 (3)
  155 + apple(3);
  156 +
  157 + const PaymentChannel(this.value);
  158 + final int value;
  159 +}
1 class ApplePayOrderResponse { 1 class ApplePayOrderResponse {
2 - const ApplePayOrderResponse({this.orderInfo}); 2 + const ApplePayOrderResponse({this.orderInfo, this.prepayData});
3 3
4 final ApplePayOrderInfo? orderInfo; 4 final ApplePayOrderInfo? orderInfo;
  5 + final String? prepayData;
5 6
6 factory ApplePayOrderResponse.fromJson(Map<String, dynamic> json) { 7 factory ApplePayOrderResponse.fromJson(Map<String, dynamic> json) {
7 return ApplePayOrderResponse( 8 return ApplePayOrderResponse(
  9 + prepayData: json['prepay_data'] as String?,
8 orderInfo: json['order_info'] == null 10 orderInfo: json['order_info'] == null
9 ? null 11 ? null
10 : ApplePayOrderInfo.fromJson( 12 : ApplePayOrderInfo.fromJson(
@@ -15,6 +17,7 @@ class ApplePayOrderResponse { @@ -15,6 +17,7 @@ class ApplePayOrderResponse {
15 17
16 Map<String, dynamic> toJson() { 18 Map<String, dynamic> toJson() {
17 final val = <String, dynamic>{}; 19 final val = <String, dynamic>{};
  20 + if (prepayData != null) val['prepay_data'] = prepayData;
18 if (orderInfo != null) val['order_info'] = orderInfo!.toJson(); 21 if (orderInfo != null) val['order_info'] = orderInfo!.toJson();
19 return val; 22 return val;
20 } 23 }
@@ -1188,7 +1188,7 @@ @@ -1188,7 +1188,7 @@
1188 "annualMemberDiscounts": "年度会员优惠", 1188 "annualMemberDiscounts": "年度会员优惠",
1189 "specialOffers": "优惠", 1189 "specialOffers": "优惠",
1190 "currentPrice": "现价", 1190 "currentPrice": "现价",
1191 - "originalPrice": "原价{price}", 1191 + "originalPrice": "原价{price}/年",
1192 "@originalPrice": { 1192 "@originalPrice": {
1193 "description": "原价标签", 1193 "description": "原价标签",
1194 "placeholders": { 1194 "placeholders": {
@@ -667,7 +667,7 @@ @@ -667,7 +667,7 @@
667 "annualMemberDiscounts": "年度会员优惠", 667 "annualMemberDiscounts": "年度会员优惠",
668 "specialOffers": "优惠", 668 "specialOffers": "优惠",
669 "currentPrice": "现价", 669 "currentPrice": "现价",
670 - "originalPrice": "原价{price}", 670 + "originalPrice": "原价{price}/年",
671 "freeRedemptionOffer": "免费兑换优惠", 671 "freeRedemptionOffer": "免费兑换优惠",
672 "cellPhoneNumber": "手机号", 672 "cellPhoneNumber": "手机号",
673 "todayOnWeeklyCalendar": "今", 673 "todayOnWeeklyCalendar": "今",
@@ -667,7 +667,7 @@ @@ -667,7 +667,7 @@
667 "annualMemberDiscounts": "年度會員優惠", 667 "annualMemberDiscounts": "年度會員優惠",
668 "specialOffers": "優惠", 668 "specialOffers": "優惠",
669 "currentPrice": "現價", 669 "currentPrice": "現價",
670 - "originalPrice": "原價{price}", 670 + "originalPrice": "原價{price}/年",
671 "freeRedemptionOffer": "免費兌換優惠", 671 "freeRedemptionOffer": "免費兌換優惠",
672 "cellPhoneNumber": "手機號", 672 "cellPhoneNumber": "手機號",
673 "todayOnWeeklyCalendar": "今", 673 "todayOnWeeklyCalendar": "今",
@@ -4132,7 +4132,7 @@ abstract class AppLocalizations { @@ -4132,7 +4132,7 @@ abstract class AppLocalizations {
4132 /// 原价标签 4132 /// 原价标签
4133 /// 4133 ///
4134 /// In zh, this message translates to: 4134 /// In zh, this message translates to:
4135 - /// **'原价{price}'** 4135 + /// **'原价{price}/年'**
4136 String originalPrice(String price); 4136 String originalPrice(String price);
4137 4137
4138 /// No description provided for @freeRedemptionOffer. 4138 /// No description provided for @freeRedemptionOffer.
@@ -2205,7 +2205,7 @@ class AppLocalizationsZh extends AppLocalizations { @@ -2205,7 +2205,7 @@ class AppLocalizationsZh extends AppLocalizations {
2205 2205
2206 @override 2206 @override
2207 String originalPrice(String price) { 2207 String originalPrice(String price) {
2208 - return '原价$price'; 2208 + return '原价$price/年';
2209 } 2209 }
2210 2210
2211 @override 2211 @override
@@ -5007,7 +5007,7 @@ class AppLocalizationsZhHans extends AppLocalizationsZh { @@ -5007,7 +5007,7 @@ class AppLocalizationsZhHans extends AppLocalizationsZh {
5007 5007
5008 @override 5008 @override
5009 String originalPrice(String price) { 5009 String originalPrice(String price) {
5010 - return '原价$price'; 5010 + return '原价$price/年';
5011 } 5011 }
5012 5012
5013 @override 5013 @override
@@ -7809,7 +7809,7 @@ class AppLocalizationsZhHant extends AppLocalizationsZh { @@ -7809,7 +7809,7 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
7809 7809
7810 @override 7810 @override
7811 String originalPrice(String price) { 7811 String originalPrice(String price) {
7812 - return '原價$price'; 7812 + return '原價$price/年';
7813 } 7813 }
7814 7814
7815 @override 7815 @override