diff --git a/assets/return.png b/assets/return.png new file mode 100644 index 0000000..9ead30d Binary files /dev/null and b/assets/return.png differ diff --git a/assets/values-en/strings.xml b/assets/values-en/strings.xml index 0015bb0..b2b3ee7 100644 --- a/assets/values-en/strings.xml +++ b/assets/values-en/strings.xml @@ -154,4 +154,12 @@ Set on switch on Camera Go back to %s. Open settings + Return + Are you sure you want to return purchase, which cost %s on %s? + +You cannot undo this. +You bonus balance will be changed and you will have %s bonus points. + Return confirmed + Purchase, which cost %s on %s is returned. + Points diff --git a/assets/values-es/strings.xml b/assets/values-es/strings.xml index 55630dd..4ed0d1d 100644 --- a/assets/values-es/strings.xml +++ b/assets/values-es/strings.xml @@ -23,7 +23,7 @@ Confirmacón No Compra por %s %s está realizada - The payment of %s %s was completed + Se completó el pago de %s %s Registro Explotación Contactos del soporte técnico @@ -150,4 +150,12 @@ Establecer en el interruptor de la cámara Vuelve a %s Abre las configuraciones + Regreso + ¿Está seguro de que desea devolver la compra, que costó %s en %s + +No puedes deshacer esto. +Se cambiará su saldo de bonificación y tendrá %s puntos de bonificación. + Regreso confirmado + Compra, cuyo costo %s en %s se devuelve. + Puntos diff --git a/assets/values-ru/strings.xml b/assets/values-ru/strings.xml index d51ed37..0397885 100644 --- a/assets/values-ru/strings.xml +++ b/assets/values-ru/strings.xml @@ -153,4 +153,12 @@ Вернитесь к приложению %s. Открыть настройки + Возврат + Вы подтверждаете возврат покупки на сумму %s от %s? + +Отменить возврат нельзя. +Бонусный баланс будет изменен и составит %s бонусов. + Возврат подтвержден + Покупка на сумму %s от %s возвращена. + Баллы diff --git a/assets/values-ua/strings.xml b/assets/values-ua/strings.xml index 44a320b..b733dd9 100644 --- a/assets/values-ua/strings.xml +++ b/assets/values-ua/strings.xml @@ -23,7 +23,7 @@ Підтвердження Ні Купівля на суму %s %s проведена - The payment of %s %s was completed + Виплату %s %s було завершено Реєстрація Використання Контакти підтримки @@ -155,4 +155,12 @@ Поверніться до додатка %s. Відкрити параметри + Повернення + Ви дійсно хочете повернути покупку, яка коштує %s на %s? + +Ви не можете скасувати це. +Баланс бонусів буде змінено, і ви отримаєте %s бонусний бал. + Повернення підтверджено + Повертається придбання, вартість якого %s на %s. + Бонусні бали diff --git a/lib/base/base_state.dart b/lib/base/base_state.dart index be78f8a..a8dfca8 100644 --- a/lib/base/base_state.dart +++ b/lib/base/base_state.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:checker/resources.dart'; import 'package:flutter/material.dart'; @@ -10,7 +11,6 @@ import 'package:checker/screens/faq.dart'; import 'package:checker/strings.dart'; import 'package:checker/db.dart'; import 'package:flutter/rendering.dart'; -import 'package:meta/meta.dart'; abstract class BaseState extends State { @@ -78,7 +78,14 @@ abstract class BaseState extends State { child: getMenuItem(help_png, StringsLocalization.help()) )); - if (Theme.of(context).platform != TargetPlatform.iOS) { + if(showReturnScreen()) { + menuItemList.add(new PopupMenuItem( + value: 3, + child: getMenuItem(return_png, StringsLocalization.returnLabel()) + )); + } + + if (Platform.isAndroid) { menuItemList.add(new PopupMenuItem( value: 2, child: getMenuItem(exit_png, StringsLocalization.exit()) @@ -95,6 +102,8 @@ abstract class BaseState extends State { ]; } + bool showReturnScreen() => false; + void onOptionsItemClick(int index) { switch (index) { case 0: { @@ -119,9 +128,14 @@ abstract class BaseState extends State { platform.invokeMethod('finish'); break; } + case 3: + openReturnScreen(); + break; } } + void openReturnScreen() {} + /// Возвращает пункт меню (Картинка с текстом) Widget getMenuItem(String image, String text) { return new Row(children: [ diff --git a/lib/consts.dart b/lib/consts.dart index 9d3f316..b705679 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -7,6 +7,7 @@ const String exit_png = 'assets/exit.png'; const String help_png = 'assets/help.png'; const String settings_png = 'assets/settings.png'; const String settings_arrow_png = 'assets/settings_arrow.png'; +const String return_png = 'assets/return.png'; const String check_png = 'assets/check.png'; const String activate_token_bg_png = 'assets/activate_token_message_background.png'; const String active_token_bg_png = 'assets/active_token_message_background.png'; diff --git a/lib/network.dart b/lib/network.dart index fd85033..200c2a0 100644 --- a/lib/network.dart +++ b/lib/network.dart @@ -106,3 +106,33 @@ getEndpoint() async { getToken() async { return await platform.invokeMethod('getAppToken'); } + +Future getUserPurchases(String token, int userId, int page) async { + + var headers = { + 'DM-Authorization': 'dmapptoken ${await getToken()}', + 'Authorization': 'dmtoken $token', + 'Accept-Language': StringsLocalization.localeCode + }; + + + var finalEndpoint = "${await getEndpoint()}users/$userId/purchases/?returned=false&sort=desc&page=$page"; + print(finalEndpoint); + + return httpClient.get(finalEndpoint, headers: headers); +} + +Future returnPurchase(String token, int userId, int purchaseId) async { + + var headers = { + 'DM-Authorization': 'dmapptoken ${await getToken()}', + 'Authorization': 'dmtoken $token', + 'Accept-Language': StringsLocalization.localeCode + }; + + + var finalEndpoint = "${await getEndpoint()}users/$userId/purchases/$purchaseId"; + print(finalEndpoint); + + return httpClient.delete(finalEndpoint, headers: headers); +} diff --git a/lib/screens/purchase.dart b/lib/screens/purchase.dart index c7309b5..5f1c51d 100644 --- a/lib/screens/purchase.dart +++ b/lib/screens/purchase.dart @@ -1,5 +1,6 @@ import 'package:checker/base/base_screen.dart'; import 'package:checker/db.dart'; +import 'package:checker/screens/return.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'dart:convert'; @@ -127,6 +128,16 @@ class PurchaseScreenState extends BaseState { return listView; } + bool showReturnScreen() => true; + + void openReturnScreen() { + Future.delayed(const Duration(milliseconds: 200), () { + Route route = MaterialPageRoute(builder: (BuildContext context) => + ReturnScreen(helper, app, user['id'], user['bonus']), fullscreenDialog: true); + Navigator.push(context, route); + }); + } + getBonusInputField() { var bonusTextField = new TextField( keyboardType: TextInputType.number, diff --git a/lib/screens/return.dart b/lib/screens/return.dart new file mode 100644 index 0000000..cd89dd6 --- /dev/null +++ b/lib/screens/return.dart @@ -0,0 +1,200 @@ +import 'dart:convert'; + +import 'package:checker/base/base_screen.dart'; +import 'package:checker/base/base_state.dart'; +import 'package:checker/db.dart'; +import 'package:checker/strings.dart'; +import 'package:flutter/material.dart'; +import 'package:checker/network.dart'; +import 'package:http/http.dart'; +import 'package:intl/intl.dart'; + +class ReturnScreen extends BaseScreen { + final int userId; + final int bonus; + + ReturnScreen(SqliteHelper helper, String app, this.userId, this.bonus) : super(helper, app); + + @override + _ReturnScreenState createState() => _ReturnScreenState(helper, app); +} + +class _ReturnScreenState extends BaseState { + static DateFormat dateFormat = DateFormat('d MMM yy'); + + ScrollController _controller; + List _purchases = []; + int _bonus; + int _page = 1; + bool _fullLoaded = false; + bool _loading = false; + + + _ReturnScreenState(SqliteHelper helper, String app) : super(helper, app); + + bool isAutomaticallyImplyLeading() => true; + + @override + Widget getScreenContent() => ListView.separated( + itemBuilder: (context, index) { + final TextStyle style = Theme.of(context).textTheme.subhead; + if(index > 0) { + final item = _purchases[index - 1]; + final DateTime date = DateTime.parse(item['date']); + final String formattedDate = dateFormat.format(date); + return GestureDetector( + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: Text(formattedDate, style: style), + ), + Expanded( + child: Text(item['sum_total'], style: style), + ), + Expanded( + child: Text(item['sum_bonus'].toString(), style: style), + ) + ], + ), + ), + onTap: () => _confirmReturnPurchase(item, formattedDate), + ); + } else { + final TextStyle headerStyle = style.copyWith(color: Colors.grey); + return Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: Container(), + ), + Expanded( + child: Text(StringsLocalization.sum(), style: headerStyle), + ), + Expanded( + child: Text(StringsLocalization.points(), style: headerStyle), + ) + ], + ), + ); + } + }, + separatorBuilder: (_, index) => index == 0 + ? Container() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Divider(height: 1,), + ), + itemCount: _purchases.length > 0 ? _purchases.length + 1 : _purchases.length, + controller: _controller); + + @override + Widget build(BuildContext context) => getMainWidget(); + + @override + String getTitle() => StringsLocalization.returnLabel(); + + @override + void initState() { + super.initState(); + setState(() { + loading = true; + }); + _loadUserPurchases(); + _bonus = widget.bonus; + _controller = ScrollController(); + _controller.addListener(_scrollListener); + } + + void _scrollListener() { + if (_controller.offset >= _controller.position.maxScrollExtent && + !_controller.position.outOfRange) { + _loadUserPurchases(); + } + } + + void _loadUserPurchases() async { + if(!_fullLoaded && !_loading) { + _loading = true; + setState(() => loading = true); + String token = await helper.getToken(); + Response response = await getUserPurchases(token, widget.userId, _page); + final responseJson = json.decode(response.body); + List purchases = responseJson['results']; + _fullLoaded = _page == responseJson['pages']; + setState(() { + loading = false; + _purchases.addAll(purchases); + }); + _page++; + _loading = false; + setState(() => loading = false); + } + } + + num extractReturnPoints(Map purchase) { + final returnPoints = purchase['sum_bonus']; + if(returnPoints is num) { + return returnPoints; + } else if(returnPoints is String) { + final RegExp returnPattern = RegExp(r"(\d+)"); + final List digits = returnPattern.allMatches(returnPoints).toList(); + return int.parse(digits[0].group(0)) - int.parse(digits[1].group(1)); + } else { + return 0; + } + } + + void _confirmReturnPurchase(Map purchase, String formattedDate) { + final num totalReturn = extractReturnPoints(purchase); + showDialog( + context: context, + builder: (context) => + AlertDialog( + title: Text(StringsLocalization.confirmation()), + content: Text(StringsLocalization.returnConfirmation( + purchase['sum_total'], formattedDate, _bonus - totalReturn)), + actions: [ + FlatButton( + child: Text(StringsLocalization.no()), + onPressed: () => Navigator.pop(context), + ), + FlatButton( + child: Text(StringsLocalization.yes()), + onPressed: () { + Navigator.pop(context); + _returnPurchase(purchase, formattedDate, totalReturn); + } + ) + ], + ) + ); + } + + void _returnPurchase(Map purchase, String formattedDate, num totalReturn) async { + String token = await helper.getToken(); + final Response response = await returnPurchase(token, widget.userId, purchase['id']); + if(response.statusCode == 204) { + _bonus += totalReturn; + setState(() { + _purchases.removeWhere((p) => p['id'] == purchase['id']); + }); + showDialog( + context: context, + builder: (context) => + AlertDialog( + title: Text(StringsLocalization.returnConfirmed()), + content: Text(StringsLocalization.returnConfirmedContent(purchase['sum_total'], formattedDate)), + actions: [ + FlatButton( + child: Text(StringsLocalization.yes()), + onPressed: () => Navigator.pop(context) + ) + ], + ) + ); + } + } +} diff --git a/lib/strings.dart b/lib/strings.dart index 8061b96..08ce633 100644 --- a/lib/strings.dart +++ b/lib/strings.dart @@ -125,6 +125,11 @@ class StringsLocalization { String trimmedVal = val.substring(0, val.length - 3); return sprintf(strings['payment_complite'], [val, declineCurrency(int.parse(trimmedVal), code)]); } + static String returnConfirmation(String sum, String date, int points) => + sprintf(strings['return_purchase_content'], [sum, date, points.toString()]); + + static String returnConfirmedContent(String sum, String date) => + sprintf(strings['return_confirmation_content'], [sum, date]); static String registration() => strings['registration']; static String usage() => strings['usage']; @@ -218,4 +223,7 @@ class StringsLocalization { static String joysMinus() => strings['joys_minus']; static String joysHint() => strings['joys_hint']; static String phone() => strings['phone']; + static String returnLabel() => strings['returnLabel']; + static String returnConfirmed() => strings['return_confirmation']; + static String points() => strings['points']; } diff --git a/pubspec.lock b/pubspec.lock index 9f407d6..f06d8c4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -48,6 +48,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.10" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.7" meta: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ae07c59..ecb8afa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ dependencies: image_picker: '^0.4.1' # use for ask permissions @ iOS xml: "^3.0.0" sentry: 2.2.0 + intl: 0.15.7 flutter: sdk: flutter @@ -41,6 +42,7 @@ flutter: - assets/settings_arrow.png - assets/help.png - assets/check.png + - assets/return.png - assets/exit.png - assets/activate_token_message_background.png - assets/active_token_message_background.png