HOME · GALAXY · 행성·성운 상세 · 팬덤 라운지 · 이클립스 화면
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user