invoice/lib/ui/company_gateway/edit/company_gateway_edit.dart

707 lines
23 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/data/models/company_gateway_model.dart';
import 'package:invoiceninja_flutter/data/models/entities.dart';
import 'package:invoiceninja_flutter/redux/static/static_selectors.dart';
import 'package:invoiceninja_flutter/ui/app/entity_dropdown.dart';
import 'package:invoiceninja_flutter/ui/app/form_card.dart';
import 'package:invoiceninja_flutter/ui/app/forms/app_form.dart';
import 'package:invoiceninja_flutter/ui/app/forms/color_picker.dart';
import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart';
import 'package:invoiceninja_flutter/ui/app/invoice/tax_rate_dropdown.dart';
import 'package:invoiceninja_flutter/ui/company_gateway/edit/company_gateway_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/settings/settings_scaffold.dart';
import 'package:invoiceninja_flutter/utils/completers.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/utils/strings.dart';
class CompanyGatewayEdit extends StatefulWidget {
const CompanyGatewayEdit({
Key key,
@required this.viewModel,
}) : super(key: key);
final CompanyGatewayEditVM viewModel;
@override
_CompanyGatewayEditState createState() => _CompanyGatewayEditState();
}
class _CompanyGatewayEditState extends State<CompanyGatewayEdit>
with SingleTickerProviderStateMixin {
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final _debouncer = Debouncer();
final FocusScopeNode _focusNode = FocusScopeNode();
TabController _controller;
@override
void initState() {
super.initState();
_controller = TabController(vsync: this, length: 3);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final viewModel = widget.viewModel;
final state = viewModel.state;
final localization = AppLocalization.of(context);
final companyGateway = viewModel.companyGateway;
return SettingsScaffold(
title: viewModel.companyGateway.isNew
? localization.newCompanyGateway
: companyGateway.gateway.name,
onSavePressed: viewModel.onSavePressed,
appBarBottom: TabBar(
key: ValueKey(state.settingsUIState.updatedAt),
controller: _controller,
tabs: [
Tab(
text: localization.credentials,
),
Tab(
text: localization.settings,
),
Tab(
text: localization.limitsAndFees,
),
],
),
body: AppTabForm(
formKey: _formKey,
focusNode: _focusNode,
tabController: _controller,
children: <Widget>[
ListView(
children: <Widget>[
FormCard(
children: <Widget>[
if (companyGateway.isNew)
EntityDropdown(
key: ValueKey('__gateway_${companyGateway.gatewayId}__'),
entityType: EntityType.gateway,
entityMap: state.staticState.gatewayMap,
entityList:
memoizedGatewayList(state.staticState.gatewayMap),
labelText: localization.provider,
initialValue: state.staticState
.gatewayMap[companyGateway.gatewayId]?.name,
onSelected: (SelectableEntity gateway) =>
viewModel.onChanged(
companyGateway.rebuild((b) => b
..gatewayId = gateway.id
..gatewayTypeId = null
..config =
''), // TODO set to gateway.defaultGatewayTypeId
),
//onFieldSubmitted: (String value) => _node.nextFocus(),
),
GatewayConfigSettings(
key: ValueKey('__${companyGateway.gatewayId}__'),
companyGateway: companyGateway,
viewModel: viewModel,
),
],
),
],
),
ListView(
children: <Widget>[
FormCard(
children: <Widget>[
SwitchListTile(
activeColor: Theme.of(context).accentColor,
title: Text(localization.billingAddress),
subtitle: Text(localization.requireBillingAddressHelp),
value: companyGateway.showBillingAddress,
onChanged: (value) => viewModel.onChanged(companyGateway
.rebuild((b) => b..showBillingAddress = value)),
),
SwitchListTile(
activeColor: Theme.of(context).accentColor,
title: Text(localization.shippingAddress),
subtitle: Text(localization.requireShippingAddressHelp),
value: companyGateway.showShippingAddress,
onChanged: (value) => viewModel.onChanged(companyGateway
.rebuild((b) => b..showShippingAddress = value)),
),
SwitchListTile(
activeColor: Theme.of(context).accentColor,
title: Text(localization.updateAddress),
subtitle: Text(localization.updateAddressHelp),
value: companyGateway.updateDetails,
onChanged: (value) => viewModel.onChanged(companyGateway
.rebuild((b) => b..updateDetails = value)),
),
],
),
FormCard(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding:
const EdgeInsets.only(left: 16, top: 16, bottom: 16),
child: Text(
localization.acceptedCardLogos,
style: Theme.of(context).textTheme.subhead,
),
),
CardListTile(
viewModel: viewModel,
cardType: kCardTypeVisa,
paymentType: kPaymentTypeVisa,
),
CardListTile(
viewModel: viewModel,
cardType: kCardTypeMasterCard,
paymentType: kPaymentTypeMasterCard,
),
CardListTile(
viewModel: viewModel,
cardType: kCardTypeAmEx,
paymentType: kPaymentTypeAmEx,
),
CardListTile(
viewModel: viewModel,
cardType: kCardTypeDiscover,
paymentType: kPaymentTypeDiscover,
),
CardListTile(
viewModel: viewModel,
cardType: kCardTypeDiners,
paymentType: kPaymentTypeDiners,
),
],
)
],
),
ListView(
children: <Widget>[
LimitEditor(
viewModel: viewModel,
companyGateway: companyGateway,
),
FeesEditor(
viewModel: viewModel,
companyGateway: companyGateway,
),
],
),
],
),
);
}
}
class CardListTile extends StatelessWidget {
const CardListTile({this.viewModel, this.paymentType, this.cardType});
final CompanyGatewayEditVM viewModel;
final String paymentType;
final int cardType;
@override
Widget build(BuildContext context) {
final staticState = viewModel.state.staticState;
final companyGateway = viewModel.companyGateway;
return CheckboxListTile(
controlAffinity: ListTileControlAffinity.leading,
activeColor: Theme.of(context).accentColor,
title: Text(staticState.paymentTypeMap[paymentType]?.name ?? ''),
value: companyGateway.supportsCard(cardType),
onChanged: (value) => viewModel.onChanged(value
? companyGateway.addCard(cardType)
: companyGateway.removeCard(cardType)),
);
}
}
class GatewayConfigSettings extends StatelessWidget {
const GatewayConfigSettings({Key key, this.companyGateway, this.viewModel})
: super(key: key);
final CompanyGatewayEntity companyGateway;
final CompanyGatewayEditVM viewModel;
Map<String, String> getGatewayTypes(BuildContext context) {
final localization = AppLocalization.of(context);
switch (companyGateway.gatewayId) {
case kGatewayStripe:
return {
kGatewayTypeCreditCard: localization.creditCard,
kGatewayTypeBankTransfer: localization.bankTransfer,
};
default:
return null;
}
}
@override
Widget build(BuildContext context) {
final state = viewModel.state;
final localization = AppLocalization.of(context);
final gateway = state.staticState.gatewayMap[companyGateway.gatewayId];
if (gateway == null) {
return SizedBox();
}
final gatewayTypes = getGatewayTypes(context);
return Column(
children: [
if (gatewayTypes != null)
InputDecorator(
decoration: InputDecoration(
labelText: localization.paymentType,
),
isEmpty: companyGateway.gatewayTypeId == null,
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: companyGateway.gatewayTypeId,
isExpanded: true,
isDense: true,
onChanged: (value) => viewModel.onChanged(
companyGateway.rebuild((b) => b..gatewayTypeId = value)),
items: gatewayTypes
.map((id, type) =>
MapEntry<String, DropdownMenuItem<String>>(
id,
DropdownMenuItem<String>(
child: Text(localization.lookup(type)),
value: id,
)))
.values
.toList()),
),
),
...gateway.parsedFields.keys
.map((field) => GatewayConfigField(
field: field,
value: companyGateway.parsedConfig[field],
defaultValue: gateway.parsedFields[field],
onChanged: (dynamic value) {
viewModel
.onChanged(companyGateway.updateConfig(field, value));
},
))
.toList()
],
);
}
}
class GatewayConfigField extends StatefulWidget {
const GatewayConfigField({
Key key,
@required this.field,
@required this.value,
@required this.defaultValue,
@required this.onChanged,
}) : super(key: key);
final String field;
final dynamic value;
final dynamic defaultValue;
final Function(dynamic) onChanged;
@override
_GatewayConfigFieldState createState() => _GatewayConfigFieldState();
}
class _GatewayConfigFieldState extends State<GatewayConfigField> {
bool autoValidate = false;
TextEditingController _textController;
@override
void initState() {
super.initState();
_textController = TextEditingController();
}
@override
void dispose() {
_textController.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
_textController.removeListener(_onChanged);
_textController.text = (widget.value ?? widget.defaultValue).toString();
_textController.addListener(_onChanged);
super.didChangeDependencies();
}
void _onChanged() {
widget.onChanged(_textController.text.trim());
}
bool _obscureText(String field) {
bool obscure = false;
['password', 'secret', 'key'].forEach((word) {
if (field.toLowerCase().contains(word)) {
obscure = true;
}
});
return obscure;
}
@override
Widget build(BuildContext context) {
if ('${widget.defaultValue}'.startsWith('[') &&
'${widget.defaultValue}'.endsWith(']')) {
final options = [
'',
...'${widget.defaultValue}'
.replaceFirst('[', '')
.replaceFirst(']', '')
.split(',')
];
final dynamic value =
widget.value == widget.defaultValue ? '' : widget.value;
return InputDecorator(
decoration: InputDecoration(
labelText: toTitleCase(widget.field),
),
isEmpty: value == null && value != '',
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
isDense: true,
value: value,
onChanged: (value) => widget.onChanged(value),
items: options
.map((value) => DropdownMenuItem<String>(
child: Text(value.trim()),
value: value.trim(),
))
.toList(),
),
),
);
} else if (widget.field.toLowerCase().contains('color')) {
return FormColorPicker(
initialValue: widget.value,
labelText: toTitleCase(widget.field),
onSelected: (value) => widget.onChanged(value),
);
} else if (widget.defaultValue.runtimeType == bool) {
return CheckboxListTile(
controlAffinity: ListTileControlAffinity.leading,
activeColor: Theme.of(context).accentColor,
title: Text(toTitleCase(widget.field)),
value: widget.value ?? false,
onChanged: (value) => widget.onChanged(value),
);
} else {
return TextFormField(
controller: _textController,
decoration: InputDecoration(
labelText: toTitleCase(widget.field),
),
onChanged: (value) => _onChanged(),
obscureText: _obscureText(widget.field),
);
}
}
}
class LimitEditor extends StatefulWidget {
const LimitEditor({this.companyGateway, this.viewModel});
final CompanyGatewayEntity companyGateway;
final CompanyGatewayEditVM viewModel;
@override
_LimitEditorState createState() => _LimitEditorState();
}
class _LimitEditorState extends State<LimitEditor> {
bool _enableMin = false;
bool _enableMax = false;
TextEditingController _minController;
TextEditingController _maxController;
@override
void initState() {
super.initState();
_minController = TextEditingController();
_maxController = TextEditingController();
}
@override
void dispose() {
_minController.dispose();
_maxController.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
_minController.removeListener(_onChanged);
_maxController.removeListener(_onChanged);
final companyGateway = widget.companyGateway;
if (companyGateway.minLimit != null) {
_enableMin = true;
}
if (companyGateway.maxLimit != null) {
_enableMax = true;
}
_minController.text = formatNumber(
(companyGateway.minLimit ?? 0).toDouble(), context,
formatNumberType: FormatNumberType.input);
_maxController.text = formatNumber(
(companyGateway.maxLimit ?? 0).toDouble(), context,
formatNumberType: FormatNumberType.input);
_minController.addListener(_onChanged);
_maxController.addListener(_onChanged);
super.didChangeDependencies();
}
void _onChanged() {
final viewModel = widget.viewModel;
final companyGateway = viewModel.companyGateway;
final updatedGateway = companyGateway.rebuild((b) => b
..minLimit = _enableMin ? parseDouble(_minController.text.trim()) : null
..maxLimit = _enableMax ? parseDouble(_maxController.text.trim()) : null);
if (companyGateway != updatedGateway) {
viewModel.onChanged(updatedGateway);
}
}
@override
Widget build(BuildContext context) {
final localization = AppLocalization.of(context);
return FormCard(
children: <Widget>[
/*
RangeSlider(
values: RangeValues((widget.companyGateway.minLimit ?? 0).toDouble(),
(widget.companyGateway.maxLimit ?? 100000).toDouble()),
min: 0,
max: 100000,
onChanged: (values) {
_minController.text = formatNumber(values.start, context,
formatNumberType: FormatNumberType.input);
_maxController.text = formatNumber(values.end, context,
formatNumberType: FormatNumberType.input);
widget.viewModel.onChanged(widget.companyGateway.rebuild((b) => b
..minLimit = values.start.toInt()
..maxLimit = values.end.toInt()));
},
),
*/
Row(
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
DecoratedFormField(
label: localization.minLimit,
enabled: _enableMin,
controller: _minController,
keyboardType: TextInputType.numberWithOptions(),
),
SizedBox(height: 10),
CheckboxListTile(
controlAffinity: ListTileControlAffinity.leading,
activeColor: Theme.of(context).accentColor,
title: Text(localization.enableMin),
value: _enableMin,
onChanged: (value) {
setState(() {
_enableMin = value;
_onChanged();
if (!value) {
_minController.text = '';
}
});
},
)
],
),
),
SizedBox(width: 40),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
DecoratedFormField(
label: localization.maxLimit,
enabled: _enableMax,
controller: _maxController,
keyboardType: TextInputType.numberWithOptions(),
),
SizedBox(height: 10),
CheckboxListTile(
controlAffinity: ListTileControlAffinity.leading,
activeColor: Theme.of(context).accentColor,
title: Text(localization.enableMax),
value: _enableMax,
onChanged: (value) {
setState(() {
_enableMax = value;
_onChanged();
if (!value) {
_maxController.text = '';
}
});
},
)
],
),
),
],
),
],
);
}
}
class FeesEditor extends StatefulWidget {
const FeesEditor({this.companyGateway, this.viewModel});
final CompanyGatewayEntity companyGateway;
final CompanyGatewayEditVM viewModel;
@override
_FeesEditorState createState() => _FeesEditorState();
}
class _FeesEditorState extends State<FeesEditor> {
final _amountController = TextEditingController();
final _percentController = TextEditingController();
final _capController = TextEditingController();
final List<TextEditingController> _controllers = [];
@override
void dispose() {
_controllers.forEach((dynamic controller) {
controller.removeListener(_onChanged);
controller.dispose();
});
super.dispose();
}
@override
void didChangeDependencies() {
final List<TextEditingController> _controllers = [
_amountController,
_percentController,
_capController,
];
final companyGateway = widget.companyGateway;
_controllers
.forEach((dynamic controller) => controller.removeListener(_onChanged));
_amountController.text = formatNumber(companyGateway.feeAmount, context,
formatNumberType: FormatNumberType.input);
_percentController.text = formatNumber(companyGateway.feePercent, context,
formatNumberType: FormatNumberType.input);
_capController.text = formatNumber(companyGateway.feeCap, context,
formatNumberType: FormatNumberType.input);
_controllers
.forEach((dynamic controller) => controller.addListener(_onChanged));
super.didChangeDependencies();
}
void _onChanged() {
final viewModel = widget.viewModel;
final companyGateway = viewModel.companyGateway;
final amount = parseDouble(_amountController.text.trim());
final percent = parseDouble(_percentController.text.trim());
final cap = parseDouble(_capController.text.trim());
final feesEnabled = amount != 0 || percent != 0;
final updatedGateway = companyGateway.rebuild((b) => b
..feeAmount = feesEnabled ? amount : null
..feePercent = feesEnabled ? percent : null
..feeCap = feesEnabled ? cap : null);
if (companyGateway != updatedGateway) {
viewModel.onChanged(updatedGateway);
}
}
@override
Widget build(BuildContext context) {
final localization = AppLocalization.of(context);
final viewModel = widget.viewModel;
final companyGateway = viewModel.companyGateway;
final company = viewModel.state.selectedCompany;
return FormCard(
children: <Widget>[
DecoratedFormField(
label: localization.feeAmount,
controller: _amountController,
),
DecoratedFormField(
label: localization.feePercent,
controller: _percentController,
),
DecoratedFormField(
label: localization.feeCap,
controller: _capController,
),
if (company.settings.enableInvoiceItemTaxes)
TaxRateDropdown(
taxRates: company.taxRates,
onSelected: (taxRate) =>
viewModel.onChanged(companyGateway.rebuild((b) => b
..taxRate1 = taxRate.rate
..taxName1 = taxRate.name)),
labelText: localization.tax,
initialTaxName: companyGateway.taxName1,
initialTaxRate: companyGateway.taxRate1,
),
if (company.settings.enableInvoiceItemTaxes &&
company.settings.enableSecondTaxRate)
TaxRateDropdown(
taxRates: company.taxRates,
onSelected: (taxRate) =>
viewModel.onChanged(companyGateway.rebuild((b) => b
..taxRate2 = taxRate.rate
..taxName2 = taxRate.name)),
labelText: localization.tax,
initialTaxName: companyGateway.taxName2,
initialTaxRate: companyGateway.taxRate2,
),
],
);
}
}