Skip to content

Commit

Permalink
Add currency toggle dropdown (#46)
Browse files Browse the repository at this point in the history
* Add currency modal bottom sheet

* Refactor currency converter and consuming pages

* Refactor modal bottom sheet list
  • Loading branch information
kirahsapong authored Feb 6, 2024
1 parent 140c9ee commit ceedceb
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 90 deletions.
39 changes: 32 additions & 7 deletions frontend/lib/features/deposit/deposit_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,28 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_starter/l10n/app_localizations.dart';
import 'package:flutter_starter/shared/currency_converter.dart';
import 'package:flutter_starter/shared/currency_modal.dart';
import 'package:flutter_starter/shared/fee_details.dart';
import 'package:flutter_starter/shared/grid.dart';
import 'package:flutter_starter/shared/number_pad.dart';
import 'package:flutter_starter/shared/utils/number_pad_input_validation_util.dart';

// replace with actual currency list
final supportedCurrencyList = [
{'label': 'USD', 'icon': Icons.attach_money, 'exchangeRate': 1},
{'label': 'MXN', 'icon': Icons.attach_money, 'exchangeRate': 17},
{'label': 'BTC', 'icon': Icons.currency_bitcoin, 'exchangeRate': 0.000024}
];

class DepositPage extends HookWidget {
const DepositPage({super.key});

@override
Widget build(BuildContext context) {
final depositAmount = useState<String>('0');
final isValidKeyPress = useState<bool>(true);
final selectedCurrencyItem =
useState<Map<String, Object>>(supportedCurrencyList[1]);

return Scaffold(
appBar: AppBar(),
Expand All @@ -30,19 +40,34 @@ class DepositPage extends HookWidget {
child: Column(
children: [
CurrencyConverter(
originAmount: depositAmount.value,
originCurrency: 'MXN',
originLabel: Loc.of(context).youDeposit,
destinationCurrency: Loc.of(context).usd,
exchangeRate: (1 / 17).toString(),
inputAmount: double.parse('0${depositAmount.value}'),
inputSelectedCurrency:
selectedCurrencyItem.value['label'].toString(),
inputLabel: Loc.of(context).youDeposit,
outputAmount: (double.parse('0${depositAmount.value}') /
double.parse(selectedCurrencyItem
.value['exchangeRate']
.toString())),
isValidKeyPress: isValidKeyPress.value,
onDropdownTap: () {
CurrencyModal.showCurrencyModal(
context,
(value) => selectedCurrencyItem.value =
supportedCurrencyList.firstWhere(
(element) => element['label'] == value),
supportedCurrencyList,
selectedCurrencyItem.value['label'].toString());
},
),
const SizedBox(height: Grid.xl),
// these will come from PFI offerings later
FeeDetails(
originCurrency: Loc.of(context).usd,
destinationCurrency: 'MXN',
exchangeRate: '17',
destinationCurrency:
selectedCurrencyItem.value['label'].toString(),
exchangeRate: selectedCurrencyItem
.value['exchangeRate']
.toString(),
serviceFee: '0')
],
),
Expand Down
40 changes: 33 additions & 7 deletions frontend/lib/features/withdraw/withdraw_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,28 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_starter/l10n/app_localizations.dart';
import 'package:flutter_starter/shared/currency_converter.dart';
import 'package:flutter_starter/shared/currency_modal.dart';
import 'package:flutter_starter/shared/fee_details.dart';
import 'package:flutter_starter/shared/grid.dart';
import 'package:flutter_starter/shared/number_pad.dart';
import 'package:flutter_starter/shared/utils/number_pad_input_validation_util.dart';

// replace with actual currency list
final supportedCurrencyList = [
{'label': 'USD', 'icon': Icons.attach_money, 'exchangeRate': 1},
{'label': 'MXN', 'icon': Icons.attach_money, 'exchangeRate': 17},
{'label': 'BTC', 'icon': Icons.currency_bitcoin, 'exchangeRate': 0.000024}
];

class WithdrawPage extends HookWidget {
const WithdrawPage({super.key});

@override
Widget build(BuildContext context) {
final withdrawAmount = useState<String>('0');
final isValidKeyPress = useState<bool>(true);
final selectedCurrencyItem =
useState<Map<String, Object>>(supportedCurrencyList[1]);

return Scaffold(
appBar: AppBar(),
Expand All @@ -30,19 +40,35 @@ class WithdrawPage extends HookWidget {
child: Column(
children: [
CurrencyConverter(
originAmount: withdrawAmount.value,
originCurrency: Loc.of(context).usd,
originLabel: Loc.of(context).youWithdraw,
destinationCurrency: 'MXN',
exchangeRate: '17',
inputAmount: double.parse('0${withdrawAmount.value}'),
inputLabel: Loc.of(context).youWithdraw,
outputSelectedCurrency:
selectedCurrencyItem.value['label'].toString(),
outputAmount:
(double.parse('0${withdrawAmount.value}') *
double.parse(selectedCurrencyItem
.value['exchangeRate']
.toString())),
isValidKeyPress: isValidKeyPress.value,
onDropdownTap: () {
CurrencyModal.showCurrencyModal(
context,
(value) => selectedCurrencyItem.value =
supportedCurrencyList.firstWhere(
(element) => element['label'] == value),
supportedCurrencyList,
selectedCurrencyItem.value['label'].toString());
},
),
const SizedBox(height: Grid.xl),
// these will come from PFI offerings later
FeeDetails(
originCurrency: Loc.of(context).usd,
destinationCurrency: 'MXN',
exchangeRate: '17',
destinationCurrency:
selectedCurrencyItem.value['label'].toString(),
exchangeRate: selectedCurrencyItem
.value['exchangeRate']
.toString(),
serviceFee: '0')
],
),
Expand Down
111 changes: 59 additions & 52 deletions frontend/lib/shared/currency_converter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,22 @@ import 'package:flutter_starter/shared/grid.dart';
import 'package:intl/intl.dart';

class CurrencyConverter extends HookWidget {
final String originAmount;
final String originCurrency;
final String originLabel;
final String destinationCurrency;
final String exchangeRate;
final double inputAmount;
final String inputLabel;
final double outputAmount;
final bool isValidKeyPress;
final String? inputSelectedCurrency;
final String? outputSelectedCurrency;
final VoidCallback? onDropdownTap;

const CurrencyConverter({
required this.originAmount,
required this.originCurrency,
required this.originLabel,
required this.destinationCurrency,
required this.exchangeRate,
required this.inputAmount,
required this.inputLabel,
required this.outputAmount,
required this.isValidKeyPress,
this.inputSelectedCurrency,
this.outputSelectedCurrency,
this.onDropdownTap,
super.key,
});

Expand All @@ -28,54 +30,59 @@ class CurrencyConverter extends HookWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
SizedBox(
child: InvalidNumberPadInputAnimation(
textValue: NumberFormat.simpleCurrency()
.format(double.parse('0$originAmount')),
textStyle: Theme.of(context).textTheme.displayMedium,
shouldAnimate: !isValidKeyPress)),
const SizedBox(width: Grid.xs),
Baseline(
baseline: 0,
baselineType: TextBaseline.alphabetic,
child: Text(
originCurrency,
style: Theme.of(context).textTheme.headlineMedium,
),
),
],
),
const SizedBox(height: Grid.xxs),
Text(
originLabel,
style: Theme.of(context).textTheme.bodyLarge,
),
_buildRow(
context,
InvalidNumberPadInputAnimation(
textValue: NumberFormat.simpleCurrency().format(inputAmount),
textStyle: Theme.of(context).textTheme.displayMedium,
shouldAnimate: !isValidKeyPress),
currency: inputSelectedCurrency ?? Loc.of(context).usd,
bottomLabel: inputLabel,
isToggle: inputSelectedCurrency?.isNotEmpty ?? false),
const SizedBox(height: Grid.sm),
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
_buildRow(
context,
Text(
NumberFormat.simpleCurrency().format(
double.parse('0$originAmount') * double.parse(exchangeRate)),
NumberFormat.simpleCurrency().format(outputAmount),
style: Theme.of(context).textTheme.displayMedium,
),
const SizedBox(width: Grid.xs),
Text(
destinationCurrency,
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
const SizedBox(height: Grid.xxs),
currency: outputSelectedCurrency ?? Loc.of(context).usd,
bottomLabel: Loc.of(context).youGet,
isToggle: outputSelectedCurrency?.isNotEmpty ?? false),
],
);
}

Widget _buildRow(BuildContext context, inputWidget,
{String currency = '', String bottomLabel = '', bool isToggle = false}) {
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
isToggle
? GestureDetector(
onTap: onDropdownTap,
child: _buildRowDetail(context, inputWidget,
currency: currency, isToggle: isToggle))
: _buildRowDetail(context, inputWidget, currency: currency),
const SizedBox(height: Grid.xxs),
Text(
bottomLabel,
style: Theme.of(context).textTheme.bodyLarge,
)
]);
}

Widget _buildRowDetail(BuildContext context, Widget inputWidget,
{String currency = '', bool isToggle = false}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
inputWidget,
const SizedBox(width: Grid.xs),
Text(
Loc.of(context).youGet,
style: Theme.of(context).textTheme.bodyLarge,
currency,
style: Theme.of(context).textTheme.headlineMedium,
),
if (isToggle) const Icon(Icons.keyboard_arrow_down)
],
);
}
Expand Down
50 changes: 50 additions & 0 deletions frontend/lib/shared/currency_modal.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter_starter/shared/grid.dart';

class CurrencyModal {
static Future<dynamic> showCurrencyModal(
BuildContext context,
Function(String) onPressed,
List<Map<String, Object>> supportedCurrencyList,
String selectedCurrency) {
return showModalBottomSheet(
useSafeArea: true,
isScrollControlled: true,
context: context,
builder: (BuildContext context) {
return SafeArea(
child: SizedBox(
height: supportedCurrencyList.length * 80,
child: Column(children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: Grid.xs),
child: Text(
'Select currency',
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
)),
Expanded(
child: ListView(
children:
supportedCurrencyList.map((Map<String, Object> map) {
IconData icon = map['icon'] as IconData;
String label = map['label'].toString();
return (ListTile(
onTap: () {
onPressed(label);
Navigator.pop(context);
},
leading: Icon(icon),
title: Text(label,
style: Theme.of(context).textTheme.titleMedium),
trailing: (selectedCurrency == label)
? const Icon(Icons.check)
: null,
));
}).toList()),
)
]),
));
});
}
}
13 changes: 13 additions & 0 deletions frontend/test/features/deposit/deposit_page_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,18 @@ void main() {
await tester.pump();
}
});

testWidgets(
'should show the currency list on tap of the currency converter dropdown toggle',
(tester) async {
await tester.pumpWidget(
WidgetHelpers.testableWidget(child: const DepositPage()),
);

await tester.tap(find.byIcon(Icons.keyboard_arrow_down));
await tester.pump();

expect(find.byType(ListView), findsOneWidget);
});
});
}
13 changes: 13 additions & 0 deletions frontend/test/features/withdraw/withdraw_page_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,18 @@ void main() {
await tester.pump();
}
});

testWidgets(
'should show the currency list on tap of the currency converter dropdown toggle',
(tester) async {
await tester.pumpWidget(
WidgetHelpers.testableWidget(child: const WithdrawPage()),
);

await tester.tap(find.byIcon(Icons.keyboard_arrow_down));
await tester.pump();

expect(find.byType(ListView), findsOneWidget);
});
});
}
Loading

0 comments on commit ceedceb

Please sign in to comment.