feat: 实现 iOS Apple Pay 内购支付功能

前端:
- 集成 in_app_purchase 插件,实现 IAP 支付流程
- 添加支付模块 (payments/) 处理产品获取、购买、验证
- 积分中心页面集成 Apple Pay 购买入口
- 设置页面重构: 关于/隐私/协议直接展示,删除 legal_center 子页面
- 修复欢迎引导页滚动检测阈值问题
- 修复解卦结果页 iOS 侧滑返回手势被阻止的问题
- 邀请码绑定按钮临时禁用(待后端实现)

后端:
- 新增 apple_iap_transactions 表记录交易
- 实现 Apple 服务器端验证 (App Store Server API)
- 支付成功后自动发放积分
- 支持 Sandbox/Production 环境切换
- 添加退款处理和交易状态机

协议:
- 更新积分流水协议,支持 purchase/refund 类型
- 新增 PAYMENT_* 错误码
This commit is contained in:
ZL-Q
2026-04-28 10:45:29 +08:00
parent b453ff7345
commit 87f92987b2
58 changed files with 3741 additions and 336 deletions
@@ -27,6 +27,7 @@ class HomeScreen extends StatefulWidget {
const HomeScreen({
super.key,
required this.account,
required this.userId,
required this.sessionStore,
required this.currentLocale,
required this.profileSettings,
@@ -43,9 +44,11 @@ class HomeScreen extends StatefulWidget {
required this.onDeleteHistorySession,
required this.onLogout,
required this.onDeleteAccount,
required this.onBalanceChanged,
});
final String account;
final String userId;
final SessionStore sessionStore;
final Locale currentLocale;
final ProfileSettingsV1 profileSettings;
@@ -56,15 +59,16 @@ class HomeScreen extends StatefulWidget {
final NotificationRepository notificationRepository;
final Future<void> Function(String languageTag) onLocaleChanged;
final Future<void> Function(ProfileSettingsV1 settings)
onProfileSettingsChanged;
onProfileSettingsChanged;
final Future<ProfileSettingsV1> Function(ProfileSettingsV1 updated)
onSaveProfile;
onSaveProfile;
final Future<ProfileSettingsV1> Function(String filePath) onUploadAvatar;
final Future<void> Function(DivinationResultData result)
onDivinationCompleted;
onDivinationCompleted;
final Future<void> Function(String threadId) onDeleteHistorySession;
final Future<void> Function() onLogout;
final Future<void> Function() onDeleteAccount;
final void Function(int newBalance) onBalanceChanged;
@override
State<HomeScreen> createState() => _HomeScreenState();
@@ -132,6 +136,7 @@ class _HomeScreenState extends State<HomeScreen> {
),
_ProfileTab(
account: widget.account,
userId: widget.userId,
settings: widget.profileSettings,
coinBalance: widget.coinBalance,
inviteRepository: _inviteRepository,
@@ -142,6 +147,7 @@ class _HomeScreenState extends State<HomeScreen> {
onUploadAvatar: widget.onUploadAvatar,
onLogout: widget.onLogout,
onDeleteAccount: widget.onDeleteAccount,
onBalanceChanged: widget.onBalanceChanged,
),
],
),
@@ -561,6 +567,7 @@ class _DivinationHistoryScreenState extends State<DivinationHistoryScreen> {
class _ProfileTab extends StatelessWidget {
const _ProfileTab({
required this.account,
required this.userId,
required this.settings,
required this.coinBalance,
required this.inviteRepository,
@@ -571,9 +578,11 @@ class _ProfileTab extends StatelessWidget {
required this.onUploadAvatar,
required this.onLogout,
required this.onDeleteAccount,
required this.onBalanceChanged,
});
final String account;
final String userId;
final ProfileSettingsV1 settings;
final int coinBalance;
final InviteRepository inviteRepository;
@@ -581,15 +590,17 @@ class _ProfileTab extends StatelessWidget {
final Future<void> Function(String languageTag) onLocaleChanged;
final Future<void> Function(ProfileSettingsV1 settings) onSettingsChanged;
final Future<ProfileSettingsV1> Function(ProfileSettingsV1 updated)
onSaveProfile;
onSaveProfile;
final Future<ProfileSettingsV1> Function(String filePath) onUploadAvatar;
final Future<void> Function() onLogout;
final Future<void> Function() onDeleteAccount;
final void Function(int newBalance) onBalanceChanged;
@override
Widget build(BuildContext context) {
return SettingsScreen(
account: account,
userId: userId,
settings: settings,
coinBalance: coinBalance,
inviteRepository: inviteRepository,
@@ -600,6 +611,7 @@ class _ProfileTab extends StatelessWidget {
onUploadAvatar: onUploadAvatar,
onLogout: onLogout,
onDeleteAccount: onDeleteAccount,
onBalanceChanged: onBalanceChanged,
);
}
}
@@ -703,14 +715,23 @@ class _WelcomeDialog extends StatefulWidget {
class _WelcomeDialogState extends State<_WelcomeDialog> {
final ScrollController _scrollController = ScrollController();
bool _hasScrolledToBottom = false;
bool _hasCheckedInitialScroll = false;
@override
void initState() {
super.initState();
_scrollController.addListener(_handleScroll);
WidgetsBinding.instance.addPostFrameCallback((_) {
_syncScrollState();
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_hasCheckedInitialScroll) {
_hasCheckedInitialScroll = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_syncScrollState();
});
}
}
@override
@@ -730,7 +751,7 @@ class _WelcomeDialogState extends State<_WelcomeDialog> {
}
final max = _scrollController.position.maxScrollExtent;
final current = _scrollController.offset;
final canReadAll = max <= AppSpacing.xs || current >= max - AppSpacing.md;
final canReadAll = max <= 50.0 || current >= max - AppSpacing.md;
if (_hasScrolledToBottom == canReadAll) {
return;
}