diff --git a/lib/core/api/proxy_client.dart b/lib/core/api/proxy_client.dart index 4648160..5274bae 100644 --- a/lib/core/api/proxy_client.dart +++ b/lib/core/api/proxy_client.dart @@ -1,3 +1,13 @@ +/// HTTP 代理客户端:业务请求统一 POST 到 [ApiConfig.proxyPath],正文经 AES 加密包装。 +/// +/// **日志**:`_log` / [logWithEmbeddedJson] 仅在 `kDebugMode` 或 [ApiConfig.debugLogs] 为 true 时输出 +///(release 排障:`flutter run --release --dart-define=APP_LOG_LEVEL=trace`)。 +/// 会打印明文入参(headers/query/body 加密前)、加密后的 JSON、`Content-Type` 等传输层头,以及解密后的响应。 +/// +/// **字段**:业务语义放在 V2 `sanctum`;`portal`/`knight` 等落在逻辑 headers,再加密进 `quest_rank` 等槽位, +/// 见下方 [ProxyKeys] 与 [ProxyClient.request]。 +library proxy_client; + import 'dart:async'; import 'dart:convert'; @@ -81,7 +91,9 @@ void _logLong(String text) { } } -/// 遇到 { 或 [ 视为 JSON 开始,直到与之匹配的 } 或 ] 结束;格式化后单条不超过 1000 字,分条时按行切分保持对齐。 +/// 遇到 `{` / `[` 视为 JSON 片段并尝试美化输出;否则按普通字符串处理。 +/// +/// 无输出条件:非 debug 且未开启 [ApiConfig.debugLogs](与 [AppLogger] 一致)。 void logWithEmbeddedJson(Object? msg) { if (!kDebugMode && !ApiConfig.debugLogs) return; @@ -163,11 +175,12 @@ void logWithEmbeddedJson(Object? msg) { if (out.isNotEmpty) _logLong(out); } +/// 代理调试输出的统一入口(走 [logWithEmbeddedJson])。 void _log(String msg) { logWithEmbeddedJson(msg); } -/// 代理请求体字段名(统一请求参数) +/// 发往网关 JSON 体中的固定键名(各槽位承载加密后的 path/method/headers/query/body)。 abstract final class ProxyKeys { static const String heroClass = 'hero_class'; static const String petSpecies = 'pet_species'; @@ -183,7 +196,7 @@ abstract final class ProxyKeys { static const String dirPath = 'dir_path'; } -/// 代理请求客户端 +/// 代理请求客户端:维护可选 `knight`(用户 Token),并对单次请求做加密与解析。 class ProxyClient { ProxyClient({ this.baseUrl, @@ -222,12 +235,14 @@ class ProxyClient { }; } - /// 发送代理请求 - /// [path] 接口路径,如 /v1/user/fast_login - /// [method] HTTP 方法,POST 或 GET - /// [headers] 请求头,使用 V2 字段名(portal、knight 等) - /// [queryParams] 查询参数,使用 V2 字段名(sentinel、asset 等) - /// [body] 请求体,使用 V2 字段名,将填入 sanctum + /// 发送代理请求。 + /// + /// `path`、`method`、`headers`、`queryParams`、`body` 会先序列化为 JSON,再分别加密写入 [ProxyKeys]; + /// 实际 HTTP 始终为 POST,`Content-Type: application/json`。 + /// 超时见 [ApiConfig.httpRequestTimeout];超时返回 `code == -1`。 + /// + /// [path] 例如 `/v1/user/fast_login`;[headers] 常用 `portal`、`knight`; + /// [queryParams] 常用 `sentinel`、`asset`;[body] 映射进包装体 `sanctum`。 Future request({ required String path, required String method, @@ -250,7 +265,7 @@ class ProxyClient { final sanctum = body ?? {}; final v2Body = _buildV2Wrapper(sanctum); - // 原始入参 + // 加密前明文(仅调试日志使用) final headersEncoded = jsonEncode(headersMap); final paramsEncoded = jsonEncode(paramsMap); final v2BodyEncoded = jsonEncode(v2Body); @@ -310,9 +325,10 @@ class ProxyClient { return _parseResponse(response); } + /// 解密响应体 JSON,映射为 [ApiResponse](业务字段:`helm`=code,`rampart`=msg,`sidekick`=data)。 ApiResponse _parseResponse(http.Response response) { try { - // 响应解密流程:Base64 解码 → AES-ECB 解密 → PKCS5 去填充 → UTF-8 字符串 + // Base64 → AES-ECB → UTF-8 JSON var responseLogStr = '========== 响应 ==========='; final decrypted = ApiCrypto.decrypt(response.body); final json = jsonDecode(decrypted) as Map; @@ -338,7 +354,7 @@ class ProxyClient { } } -/// 统一 API 响应 +/// 代理层统一响应:`code == 0` 表示成功。 class ApiResponse { ApiResponse({ required this.code, diff --git a/lib/core/api/services/user_api.dart b/lib/core/api/services/user_api.dart index e3a040b..f93279c 100644 --- a/lib/core/api/services/user_api.dart +++ b/lib/core/api/services/user_api.dart @@ -113,4 +113,24 @@ abstract final class UserApi { }, ); } + + /// 获取积分记录分页数据 + /// trophy=page, heatmap=size, accolade=type(可选) + static Future getCreditsPage({ + String? sentinel, + required String trophy, + required String heatmap, + String? accolade, + }) async { + return _client.request( + path: '/v1/user/credits-page', + method: 'GET', + queryParams: { + 'sentinel': sentinel ?? ApiConfig.appId, + 'trophy': trophy, + 'heatmap': heatmap, + if (accolade != null && accolade.isNotEmpty) 'accolade': accolade, + }, + ); + } } diff --git a/lib/core/config/facebook_config.dart b/lib/core/config/facebook_config.dart index 41d874c..e9e899d 100644 --- a/lib/core/config/facebook_config.dart +++ b/lib/core/config/facebook_config.dart @@ -2,30 +2,39 @@ import 'package:flutter/foundation.dart'; import '../api/api_config.dart'; -/// Facebook SDK 配置 +/// Facebook / Meta 相关常量与客户端开关。 /// -/// 用于 Adjust Meta Install Referrer 归因与 Facebook App Events 埋点。 +/// **用途** +/// - **Adjust**:Meta Install Referrer 归因(配合 [installReferrerDecryptionKey])。 +/// - **Facebook App Events**:`AdjustEvents` 中通过 `facebook_app_events` 上报自定义事件等。 +/// +/// **控制台日志**:[debugLogs] 为 true 时,Dart 侧会打印 FB 事件调试信息(与 [ApiConfig.debugLogs]、 +/// `--dart-define=APP_LOG_LEVEL=trace` 对齐);release 默认关闭,避免刷屏。 abstract final class FacebookConfig { - /// Dart 层 FB 事件控制台日志:debug 构建或 `--dart-define=APP_LOG_LEVEL=trace` 等为 true。 + /// 是否在 Dart 控制台打印 FB SDK / 埋点相关调试输出。 + /// + /// `true`:`kDebugMode` **或** [ApiConfig.debugLogs](含 `APP_LOG_LEVEL=trace` 等)。 static bool get debugLogs => kDebugMode || ApiConfig.debugLogs; - /// Facebook 应用 ID(应用编号) + /// Facebook 应用编号(Application ID),用于 AndroidManifest / Info.plist 元数据等。 static const String appId = '1684216162986495'; - /// Facebook Client Token(客户端口令) + /// Facebook **客户端**口令(Client Token)。 /// - /// 在 Facebook 开发者后台获取:应用 → 设置 → 高级 → 客户端口令 + /// 路径:Meta 开发者后台 → 应用 → **设置 → 高级 → 客户端口令**。\ + /// 若为空字符串,部分 SDK 初始化可能受限;[hasClientToken] 可用于分支判断。 static const String clientToken = ''; - /// 安装引荐来源解密密钥 + /// Meta / FB **安装广告**引荐来源的解密密钥(与客户端 Client Token 不同)。 /// - /// 用于解密 Facebook App Install Ads 的加密 referrer。 - /// 请在 Adjust 控制台填写此密钥以启用解密 - /// (路径:App Settings → Partner setup → Meta/Facebook)。 + /// 用于解密加密后的 Install Referrer;需在 **Adjust** 控制台 Partner setup → Meta/Facebook 等处配置, + /// 与 Adjust 文档路径一致。 static const String installReferrerDecryptionKey = '068aff9bac7e8846b94e9fc73d51c7a5ab7c8ac39fe9a2b16d0ff8b74f98f'; - /// 应用密钥(App Secret)仅用于服务端,勿放入客户端。 + /// [clientToken] 是否已配置(非空)。 + /// + /// **App Secret** 仅用于服务端,切勿打进客户端包。 static bool get hasClientToken => clientToken.isNotEmpty; } diff --git a/lib/features/recharge/models/credits_record_item.dart b/lib/features/recharge/models/credits_record_item.dart new file mode 100644 index 0000000..c327d7e --- /dev/null +++ b/lib/features/recharge/models/credits_record_item.dart @@ -0,0 +1,27 @@ +class CreditsRecordItem { + const CreditsRecordItem({ + required this.credits, + required this.type, + required this.createTime, + }); + + final int credits; + final int type; // 1: increase, 2: decrease + final int createTime; // unix timestamp (seconds or milliseconds) + + factory CreditsRecordItem.fromJson(Map json) { + return CreditsRecordItem( + credits: (json['greaves'] as num?)?.toInt() ?? 0, + type: (json['accolade'] as num?)?.toInt() ?? 0, + createTime: (json['discover'] as num?)?.toInt() ?? 0, + ); + } + + bool get isIncrease => type == 1; + + String get deltaText { + final abs = credits.abs(); + final sign = isIncrease ? '+' : '-'; + return '$sign$abs'; + } +} diff --git a/lib/features/recharge/points_history_screen.dart b/lib/features/recharge/points_history_screen.dart new file mode 100644 index 0000000..909e5fb --- /dev/null +++ b/lib/features/recharge/points_history_screen.dart @@ -0,0 +1,302 @@ +import 'package:flutter/material.dart'; + +import '../../core/api/api_config.dart'; +import '../../core/api/services/user_api.dart'; +import '../../core/auth/auth_service.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/theme/app_spacing.dart'; +import '../../core/theme/app_typography.dart'; +import '../../core/user/user_state.dart'; +import '../../shared/widgets/top_nav_bar.dart'; +import 'models/credits_record_item.dart'; + +class PointsHistoryScreen extends StatefulWidget { + const PointsHistoryScreen({super.key}); + + @override + State createState() => _PointsHistoryScreenState(); +} + +class _PointsHistoryScreenState extends State { + static const int _pageSize = 20; + final ScrollController _scrollController = ScrollController(); + final List _records = []; + + bool _loading = true; + bool _loadingMore = false; + bool _hasNext = true; + String? _error; + int _currentPage = 1; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + _loadRecords(refresh: true); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void _onScroll() { + if (!_scrollController.hasClients || + _loadingMore || + !_hasNext || + _loading) { + return; + } + final pos = _scrollController.position; + if (pos.pixels >= pos.maxScrollExtent - 200) { + _loadRecords(refresh: false); + } + } + + Future _loadRecords({required bool refresh}) async { + if (refresh) { + setState(() { + _loading = true; + _error = null; + _currentPage = 1; + _hasNext = true; + }); + } else { + if (_loadingMore || !_hasNext) return; + setState(() => _loadingMore = true); + } + + try { + await AuthService.loginComplete; + final page = refresh ? 1 : _currentPage; + final res = await UserApi.getCreditsPage( + sentinel: ApiConfig.appId, + trophy: page.toString(), + heatmap: _pageSize.toString(), + ); + if (!mounted) return; + if (res.isSuccess && res.data is Map) { + final data = res.data as Map; + final intensify = data['intensify'] as List? ?? []; + final list = intensify + .whereType>() + .map(CreditsRecordItem.fromJson) + .toList(); + final pages = (data['coordinate'] as num?)?.toInt() ?? page; + final current = (data['empower'] as num?)?.toInt() ?? page; + setState(() { + if (refresh) { + _records + ..clear() + ..addAll(list); + } else { + _records.addAll(list); + } + _currentPage = current + 1; + _hasNext = current < pages; + _loading = false; + _loadingMore = false; + }); + } else { + setState(() { + if (refresh) _records.clear(); + _loading = false; + _loadingMore = false; + _error = 'Unable to load points history right now. Please try again.'; + }); + } + } catch (_) { + if (!mounted) return; + setState(() { + if (refresh) _records.clear(); + _loading = false; + _loadingMore = false; + _error = + 'Something went wrong while loading points history. Please try again.'; + }); + } + } + + @override + Widget build(BuildContext context) { + final points = UserCreditsData.of(context)?.creditsDisplay ?? '--'; + return Scaffold( + backgroundColor: AppColors.background, + appBar: const PreferredSize( + preferredSize: Size.fromHeight(56), + child: TopNavBar( + title: 'Points History', + showBackButton: true, + ), + ), + body: _loading + ? const Center(child: CircularProgressIndicator()) + : _error != null + ? Center( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: AppSpacing.xxl), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _error!, + textAlign: TextAlign.center, + style: AppTypography.bodyRegular.copyWith( + color: AppColors.textSecondary, + ), + ), + const SizedBox(height: AppSpacing.lg), + TextButton( + onPressed: () => _loadRecords(refresh: true), + child: const Text('Retry'), + ), + ], + ), + ), + ) + : RefreshIndicator( + onRefresh: () => _loadRecords(refresh: true), + child: ListView( + controller: _scrollController, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.fromLTRB( + AppSpacing.screenPadding, + AppSpacing.xxl, + AppSpacing.screenPadding, + AppSpacing.screenPaddingLarge, + ), + children: [ + _CurrentPointsCard(points: points), + const SizedBox(height: AppSpacing.xl), + if (_records.isEmpty) + Padding( + padding: const EdgeInsets.symmetric( + vertical: AppSpacing.screenPaddingLarge, + ), + child: Center( + child: Text( + 'No history yet', + style: AppTypography.bodyRegular.copyWith( + color: AppColors.textSecondary, + ), + ), + ), + ) + else + ..._records.map( + (item) => Padding( + padding: + const EdgeInsets.only(bottom: AppSpacing.md), + child: _HistoryRow(item: item), + ), + ), + if (_loadingMore) + const Padding( + padding: + EdgeInsets.symmetric(vertical: AppSpacing.md), + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + ), + ], + ), + ), + ); + } +} + +class _CurrentPointsCard extends StatelessWidget { + const _CurrentPointsCard({required this.points}); + + final String points; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xl, + vertical: AppSpacing.xl, + ), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.border), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Current Points', + style: + AppTypography.caption.copyWith(color: AppColors.textSecondary), + ), + const SizedBox(height: AppSpacing.sm), + Text( + points, + style: AppTypography.bodyLarge.copyWith( + color: AppColors.textPrimary, + fontSize: 34, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ); + } +} + +class _HistoryRow extends StatelessWidget { + const _HistoryRow({required this.item}); + + final CreditsRecordItem item; + + @override + Widget build(BuildContext context) { + final isPositive = item.isIncrease; + return Container( + padding: const EdgeInsets.symmetric( + horizontal: AppSpacing.xl, + vertical: AppSpacing.lg, + ), + decoration: BoxDecoration( + color: AppColors.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.border), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _formatTime(item.createTime), + style: + AppTypography.caption.copyWith(color: AppColors.textSecondary), + ), + Text( + item.deltaText, + style: AppTypography.bodyMedium.copyWith( + color: isPositive + ? const Color(0xFF16A34A) + : const Color(0xFFDC2626), + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ); + } +} + +String _formatTime(int raw) { + if (raw <= 0) return '--'; + final dt = raw > 1000000000000 + ? DateTime.fromMillisecondsSinceEpoch(raw) + : DateTime.fromMillisecondsSinceEpoch(raw * 1000); + String two(int n) => n.toString().padLeft(2, '0'); + return '${dt.year}-${two(dt.month)}-${two(dt.day)} ${two(dt.hour)}:${two(dt.minute)}'; +} diff --git a/lib/features/recharge/recharge_screen.dart b/lib/features/recharge/recharge_screen.dart index 6121d1a..2b1d1f5 100644 --- a/lib/features/recharge/recharge_screen.dart +++ b/lib/features/recharge/recharge_screen.dart @@ -19,6 +19,7 @@ import 'google_play_purchase_service.dart'; import 'models/activity_item.dart'; import 'models/payment_method_item.dart'; import 'payment_webview_screen.dart'; +import 'points_history_screen.dart'; /// Recharge screen - matches Pencil tPjdN /// Tier cards (DC6NS, YRaOG, QlNz6) 对接 getGooglePayActivities / getApplePayActivities @@ -38,7 +39,7 @@ class _RechargeScreenState extends State /// 当前正在支付的商品 code,仅该 item 的 Buy 显示 loading String? _loadingProductId; - + /// 待处理的订单信息,用于系统浏览器返回后触发轮询 String? _pendingOrderId; String? _pendingUserId; @@ -67,7 +68,7 @@ class _RechargeScreenState extends State mounted) { setState(() => _loadingProductId = null); } - + // 系统浏览器返回后,触发订单轮询 if (state == AppLifecycleState.resumed && _pendingOrderId != null && @@ -119,7 +120,8 @@ class _RechargeScreenState extends State setState(() { _activities = []; _loadingTiers = false; - _tierError = 'Unable to load plans right now. Please check your connection and try again.'; + _tierError = + 'Unable to load plans right now. Please check your connection and try again.'; }); } } catch (e) { @@ -127,7 +129,8 @@ class _RechargeScreenState extends State setState(() { _activities = []; _loadingTiers = false; - _tierError = 'Something went wrong while loading plans. Please try again.'; + _tierError = + 'Something went wrong while loading plans. Please try again.'; }); } } @@ -237,7 +240,9 @@ class _RechargeScreenState extends State ); } catch (e) { if (mounted) { - _showSnackBar(context, 'Payment could not be completed. Please try again.', isError: true); + _showSnackBar( + context, 'Payment could not be completed. Please try again.', + isError: true); } AdjustEvents.trackPaymentFailed(); } finally { @@ -295,10 +300,11 @@ class _RechargeScreenState extends State if (payUrl != null && payUrl.isNotEmpty) { if (mounted) { setState(() => _loadingProductId = null); - + if (_shouldUseSystemBrowser(openType)) { // 打开系统浏览器 - await launchUrl(Uri.parse(payUrl), mode: LaunchMode.externalApplication); + await launchUrl(Uri.parse(payUrl), + mode: LaunchMode.externalApplication); // 系统浏览器返回后,用户回到应用时会通过 didChangeAppLifecycleState 触发轮询 _pendingOrderId = orderId; _pendingUserId = userId; @@ -340,7 +346,9 @@ class _RechargeScreenState extends State } } catch (e) { if (mounted) { - _showSnackBar(context, 'Payment could not be completed. Please try again.', isError: true); + _showSnackBar( + context, 'Payment could not be completed. Please try again.', + isError: true); } AdjustEvents.trackPaymentFailed(); } finally { @@ -447,7 +455,9 @@ class _RechargeScreenState extends State serverOrderId: orderId, userId: userId); } catch (e) { if (mounted) { - _showSnackBar(context, 'Payment could not be completed. Please try again.', isError: true); + _showSnackBar( + context, 'Payment could not be completed. Please try again.', + isError: true); } AdjustEvents.trackPaymentFailed(); } finally { @@ -525,7 +535,8 @@ class _RechargeScreenState extends State } } catch (e) { if (mounted) { - _showSnackBar(context, 'Google Pay is temporarily unavailable. Please try again.', + _showSnackBar( + context, 'Google Pay is temporarily unavailable. Please try again.', isError: true); } AdjustEvents.trackPaymentFailed(); @@ -556,6 +567,40 @@ class _RechargeScreenState extends State child: TopNavBar( title: 'Recharge', showBackButton: true, + trailing: GestureDetector( + onTap: _loadingProductId != null + ? null + : () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => const PointsHistoryScreen(), + ), + ), + behavior: HitTestBehavior.opaque, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs), + child: Row( + children: [ + Icon( + LucideIcons.list, + size: 14, + color: _loadingProductId != null + ? AppColors.textMuted + : AppColors.primary, + ), + const SizedBox(width: 4), + Text( + 'History', + style: AppTypography.caption.copyWith( + color: _loadingProductId != null + ? AppColors.textMuted + : AppColors.primary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), onBack: _loadingProductId != null ? () {} : () => Navigator.of(context).pop(), @@ -630,19 +675,13 @@ class _RechargeScreenState extends State else ...List.generate(_activities.length, (i) { final item = _activities[i]; - const isRecommended = false; - const isPopular = false; return Padding( padding: EdgeInsets.only( bottom: i < _activities.length - 1 ? AppSpacing.xl : 0, ), child: _TierCardFromActivity( item: item, - badge: isRecommended - ? _TierBadge.recommended - : isPopular - ? _TierBadge.popular - : _TierBadge.none, + badge: _TierBadge.none, loading: _loadingProductId == item.code, onBuy: () => _onBuy(item), ), @@ -782,7 +821,7 @@ class _PaymentMethodItem extends StatelessWidget { Widget build(BuildContext context) { return GestureDetector( onTap: onTap, - child: AnimatedContainer( + child: AnimatedContainer( duration: const Duration(milliseconds: 150), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), constraints: const BoxConstraints(minHeight: 64), @@ -931,6 +970,8 @@ class _TierCardFromActivity extends StatelessWidget { @override Widget build(BuildContext context) { + final isRecommended = badge == _TierBadge.recommended; + final isPopular = badge == _TierBadge.popular; final content = Container( margin: const EdgeInsets.symmetric(horizontal: AppSpacing.screenPadding), padding: const EdgeInsets.symmetric( @@ -1034,11 +1075,14 @@ class _TierCardFromActivity extends StatelessWidget { ), ), child: Text( - badge == _TierBadge.recommended ? 'Recommended' : 'Most Popular', + isRecommended + ? 'Recommended' + : isPopular + ? 'Most Popular' + : '', style: AppTypography.label.copyWith( - color: badge == _TierBadge.recommended - ? AppColors.primary - : AppColors.accentOrange, + color: + isRecommended ? AppColors.primary : AppColors.accentOrange, fontWeight: FontWeight.w600, ), ), diff --git a/lib/shared/widgets/top_nav_bar.dart b/lib/shared/widgets/top_nav_bar.dart index ddc3aa4..c49f2cd 100644 --- a/lib/shared/widgets/top_nav_bar.dart +++ b/lib/shared/widgets/top_nav_bar.dart @@ -11,6 +11,7 @@ class TopNavBar extends StatelessWidget implements PreferredSizeWidget { super.key, required this.title, this.credits, + this.trailing, this.showBackButton = false, this.onBack, this.onCreditsTap, @@ -20,11 +21,14 @@ class TopNavBar extends StatelessWidget implements PreferredSizeWidget { final String title; final String? credits; + final Widget? trailing; final bool showBackButton; final VoidCallback? onBack; final VoidCallback? onCreditsTap; + /// 例如全屏背景页上叠半透明导航栏时用 [Colors.transparent] final Color backgroundColor; + /// 标题与返回键颜色;默认 [AppColors.textPrimary] final Color? foregroundColor; @@ -87,13 +91,14 @@ class TopNavBar extends StatelessWidget implements PreferredSizeWidget { ), ), ), - if (credits != null) + if (trailing != null) + trailing! + else if (credits != null) CreditsBadge( credits: credits!, onTap: onCreditsTap, foregroundColor: foregroundColor, - capsuleColor: - foregroundColor?.withValues(alpha: 0.22), + capsuleColor: foregroundColor?.withValues(alpha: 0.22), ) else const SizedBox(width: 40), diff --git a/pubspec.yaml b/pubspec.yaml index 27ef1b3..886e263 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: pets_hero_ai description: PetsHero AI Application. publish_to: 'none' -version: 1.1.17+28 +version: 1.2.0+120 environment: sdk: '>=3.0.0 <4.0.0'