Files

272 lines
8.8 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<void> showItemSheet(
BuildContext context, {
required List<ItemProduct> items,
required bool purchase,
String fandomId = '',
}) => showModalBottomSheet<void>(
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<ItemProduct> 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<void> _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,
),
),
],
),
],
),
),
),
);
}