feat: integrate invite API and improve notification handling

- Add invite code display and binding functionality via API
- Fix notification unread count sync on auth state change
- Improve notification mark read with server state validation
- Add auth state listener to trigger notification refresh
- Add YaoCoinConverter for coin-to-yao type conversion
- Remove YaoLegend from divination screens (UI cleanup)
- Abbreviate relation labels in yao detail view
- Add re-register notice to account delete screen
- Update 'coins' terminology to 'points' in localization
- Fix backend points consumption to only run in CHAT mode
- Add HttpxAuthNoiseFilter to suppress auth endpoint logging
- Fix notification static_schema import path
- Add test coverage for notification bloc error handling
- Update AGENTS.md page header rules and image handling
- Delete deprecated run-dev.sh script
This commit is contained in:
qzl
2026-04-13 14:52:22 +08:00
parent da947f9f08
commit 1e22f27de2
52 changed files with 1419 additions and 307 deletions
@@ -1,44 +1,125 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../core/logging/logger.dart';
import '../../../../l10n/app_localizations.dart';
import '../../../../shared/theme/app_color_palette.dart';
import '../../../../shared/theme/design_tokens.dart';
import '../../../../shared/widgets/toast/toast.dart';
import '../../../../shared/widgets/toast/toast_type.dart';
import '../../data/repositories/invite_repository.dart';
class InviteScreen extends StatefulWidget {
const InviteScreen({super.key});
const InviteScreen({super.key, required this.inviteRepository});
final InviteRepository inviteRepository;
@override
State<InviteScreen> createState() => _InviteScreenState();
}
class _InviteScreenState extends State<InviteScreen> {
final Logger _logger = getLogger('features.settings.invite_screen');
final _bindCodeController = TextEditingController();
final _formKey = GlobalKey<FormState>();
bool _isBinding = false;
bool _isGenerating = false;
bool _isLoading = true;
bool _hasError = false;
// Mock data - will be replaced with API calls
final String _myInviteCode = 'ABC123';
final int _invitedCount = 3;
String? _myInviteCode;
int _invitedCount = 0;
final bool _hasInviter = false;
@override
void initState() {
super.initState();
_loadInviteCode();
}
Future<void> _loadInviteCode() async {
setState(() {
_isLoading = true;
_hasError = false;
});
try {
final result = await widget.inviteRepository.getMyInviteCode();
if (!mounted) return;
setState(() {
_myInviteCode = result.code;
_invitedCount = result.usedCount;
_isLoading = false;
});
} catch (error, stackTrace) {
_logger.error(
message: 'Failed to load invite code',
error: error,
stackTrace: stackTrace,
);
if (!mounted) return;
setState(() {
_hasError = true;
_isLoading = false;
});
}
}
@override
void dispose() {
_bindCodeController.dispose();
super.dispose();
}
bool get _hasMyInviteCode => _myInviteCode.isNotEmpty;
bool get _hasMyInviteCode =>
_myInviteCode != null && _myInviteCode!.isNotEmpty;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final colors = Theme.of(context).colorScheme;
if (_isLoading) {
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
title: Text(l10n.settingsInviteTitle),
centerTitle: true,
backgroundColor: colors.surfaceContainerLow,
surfaceTintColor: colors.surfaceContainerLow,
),
body: const Center(child: CircularProgressIndicator()),
);
}
if (_hasError) {
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
title: Text(l10n.settingsInviteTitle),
centerTitle: true,
backgroundColor: colors.surfaceContainerLow,
surfaceTintColor: colors.surfaceContainerLow,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.settingsInviteEmptyTitle,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: AppSpacing.md),
FilledButton(
onPressed: _loadInviteCode,
child: const Text('Retry'),
),
],
),
),
);
}
return Scaffold(
backgroundColor: colors.surfaceContainerLow,
appBar: AppBar(
@@ -51,7 +132,10 @@ class _InviteScreenState extends State<InviteScreen> {
padding: const EdgeInsets.all(AppSpacing.lg),
children: [
if (_hasMyInviteCode) ...[
_InviteCodeCard(inviteCode: _myInviteCode, onCopy: _copyInviteCode),
_InviteCodeCard(
inviteCode: _myInviteCode!,
onCopy: _copyInviteCode,
),
const SizedBox(height: AppSpacing.lg),
_InviteStatsCard(count: _invitedCount),
const SizedBox(height: AppSpacing.xl),
@@ -79,7 +163,7 @@ class _InviteScreenState extends State<InviteScreen> {
void _copyInviteCode() {
final l10n = AppLocalizations.of(context)!;
Clipboard.setData(ClipboardData(text: _myInviteCode));
Clipboard.setData(ClipboardData(text: _myInviteCode!));
Toast.show(
context,
l10n.settingsInviteCopySuccess,