签到接口
This commit is contained in:
parent
5b607800be
commit
5b0b5b862e
@ -34,7 +34,6 @@ import 'services/auth/authentication_manager.dart';
|
||||
import 'services/gift/gift_animation_manager.dart';
|
||||
import 'services/gift/gift_system_manager.dart';
|
||||
import 'services/payment/google_payment_manager.dart';
|
||||
import 'services/payment/mifa_pay_manager.dart';
|
||||
import 'services/localization/localization_manager.dart';
|
||||
import 'services/room/rc_room_manager.dart';
|
||||
import 'services/audio/rtc_manager.dart';
|
||||
@ -262,10 +261,6 @@ class RootAppWithProviders extends StatelessWidget {
|
||||
lazy: true,
|
||||
create: (context) => IOSPaymentProcessor(),
|
||||
),
|
||||
ChangeNotifierProvider<MiFaPayManager>(
|
||||
lazy: true,
|
||||
create: (context) => MiFaPayManager(),
|
||||
),
|
||||
ChangeNotifierProvider<ShopManager>(
|
||||
lazy: true,
|
||||
create: (context) => ShopManager(),
|
||||
|
||||
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_sign_in_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||
import 'package:yumi/modules/home/index_home_page.dart';
|
||||
import 'package:yumi/modules/chat/message/sc_message_page.dart';
|
||||
@ -318,6 +317,7 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
return;
|
||||
}
|
||||
_hasShownEntryDialogs = true;
|
||||
debugPrint('[SignInReward][Home] start entry dialog flow');
|
||||
|
||||
await _waitForRegisterRewardSocketIfNeeded();
|
||||
if (!mounted) {
|
||||
@ -330,9 +330,18 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
}
|
||||
|
||||
final dialogData = await _loadDailySignInDialogData();
|
||||
if (!mounted) {
|
||||
if (!mounted || dialogData == null) {
|
||||
debugPrint(
|
||||
'[SignInReward][Home] skip daily sign-in dialog reason=no-data',
|
||||
);
|
||||
return;
|
||||
}
|
||||
debugPrint(
|
||||
'[SignInReward][Home] show daily sign-in dialog '
|
||||
'checkedToday=${dialogData.checkedToday} '
|
||||
'currentDay=${dialogData.currentItem?.day ?? 0} '
|
||||
'items=${dialogData.items.length}',
|
||||
);
|
||||
_isShowingDailySignInDialog = true;
|
||||
await DailySignInDialog.show(context, data: dialogData);
|
||||
_isShowingDailySignInDialog = false;
|
||||
@ -401,23 +410,33 @@ class _SCIndexPageState extends State<SCIndexPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<DailySignInDialogData> _loadDailySignInDialogData() async {
|
||||
DailySignInDialogData dialogData;
|
||||
Future<DailySignInDialogData?> _loadDailySignInDialogData() async {
|
||||
try {
|
||||
final result = await Future.wait<dynamic>([
|
||||
SCAccountRepository().checkInToday(),
|
||||
SCAccountRepository().sginListAward(),
|
||||
]);
|
||||
final checkedToday = result[0] as bool;
|
||||
final signInRes = result[1] as SCSignInRes;
|
||||
dialogData = DailySignInDialogData.fromLegacy(
|
||||
signInRes: signInRes,
|
||||
checkedToday: checkedToday,
|
||||
debugPrint('[SignInReward][Home] loading status');
|
||||
final statusRes = await SCAccountRepository().signInRewardStatus();
|
||||
if (!statusRes.canShowDialog) {
|
||||
debugPrint(
|
||||
'[SignInReward][Home] dialog disabled '
|
||||
'configured=${statusRes.configured} '
|
||||
'enabled=${statusRes.enabled} '
|
||||
'available=${statusRes.available} '
|
||||
'signedToday=${statusRes.signedToday} '
|
||||
'days=${statusRes.days.length}',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
final dialogData = DailySignInDialogData.fromStatus(statusRes: statusRes);
|
||||
debugPrint(
|
||||
'[SignInReward][Home] mapped dialog data '
|
||||
'checkedToday=${dialogData.checkedToday} '
|
||||
'currentDay=${dialogData.currentItem?.day ?? 0} '
|
||||
'days=${dialogData.items.length}',
|
||||
);
|
||||
} catch (_) {
|
||||
dialogData = DailySignInDialogData.mock();
|
||||
return dialogData;
|
||||
} catch (error) {
|
||||
debugPrint('[SignInReward][Home] load status failed error=$error');
|
||||
return null;
|
||||
}
|
||||
return dialogData;
|
||||
}
|
||||
|
||||
Widget _buildBottomTabIcon({
|
||||
|
||||
@ -1,120 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/shared/tools/sc_url_launcher_utils.dart';
|
||||
import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class MiFaPayWebViewPage extends StatefulWidget {
|
||||
const MiFaPayWebViewPage({
|
||||
super.key,
|
||||
required this.requestUrl,
|
||||
this.title = 'MiFaPay',
|
||||
});
|
||||
|
||||
final String requestUrl;
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MiFaPayWebViewPage> createState() => _MiFaPayWebViewPageState();
|
||||
}
|
||||
|
||||
class _MiFaPayWebViewPageState extends State<MiFaPayWebViewPage> {
|
||||
late final WebViewController _controller;
|
||||
|
||||
bool _isLoading = true;
|
||||
double _progress = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller =
|
||||
WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onNavigationRequest: (NavigationRequest request) async {
|
||||
if (!SCUrlLauncherUtils.shouldOpenExternally(request.url)) {
|
||||
return NavigationDecision.navigate;
|
||||
}
|
||||
|
||||
final bool launched = await SCUrlLauncherUtils.launchExternal(
|
||||
request.url,
|
||||
);
|
||||
if (launched) {
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
|
||||
return SCUrlLauncherUtils.isHttpUrl(request.url)
|
||||
? NavigationDecision.navigate
|
||||
: NavigationDecision.prevent;
|
||||
},
|
||||
onProgress: (int progress) {
|
||||
_updateState(() {
|
||||
_progress = progress / 100;
|
||||
});
|
||||
},
|
||||
onPageStarted: (_) {
|
||||
_updateState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
},
|
||||
onPageFinished: (_) {
|
||||
_updateState(() {
|
||||
_isLoading = false;
|
||||
_progress = 1;
|
||||
});
|
||||
},
|
||||
onWebResourceError: (_) {
|
||||
_updateState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
..loadRequest(Uri.parse(widget.requestUrl));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateState(VoidCallback action) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(action);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: SocialChatStandardAppBar(
|
||||
title: widget.title,
|
||||
actions: const <Widget>[],
|
||||
backgroundColor: Colors.white,
|
||||
backButtonColor: Colors.black,
|
||||
gradient: const LinearGradient(
|
||||
colors: <Color>[Colors.white, Colors.white],
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
WebViewWidget(controller: _controller),
|
||||
if (_isLoading || _progress < 1)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: LinearProgressIndicator(
|
||||
value: _progress > 0 && _progress < 1 ? _progress : null,
|
||||
minHeight: 2,
|
||||
color: const Color(0xff18F2B1),
|
||||
backgroundColor: const Color(0xffEDEDED),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,463 +0,0 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||||
|
||||
enum RechargeMethodType { miFaPay, nativeStore }
|
||||
|
||||
class RechargeMethodOption {
|
||||
const RechargeMethodOption({
|
||||
required this.type,
|
||||
required this.title,
|
||||
this.assetIconPath,
|
||||
this.iconData,
|
||||
});
|
||||
|
||||
final RechargeMethodType type;
|
||||
final String title;
|
||||
final String? assetIconPath;
|
||||
final IconData? iconData;
|
||||
}
|
||||
|
||||
class RechargePackageOption {
|
||||
const RechargePackageOption({
|
||||
required this.id,
|
||||
required this.coins,
|
||||
required this.bonusCoins,
|
||||
required this.priceLabel,
|
||||
required this.badge,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final int coins;
|
||||
final int bonusCoins;
|
||||
final String priceLabel;
|
||||
final String badge;
|
||||
}
|
||||
|
||||
class RechargeChannelOption {
|
||||
const RechargeChannelOption({
|
||||
required this.code,
|
||||
required this.name,
|
||||
this.typeLabel = '',
|
||||
});
|
||||
|
||||
final String code;
|
||||
final String name;
|
||||
final String typeLabel;
|
||||
}
|
||||
|
||||
class RechargeMethodBottomSheet extends StatefulWidget {
|
||||
const RechargeMethodBottomSheet({
|
||||
super.key,
|
||||
required this.method,
|
||||
required this.packages,
|
||||
required this.channels,
|
||||
required this.onConfirm,
|
||||
this.initialPackageId,
|
||||
this.initialChannelCode,
|
||||
});
|
||||
|
||||
final RechargeMethodOption method;
|
||||
final List<RechargePackageOption> packages;
|
||||
final List<RechargeChannelOption> channels;
|
||||
final void Function(
|
||||
RechargePackageOption package,
|
||||
RechargeChannelOption channel,
|
||||
)
|
||||
onConfirm;
|
||||
final String? initialPackageId;
|
||||
final String? initialChannelCode;
|
||||
|
||||
@override
|
||||
State<RechargeMethodBottomSheet> createState() =>
|
||||
_RechargeMethodBottomSheetState();
|
||||
}
|
||||
|
||||
class _RechargeMethodBottomSheetState extends State<RechargeMethodBottomSheet> {
|
||||
late String _selectedPackageId;
|
||||
late String _selectedChannelCode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedPackageId =
|
||||
widget.packages.isEmpty
|
||||
? ''
|
||||
: widget.initialPackageId ?? widget.packages.first.id;
|
||||
_selectedChannelCode =
|
||||
widget.channels.isEmpty
|
||||
? ''
|
||||
: widget.initialChannelCode ?? widget.channels.first.code;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool canConfirm =
|
||||
widget.packages.isNotEmpty &&
|
||||
widget.channels.isNotEmpty &&
|
||||
_selectedPackageId.isNotEmpty &&
|
||||
_selectedChannelCode.isNotEmpty;
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
child: Container(
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: MediaQuery.of(context).size.height * 0.72,
|
||||
padding: EdgeInsets.fromLTRB(14.w, 14.w, 14.w, 14.w),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xff03523a),
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(14.w),
|
||||
topRight: Radius.circular(14.w),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.method.title,
|
||||
style: TextStyle(
|
||||
fontSize: 18.sp,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
SCDebounceWidget(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Icon(
|
||||
CupertinoIcons.clear_thick_circled,
|
||||
size: 22.w,
|
||||
color: Colors.white.withValues(alpha: 0.86),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 6.w),
|
||||
Text(
|
||||
'Select amount',
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.white.withValues(alpha: 0.68),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.w),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.packages.isEmpty)
|
||||
_buildEmptyHint('MiFaPay products are loading')
|
||||
else
|
||||
GridView.builder(
|
||||
itemCount: widget.packages.length,
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 10.w,
|
||||
crossAxisSpacing: 10.w,
|
||||
mainAxisExtent: 136.w,
|
||||
),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final RechargePackageOption item =
|
||||
widget.packages[index];
|
||||
final bool isSelected = item.id == _selectedPackageId;
|
||||
return _buildPackageCard(item, index, isSelected);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16.w),
|
||||
Text(
|
||||
'Payment channel',
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.white.withValues(alpha: 0.68),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.w),
|
||||
if (widget.channels.isEmpty)
|
||||
_buildEmptyHint('No channel available')
|
||||
else
|
||||
Wrap(
|
||||
spacing: 10.w,
|
||||
runSpacing: 10.w,
|
||||
children: widget.channels
|
||||
.map(_buildChannelChip)
|
||||
.toList(growable: false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
SCDebounceWidget(
|
||||
onTap: () {
|
||||
if (!canConfirm) {
|
||||
return;
|
||||
}
|
||||
_confirmSelection();
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 42.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
canConfirm
|
||||
? SCGlobalConfig.businessLogicStrategy
|
||||
.getRechargePageButtonBackgroundColor()
|
||||
: Colors.white.withValues(alpha: 0.18),
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
child: Text(
|
||||
'Confirm',
|
||||
style: TextStyle(
|
||||
fontSize: 15.sp,
|
||||
color:
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getRechargePageButtonTextColor(),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmSelection() {
|
||||
if (widget.packages.isEmpty || widget.channels.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final RechargePackageOption selected = widget.packages.firstWhere(
|
||||
(RechargePackageOption item) => item.id == _selectedPackageId,
|
||||
orElse: () => widget.packages.first,
|
||||
);
|
||||
final RechargeChannelOption selectedChannel = widget.channels.firstWhere(
|
||||
(RechargeChannelOption item) => item.code == _selectedChannelCode,
|
||||
orElse: () => widget.channels.first,
|
||||
);
|
||||
widget.onConfirm(selected, selectedChannel);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
Widget _buildPackageCard(
|
||||
RechargePackageOption item,
|
||||
int index,
|
||||
bool isSelected,
|
||||
) {
|
||||
return SCDebounceWidget(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedPackageId = item.id;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
border: Border.all(
|
||||
color:
|
||||
isSelected ? SocialChatTheme.primaryLight : Colors.transparent,
|
||||
width: 1.4,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 24.w,
|
||||
child:
|
||||
item.badge.isEmpty
|
||||
? null
|
||||
: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(top: 8.w, right: 8.w),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 7.w,
|
||||
vertical: 2.w,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: SocialChatTheme.primaryLight.withValues(
|
||||
alpha: 0.14,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(999.w),
|
||||
),
|
||||
child: Text(
|
||||
item.badge,
|
||||
style: TextStyle(
|
||||
fontSize: 9.sp,
|
||||
color: const Color(0xff03523a),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Image.asset(
|
||||
SCGlobalConfig.businessLogicStrategy
|
||||
.getRechargePageGoldIconByIndex(index),
|
||||
width: 38.w,
|
||||
height: 38.w,
|
||||
),
|
||||
SizedBox(height: 6.w),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6.w),
|
||||
child: Text(
|
||||
_formatWholeNumber(item.coins),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 15.sp,
|
||||
color: Colors.black.withValues(alpha: 0.82),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2.w),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6.w),
|
||||
child: Text(
|
||||
item.bonusCoins > 0
|
||||
? '+${_formatWholeNumber(item.bonusCoins)}'
|
||||
: 'No bonus',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 11.sp,
|
||||
color:
|
||||
item.bonusCoins > 0
|
||||
? SocialChatTheme.primaryLight
|
||||
: Colors.black.withValues(alpha: 0.45),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 26.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF5C550),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(10.w),
|
||||
bottomRight: Radius.circular(10.w),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
item.priceLabel,
|
||||
style: TextStyle(
|
||||
fontSize: 11.sp,
|
||||
color: const Color(0xff6F4B00),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChannelChip(RechargeChannelOption option) {
|
||||
final bool isSelected = option.code == _selectedChannelCode;
|
||||
return SCDebounceWidget(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedChannelCode = option.code;
|
||||
});
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isSelected
|
||||
? Colors.white.withValues(alpha: 0.16)
|
||||
: Colors.white.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
border: Border.all(
|
||||
color:
|
||||
isSelected
|
||||
? SocialChatTheme.primaryLight
|
||||
: Colors.white.withValues(alpha: 0.14),
|
||||
width: 1.1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
option.name,
|
||||
style: TextStyle(
|
||||
fontSize: 13.sp,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
if (option.typeLabel.isNotEmpty) ...[
|
||||
SizedBox(height: 2.w),
|
||||
Text(
|
||||
option.typeLabel,
|
||||
style: TextStyle(
|
||||
fontSize: 10.sp,
|
||||
color: Colors.white.withValues(alpha: 0.62),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyHint(String message) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 14.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
child: Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: Colors.white.withValues(alpha: 0.68),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatWholeNumber(int value) {
|
||||
final String raw = value.toString();
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
for (int i = 0; i < raw.length; i++) {
|
||||
final int position = raw.length - i;
|
||||
buffer.write(raw[i]);
|
||||
if (position > 1 && position % 3 == 1) {
|
||||
buffer.write(',');
|
||||
}
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
@ -9,17 +8,12 @@ import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/app/constants/sc_screen.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/modules/wallet/recharge/recharge_method_bottom_sheet.dart';
|
||||
import 'package:yumi/modules/wallet/wallet_route.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/services/payment/apple_payment_manager.dart';
|
||||
import 'package:yumi/services/payment/google_payment_manager.dart';
|
||||
import 'package:yumi/services/payment/mifa_pay_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_mifa_pay_res.dart';
|
||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||
import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
|
||||
class RechargePage extends StatefulWidget {
|
||||
@ -34,16 +28,10 @@ class _RechargePageState extends State<RechargePage> {
|
||||
static const Color _surfaceTextColor = Colors.white;
|
||||
static const Color _surfaceSubtleTextColor = Color(0xCCFFFFFF);
|
||||
|
||||
late final List<RechargeMethodOption> _methodOptions;
|
||||
|
||||
RechargeMethodType _selectedMethodType = RechargeMethodType.nativeStore;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_methodOptions = _buildMethodOptions();
|
||||
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
|
||||
Provider.of<MiFaPayManager>(context, listen: false).initialize();
|
||||
if (Platform.isAndroid) {
|
||||
Provider.of<AndroidPaymentProcessor>(
|
||||
context,
|
||||
@ -59,8 +47,6 @@ class _RechargePageState extends State<RechargePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final MiFaPayManager miFaPayManager = context.watch<MiFaPayManager>();
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Image.asset(
|
||||
@ -102,18 +88,10 @@ class _RechargePageState extends State<RechargePage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildMethodSection(miFaPayManager),
|
||||
_buildMethodSection(),
|
||||
SizedBox(height: 14.w),
|
||||
Expanded(
|
||||
child:
|
||||
_selectedMethodType ==
|
||||
RechargeMethodType.nativeStore
|
||||
? _buildNativeProductList()
|
||||
: _buildMiFaPayContent(miFaPayManager),
|
||||
),
|
||||
if (_selectedMethodType ==
|
||||
RechargeMethodType.nativeStore &&
|
||||
Platform.isIOS) ...[
|
||||
Expanded(child: _buildNativeProductList()),
|
||||
if (Platform.isIOS) ...[
|
||||
SizedBox(height: 12.w),
|
||||
_buildFooterButton(
|
||||
title:
|
||||
@ -130,19 +108,8 @@ class _RechargePageState extends State<RechargePage> {
|
||||
],
|
||||
SizedBox(height: 12.w),
|
||||
_buildFooterButton(
|
||||
title:
|
||||
_selectedMethodType ==
|
||||
RechargeMethodType.nativeStore
|
||||
? SCAppLocalizations.of(context)!.recharge
|
||||
: _buildMiFaPayPrimaryTitle(miFaPayManager),
|
||||
onTap: () {
|
||||
if (_selectedMethodType ==
|
||||
RechargeMethodType.nativeStore) {
|
||||
_processNativePurchase();
|
||||
return;
|
||||
}
|
||||
_handleMiFaPayPrimaryAction(miFaPayManager);
|
||||
},
|
||||
title: SCAppLocalizations.of(context)!.recharge,
|
||||
onTap: _processNativePurchase,
|
||||
),
|
||||
SizedBox(height: 14.w),
|
||||
],
|
||||
@ -157,23 +124,6 @@ class _RechargePageState extends State<RechargePage> {
|
||||
);
|
||||
}
|
||||
|
||||
List<RechargeMethodOption> _buildMethodOptions() {
|
||||
return <RechargeMethodOption>[
|
||||
RechargeMethodOption(
|
||||
type: RechargeMethodType.nativeStore,
|
||||
title: Platform.isAndroid ? 'Google Pay' : 'Apple Pay',
|
||||
assetIconPath:
|
||||
Platform.isAndroid ? 'sc_images/login/sc_icon_google.png' : null,
|
||||
iconData: Platform.isIOS ? Icons.apple : null,
|
||||
),
|
||||
const RechargeMethodOption(
|
||||
type: RechargeMethodType.miFaPay,
|
||||
title: 'MiFaPay',
|
||||
assetIconPath: 'sc_images/index/sc_icon_recharge_agency.png',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _showRecordDialog() {
|
||||
showGeneralDialog(
|
||||
context: context,
|
||||
@ -288,7 +238,7 @@ class _RechargePageState extends State<RechargePage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMethodSection(MiFaPayManager miFaPayManager) {
|
||||
Widget _buildMethodSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -301,47 +251,13 @@ class _RechargePageState extends State<RechargePage> {
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10.w),
|
||||
..._methodOptions.map(
|
||||
(RechargeMethodOption option) =>
|
||||
_buildMethodTile(option, miFaPayManager),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMethodTile(
|
||||
RechargeMethodOption option,
|
||||
MiFaPayManager miFaPayManager,
|
||||
) {
|
||||
final bool isSelected = _selectedMethodType == option.type;
|
||||
String trailingText = '';
|
||||
if (option.type == RechargeMethodType.miFaPay &&
|
||||
miFaPayManager.selectedCommodity != null) {
|
||||
trailingText = _formatMiFaPriceLabel(miFaPayManager.selectedCommodity!);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: 10.w),
|
||||
child: SCDebounceWidget(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedMethodType = option.type;
|
||||
});
|
||||
if (option.type == RechargeMethodType.miFaPay) {
|
||||
_openMiFaPaySheet(miFaPayManager);
|
||||
}
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 58.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors:
|
||||
isSelected
|
||||
? const [Color(0x42FFFFFF), Color(0x24FFFFFF)]
|
||||
: const [Color(0x33FFFFFF), Color(0x18FFFFFF)],
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0x42FFFFFF), Color(0x24FFFFFF)],
|
||||
begin: AlignmentDirectional.centerStart,
|
||||
end: AlignmentDirectional.centerEnd,
|
||||
),
|
||||
@ -356,11 +272,11 @@ class _RechargePageState extends State<RechargePage> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildMethodLeading(option),
|
||||
_buildMethodLeading(),
|
||||
SizedBox(width: 14.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
option.title,
|
||||
Platform.isAndroid ? 'Google Pay' : 'Apple Pay',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
@ -370,49 +286,28 @@ class _RechargePageState extends State<RechargePage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (trailingText.isNotEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(end: 8.w),
|
||||
child: Text(
|
||||
trailingText,
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: _surfaceSubtleTextColor.withValues(alpha: 0.82),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isSelected
|
||||
? Icons.radio_button_checked
|
||||
: Icons.radio_button_off_outlined,
|
||||
Icons.radio_button_checked,
|
||||
size: 18.w,
|
||||
color:
|
||||
isSelected
|
||||
? _accentGoldColor
|
||||
: _surfaceSubtleTextColor.withValues(alpha: 0.45),
|
||||
color: _accentGoldColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMethodLeading(RechargeMethodOption option) {
|
||||
if (option.assetIconPath != null) {
|
||||
Widget _buildMethodLeading() {
|
||||
if (Platform.isAndroid) {
|
||||
return Image.asset(
|
||||
option.assetIconPath!,
|
||||
'sc_images/login/sc_icon_google.png',
|
||||
width: 28.w,
|
||||
height: 28.w,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
}
|
||||
return Icon(
|
||||
option.iconData ?? CupertinoIcons.creditcard,
|
||||
size: 24.w,
|
||||
color: _surfaceTextColor,
|
||||
);
|
||||
return Icon(Icons.apple, size: 24.w, color: _surfaceTextColor);
|
||||
}
|
||||
|
||||
Widget _buildNativeProductList() {
|
||||
@ -459,122 +354,6 @@ class _RechargePageState extends State<RechargePage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMiFaPayContent(MiFaPayManager miFaPayManager) {
|
||||
if (miFaPayManager.isLoading && miFaPayManager.commodities.isEmpty) {
|
||||
return Center(child: CupertinoActivityIndicator(radius: 14.w));
|
||||
}
|
||||
|
||||
if (miFaPayManager.errorMessage.isNotEmpty &&
|
||||
miFaPayManager.commodities.isEmpty) {
|
||||
return _buildMiFaPayStatusCard(
|
||||
title: 'MiFaPay is temporarily unavailable',
|
||||
detail: miFaPayManager.errorMessage,
|
||||
actionText: 'Retry',
|
||||
onTap: () {
|
||||
miFaPayManager.reload();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (miFaPayManager.commodities.isEmpty) {
|
||||
return _buildMiFaPayStatusCard(
|
||||
title: 'No MiFaPay products available',
|
||||
detail: 'Please try again later.',
|
||||
);
|
||||
}
|
||||
|
||||
final SCMiFaPayCommodityItemRes? commodity =
|
||||
miFaPayManager.selectedCommodity;
|
||||
final SCMiFaPayChannelRes? channel = miFaPayManager.selectedChannel;
|
||||
final SCMiFaPayCountryRes? country = miFaPayManager.selectedCountry;
|
||||
|
||||
return _buildMiFaPayStatusCard(
|
||||
title:
|
||||
commodity == null
|
||||
? 'Tap MiFaPay to choose an amount'
|
||||
: 'Selected ${_formatMiFaPriceLabel(commodity)}',
|
||||
detail:
|
||||
commodity == null
|
||||
? 'Choose amount and payment channel in the bottom sheet.'
|
||||
: '${_formatWholeNumber(_parseWholeNumber(commodity.content))} +${_formatWholeNumber(_parseWholeNumber(commodity.awardContent))}',
|
||||
actionText:
|
||||
channel == null
|
||||
? null
|
||||
: '${channel.channelName ?? ''}${country?.alphaTwo?.isNotEmpty == true ? ' ${country!.alphaTwo}' : ''}',
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMiFaPayStatusCard({
|
||||
required String title,
|
||||
required String detail,
|
||||
String? actionText,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 16.w),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0x33FFFFFF), Color(0x18FFFFFF)],
|
||||
begin: AlignmentDirectional.topStart,
|
||||
end: AlignmentDirectional.bottomEnd,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 10.w,
|
||||
offset: Offset(0, 3.w),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: _surfaceTextColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
Text(
|
||||
detail,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: _surfaceSubtleTextColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if ((actionText ?? '').isNotEmpty) ...[
|
||||
SizedBox(height: 10.w),
|
||||
GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Text(
|
||||
actionText!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color:
|
||||
onTap != null
|
||||
? _accentGoldColor
|
||||
: _surfaceSubtleTextColor.withValues(alpha: 0.82),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRechargeEmptyState() {
|
||||
return Center(
|
||||
child: Column(
|
||||
@ -641,167 +420,6 @@ class _RechargePageState extends State<RechargePage> {
|
||||
}
|
||||
}
|
||||
|
||||
String _buildMiFaPayPrimaryTitle(MiFaPayManager miFaPayManager) {
|
||||
if (miFaPayManager.isCreatingOrder) {
|
||||
return 'Processing...';
|
||||
}
|
||||
if (miFaPayManager.isLoading && miFaPayManager.commodities.isEmpty) {
|
||||
return 'Loading...';
|
||||
}
|
||||
if (miFaPayManager.errorMessage.isNotEmpty &&
|
||||
miFaPayManager.commodities.isEmpty) {
|
||||
return 'Retry';
|
||||
}
|
||||
return miFaPayManager.canCreateRecharge ? 'Pay now' : 'Choose amount';
|
||||
}
|
||||
|
||||
void _handleMiFaPayPrimaryAction(MiFaPayManager miFaPayManager) {
|
||||
if (miFaPayManager.isCreatingOrder) {
|
||||
return;
|
||||
}
|
||||
if (miFaPayManager.errorMessage.isNotEmpty &&
|
||||
miFaPayManager.commodities.isEmpty) {
|
||||
miFaPayManager.reload();
|
||||
return;
|
||||
}
|
||||
if (miFaPayManager.canCreateRecharge) {
|
||||
miFaPayManager.createRecharge(context);
|
||||
return;
|
||||
}
|
||||
_openMiFaPaySheet(miFaPayManager);
|
||||
}
|
||||
|
||||
void _openMiFaPaySheet(MiFaPayManager miFaPayManager) {
|
||||
if (miFaPayManager.isLoading && miFaPayManager.commodities.isEmpty) {
|
||||
SCTts.show('MiFaPay products are loading');
|
||||
return;
|
||||
}
|
||||
|
||||
final List<RechargePackageOption> packages = _buildMiFaPackages(
|
||||
miFaPayManager.commodities,
|
||||
);
|
||||
final List<RechargeChannelOption> channels = _buildMiFaChannels(
|
||||
miFaPayManager.channels,
|
||||
);
|
||||
|
||||
if (packages.isEmpty || channels.isEmpty) {
|
||||
if (miFaPayManager.errorMessage.isNotEmpty) {
|
||||
SCTts.show(miFaPayManager.errorMessage);
|
||||
} else {
|
||||
SCTts.show('No MiFaPay product is available now');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
showBottomInBottomDialog(
|
||||
context,
|
||||
RechargeMethodBottomSheet(
|
||||
method: _methodOptions.firstWhere(
|
||||
(RechargeMethodOption item) =>
|
||||
item.type == RechargeMethodType.miFaPay,
|
||||
),
|
||||
packages: packages,
|
||||
channels: channels,
|
||||
initialPackageId: miFaPayManager.selectedCommodity?.id,
|
||||
initialChannelCode: miFaPayManager.selectedChannel?.channelCode,
|
||||
onConfirm: (
|
||||
RechargePackageOption package,
|
||||
RechargeChannelOption channel,
|
||||
) {
|
||||
setState(() {
|
||||
_selectedMethodType = RechargeMethodType.miFaPay;
|
||||
});
|
||||
miFaPayManager.chooseCommodityById(package.id);
|
||||
miFaPayManager.chooseChannelByCode(channel.code);
|
||||
},
|
||||
),
|
||||
barrierColor: Colors.black.withValues(alpha: 0.35),
|
||||
);
|
||||
}
|
||||
|
||||
List<RechargePackageOption> _buildMiFaPackages(
|
||||
List<SCMiFaPayCommodityItemRes> commodities,
|
||||
) {
|
||||
return commodities
|
||||
.where((SCMiFaPayCommodityItemRes item) => (item.id ?? '').isNotEmpty)
|
||||
.map((SCMiFaPayCommodityItemRes item) {
|
||||
final int coins = _parseWholeNumber(item.content);
|
||||
final int bonusCoins = _parseWholeNumber(item.awardContent);
|
||||
return RechargePackageOption(
|
||||
id: item.id ?? '',
|
||||
coins: coins,
|
||||
bonusCoins: bonusCoins,
|
||||
priceLabel: _formatMiFaPriceLabel(item),
|
||||
badge: _buildMiFaBadge(coins, bonusCoins),
|
||||
);
|
||||
})
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
List<RechargeChannelOption> _buildMiFaChannels(
|
||||
List<SCMiFaPayChannelRes> channels,
|
||||
) {
|
||||
return channels
|
||||
.where(
|
||||
(SCMiFaPayChannelRes item) => (item.channelCode ?? '').isNotEmpty,
|
||||
)
|
||||
.map((SCMiFaPayChannelRes item) {
|
||||
return RechargeChannelOption(
|
||||
code: item.channelCode ?? '',
|
||||
name: item.channelName ?? '',
|
||||
typeLabel: _buildChannelTypeLabel(item),
|
||||
);
|
||||
})
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
String _buildMiFaBadge(int coins, int bonusCoins) {
|
||||
if (coins <= 0 || bonusCoins <= 0) {
|
||||
return '';
|
||||
}
|
||||
final double percent = bonusCoins * 100 / coins;
|
||||
return '+${percent.toStringAsFixed(percent >= 10 ? 0 : 1)}%';
|
||||
}
|
||||
|
||||
String _buildChannelTypeLabel(SCMiFaPayChannelRes channel) {
|
||||
final List<String> parts = <String>[];
|
||||
if ((channel.channelType ?? '').isNotEmpty) {
|
||||
parts.add(channel.channelType!);
|
||||
}
|
||||
if ((channel.factoryChannel ?? '').isNotEmpty) {
|
||||
parts.add(channel.factoryChannel!);
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
String _formatMiFaPriceLabel(SCMiFaPayCommodityItemRes item) {
|
||||
final String currency = item.currency ?? 'USD';
|
||||
final num amount = item.amount ?? item.amountUsd ?? 0;
|
||||
final String formattedAmount =
|
||||
amount % 1 == 0 ? amount.toInt().toString() : amount.toString();
|
||||
return '$currency $formattedAmount';
|
||||
}
|
||||
|
||||
int _parseWholeNumber(String? value) {
|
||||
if ((value ?? '').isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
return num.tryParse(value!)?.toInt() ?? 0;
|
||||
}
|
||||
|
||||
String _formatWholeNumber(num value) {
|
||||
final String raw = value.toInt().toString();
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
for (int i = 0; i < raw.length; i++) {
|
||||
final int position = raw.length - i;
|
||||
buffer.write(raw[i]);
|
||||
if (position > 1 && position % 3 == 1) {
|
||||
buffer.write(',');
|
||||
}
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
Widget _buildGoogleProductItem(
|
||||
SelecteProductConfig productConfig,
|
||||
AndroidPaymentProcessor ref,
|
||||
|
||||
@ -1,451 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/modules/wallet/recharge/mifa_pay_webview_page.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_mifa_pay_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
|
||||
class MiFaPayManager extends ChangeNotifier {
|
||||
static const String defaultApplicationId = '2048200000000000701';
|
||||
static const String defaultPayCountryId = '2048200000000000401';
|
||||
static const String defaultPayCountryName = 'Saudi Arabia';
|
||||
static const String defaultPayCountryCode = 'SA';
|
||||
|
||||
bool _initialized = false;
|
||||
bool _isLoading = false;
|
||||
bool _isCreatingOrder = false;
|
||||
String _errorMessage = '';
|
||||
String _applicationId = defaultApplicationId;
|
||||
|
||||
List<SCMiFaPayCountryRes> _countries = <SCMiFaPayCountryRes>[];
|
||||
SCMiFaPayCountryRes? _selectedCountry;
|
||||
SCMiFaPayCommodityRes? _commodityRes;
|
||||
SCMiFaPayCommodityItemRes? _selectedCommodity;
|
||||
SCMiFaPayChannelRes? _selectedChannel;
|
||||
|
||||
bool get initialized => _initialized;
|
||||
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
bool get isCreatingOrder => _isCreatingOrder;
|
||||
|
||||
String get errorMessage => _errorMessage;
|
||||
|
||||
String get applicationId => _applicationId;
|
||||
|
||||
List<SCMiFaPayCountryRes> get countries => _countries;
|
||||
|
||||
SCMiFaPayCountryRes? get selectedCountry => _selectedCountry;
|
||||
|
||||
SCMiFaPayCommodityRes? get commodityRes => _commodityRes;
|
||||
|
||||
List<SCMiFaPayCommodityItemRes> get commodities =>
|
||||
_commodityRes?.commodity ?? <SCMiFaPayCommodityItemRes>[];
|
||||
|
||||
List<SCMiFaPayChannelRes> get channels =>
|
||||
_commodityRes?.channels ?? <SCMiFaPayChannelRes>[];
|
||||
|
||||
SCMiFaPayCommodityItemRes? get selectedCommodity => _selectedCommodity;
|
||||
|
||||
SCMiFaPayChannelRes? get selectedChannel => _selectedChannel;
|
||||
|
||||
bool get canCreateRecharge =>
|
||||
_selectedCountry != null &&
|
||||
_selectedCommodity != null &&
|
||||
_selectedChannel != null &&
|
||||
!_isLoading &&
|
||||
!_isCreatingOrder;
|
||||
|
||||
Future<void> initialize({bool force = false}) async {
|
||||
if (_isLoading) {
|
||||
return;
|
||||
}
|
||||
if (_initialized && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
_errorMessage = '';
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await _loadCountries();
|
||||
await _loadCommodity();
|
||||
_initialized = true;
|
||||
} catch (error) {
|
||||
_errorMessage = _readErrorMessage(error);
|
||||
debugPrint('MiFaPay initialize failed: $error');
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reload() async {
|
||||
await initialize(force: true);
|
||||
}
|
||||
|
||||
void chooseCommodityById(String goodsId) {
|
||||
if (goodsId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
for (final SCMiFaPayCommodityItemRes item in commodities) {
|
||||
if (item.id == goodsId) {
|
||||
_selectedCommodity = item;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void chooseChannelByCode(String channelCode) {
|
||||
if (channelCode.isEmpty) {
|
||||
return;
|
||||
}
|
||||
for (final SCMiFaPayChannelRes item in channels) {
|
||||
if (item.channelCode == channelCode) {
|
||||
_selectedChannel = item;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createRecharge(BuildContext context) async {
|
||||
final SCMiFaPayCountryRes? country = _selectedCountry;
|
||||
final SCMiFaPayCommodityItemRes? commodity = _selectedCommodity;
|
||||
final SCMiFaPayChannelRes? channel = _selectedChannel;
|
||||
final String userId =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id ?? "";
|
||||
final NavigatorState navigator = Navigator.of(context);
|
||||
final String purchaseSuccessMessage =
|
||||
SCAppLocalizations.of(context)!.purchaseIsSuccessful;
|
||||
const String paymentPendingMessage = 'Payment confirmation in progress';
|
||||
final Map<String, dynamic> payload = <String, dynamic>{
|
||||
"applicationId": _applicationId,
|
||||
"goodsId": commodity?.id ?? '',
|
||||
"payCountryId": country?.id ?? '',
|
||||
"userId": userId,
|
||||
"channelCode": channel?.channelCode ?? '',
|
||||
"newVersion": true,
|
||||
};
|
||||
|
||||
if (country?.id?.isNotEmpty != true ||
|
||||
commodity?.id?.isNotEmpty != true ||
|
||||
channel?.channelCode?.isNotEmpty != true) {
|
||||
SCTts.show('Please select MiFaPay amount and channel first');
|
||||
return;
|
||||
}
|
||||
if (userId.isEmpty) {
|
||||
SCTts.show('User id is empty');
|
||||
return;
|
||||
}
|
||||
if (_isCreatingOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
final SocialChatUserProfileManager profileManager =
|
||||
Provider.of<SocialChatUserProfileManager>(context, listen: false);
|
||||
final double balanceBeforeRecharge = profileManager.myBalance;
|
||||
|
||||
try {
|
||||
_isCreatingOrder = true;
|
||||
notifyListeners();
|
||||
SCLoadingManager.show(context: context);
|
||||
_logRequest(
|
||||
method: 'POST',
|
||||
path: '/order/web/pay/recharge',
|
||||
body: payload,
|
||||
);
|
||||
final SCMiFaPayRechargeRes res = await SCConfigRepositoryImp()
|
||||
.mifaPayRecharge(
|
||||
applicationId: _applicationId,
|
||||
goodsId: commodity!.id!,
|
||||
payCountryId: country!.id!,
|
||||
userId: userId,
|
||||
channelCode: channel!.channelCode!,
|
||||
);
|
||||
SCLoadingManager.hide();
|
||||
_logResponse(tag: 'recharge', data: res.toJson());
|
||||
|
||||
if ((res.requestUrl ?? "").isEmpty) {
|
||||
SCTts.show('MiFaPay requestUrl is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
await navigator.push(
|
||||
MaterialPageRoute<void>(
|
||||
builder:
|
||||
(_) => MiFaPayWebViewPage(
|
||||
title: 'MiFaPay',
|
||||
requestUrl: res.requestUrl!,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final bool rechargeSucceeded = await _waitForRechargeResult(
|
||||
profileManager,
|
||||
balanceBeforeRecharge,
|
||||
);
|
||||
SCTts.show(
|
||||
rechargeSucceeded ? purchaseSuccessMessage : paymentPendingMessage,
|
||||
);
|
||||
} catch (error) {
|
||||
_logError(
|
||||
tag: 'recharge',
|
||||
path: '/order/web/pay/recharge',
|
||||
error: error,
|
||||
body: payload,
|
||||
);
|
||||
final String message = _readErrorMessage(error);
|
||||
SCTts.show(message);
|
||||
debugPrint('MiFaPay createRecharge failed: $error');
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
_isCreatingOrder = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadCountries() async {
|
||||
List<SCMiFaPayCountryRes> countries = <SCMiFaPayCountryRes>[];
|
||||
try {
|
||||
_logRequest(method: 'GET', path: '/order/web/pay/country');
|
||||
countries = await SCConfigRepositoryImp().mifaPayCountries();
|
||||
_logResponse(
|
||||
tag: 'country',
|
||||
data:
|
||||
countries.map((SCMiFaPayCountryRes item) => item.toJson()).toList(),
|
||||
);
|
||||
} catch (error) {
|
||||
_logError(tag: 'country', path: '/order/web/pay/country', error: error);
|
||||
debugPrint('MiFaPay countries fallback: $error');
|
||||
}
|
||||
|
||||
if (countries.isEmpty) {
|
||||
countries = <SCMiFaPayCountryRes>[_fallbackCountry()];
|
||||
}
|
||||
|
||||
_countries = countries;
|
||||
final String currentCountryId = _selectedCountry?.id ?? '';
|
||||
_selectedCountry = countries.firstWhere(
|
||||
(SCMiFaPayCountryRes item) => item.id == currentCountryId,
|
||||
orElse: () => countries.first,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadCommodity() async {
|
||||
final String payCountryId =
|
||||
_selectedCountry?.id?.isNotEmpty == true
|
||||
? _selectedCountry!.id!
|
||||
: defaultPayCountryId;
|
||||
final Map<String, dynamic> queryParams = <String, dynamic>{
|
||||
"applicationId": _applicationId,
|
||||
"payCountryId": payCountryId,
|
||||
"type": "GOLD",
|
||||
};
|
||||
|
||||
try {
|
||||
_logRequest(
|
||||
method: 'GET',
|
||||
path: '/order/web/pay/commodity',
|
||||
queryParams: queryParams,
|
||||
);
|
||||
final SCMiFaPayCommodityRes res = await SCConfigRepositoryImp()
|
||||
.mifaPayCommodity(
|
||||
applicationId: _applicationId,
|
||||
payCountryId: payCountryId,
|
||||
);
|
||||
_logResponse(tag: 'commodity', data: res.toJson());
|
||||
|
||||
_commodityRes = res;
|
||||
_applicationId = res.application?.id ?? _applicationId;
|
||||
|
||||
final List<SCMiFaPayCommodityItemRes> commodityList = commodities;
|
||||
final String currentGoodsId = _selectedCommodity?.id ?? '';
|
||||
_selectedCommodity =
|
||||
commodityList.isEmpty
|
||||
? null
|
||||
: commodityList.firstWhere(
|
||||
(SCMiFaPayCommodityItemRes item) => item.id == currentGoodsId,
|
||||
orElse: () => commodityList.first,
|
||||
);
|
||||
|
||||
final List<SCMiFaPayChannelRes> channelList = channels;
|
||||
final String currentChannelCode = _selectedChannel?.channelCode ?? '';
|
||||
_selectedChannel =
|
||||
channelList.isEmpty
|
||||
? null
|
||||
: channelList.firstWhere(
|
||||
(SCMiFaPayChannelRes item) =>
|
||||
item.channelCode == currentChannelCode,
|
||||
orElse: () => channelList.first,
|
||||
);
|
||||
} catch (error) {
|
||||
_logError(
|
||||
tag: 'commodity',
|
||||
path: '/order/web/pay/commodity',
|
||||
error: error,
|
||||
queryParams: queryParams,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshWalletState(
|
||||
SocialChatUserProfileManager profileManager,
|
||||
) async {
|
||||
try {
|
||||
final double balance = await SCAccountRepository().balance();
|
||||
profileManager.updateBalance(balance);
|
||||
} catch (error) {
|
||||
debugPrint('MiFaPay balance refresh failed: $error');
|
||||
}
|
||||
|
||||
try {
|
||||
await profileManager.fetchUserProfileData(loadGuardCount: false);
|
||||
} catch (error) {
|
||||
debugPrint('MiFaPay profile refresh failed: $error');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pollWalletState(
|
||||
SocialChatUserProfileManager profileManager,
|
||||
) async {
|
||||
for (int index = 0; index < 2; index++) {
|
||||
await Future<void>.delayed(const Duration(seconds: 3));
|
||||
await _refreshWalletState(profileManager);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _waitForRechargeResult(
|
||||
SocialChatUserProfileManager profileManager,
|
||||
double previousBalance,
|
||||
) async {
|
||||
await _refreshWalletState(profileManager);
|
||||
if (profileManager.myBalance > previousBalance) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int index = 0; index < 5; index++) {
|
||||
await Future<void>.delayed(const Duration(seconds: 2));
|
||||
await _refreshWalletState(profileManager);
|
||||
if (profileManager.myBalance > previousBalance) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
unawaited(_pollWalletState(profileManager));
|
||||
return false;
|
||||
}
|
||||
|
||||
SCMiFaPayCountryRes _fallbackCountry() {
|
||||
return SCMiFaPayCountryRes(
|
||||
id: defaultPayCountryId,
|
||||
countryName: defaultPayCountryName,
|
||||
alphaTwo: defaultPayCountryCode,
|
||||
nationalFlag: '',
|
||||
);
|
||||
}
|
||||
|
||||
String _readErrorMessage(Object error) {
|
||||
final String raw = error.toString();
|
||||
return raw
|
||||
.replaceFirst('Exception: ', '')
|
||||
.replaceFirst('DioException [unknown]: ', '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
void _logRequest({
|
||||
required String method,
|
||||
required String path,
|
||||
Map<String, dynamic>? queryParams,
|
||||
Map<String, dynamic>? body,
|
||||
}) {
|
||||
debugPrint('[MiFaPay][Request] $method ${_buildUrl(path)}');
|
||||
if (queryParams != null && queryParams.isNotEmpty) {
|
||||
debugPrint('[MiFaPay][Query] ${_safeJson(queryParams)}');
|
||||
}
|
||||
if (body != null && body.isNotEmpty) {
|
||||
debugPrint('[MiFaPay][Body] ${_safeJson(body)}');
|
||||
}
|
||||
}
|
||||
|
||||
void _logResponse({required String tag, required dynamic data}) {
|
||||
debugPrint('[MiFaPay][Response][$tag] ${_safeJson(data)}');
|
||||
}
|
||||
|
||||
void _logError({
|
||||
required String tag,
|
||||
required String path,
|
||||
required Object error,
|
||||
Map<String, dynamic>? queryParams,
|
||||
Map<String, dynamic>? body,
|
||||
}) {
|
||||
debugPrint('[MiFaPay][Error][$tag] ${_buildUrl(path)}');
|
||||
if (queryParams != null && queryParams.isNotEmpty) {
|
||||
debugPrint('[MiFaPay][ErrorQuery][$tag] ${_safeJson(queryParams)}');
|
||||
}
|
||||
if (body != null && body.isNotEmpty) {
|
||||
debugPrint('[MiFaPay][ErrorBody][$tag] ${_safeJson(body)}');
|
||||
}
|
||||
|
||||
if (error is DioException) {
|
||||
debugPrint('[MiFaPay][Dio][$tag] type=${error.type}');
|
||||
debugPrint(
|
||||
'[MiFaPay][Dio][$tag] status=${error.response?.statusCode} message=${error.message}',
|
||||
);
|
||||
if (error.requestOptions.path.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[MiFaPay][Dio][$tag] requestPath=${error.requestOptions.path}',
|
||||
);
|
||||
}
|
||||
if (error.requestOptions.queryParameters.isNotEmpty) {
|
||||
debugPrint(
|
||||
'[MiFaPay][Dio][$tag] requestQuery=${_safeJson(error.requestOptions.queryParameters)}',
|
||||
);
|
||||
}
|
||||
if (error.requestOptions.data != null) {
|
||||
debugPrint(
|
||||
'[MiFaPay][Dio][$tag] requestData=${_safeJson(error.requestOptions.data)}',
|
||||
);
|
||||
}
|
||||
if (error.response?.data != null) {
|
||||
debugPrint(
|
||||
'[MiFaPay][Dio][$tag] responseData=${_safeJson(error.response?.data)}',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debugPrint('[MiFaPay][Error][$tag] $error');
|
||||
}
|
||||
}
|
||||
|
||||
String _buildUrl(String path) {
|
||||
final String host = SCGlobalConfig.apiHost;
|
||||
if (host.endsWith('/') && path.startsWith('/')) {
|
||||
return '${host.substring(0, host.length - 1)}$path';
|
||||
}
|
||||
if (!host.endsWith('/') && !path.startsWith('/')) {
|
||||
return '$host/$path';
|
||||
}
|
||||
return '$host$path';
|
||||
}
|
||||
|
||||
String _safeJson(dynamic data) {
|
||||
try {
|
||||
return jsonEncode(data);
|
||||
} catch (_) {
|
||||
return data.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -298,6 +298,42 @@ class SCMiFaPayRechargeRes {
|
||||
}
|
||||
}
|
||||
|
||||
class SCMiFaPayOrderStatusRes {
|
||||
SCMiFaPayOrderStatusRes({bool? paid, String? statusText, dynamic rawBody}) {
|
||||
_paid = paid;
|
||||
_statusText = statusText;
|
||||
_rawBody = rawBody;
|
||||
}
|
||||
|
||||
SCMiFaPayOrderStatusRes.fromJson(dynamic json) {
|
||||
_rawBody = json;
|
||||
_paid = _resolvePaid(json);
|
||||
_statusText = _resolveStatusText(json);
|
||||
}
|
||||
|
||||
bool? _paid;
|
||||
String? _statusText;
|
||||
dynamic _rawBody;
|
||||
|
||||
bool get isPaid => _paid == true;
|
||||
|
||||
String? get statusText => _statusText;
|
||||
|
||||
dynamic get rawBody => _rawBody;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final dynamic body = _rawBody;
|
||||
if (body is Map<String, dynamic>) {
|
||||
return body;
|
||||
}
|
||||
return <String, dynamic>{
|
||||
'paid': _paid,
|
||||
'statusText': _statusText,
|
||||
'rawBody': body,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
String? _stringValue(dynamic value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
@ -315,3 +351,102 @@ num? _numValue(dynamic value) {
|
||||
}
|
||||
return num.tryParse(value.toString());
|
||||
}
|
||||
|
||||
bool? _boolValue(dynamic value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value != 0;
|
||||
}
|
||||
final String normalized = value.toString().trim().toLowerCase();
|
||||
if (<String>[
|
||||
'true',
|
||||
'success',
|
||||
'succeeded',
|
||||
'paid',
|
||||
'done',
|
||||
'completed',
|
||||
].contains(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (<String>[
|
||||
'false',
|
||||
'pending',
|
||||
'processing',
|
||||
'failed',
|
||||
'cancelled',
|
||||
].contains(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool? _resolvePaid(dynamic json) {
|
||||
final bool? direct = _boolValue(json);
|
||||
if (direct != null) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
if (json is Map) {
|
||||
const List<String> boolKeys = <String>[
|
||||
'paid',
|
||||
'success',
|
||||
'paySuccess',
|
||||
'orderPaid',
|
||||
'finished',
|
||||
'completed',
|
||||
'result',
|
||||
];
|
||||
for (final String key in boolKeys) {
|
||||
final bool? value = _boolValue(json[key]);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
const List<String> statusKeys = <String>[
|
||||
'status',
|
||||
'orderStatus',
|
||||
'payStatus',
|
||||
'tradeStatus',
|
||||
];
|
||||
for (final String key in statusKeys) {
|
||||
final bool? value = _boolValue(json[key]);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _resolveStatusText(dynamic json) {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
if (json is String) {
|
||||
return json;
|
||||
}
|
||||
if (json is Map) {
|
||||
const List<String> keys = <String>[
|
||||
'status',
|
||||
'orderStatus',
|
||||
'payStatus',
|
||||
'tradeStatus',
|
||||
'message',
|
||||
'msg',
|
||||
];
|
||||
for (final String key in keys) {
|
||||
final String? value = _stringValue(json[key]);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return _stringValue(json);
|
||||
}
|
||||
|
||||
211
lib/shared/business_logic/models/res/sc_sign_in_reward_res.dart
Normal file
211
lib/shared/business_logic/models/res/sc_sign_in_reward_res.dart
Normal file
@ -0,0 +1,211 @@
|
||||
Map<String, dynamic> _asMap(dynamic json) {
|
||||
if (json is Map<String, dynamic>) {
|
||||
return json;
|
||||
}
|
||||
return <String, dynamic>{};
|
||||
}
|
||||
|
||||
List<dynamic> _asList(dynamic json) {
|
||||
if (json is List) {
|
||||
return json;
|
||||
}
|
||||
return const <dynamic>[];
|
||||
}
|
||||
|
||||
String _asString(dynamic value) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
int _asInt(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value.toInt();
|
||||
}
|
||||
return int.tryParse(_asString(value)) ?? 0;
|
||||
}
|
||||
|
||||
bool _asBool(dynamic value) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value != 0;
|
||||
}
|
||||
final normalized = _asString(value).trim().toLowerCase();
|
||||
return normalized == 'true' || normalized == '1';
|
||||
}
|
||||
|
||||
class SCSignInRewardItem {
|
||||
const SCSignInRewardItem({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.name,
|
||||
required this.content,
|
||||
required this.quantity,
|
||||
required this.cover,
|
||||
required this.remark,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String type;
|
||||
final String name;
|
||||
final String content;
|
||||
final int quantity;
|
||||
final String cover;
|
||||
final String remark;
|
||||
|
||||
factory SCSignInRewardItem.fromJson(dynamic json) {
|
||||
final map = _asMap(json);
|
||||
return SCSignInRewardItem(
|
||||
id: _asString(map['id']),
|
||||
type: _asString(map['type']),
|
||||
name: _asString(map['name']),
|
||||
content: _asString(map['content']),
|
||||
quantity: _asInt(map['quantity']),
|
||||
cover: _asString(map['cover']),
|
||||
remark: _asString(map['remark']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SCSignInRewardDay {
|
||||
const SCSignInRewardDay({
|
||||
required this.dayIndex,
|
||||
required this.rewardGroupId,
|
||||
required this.rewardGroupName,
|
||||
required this.rewardItems,
|
||||
required this.signed,
|
||||
required this.todayTarget,
|
||||
});
|
||||
|
||||
final int dayIndex;
|
||||
final String rewardGroupId;
|
||||
final String rewardGroupName;
|
||||
final List<SCSignInRewardItem> rewardItems;
|
||||
final bool signed;
|
||||
final bool todayTarget;
|
||||
|
||||
factory SCSignInRewardDay.fromJson(dynamic json) {
|
||||
final map = _asMap(json);
|
||||
return SCSignInRewardDay(
|
||||
dayIndex: _asInt(map['dayIndex']),
|
||||
rewardGroupId: _asString(map['rewardGroupId']),
|
||||
rewardGroupName: _asString(map['rewardGroupName']),
|
||||
rewardItems:
|
||||
_asList(
|
||||
map['rewardItems'],
|
||||
).map((item) => SCSignInRewardItem.fromJson(item)).toList(),
|
||||
signed: _asBool(map['signed']),
|
||||
todayTarget: _asBool(map['todayTarget']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SCSignInRewardStatusRes {
|
||||
const SCSignInRewardStatusRes({
|
||||
required this.userId,
|
||||
required this.sysOrigin,
|
||||
required this.configured,
|
||||
required this.enabled,
|
||||
required this.available,
|
||||
required this.timezone,
|
||||
required this.cycleDays,
|
||||
required this.currentDate,
|
||||
required this.signedToday,
|
||||
required this.currentStreak,
|
||||
required this.nextDayIndex,
|
||||
required this.days,
|
||||
});
|
||||
|
||||
final String userId;
|
||||
final String sysOrigin;
|
||||
final bool configured;
|
||||
final bool enabled;
|
||||
final bool available;
|
||||
final String timezone;
|
||||
final int cycleDays;
|
||||
final String currentDate;
|
||||
final bool signedToday;
|
||||
final int currentStreak;
|
||||
final int nextDayIndex;
|
||||
final List<SCSignInRewardDay> days;
|
||||
|
||||
bool get canShowDialog =>
|
||||
configured && enabled && available && !signedToday && days.isNotEmpty;
|
||||
|
||||
factory SCSignInRewardStatusRes.fromJson(dynamic json) {
|
||||
final map = _asMap(json);
|
||||
return SCSignInRewardStatusRes(
|
||||
userId: _asString(map['userId']),
|
||||
sysOrigin: _asString(map['sysOrigin']),
|
||||
configured: _asBool(map['configured']),
|
||||
enabled: _asBool(map['enabled']),
|
||||
available: _asBool(map['available']),
|
||||
timezone: _asString(map['timezone']),
|
||||
cycleDays: _asInt(map['cycleDays']),
|
||||
currentDate: _asString(map['currentDate']),
|
||||
signedToday: _asBool(map['signedToday']),
|
||||
currentStreak: _asInt(map['currentStreak']),
|
||||
nextDayIndex: _asInt(map['nextDayIndex']),
|
||||
days:
|
||||
_asList(
|
||||
map['days'],
|
||||
).map((day) => SCSignInRewardDay.fromJson(day)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SCSignInRewardCheckInRes {
|
||||
const SCSignInRewardCheckInRes({
|
||||
required this.success,
|
||||
required this.alreadySigned,
|
||||
required this.userId,
|
||||
required this.sysOrigin,
|
||||
required this.claimDate,
|
||||
required this.dayIndex,
|
||||
required this.streakCount,
|
||||
required this.rewardGroupId,
|
||||
required this.rewardGroupName,
|
||||
required this.rewardItems,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
final bool success;
|
||||
final bool alreadySigned;
|
||||
final String userId;
|
||||
final String sysOrigin;
|
||||
final String claimDate;
|
||||
final int dayIndex;
|
||||
final int streakCount;
|
||||
final String rewardGroupId;
|
||||
final String rewardGroupName;
|
||||
final List<SCSignInRewardItem> rewardItems;
|
||||
final String status;
|
||||
|
||||
bool get isClaimed => success || alreadySigned;
|
||||
|
||||
factory SCSignInRewardCheckInRes.fromJson(dynamic json) {
|
||||
final map = _asMap(json);
|
||||
return SCSignInRewardCheckInRes(
|
||||
success: _asBool(map['success']),
|
||||
alreadySigned: _asBool(map['alreadySigned']),
|
||||
userId: _asString(map['userId']),
|
||||
sysOrigin: _asString(map['sysOrigin']),
|
||||
claimDate: _asString(map['claimDate']),
|
||||
dayIndex: _asInt(map['dayIndex']),
|
||||
streakCount: _asInt(map['streakCount']),
|
||||
rewardGroupId: _asString(map['rewardGroupId']),
|
||||
rewardGroupName: _asString(map['rewardGroupName']),
|
||||
rewardItems:
|
||||
_asList(
|
||||
map['rewardItems'],
|
||||
).map((item) => SCSignInRewardItem.fromJson(item)).toList(),
|
||||
status: _asString(map['status']),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -77,6 +77,9 @@ abstract class SocialChatConfigRepository {
|
||||
required String channelCode,
|
||||
});
|
||||
|
||||
///MiFaPay 订单状态
|
||||
Future<SCMiFaPayOrderStatusRes> mifaPayOrderStatus({required String orderId});
|
||||
|
||||
///等级资源
|
||||
Future<SCLevelConfigRes> configLevel();
|
||||
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import 'package:yumi/shared/business_logic/models/res/join_room_res.dart' hide WearBadge;
|
||||
import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'
|
||||
hide WearBadge;
|
||||
|
||||
import 'package:yumi/shared/business_logic/models/req/sc_mobile_auth_cmd.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_edit_room_info_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart' hide WearBadge;
|
||||
import 'package:yumi/shared/business_logic/models/res/follow_user_res.dart' hide WearBadge;
|
||||
import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart'
|
||||
hide WearBadge;
|
||||
import 'package:yumi/shared/business_logic/models/res/follow_user_res.dart'
|
||||
hide WearBadge;
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_gold_record_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/message_friend_user_res.dart';
|
||||
@ -12,9 +15,11 @@ import 'package:yumi/shared/business_logic/models/res/sc_prop_coupon_list_res.da
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_prop_coupon_record_list_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_black_list_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/room_res.dart' hide WearBadge;
|
||||
import 'package:yumi/shared/business_logic/models/res/room_res.dart'
|
||||
hide WearBadge;
|
||||
import 'package:yumi/shared/business_logic/models/req/sc_user_profile_cmd.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_rtc_token_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_sign_in_reward_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_sign_in_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart';
|
||||
@ -63,15 +68,15 @@ abstract class SocialChatUserRepository {
|
||||
///发送心跳
|
||||
Future heartbeat(String status, bool upMick, {String? roomId});
|
||||
|
||||
///上麦心跳
|
||||
Future anchorHeartbeat(String roomId);
|
||||
|
||||
///声网token
|
||||
Future<SCRtcTokenRes> getRtcToken(
|
||||
String channel,
|
||||
String userId, {
|
||||
bool isPublisher = false,
|
||||
});
|
||||
///上麦心跳
|
||||
Future anchorHeartbeat(String roomId);
|
||||
|
||||
///声网token
|
||||
Future<SCRtcTokenRes> getRtcToken(
|
||||
String channel,
|
||||
String userId, {
|
||||
bool isPublisher = false,
|
||||
});
|
||||
|
||||
///退出房间
|
||||
Future<bool> quitRoom(String roomId);
|
||||
@ -146,7 +151,6 @@ abstract class SocialChatUserRepository {
|
||||
///获取用户关系计数 SUBSCRIPTION:订阅、FANS:粉丝、FRIEND:朋友,访客.
|
||||
Future<List<SCUserCounterRes>> userCounter(String userId);
|
||||
|
||||
|
||||
///用户等级
|
||||
Future<SCUserLevelExpRes> userLevelConsumptionExp(String userId, String type);
|
||||
|
||||
@ -159,6 +163,12 @@ abstract class SocialChatUserRepository {
|
||||
///签到领奖
|
||||
Future<int> checkInReceive(String id, String resourceGroupId);
|
||||
|
||||
///签到奖励状态
|
||||
Future<SCSignInRewardStatusRes> signInRewardStatus();
|
||||
|
||||
///执行签到
|
||||
Future<SCSignInRewardCheckInRes> signInRewardCheckIn();
|
||||
|
||||
///添加代理
|
||||
Future<bool> teamCreate(String ownUserId);
|
||||
|
||||
@ -226,7 +236,6 @@ abstract class SocialChatUserRepository {
|
||||
///赠送道具券
|
||||
Future<bool> couponSend(String couponNo, String receiverId);
|
||||
|
||||
|
||||
///处理用户违规
|
||||
Future<SCViolationHandleRes> userViolationHandle(
|
||||
String userId,
|
||||
@ -236,7 +245,6 @@ abstract class SocialChatUserRepository {
|
||||
List<String>? imageUrls,
|
||||
});
|
||||
|
||||
|
||||
///验证是否是好友
|
||||
Future<bool> friendRelationCheck(String userId);
|
||||
|
||||
@ -269,5 +277,4 @@ abstract class SocialChatUserRepository {
|
||||
|
||||
///注销账号
|
||||
Future<bool> logoutAccount();
|
||||
|
||||
}
|
||||
|
||||
@ -242,6 +242,19 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
return result;
|
||||
}
|
||||
|
||||
///order/web/pay/order-status
|
||||
@override
|
||||
Future<SCMiFaPayOrderStatusRes> mifaPayOrderStatus({
|
||||
required String orderId,
|
||||
}) async {
|
||||
final result = await http.get<SCMiFaPayOrderStatusRes>(
|
||||
"/order/web/pay/order-status",
|
||||
queryParams: {"orderId": orderId},
|
||||
fromJson: (json) => SCMiFaPayOrderStatusRes.fromJson(json),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
///sys/static-config/level
|
||||
@override
|
||||
Future<SCLevelConfigRes> configLevel() async {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_sign_in_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_sign_in_reward_res.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
||||
@ -22,8 +22,6 @@ class DailySignInDialogItem {
|
||||
required this.isFinalDay,
|
||||
this.subtitle = '',
|
||||
this.cover = '',
|
||||
this.rewardId = '',
|
||||
this.resourceGroupId = '',
|
||||
});
|
||||
|
||||
final int day;
|
||||
@ -32,13 +30,8 @@ class DailySignInDialogItem {
|
||||
final String cover;
|
||||
final DailySignInRewardStatus status;
|
||||
final bool isFinalDay;
|
||||
final String rewardId;
|
||||
final String resourceGroupId;
|
||||
|
||||
bool get canClaim =>
|
||||
status == DailySignInRewardStatus.current &&
|
||||
rewardId.isNotEmpty &&
|
||||
resourceGroupId.isNotEmpty;
|
||||
bool get canClaim => status == DailySignInRewardStatus.current;
|
||||
|
||||
DailySignInDialogItem copyWith({
|
||||
int? day,
|
||||
@ -47,8 +40,6 @@ class DailySignInDialogItem {
|
||||
String? cover,
|
||||
DailySignInRewardStatus? status,
|
||||
bool? isFinalDay,
|
||||
String? rewardId,
|
||||
String? resourceGroupId,
|
||||
}) {
|
||||
return DailySignInDialogItem(
|
||||
day: day ?? this.day,
|
||||
@ -57,8 +48,6 @@ class DailySignInDialogItem {
|
||||
cover: cover ?? this.cover,
|
||||
status: status ?? this.status,
|
||||
isFinalDay: isFinalDay ?? this.isFinalDay,
|
||||
rewardId: rewardId ?? this.rewardId,
|
||||
resourceGroupId: resourceGroupId ?? this.resourceGroupId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -132,42 +121,41 @@ class DailySignInDialogData {
|
||||
);
|
||||
}
|
||||
|
||||
factory DailySignInDialogData.fromLegacy({
|
||||
required SCSignInRes signInRes,
|
||||
required bool checkedToday,
|
||||
factory DailySignInDialogData.fromStatus({
|
||||
required SCSignInRewardStatusRes statusRes,
|
||||
}) {
|
||||
final rewards = List<Rewards>.from(
|
||||
signInRes.rewards ?? const <Rewards>[],
|
||||
)..sort(
|
||||
(left, right) => (left.rule?.sort ?? 0).compareTo(right.rule?.sort ?? 0),
|
||||
);
|
||||
|
||||
final signedDays = (signInRes.days ?? 0).toInt().clamp(0, 7);
|
||||
final currentDay = checkedToday ? -1 : math.min(signedDays + 1, 7);
|
||||
final sortedDays = List<SCSignInRewardDay>.from(statusRes.days)
|
||||
..sort((left, right) => left.dayIndex.compareTo(right.dayIndex));
|
||||
final dayMap = <int, SCSignInRewardDay>{
|
||||
for (final day in sortedDays) day.dayIndex: day,
|
||||
};
|
||||
|
||||
final items = List<DailySignInDialogItem>.generate(7, (index) {
|
||||
final day = index + 1;
|
||||
final reward = index < rewards.length ? rewards[index] : null;
|
||||
final rewardDay = dayMap[day];
|
||||
final status =
|
||||
day <= signedDays
|
||||
rewardDay?.signed == true
|
||||
? DailySignInRewardStatus.claimed
|
||||
: day == currentDay
|
||||
: !statusRes.signedToday &&
|
||||
((rewardDay?.todayTarget ?? false) ||
|
||||
statusRes.nextDayIndex == day)
|
||||
? DailySignInRewardStatus.current
|
||||
: DailySignInRewardStatus.pending;
|
||||
|
||||
return DailySignInDialogItem(
|
||||
day: day,
|
||||
title: _resolveTitle(reward),
|
||||
subtitle: _resolveSubtitle(reward),
|
||||
cover: _resolveCover(reward),
|
||||
title: _resolveTitle(rewardDay),
|
||||
subtitle: _resolveSubtitle(rewardDay),
|
||||
cover: _resolveCover(rewardDay),
|
||||
status: status,
|
||||
isFinalDay: day == 7,
|
||||
rewardId: reward?.rule?.id?.trim() ?? '',
|
||||
resourceGroupId: reward?.rule?.resourceGroupId?.trim() ?? '',
|
||||
);
|
||||
});
|
||||
|
||||
return DailySignInDialogData(items: items, checkedToday: checkedToday);
|
||||
return DailySignInDialogData(
|
||||
items: items,
|
||||
checkedToday: statusRes.signedToday,
|
||||
);
|
||||
}
|
||||
|
||||
DailySignInDialogData copyWith({
|
||||
@ -180,12 +168,12 @@ class DailySignInDialogData {
|
||||
);
|
||||
}
|
||||
|
||||
static ActivityRewardProps? _firstRewardProp(Rewards? reward) {
|
||||
final props = reward?.propsGroup?.activityRewardProps;
|
||||
if (props == null || props.isEmpty) {
|
||||
static SCSignInRewardItem? _firstRewardItem(SCSignInRewardDay? rewardDay) {
|
||||
final rewardItems = rewardDay?.rewardItems;
|
||||
if (rewardItems == null || rewardItems.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return props.first;
|
||||
return rewardItems.first;
|
||||
}
|
||||
|
||||
static String _pickFirstNonEmpty(List<String?> values, String fallback) {
|
||||
@ -198,38 +186,32 @@ class DailySignInDialogData {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
static String _resolveTitle(Rewards? reward) {
|
||||
final prop = _firstRewardProp(reward);
|
||||
static String _resolveTitle(SCSignInRewardDay? rewardDay) {
|
||||
final rewardItem = _firstRewardItem(rewardDay);
|
||||
return _pickFirstNonEmpty([
|
||||
reward?.propsGroup?.name,
|
||||
prop?.content,
|
||||
prop?.remark,
|
||||
prop?.detailType,
|
||||
rewardItem?.name,
|
||||
rewardItem?.content,
|
||||
rewardDay?.rewardGroupName,
|
||||
rewardItem?.type,
|
||||
rewardItem?.remark,
|
||||
], 'Profile Frame');
|
||||
}
|
||||
|
||||
static String _resolveSubtitle(Rewards? reward) {
|
||||
final prop = _firstRewardProp(reward);
|
||||
final quantity = prop?.quantity;
|
||||
if (quantity != null && quantity > 1) {
|
||||
return 'x${quantity.toInt()}';
|
||||
}
|
||||
|
||||
final amount = prop?.amount;
|
||||
if (amount != null && amount > 0) {
|
||||
return amount == amount.toInt() ? '${amount.toInt()}' : '$amount';
|
||||
static String _resolveSubtitle(SCSignInRewardDay? rewardDay) {
|
||||
final rewardItem = _firstRewardItem(rewardDay);
|
||||
if ((rewardItem?.quantity ?? 0) > 1) {
|
||||
return 'x${rewardItem!.quantity}';
|
||||
}
|
||||
|
||||
final textValue = _pickFirstNonEmpty([
|
||||
prop?.type,
|
||||
prop?.detailType,
|
||||
prop?.remark,
|
||||
rewardItem?.remark,
|
||||
rewardItem?.type,
|
||||
], '');
|
||||
return textValue == 'Profile Frame' ? '' : textValue;
|
||||
}
|
||||
|
||||
static String _resolveCover(Rewards? reward) {
|
||||
return _firstRewardProp(reward)?.cover?.trim() ?? '';
|
||||
static String _resolveCover(SCSignInRewardDay? rewardDay) {
|
||||
return _firstRewardItem(rewardDay)?.cover.trim() ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@ -277,12 +259,19 @@ class _DailySignInDialogState extends State<DailySignInDialog> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_data = widget.data;
|
||||
debugPrint(
|
||||
'[SignInReward][Dialog] init '
|
||||
'checkedToday=${_data.checkedToday} '
|
||||
'currentDay=${_data.currentItem?.day ?? 0} '
|
||||
'items=${_debugItemsSummary(_data.items)}',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleCheckIn() async {
|
||||
if (_isSubmitting) {
|
||||
return;
|
||||
}
|
||||
final l10n = SCAppLocalizations.of(context)!;
|
||||
if (_data.checkedToday) {
|
||||
SmartDialog.dismiss(tag: DailySignInDialog.dialogTag);
|
||||
return;
|
||||
@ -298,25 +287,28 @@ class _DailySignInDialogState extends State<DailySignInDialog> {
|
||||
_isSubmitting = true;
|
||||
});
|
||||
SCLoadingManager.show();
|
||||
debugPrint(
|
||||
'[SignInReward][Dialog] tap check-in '
|
||||
'currentDay=${currentItem.day}',
|
||||
);
|
||||
|
||||
try {
|
||||
await SCAccountRepository().checkInReceive(
|
||||
currentItem.rewardId,
|
||||
currentItem.resourceGroupId,
|
||||
);
|
||||
final checkInResult = await SCAccountRepository().signInRewardCheckIn();
|
||||
if (!checkInResult.isClaimed) {
|
||||
throw Exception(l10n.thisFeatureIsCurrentlyUnavailable);
|
||||
}
|
||||
|
||||
DailySignInDialogData latestData;
|
||||
try {
|
||||
final result = await Future.wait<dynamic>([
|
||||
SCAccountRepository().checkInToday(),
|
||||
SCAccountRepository().sginListAward(),
|
||||
]);
|
||||
latestData = DailySignInDialogData.fromLegacy(
|
||||
signInRes: result[1] as SCSignInRes,
|
||||
checkedToday: result[0] as bool,
|
||||
);
|
||||
debugPrint('[SignInReward][Dialog] reload status after check-in');
|
||||
final latestStatus = await SCAccountRepository().signInRewardStatus();
|
||||
latestData = DailySignInDialogData.fromStatus(statusRes: latestStatus);
|
||||
} catch (_) {
|
||||
latestData = _markCurrentItemAsClaimed(_data);
|
||||
debugPrint(
|
||||
'[SignInReward][Dialog] reload status failed, fallback to local update '
|
||||
'dayIndex=${checkInResult.dayIndex}',
|
||||
);
|
||||
latestData = _markDayAsClaimed(_data, checkInResult.dayIndex);
|
||||
}
|
||||
|
||||
SCLoadingManager.hide();
|
||||
@ -326,9 +318,19 @@ class _DailySignInDialogState extends State<DailySignInDialog> {
|
||||
setState(() {
|
||||
_data = latestData;
|
||||
});
|
||||
SCTts.show(SCAppLocalizations.of(context)!.receiveSucc);
|
||||
debugPrint(
|
||||
'[SignInReward][Dialog] check-in success '
|
||||
'alreadySigned=${checkInResult.alreadySigned} '
|
||||
'dayIndex=${checkInResult.dayIndex} '
|
||||
'checkedToday=${latestData.checkedToday}',
|
||||
);
|
||||
SCTts.show(
|
||||
checkInResult.alreadySigned ? l10n.signedin : l10n.receiveSucc,
|
||||
);
|
||||
SmartDialog.dismiss(tag: DailySignInDialog.dialogTag);
|
||||
} catch (error) {
|
||||
SCLoadingManager.hide();
|
||||
debugPrint('[SignInReward][Dialog] check-in failed error=$error');
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
@ -345,12 +347,17 @@ class _DailySignInDialogState extends State<DailySignInDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
DailySignInDialogData _markCurrentItemAsClaimed(DailySignInDialogData data) {
|
||||
DailySignInDialogData _markDayAsClaimed(
|
||||
DailySignInDialogData data,
|
||||
int dayIndex,
|
||||
) {
|
||||
return data.copyWith(
|
||||
checkedToday: true,
|
||||
items:
|
||||
data.items.map((item) {
|
||||
if (item.status == DailySignInRewardStatus.current) {
|
||||
if ((dayIndex > 0 && item.day == dayIndex) ||
|
||||
(dayIndex <= 0 &&
|
||||
item.status == DailySignInRewardStatus.current)) {
|
||||
return item.copyWith(status: DailySignInRewardStatus.claimed);
|
||||
}
|
||||
return item;
|
||||
@ -358,6 +365,15 @@ class _DailySignInDialogState extends State<DailySignInDialog> {
|
||||
);
|
||||
}
|
||||
|
||||
String _debugItemsSummary(List<DailySignInDialogItem> items) {
|
||||
return items
|
||||
.map(
|
||||
(item) =>
|
||||
'{day:${item.day},title:${item.title},status:${item.status.name}}',
|
||||
)
|
||||
.join(',');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.sizeOf(context).width;
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
5
需求进度.md
5
需求进度.md
@ -22,7 +22,7 @@
|
||||
## 已完成模块
|
||||
- 已按 2026-04-20 最新首页视觉需求,为 Party 房间列表前 3 个房卡接入新的本地排名边框 SVGA:桌面“房间排序前三的框”中的 3 份素材已导入工程并挂到首页房卡最上层,仅作用于当前列表前 3 项,其余房卡保持原样;后续又继续为前三房卡底部信息区补了横向与底部安全区,避免外扩边框直接压住国旗、房名和在线人数。同时 `Me` 板块里的 `Recent / Followed` tab 已继续对齐上方首页 tab 的斜体字样式,并移除了点击时的波浪反馈。
|
||||
- 已按 2026-04-20 最新房间联调继续修正房主在自己房间里的麦位操作回归:当前房主/管理员点击自己所在麦位时,不再被“直接打开资料卡”逻辑短路,会重新回到底部麦位菜单,从而正常看到自己可用的上下麦/禁麦操作;同时上麦、下麦、禁麦、解禁、锁麦、解锁这些动作在接口成功后会立即回写本地麦位状态,并主动补拉一次最新麦位列表,减少房主端自己操作后 UI 状态延迟或短暂错乱。
|
||||
- 已按 2026-04-20 最新确认继续完成 `Wallet -> Recharge` 的 MiFaPay 真链路接入:页面仍保持“原页面结构 + 局部新增”的方案,只在原充值页白色内容区顶部新增 `Recharge methods`,且该标题已改成项目常见的土豪金色;`Google Pay / Apple Pay` 继续保留原生内购列表,`MiFaPay` 已从占位切成真实接口驱动,进入页面会拉 `/order/web/pay/country`、`/order/web/pay/commodity`,底部弹窗会展示真实商品与支付渠道,确认后调用 `/order/web/pay/recharge` 下单并拉起 MiFaPay H5 WebView。用户关闭收银台返回 App 后,当前会立即刷新钱包余额并轮询最新金币额;命中到账后会直接提示购买成功,避免只停留在“支付确认中”。
|
||||
- 已按 2026-04-20 最新调整撤掉 `Wallet -> Recharge` 中新增的 MiFaPay 第三方支付 UI 与页面逻辑:当前充值页仅保留原生 `Google Pay / Apple Pay` 入口与商品列表,`Recharge methods` 区域也已收敛为单一原生支付卡片;此前接入的 MiFaPay 方法选择、底部弹窗、H5 收银台页以及对应页面级状态管理已从现有充值链路移除,避免继续对当前版本产生影响。
|
||||
- 已按 2026-04-18 联调要求继续收口幸运礼物链路:项目默认 API host 已临时切到 `http://192.168.110.43:1100/` 方便直连测试环境;`/gift/give/lucky-gift` 请求体也已补齐为最新结构,除原有 `giftId/quantity/roomId/acceptUserIds/checkCombo` 外,会稳定携带 `gameId: null`、`accepts: []`、`dynamicContentId: null`、`songId: null` 这几组字段,便于和当前后端参数定义保持一致。
|
||||
- 已继续补齐 `GAME_LUCKY_GIFT` 的 socket 播报与中奖动效:房间群消息收到该类型后,现在会统一落聊天室中奖消息、奖励弹层队列与发送者余额回写,不再只在 `3x+` 时才触发顶部奖励动画;同时全局飘窗已改为按服务端 `globalNews` 字段决定是否展示,避免再用前端本地倍率硬编码去猜。
|
||||
- 已按最新 UI 口径重排幸运礼物中奖展示:顶部 `LuckGiftNomorAnimWidget` 不再叠加大头像、倍率和奖励框,只在“单个礼物倍率 `> 10x`”或“单次中奖金币 `> 5000`”时负责播全屏 `luck_gift_reward_burst.svga`;原本的 `luck_gift_reward_frame.svga` 已改挂到房间礼物播报条最右侧,并在框内显示 `+formattedAwardAmount`,金额文案直接复用此前大头像下方那一份中奖金币额。
|
||||
@ -164,9 +164,6 @@
|
||||
- `需求进度.md`
|
||||
- `lib/main.dart`
|
||||
- `lib/modules/wallet/recharge/recharge_page.dart`
|
||||
- `lib/modules/wallet/recharge/recharge_method_bottom_sheet.dart`
|
||||
- `lib/modules/wallet/recharge/mifa_pay_webview_page.dart`
|
||||
- `lib/services/payment/mifa_pay_manager.dart`
|
||||
- `lib/shared/business_logic/models/res/sc_mifa_pay_res.dart`
|
||||
- `lib/shared/business_logic/repositories/config_repository.dart`
|
||||
- `lib/shared/data_sources/sources/repositories/sc_config_repository_imp.dart`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user