291 lines
9.8 KiB
Dart
291 lines
9.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../../l10n/app_localizations.dart';
|
|
import '../../models/payment_card_model.dart';
|
|
import '../../providers/payment_card_provider.dart';
|
|
import '../../utils/payment_card_utils.dart';
|
|
|
|
class PaymentCardFormSheet extends StatefulWidget {
|
|
final PaymentCardModel? card;
|
|
final String? initialIssuerName;
|
|
final String? initialLast4;
|
|
final String? initialColorHex;
|
|
final String? initialIconName;
|
|
|
|
const PaymentCardFormSheet({
|
|
super.key,
|
|
this.card,
|
|
this.initialIssuerName,
|
|
this.initialLast4,
|
|
this.initialColorHex,
|
|
this.initialIconName,
|
|
});
|
|
|
|
static Future<String?> show(
|
|
BuildContext context, {
|
|
PaymentCardModel? card,
|
|
String? initialIssuerName,
|
|
String? initialLast4,
|
|
String? initialColorHex,
|
|
String? initialIconName,
|
|
}) async {
|
|
return showModalBottomSheet<String>(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
useSafeArea: true,
|
|
builder: (_) => PaymentCardFormSheet(
|
|
card: card,
|
|
initialIssuerName: initialIssuerName,
|
|
initialLast4: initialLast4,
|
|
initialColorHex: initialColorHex,
|
|
initialIconName: initialIconName,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
State<PaymentCardFormSheet> createState() => _PaymentCardFormSheetState();
|
|
}
|
|
|
|
class _PaymentCardFormSheetState extends State<PaymentCardFormSheet> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
late TextEditingController _issuerController;
|
|
late TextEditingController _last4Controller;
|
|
late String _selectedColor;
|
|
late String _selectedIcon;
|
|
late bool _isDefault;
|
|
bool _isSaving = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_issuerController = TextEditingController(
|
|
text: widget.card?.issuerName ?? widget.initialIssuerName ?? '',
|
|
);
|
|
_last4Controller = TextEditingController(
|
|
text: widget.card?.last4 ?? widget.initialLast4 ?? '',
|
|
);
|
|
_selectedColor = widget.card?.colorHex ??
|
|
widget.initialColorHex ??
|
|
PaymentCardUtils.colorPalette.first;
|
|
_selectedIcon = widget.card?.iconName ??
|
|
widget.initialIconName ??
|
|
PaymentCardUtils.iconMap.keys.first;
|
|
_isDefault = widget.card?.isDefault ?? false;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_issuerController.dispose();
|
|
_last4Controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final loc = AppLocalizations.of(context);
|
|
final isEditing = widget.card != null;
|
|
|
|
return Padding(
|
|
padding: EdgeInsets.only(
|
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
|
),
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.fromLTRB(24, 24, 24, 32),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(
|
|
isEditing ? loc.editPaymentCard : loc.addPaymentCard,
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
const Spacer(),
|
|
IconButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
icon: const Icon(Icons.close),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
TextFormField(
|
|
controller: _issuerController,
|
|
decoration: InputDecoration(
|
|
labelText: loc.paymentCardIssuer,
|
|
border: const OutlineInputBorder(),
|
|
),
|
|
textInputAction: TextInputAction.next,
|
|
validator: (value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return loc.requiredFieldsError;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextFormField(
|
|
controller: _last4Controller,
|
|
decoration: InputDecoration(
|
|
labelText: loc.paymentCardLast4,
|
|
border: const OutlineInputBorder(),
|
|
counterText: '',
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
inputFormatters: [
|
|
FilteringTextInputFormatter.digitsOnly,
|
|
LengthLimitingTextInputFormatter(4),
|
|
],
|
|
validator: (value) {
|
|
if (value == null || value.length != 4) {
|
|
return loc.paymentCardLast4;
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
loc.paymentCardColor,
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: PaymentCardUtils.colorPalette.map((hex) {
|
|
final color = PaymentCardUtils.colorFromHex(hex);
|
|
final selected = _selectedColor == hex;
|
|
return GestureDetector(
|
|
onTap: () {
|
|
setState(() {
|
|
_selectedColor = hex;
|
|
});
|
|
},
|
|
child: Container(
|
|
width: 36,
|
|
height: 36,
|
|
decoration: BoxDecoration(
|
|
color: color,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: selected
|
|
? Theme.of(context).colorScheme.onSurface
|
|
: Colors.transparent,
|
|
width: 2,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
loc.paymentCardIcon,
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: PaymentCardUtils.iconMap.entries.map((entry) {
|
|
final selected = _selectedIcon == entry.key;
|
|
return ChoiceChip(
|
|
label: Icon(entry.value,
|
|
color: selected
|
|
? Theme.of(context).colorScheme.onPrimary
|
|
: Theme.of(context).colorScheme.onSurface),
|
|
selected: selected,
|
|
onSelected: (_) {
|
|
setState(() {
|
|
_selectedIcon = entry.key;
|
|
});
|
|
},
|
|
selectedColor: Theme.of(context).colorScheme.primary,
|
|
backgroundColor: Theme.of(context)
|
|
.colorScheme
|
|
.surfaceContainerHighest,
|
|
);
|
|
}).toList(),
|
|
),
|
|
const SizedBox(height: 16),
|
|
SwitchListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
title: Text(loc.setAsDefaultCard),
|
|
value: _isDefault,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
_isDefault = value;
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(height: 24),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: FilledButton(
|
|
onPressed: _isSaving ? null : _handleSubmit,
|
|
child: _isSaving
|
|
? const SizedBox(
|
|
width: 18,
|
|
height: 18,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: Text(loc.save),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _handleSubmit() async {
|
|
if (!_formKey.currentState!.validate()) return;
|
|
setState(() {
|
|
_isSaving = true;
|
|
});
|
|
try {
|
|
final provider = context.read<PaymentCardProvider>();
|
|
String cardId;
|
|
if (widget.card == null) {
|
|
final card = await provider.addCard(
|
|
issuerName: _issuerController.text.trim(),
|
|
last4: _last4Controller.text.trim(),
|
|
colorHex: _selectedColor,
|
|
iconName: _selectedIcon,
|
|
isDefault: _isDefault,
|
|
);
|
|
cardId = card.id;
|
|
} else {
|
|
widget.card!
|
|
..issuerName = _issuerController.text.trim()
|
|
..last4 = _last4Controller.text.trim()
|
|
..colorHex = _selectedColor
|
|
..iconName = _selectedIcon
|
|
..isDefault = _isDefault;
|
|
await provider.updateCard(widget.card!);
|
|
cardId = widget.card!.id;
|
|
}
|
|
if (mounted) {
|
|
Navigator.of(context).pop(cardId);
|
|
}
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_isSaving = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|