import 'package:flutter/material.dart'; import '../../../core/data/backend.dart'; import '../../../core/monetization/store.dart'; import '../../../core/theme/tokens.dart'; import '../../../core/ui/app_icons.dart'; import '../home_data.dart'; import '../home_screen.dart' show formatPop; import '../../../core/i18n/app_locale.dart'; /// 아이템 시트 — Figma 사본 「아이템 구입」(헤더 지갑 · `252:160233`) · /// 「아이템 선택」(ZAP 옵션 「아이템」 · `343:204868`). /// /// 두 시트는 제목과 카드 아랫줄만 다르다 — 구입은 「보유 : N」 + 가격, 선택은 「보유 : N개」. /// 구입은 스토어 결제 → `fn-iap-verify` 검증 → 구매자 기여 POP · 팬덤 Drop(원장 B-84)이고 /// 결과는 앱 전역 알림으로 온다([Store.messages]). 사본의 결제 시트(「앱 내 구입」)는 OS 결제 /// 창이라 앱이 그리지 않는다. 구매가 곧 발동이라 보유 재고가 없어 「선택」(사용)은 준비 중이다. Future showItemSheet( BuildContext context, { required List items, required bool purchase, String fandomId = '', }) => showModalBottomSheet( context: context, isScrollControlled: true, // 사본처럼 하단 탭 위를 덮는다. useRootNavigator: true, backgroundColor: Colors.transparent, barrierColor: const Color(0xB3000000), builder: (_) => _ItemSheet(items: items, purchase: purchase, fandomId: fandomId), ); class _ItemSheet extends StatelessWidget { const _ItemSheet({ required this.items, required this.purchase, required this.fandomId, }); final List items; final bool purchase; final String fandomId; void _say(BuildContext context, String message) { ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar(content: Text(message), behavior: SnackBarBehavior.floating), ); } Future _tap(BuildContext context, ItemProduct item) async { if (purchase && fandomId.isEmpty) { // 결제 창을 열기 전에 막는다 — 서버도 소속 행성 없는 구매를 거부한다(E-PAY-01). _say(context, context.l10n.itemJoinFirst); return; } final store = Store.instance; if (!purchase || store == null || item.productId.isEmpty) { _say(context, purchase ? context.l10n.itemBuyNotReady : context.l10n.itemUseNotReady); return; } try { await store.buy(item.productId); } on BackendException catch (error) { if (context.mounted) _say(context, error.message); } on Exception { if (context.mounted) _say(context, context.l10n.payOpenFailed); } } @override Widget build(BuildContext context) => Container( // 사본 `Frame 1984081232` — #101010 · 위 r24 · 패딩 20/0 · 홈 인디케이터 자리까지 같은 색. decoration: const BoxDecoration( color: AppColors.cosmicSurface, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), padding: EdgeInsets.fromLTRB( AppSpace.s4, 20, AppSpace.s4, 20 + MediaQuery.paddingOf(context).bottom, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 6), child: Row( children: [ Expanded( child: Text( purchase ? context.l10n.itemBuy : context.l10n.itemSelect, style: const TextStyle( fontFamily: AppFonts.display, fontSize: 16, fontWeight: FontWeight.w400, height: 1.4, color: AppColors.onCosmic, ), ), ), // 사본 X 는 12 글리프 — 행 높이(22)를 늘리지 않도록 IconButton 대신 쓴다. Semantics( button: true, label: context.l10n.commonClose, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => Navigator.of(context).pop(), child: const ImageIcon( AppIcons.x, size: 20, color: AppColors.onCosmic, ), ), ), ], ), ), const SizedBox(height: AppSpace.s5), if (items.isEmpty) SizedBox( height: 91, child: Center( child: Text( context.l10n.commonComingSoon, style: TextStyle( fontFamily: AppFonts.body, fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.onCosmicMuted, ), ), ), ), for (final (i, item) in items.indexed) ...[ if (i > 0) const SizedBox(height: AppSpace.s4), _ItemCard( item: item, purchase: purchase, onTap: () => _tap(context, item), ), ], ], ), ); } /// 사본 `Button` — 343×91 · #1A1A1C · 1 테두리 #583118 · r8 · 패딩 8/12 · 간격 12. class _ItemCard extends StatelessWidget { const _ItemCard({ required this.item, required this.purchase, required this.onTap, }); final ItemProduct item; final bool purchase; final VoidCallback onTap; static const _chip = TextStyle( fontFamily: AppFonts.body, fontSize: 10, fontWeight: FontWeight.w600, height: 1, ); Widget _tag(String text, Color fill, Color ink) => Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), decoration: BoxDecoration( color: fill, borderRadius: BorderRadius.circular(2), ), child: Text(text, style: _chip.copyWith(color: ink)), ); @override Widget build(BuildContext context) => Material( color: AppColors.cosmicSurfaceContainer, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppRadius.sm), side: const BorderSide(color: Color(0xFF583118)), ), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(AppRadius.sm), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( item.name, style: const TextStyle( fontFamily: AppFonts.body, fontSize: 14, fontWeight: FontWeight.w600, height: 1.4, color: AppColors.onCosmic, ), ), const SizedBox(width: 8), _tag( context.l10n.itemSendPop(formatPop(item.dropPop)), const Color(0xFF512F24), AppColors.cosmicOrange, ), const SizedBox(width: 6), _tag( context.l10n.itemBuyerPop(formatPop(item.buyerPop)), const Color(0xFF204037), const Color(0xFF43AD86), ), ], ), const SizedBox(height: 6), Text( item.description, style: const TextStyle( fontFamily: AppFonts.body, fontSize: 12, fontWeight: FontWeight.w500, height: 1.4, color: AppColors.onCosmicMuted, ), ), const SizedBox(height: 12), Row( children: [ Text( purchase ? context.l10n.itemOwned('${item.owned}') : context.l10n.itemOwnedCount('${item.owned}'), style: TextStyle( fontFamily: AppFonts.body, fontSize: 14, fontWeight: purchase ? FontWeight.w500 : FontWeight.w600, height: 1.4, color: purchase ? const Color(0xFFDDDDDD) : AppColors.onCosmic, ), ), const Spacer(), if (purchase) Text( '₩${formatPop(item.priceWon)}', style: const TextStyle( fontFamily: AppFonts.body, fontSize: 14, fontWeight: FontWeight.w600, height: 1.4, color: AppColors.onCosmic, ), ), ], ), ], ), ), ), ); }