EnakPoint & EnakCoin wallet, PIN, outlet, order, community, reward
- Wallet EnakPoint & EnakCoin: beranda, riwayat, saldo kedaluwarsa, kode bayar, tukar EnakCoin, transfer - PIN (buat, ganti, lupa), outlet, riwayat order, game spin - Halaman community, webview, coming soon, dan ikon menu beranda baru - Redesign Reward Saya: kartu tiket, filter chip, detail kode + QR - Hapus customer point loader, menu, dan checkout lama - Base URL ke enaklo-pos-api.altru.id Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5954510b93
commit
f5e3075205
@@ -0,0 +1,651 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||
|
||||
import '../../../application/wallet/exchange_form/exchange_form_bloc.dart';
|
||||
import '../../../application/wallet/wallet_history_loader/wallet_history_loader_bloc.dart';
|
||||
import '../../../application/wallet/wallet_loader/wallet_loader_bloc.dart';
|
||||
import '../../../common/extension/extension.dart';
|
||||
import '../../../common/function/app_function.dart';
|
||||
import '../../../common/theme/theme.dart';
|
||||
import '../../../domain/wallet/wallet.dart';
|
||||
import '../../../injection.dart';
|
||||
import '../../components/button/button.dart';
|
||||
import '../../components/toast/flushbar.dart';
|
||||
import '../pin/pin_guard.dart';
|
||||
import '../pin/widgets/pin_sheet.dart';
|
||||
import 'wallet_ui.dart';
|
||||
import 'widgets/wallet_confirm_dialog.dart';
|
||||
import 'widgets/wallet_history_section.dart';
|
||||
import 'widgets/wallet_result_view.dart';
|
||||
|
||||
/// Tukar EnakCoin → EnakPoint (§8.1).
|
||||
@RoutePage()
|
||||
class ExchangePage extends StatefulWidget implements AutoRouteWrapper {
|
||||
const ExchangePage({super.key});
|
||||
|
||||
@override
|
||||
State<ExchangePage> createState() => _ExchangePageState();
|
||||
|
||||
@override
|
||||
Widget wrappedRoute(BuildContext context) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (_) => getIt<ExchangeFormBloc>()),
|
||||
// Riwayat dilihat dari sisi EnakPoint: berapa poin yang didapat.
|
||||
BlocProvider(
|
||||
create: (_) => getIt<WalletHistoryLoaderBloc>()
|
||||
..add(
|
||||
const WalletHistoryLoaderEvent.filterChanged(
|
||||
types: [WalletTransactionType.exchangeIn],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: this,
|
||||
);
|
||||
}
|
||||
|
||||
class _ExchangePageState extends State<ExchangePage> {
|
||||
final TextEditingController _coinsController = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
Timer? _debounce;
|
||||
|
||||
/// Kurs terakhir yang diketahui, tetap ditampilkan selama preview baru
|
||||
/// sedang dihitung.
|
||||
ExchangePreview? _rate;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
context.read<WalletLoaderBloc>().add(const WalletLoaderEvent.fetched());
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounce?.cancel();
|
||||
_coinsController.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
final position = _scrollController.position;
|
||||
if (position.pixels >= position.maxScrollExtent - 200) {
|
||||
context.read<WalletHistoryLoaderBloc>().add(
|
||||
const WalletHistoryLoaderEvent.loadedMore(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onRefresh() async {
|
||||
context.read<WalletLoaderBloc>().add(const WalletLoaderEvent.fetched());
|
||||
final history = context.read<WalletHistoryLoaderBloc>();
|
||||
history.add(const WalletHistoryLoaderEvent.refreshed());
|
||||
await history.stream.firstWhere((s) => !s.isFetching);
|
||||
}
|
||||
|
||||
void _onCoinsChanged(String value) {
|
||||
_debounce?.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 500), () {
|
||||
if (!mounted) return;
|
||||
context.read<ExchangeFormBloc>().add(
|
||||
ExchangeFormEvent.coinsChanged(int.tryParse(digitsOnly(value)) ?? 0),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Isi jumlah langsung tanpa debounce, mis. dari tombol "Tukar semua".
|
||||
void _fillCoins(int coins) {
|
||||
_debounce?.cancel();
|
||||
final text = coins.thousandFormat;
|
||||
_coinsController.value = TextEditingValue(
|
||||
text: text,
|
||||
selection: TextSelection.collapsed(offset: text.length),
|
||||
);
|
||||
context.read<ExchangeFormBloc>().add(ExchangeFormEvent.coinsChanged(coins));
|
||||
}
|
||||
|
||||
/// Bulatkan ke bawah ke kelipatan kurs bila kursnya sudah diketahui.
|
||||
int _roundToRate(int coins) {
|
||||
final step = _rate?.coinAmount ?? 0;
|
||||
return step > 0 ? coins - coins % step : coins;
|
||||
}
|
||||
|
||||
Future<void> _confirm(ExchangeFormState state) async {
|
||||
dismissKeyboard(context);
|
||||
final preview = state.preview!;
|
||||
final confirmed = await showWalletConfirmDialog(
|
||||
context,
|
||||
title: 'Tukar EnakCoin?',
|
||||
message:
|
||||
'Tukar ${preview.coins.thousandFormat} EnakCoin menjadi '
|
||||
'${preview.points.thousandFormat} EnakPoint. Penukaran tidak bisa '
|
||||
'dibatalkan.',
|
||||
confirmLabel: 'Tukar',
|
||||
);
|
||||
if (!confirmed || !mounted) return;
|
||||
if (!await ensurePinReady(context) || !mounted) return;
|
||||
|
||||
final success = await showPinSheet(
|
||||
context,
|
||||
title: 'Tukar EnakCoin',
|
||||
onSubmit: (pin) async {
|
||||
final bloc = context.read<ExchangeFormBloc>();
|
||||
bloc.add(ExchangeFormEvent.submitted(pin));
|
||||
final result = await bloc.stream.firstWhere((s) => !s.isSubmitting);
|
||||
return result.failureOption.toNullable();
|
||||
},
|
||||
);
|
||||
|
||||
if (success && mounted) {
|
||||
context.read<WalletLoaderBloc>().add(const WalletLoaderEvent.fetched());
|
||||
}
|
||||
}
|
||||
|
||||
void _showFailure(WalletFailure failure) {
|
||||
if (failure.isPinError) return;
|
||||
|
||||
if (failure.isNetworkError) {
|
||||
AppFlushbar.showError(
|
||||
context,
|
||||
'Koneksi terputus, status penukaran belum diketahui. Tekan Tukar lagi '
|
||||
'untuk mengulang. Saldo tidak akan terpotong dua kali.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
AppFlushbar.showError(
|
||||
context,
|
||||
walletFailureMessage(
|
||||
failure,
|
||||
rejectedMessage:
|
||||
'Penukaran tidak bisa diproses. Periksa jumlah dan saldo '
|
||||
'EnakCoin kamu.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<ExchangeFormBloc, ExchangeFormState>(
|
||||
listenWhen: (p, c) => p.failureOption != c.failureOption,
|
||||
listener: (context, state) =>
|
||||
state.failureOption.fold(() {}, _showFailure),
|
||||
),
|
||||
BlocListener<ExchangeFormBloc, ExchangeFormState>(
|
||||
listenWhen: (p, c) => p.preview != c.preview && c.preview != null,
|
||||
listener: (context, state) => setState(() => _rate = state.preview),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<ExchangeFormBloc, ExchangeFormState>(
|
||||
builder: (context, state) {
|
||||
final result = state.result;
|
||||
return Scaffold(
|
||||
backgroundColor: AppColor.background,
|
||||
appBar: AppBar(title: const Text('Tukar EnakCoin')),
|
||||
body: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: result != null
|
||||
? KeyedSubtree(
|
||||
key: const ValueKey('success'),
|
||||
child: _buildSuccess(result),
|
||||
)
|
||||
: KeyedSubtree(
|
||||
key: const ValueKey('form'),
|
||||
child: _buildForm(state),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildForm(ExchangeFormState state) {
|
||||
return RefreshIndicator(
|
||||
color: AppColor.primary,
|
||||
onRefresh: _onRefresh,
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 28),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: BlocBuilder<WalletLoaderBloc, WalletLoaderState>(
|
||||
buildWhen: (p, c) =>
|
||||
p.summary != c.summary || p.hasLoaded != c.hasLoaded,
|
||||
builder: (context, walletState) =>
|
||||
_buildFormContent(state, walletState),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 32),
|
||||
sliver: WalletHistorySection(
|
||||
title: 'Riwayat penukaran',
|
||||
emptyIcon: Icons.currency_exchange_rounded,
|
||||
emptyTitle: 'Belum ada penukaran',
|
||||
emptySubtitle: (_) =>
|
||||
'EnakPoint yang kamu dapat dari tukar EnakCoin akan '
|
||||
'muncul di sini.',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormContent(
|
||||
ExchangeFormState state,
|
||||
WalletLoaderState walletState,
|
||||
) {
|
||||
final summary = walletState.summary;
|
||||
final coinBalance = summary.coinBalance;
|
||||
final exceedsBalance = walletState.hasLoaded && state.coins > coinBalance;
|
||||
final preview = state.preview;
|
||||
final points = preview != null && preview.valid && !exceedsBalance
|
||||
? preview.points
|
||||
: 0;
|
||||
final maxCoins = _roundToRate(coinBalance);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_ExchangeCard(
|
||||
label: 'Kamu tukar',
|
||||
currency: WalletCurrency.coin,
|
||||
trailing: Text(
|
||||
walletState.hasLoaded
|
||||
? 'Saldo ${coinBalance.thousandFormat}'
|
||||
: 'Memuat saldo…',
|
||||
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _coinsController,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.done,
|
||||
enabled: !state.isSubmitting,
|
||||
cursorColor: AppColor.primary,
|
||||
inputFormatters: [ThousandsInputFormatter()],
|
||||
onChanged: _onCoinsChanged,
|
||||
style: AppStyle.h4.copyWith(fontWeight: FontWeight.w700),
|
||||
decoration: InputDecoration(
|
||||
hintText: '0',
|
||||
hintStyle: AppStyle.h4.copyWith(
|
||||
color: AppColor.textLight,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
disabledBorder: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: _QuickChip(
|
||||
label: 'Tukar semua',
|
||||
onTap:
|
||||
walletState.hasLoaded &&
|
||||
maxCoins > 0 &&
|
||||
!state.isSubmitting
|
||||
? () => _fillCoins(maxCoins)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
_ExchangeCard(
|
||||
label: 'Kamu dapat',
|
||||
currency: WalletCurrency.point,
|
||||
highlighted: points > 0,
|
||||
trailing: Text(
|
||||
walletState.hasLoaded
|
||||
? 'Saldo ${summary.pointBalance.thousandFormat}'
|
||||
: '',
|
||||
style: AppStyle.sm.copyWith(color: AppColor.textSecondary),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: state.isPreviewing ? 0.35 : 1,
|
||||
child: TweenAnimationBuilder<int>(
|
||||
tween: IntTween(begin: 0, end: points),
|
||||
duration: const Duration(milliseconds: 450),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, value, _) => Text(
|
||||
value.thousandFormat,
|
||||
style: AppStyle.h4.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: points > 0
|
||||
? AppColor.success
|
||||
: AppColor.textLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Positioned(
|
||||
top: -24,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(child: _SwapBadge()),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
alignment: Alignment.topCenter,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: _buildStatus(state, exceedsBalance),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
AppElevatedButton(
|
||||
title: 'Tukar',
|
||||
height: 48,
|
||||
isLoading: state.isSubmitting,
|
||||
onPressed: state.canSubmit && !exceedsBalance
|
||||
? () => _confirm(state)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Satu baris keterangan di bawah kartu: kurs, proses hitung, atau error.
|
||||
Widget _buildStatus(ExchangeFormState state, bool exceedsBalance) {
|
||||
if (state.isPreviewing) {
|
||||
return const _StatusLine(
|
||||
key: ValueKey('previewing'),
|
||||
icon: SpinKitThreeBounce(color: AppColor.primary, size: 14),
|
||||
text: 'Menghitung…',
|
||||
);
|
||||
}
|
||||
|
||||
if (exceedsBalance) {
|
||||
return const _StatusLine(
|
||||
key: ValueKey('exceeds'),
|
||||
icon: Icon(
|
||||
Icons.error_outline_rounded,
|
||||
size: 16,
|
||||
color: AppColor.error,
|
||||
),
|
||||
text: 'Melebihi saldo EnakCoin kamu',
|
||||
color: AppColor.error,
|
||||
);
|
||||
}
|
||||
|
||||
final previewFailure = state.previewFailureOption.toNullable();
|
||||
if (previewFailure != null) {
|
||||
return _StatusLine(
|
||||
key: const ValueKey('failure'),
|
||||
icon: const Icon(
|
||||
Icons.error_outline_rounded,
|
||||
size: 16,
|
||||
color: AppColor.error,
|
||||
),
|
||||
text: walletFailureMessage(
|
||||
previewFailure,
|
||||
rejectedMessage: 'Jumlah EnakCoin tidak valid.',
|
||||
),
|
||||
color: AppColor.error,
|
||||
);
|
||||
}
|
||||
|
||||
final preview = state.preview;
|
||||
if (preview != null && !preview.valid) {
|
||||
final rounded = _roundToRate(state.coins);
|
||||
return _StatusLine(
|
||||
key: const ValueKey('invalid'),
|
||||
icon: const Icon(
|
||||
Icons.error_outline_rounded,
|
||||
size: 16,
|
||||
color: AppColor.error,
|
||||
),
|
||||
text:
|
||||
preview.reason ??
|
||||
'Jumlah harus kelipatan ${preview.coinAmount.thousandFormat} '
|
||||
'EnakCoin.',
|
||||
color: AppColor.error,
|
||||
action: rounded > 0 && rounded != state.coins
|
||||
? _QuickChip(
|
||||
label: 'Pakai ${rounded.thousandFormat}',
|
||||
onTap: () => _fillCoins(rounded),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
final rate = _rate;
|
||||
if (rate == null) {
|
||||
return const _StatusLine(
|
||||
key: ValueKey('hint'),
|
||||
icon: Icon(
|
||||
Icons.info_outline_rounded,
|
||||
size: 16,
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
text: 'Masukkan jumlah EnakCoin untuk melihat kurs penukaran.',
|
||||
);
|
||||
}
|
||||
|
||||
return _StatusLine(
|
||||
key: const ValueKey('rate'),
|
||||
icon: const Icon(
|
||||
Icons.currency_exchange_rounded,
|
||||
size: 16,
|
||||
color: AppColor.textSecondary,
|
||||
),
|
||||
text:
|
||||
'Kurs ${rate.coinAmount.thousandFormat} EnakCoin = '
|
||||
'${rate.pointAmount.thousandFormat} EnakPoint · penukaran tidak '
|
||||
'bisa dibatalkan',
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuccess(ExchangeResult result) {
|
||||
return WalletResultView(
|
||||
title: 'Penukaran berhasil',
|
||||
amountText: '+${result.points.thousandFormat} EnakPoint',
|
||||
lines: [
|
||||
'Ditukar dari ${result.coins.thousandFormat} EnakCoin',
|
||||
'Saldo sekarang: ${result.pointBalance.thousandFormat} EnakPoint · '
|
||||
'${result.coinBalance.thousandFormat} EnakCoin',
|
||||
...result.lots
|
||||
.where((lot) => lot.expiresAt != null)
|
||||
.map(
|
||||
(lot) =>
|
||||
'${lot.amount.thousandFormat} EnakPoint ini berlaku sampai '
|
||||
'${formatWibDate(lot.expiresAt!)}',
|
||||
),
|
||||
],
|
||||
onDone: () => context.router.maybePop(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Kartu satu sisi penukaran (asal atau tujuan).
|
||||
class _ExchangeCard extends StatelessWidget {
|
||||
const _ExchangeCard({
|
||||
required this.label,
|
||||
required this.currency,
|
||||
required this.trailing,
|
||||
required this.child,
|
||||
this.highlighted = false,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final WalletCurrency currency;
|
||||
final Widget trailing;
|
||||
final Widget child;
|
||||
final bool highlighted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
|
||||
decoration: BoxDecoration(
|
||||
color: highlighted ? AppColor.successWithOpacity(0.06) : AppColor.white,
|
||||
borderRadius: BorderRadius.circular(AppValue.borderRadius),
|
||||
border: Border.all(
|
||||
color: highlighted
|
||||
? AppColor.successWithOpacity(0.4)
|
||||
: AppColor.white,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: AppStyle.sm.copyWith(
|
||||
color: AppColor.textSecondary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
trailing,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: child),
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(6, 6, 12, 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.background,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
WalletCurrencyIcon(currency: currency, size: 24),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
currency.label,
|
||||
style: AppStyle.sm.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Lingkaran panah di antara dua kartu.
|
||||
class _SwapBadge extends StatelessWidget {
|
||||
const _SwapBadge();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primary,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColor.background, width: 4),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.arrow_downward_rounded,
|
||||
size: 18,
|
||||
color: AppColor.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _QuickChip extends StatelessWidget {
|
||||
const _QuickChip({required this.label, required this.onTap});
|
||||
|
||||
final String label;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final enabled = onTap != null;
|
||||
return Material(
|
||||
color: enabled ? AppColor.primaryWithOpacity(0.08) : AppColor.background,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: Text(
|
||||
label,
|
||||
style: AppStyle.xs.copyWith(
|
||||
color: enabled ? AppColor.primary : AppColor.textLight,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusLine extends StatelessWidget {
|
||||
const _StatusLine({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.text,
|
||||
this.color = AppColor.textSecondary,
|
||||
this.action,
|
||||
});
|
||||
|
||||
final Widget icon;
|
||||
final String text;
|
||||
final Color color;
|
||||
final Widget? action;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 20, child: Center(child: icon)),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(text, style: AppStyle.xs.copyWith(color: color)),
|
||||
),
|
||||
if (action != null) ...[const SizedBox(width: 8), action!],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user