diff --git a/android/app/src/main/java/com/dinect/checker/MainActivity.java b/android/app/src/main/java/com/dinect/checker/MainActivity.java index 8566293..be9a587 100644 --- a/android/app/src/main/java/com/dinect/checker/MainActivity.java +++ b/android/app/src/main/java/com/dinect/checker/MainActivity.java @@ -38,6 +38,7 @@ public class MainActivity extends FlutterActivity { private MethodChannel mChannel; private Map mScannerArgs; + private Result scannerResult; @Override protected void onCreate(Bundle savedInstanceState) { @@ -59,6 +60,7 @@ public class MainActivity extends FlutterActivity { result.success(BuildConfig.currency); break; case "startScanner": + scannerResult = result; startScannerActivity(call); break; case "isOnline": @@ -169,7 +171,7 @@ public class MainActivity extends FlutterActivity { protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == START_SCANNER_REQUEST_CODE) { if (resultCode == RESULT_CANCELED) { - finish(); + scannerResult.error("Scanning is cancelled", null, null); } else if (resultCode == RESULT_OK) { if (data != null && data.getExtras() != null) { String user = data.getExtras().getString("user", null); @@ -178,7 +180,9 @@ public class MainActivity extends FlutterActivity { ArrayList args = new ArrayList<>(2); args.add(user); args.add(card); - mChannel.invokeMethod("purchase", args); + if(scannerResult != null) { + scannerResult.success(args); + } } else { String menuItem = data.getExtras().getString("item", null); if (menuItem != null) { diff --git a/assets/values-en/strings.xml b/assets/values-en/strings.xml index 5b7b782..f278bbe 100644 --- a/assets/values-en/strings.xml +++ b/assets/values-en/strings.xml @@ -153,4 +153,7 @@ Set on switch on Camera Go back to %s. Open settings + Cancel + Are you sure you want to cancel this purchase? + Total amount: %s %s, with discount of %s %s (%s%%) diff --git a/assets/values-es/strings.xml b/assets/values-es/strings.xml index 1fa7283..7a9806e 100644 --- a/assets/values-es/strings.xml +++ b/assets/values-es/strings.xml @@ -149,4 +149,5 @@ Establecer en el interruptor de la cámara Vuelve a %s Abre las configuraciones + Cancelar diff --git a/assets/values-ru/strings.xml b/assets/values-ru/strings.xml index a6d0f16..91a4437 100644 --- a/assets/values-ru/strings.xml +++ b/assets/values-ru/strings.xml @@ -152,4 +152,6 @@ Вернитесь к приложению %s. Открыть настройки + Отмена + Вы уверены что хотите отменить покупку? diff --git a/assets/values-ua/strings.xml b/assets/values-ua/strings.xml index 6e70247..510ae3c 100644 --- a/assets/values-ua/strings.xml +++ b/assets/values-ua/strings.xml @@ -154,4 +154,5 @@ Поверніться до додатка %s. Відкрити параметри + Скасувати diff --git a/ios/Runner/ScannerViewController.swift b/ios/Runner/ScannerViewController.swift index e41cbcf..2eb7101 100644 --- a/ios/Runner/ScannerViewController.swift +++ b/ios/Runner/ScannerViewController.swift @@ -272,7 +272,7 @@ import ZXingObjC } else { print("Result is not nil (ios code)"); self.dismiss(animated: true) { - self.platformChannel?.invokeMethod("purchase", arguments: [result, str]) + self.platformChannel?.invokeMethod("scanSuccess", arguments: [result!, str]) } } }) diff --git a/lib/base/base_state.dart b/lib/base/base_state.dart index be78f8a..83971af 100644 --- a/lib/base/base_state.dart +++ b/lib/base/base_state.dart @@ -10,7 +10,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 { diff --git a/lib/screens/finish_registration.dart b/lib/screens/finish_registration.dart index e9435e5..6cc42ef 100644 --- a/lib/screens/finish_registration.dart +++ b/lib/screens/finish_registration.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'dart:async'; import 'package:checker/base/base_screen.dart'; import 'package:checker/base/base_state.dart'; diff --git a/lib/screens/purchase.dart b/lib/screens/purchase.dart index 5e0c20e..9b0b54c 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/purchase_sum.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'dart:convert'; @@ -14,13 +15,16 @@ import 'package:checker/network.dart'; import 'package:checker/base/base_state.dart'; import 'package:checker/screens/purchase_success.dart'; +typedef PurchaseResponseCallback = void Function(Map, String, String); + /// Экран проведения покупки. class PurchaseScreen extends BaseScreen { - PurchaseScreen(helper, app, this.user, this.card) : super(helper, app); + PurchaseScreen(helper, app, this.user, this.card, this.sum) : super(helper, app); final String user; final String card; + final String sum; @override State createState() => new PurchaseScreenState(helper, app, user, card); @@ -30,7 +34,7 @@ class PurchaseScreenState extends BaseState { /// Объект, помогающий вручную изменять введенный пользователем текст. /// Используется для форматирования введенных пользователем данных /// (удаляет запрещенные символы до их отображаения). - TextEditingController controller = new TextEditingController(); + TextEditingController controller; TextEditingController bonusController = new TextEditingController(); @@ -43,6 +47,7 @@ class PurchaseScreenState extends BaseState { @override void initState() { + controller = new TextEditingController(text: widget.sum ?? ""); loading = true; requestAsyncData(user); buildFocusNode(); @@ -116,9 +121,9 @@ class PurchaseScreenState extends BaseState { widgetList.add(wrapButton( getScreenMargins(24.0), - getScanButton( + getCancelScanButton( context, - StringsLocalization.scan(), + StringsLocalization.cancel(), Resources.getPrimaryColor(app) ) )); @@ -185,12 +190,12 @@ class PurchaseScreenState extends BaseState { StringsLocalization.completePurchase(), () => onPurchaseClick()); } - Widget getScanButton(BuildContext context, String title, Color textColor) { + Widget getCancelScanButton(BuildContext context, String title, Color textColor) { return new Container( height: buttonHeight, child: new FlatButton( child: new Text(title, style: new TextStyle(color: textColor)), - onPressed: () => restartScanner()), + onPressed: () => restartFlow()), decoration: new BoxDecoration( border: new Border.all( color: Resources.getButtonColor(app), width: 1.0), @@ -249,8 +254,8 @@ class PurchaseScreenState extends BaseState { loading = false; this.coupons = coupons['results']; this.loyalityType = loyality['type']; - setBonuses(loyality, showBonus); }); + setBonuses(loyality, showBonus); } } @@ -293,16 +298,26 @@ class PurchaseScreenState extends BaseState { } onPurchaseClick() { + String val = _parseSum(controller.text); + purchase(val, false, _showPrecalculatedValues); + } + + void _showPrecalculatedValues(Map response, String sumTotal, String token) { String val = _parseSum(controller.text); helper.getCurrency().then((currency) { print(currency.toString()); + final String totalAmount = response['sum_total']; + final String totalDiscount = response['sum_discount']; + final String discount = (response['discount'] as int).toString(); + print(response); showDialog( context: context, - builder: (_) => new AlertDialog( + builder: (_) => + new AlertDialog( title: new Text(StringsLocalization.confirmation()), - content: - new Text( - StringsLocalization.confirmPurchase(val, currency) + content: Text( + StringsLocalization.purchaseDetails(totalAmount, + totalDiscount, discount, currency) ), actions: [ new FlatButton( @@ -315,13 +330,42 @@ class PurchaseScreenState extends BaseState { child: new Text(StringsLocalization.yes()), onPressed: () { Navigator.of(context).pop(); - purchase(val); + purchase(val, true, _paymentConfirmed); }, ) ])); }); } + void _paymentConfirmed(Map purchase, String sumTotal, String token) async { + var couponsResponse; + + try { + couponsResponse = await getCouponsRequest(purchase['coupons_url'], token); + print(couponsResponse.body); + } catch(error) { + purchaseInProgress = false; + print(error.toString()); + } + + Map coupons = json.decode(couponsResponse.body); + + new Future.delayed(const Duration(milliseconds: 200), () { + print('show purchase success!'); + var route = new MaterialPageRoute(builder: (BuildContext context) => new PurchaseSuccessScreen( + sumTotal, + user['first_name'] == null ? '' : user['first_name'], + helper, + app, + purchase, + coupons['results'] + ), fullscreenDialog: true); + Navigator.of(context).push(route).then((token) { + Navigator.of(context).pop(token); + }); + }); + } + apiErrorAlert(String errorText) { showDialog( context: context, @@ -339,13 +383,13 @@ class PurchaseScreenState extends BaseState { ); } - purchase(String sumTotal) async { + purchase(String sumTotal, bool commit, PurchaseResponseCallback callback) async { setState(() { loading = true; }); if (await platform.invokeMethod('isOnline')) { if (!purchaseInProgress) { - purchaseInProgress = true; + purchaseInProgress = commit; String token = await helper.getToken(); var result = await helper.getMerchantID(); @@ -361,10 +405,13 @@ class PurchaseScreenState extends BaseState { var body = { 'doc_id': result, 'curr_iso_code': currency.toString(), - 'commit': 'true', 'sum_total': sumTotal, }; + if(commit) { + body['commit'] = 'true'; + } + if (bonusController.text.length > 0) { body['bonus_payment'] = bonusController.text; } @@ -392,32 +439,7 @@ class PurchaseScreenState extends BaseState { purchaseInProgress = false; apiErrorAlert(errors[0]); } else { - var couponsResponse; - - try { - couponsResponse = await getCouponsRequest(purchase['coupons_url'], token); - print(couponsResponse.body); - } catch(error) { - purchaseInProgress = false; - print(error.toString()); - } - - Map coupons = json.decode(couponsResponse.body); - - new Future.delayed(const Duration(milliseconds: 200), () { - print('show purchase success!'); - var route = new MaterialPageRoute(builder: (BuildContext context) => new PurchaseSuccessScreen( - sumTotal, - user['first_name'] == null ? '' : user['first_name'], - helper, - app, - purchase, - coupons['results'] - ), fullscreenDialog: true); - Navigator.of(context).push(route).then((token) { - Navigator.of(context).pop(token); - }); - }); + callback(purchase, sumTotal, token); } } } @@ -426,24 +448,47 @@ class PurchaseScreenState extends BaseState { void setBonuses(Map bonuses, bool showBonus) { print('loyalityType ' + this.loyalityType); if (bonuses['type'] == 'amount') { - this.loyalty = '${user['discount']}%'; + setState(() => this.loyalty = '${user['discount']}%'); } else { double loyaltyVal = (double.parse(bonuses['amount_to_bonus'][1]) / bonuses['amount_to_bonus'][0]) * 100; - this.loyalty = '${loyaltyVal.toStringAsFixed(0)}%'; + setState(() => this.loyalty = '${loyaltyVal.toStringAsFixed(0)}%'); } if (showBonus && (this.loyalityType == 'bonus')) { - this.bonus = '${user['bonus']}'; + setState(() => this.bonus = '${user['bonus']}'); } print('loyalty ' + this.loyalty); print('bonus ' + this.bonus); } - restartScanner() { - helper.getToken().then((token) { - Navigator.of(context).pop(token); - }); + restartFlow() { + showDialog( + context: context, + builder: (context) => AlertDialog( + content: Text(StringsLocalization.cancelDialog()), + actions: [ + new FlatButton( + child: new Text(StringsLocalization.no()), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + new FlatButton( + child: new Text(StringsLocalization.yes()), + onPressed: () { + Navigator.of(context).pop(); + helper.getToken().then((token) { + Navigator.pushAndRemoveUntil(context, + MaterialPageRoute(builder: (context) => + PurchaseSumScreen(widget.helper, widget.app, token)), + (route) => false); + }); + }, + ) + ], + ) + ); } FocusNode bonusFocusNode = new FocusNode(); @@ -456,25 +501,14 @@ class PurchaseScreenState extends BaseState { sumFocusNode.addListener(() { setState(() { - - if (sumFocusNode.hasFocus && bonusFocusNode.hasFocus) { - bonusFocusNode.unfocus(); - } - if (sumFocusNode.hasFocus) { - scrollController.animateTo(pos, duration: new Duration(seconds: 1), curve: Curves.ease); + sumFocusNode.unfocus(); } - }); }); bonusFocusNode.addListener(() { setState(() { - - if (bonusFocusNode.hasFocus && sumFocusNode.hasFocus) { - sumFocusNode.unfocus(); - } - if (bonusFocusNode.hasFocus) { scrollController.animateTo(pos, duration: new Duration(seconds: 1), curve: Curves.ease); } diff --git a/lib/screens/purchase_success.dart b/lib/screens/purchase_success.dart index 16ff417..bd7b490 100644 --- a/lib/screens/purchase_success.dart +++ b/lib/screens/purchase_success.dart @@ -2,6 +2,7 @@ import 'package:checker/base/base_state.dart'; import 'package:checker/common.dart'; import 'package:checker/consts.dart'; import 'package:checker/db.dart'; +import 'package:checker/screens/purchase_sum.dart'; import 'package:checker/strings.dart'; import 'package:flutter/material.dart'; @@ -135,7 +136,11 @@ class PurchaseSuccessScreenState extends BaseState { getScanButton() { String title = StringsLocalization.scan(); - return buildRaisedButton(title, () => Navigator.of(context).pop(token)); + return buildRaisedButton(title, + () => Navigator.pushAndRemoveUntil(context, + MaterialPageRoute(builder: (context) => + PurchaseSumScreen(widget.helper, widget.app, token)), + (route) => false)); } diff --git a/lib/screens/purchase_sum.dart b/lib/screens/purchase_sum.dart new file mode 100644 index 0000000..d507063 --- /dev/null +++ b/lib/screens/purchase_sum.dart @@ -0,0 +1,202 @@ +import 'dart:convert'; + +import 'package:checker/base/base_screen.dart'; +import 'package:checker/db.dart'; +import 'package:checker/screens/purchase.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart'; +import 'dart:core'; + +import 'package:checker/resources.dart'; +import 'package:checker/strings.dart'; +import 'package:checker/common.dart'; +import 'package:checker/consts.dart'; +import 'package:checker/base/base_state.dart'; +import 'package:checker/network.dart'; + +/// Экран ввода суммы покупки +class PurchaseSumScreen extends BaseScreen { + PurchaseSumScreen(helper, app, this.token) : super(helper, app); + + final String token; + + @override + State createState() => + new PurchaseSumScreenState(helper, app); +} + +class PurchaseSumScreenState extends BaseState { + TextEditingController _sumController = new TextEditingController(); + + bool isAutomaticallyImplyLeading() => false; + + PurchaseSumScreenState(SqliteHelper helper, String app) : super(helper, app); + + @override + void initState() { + super.initState(); + _subscribe(); + } + + @override + Widget build(BuildContext ctx) { + return getMainWidget(); + } + + @override + Widget getScreenContent() => Column( + children: [ + getHintLabel(), + getInputField(), + wrapButton( + _getScreenMargins(24.0), + getScanButton(context, StringsLocalization.scan(), + Resources.getPrimaryColor(app))) + ], + ); + + EdgeInsets _getScreenMargins(double top) { + double side = 42.0; + return new EdgeInsets.only(top: top, left: side, right: side); + } + + Widget getScanButton(BuildContext context, String title, Color textColor) { + return new Container( + height: buttonHeight, + child: new FlatButton( + child: new Text(title, style: new TextStyle(color: textColor)), + onPressed: () => _startScanner()), + decoration: new BoxDecoration( + border: new Border.all( + color: Resources.getButtonColor(app), width: 1.0), + borderRadius: new BorderRadius.all(new Radius.circular(4.0)))); + } + + void _startScanner() { + platform.invokeMethod('getEndpoint').then((url) { + platform.invokeMethod('getAppToken').then((appToken) { + Map args = StringsLocalization.strings; + args['token'] = widget.token; + args['url'] = url; + args['appToken'] = appToken; + args['localeCode'] = StringsLocalization.localeCode; + args['color'] = Resources.getPrimaryColor(app).value.toString(); + platform.invokeMethod('startScanner', args).then((result) { + _processResult(result); + }); + }); + }); + } + + @override + String getTitle() { + return StringsLocalization.sum(); + } + + @override + getHintString() { + return StringsLocalization.sum(); + } + + @override + getTextWidget() { + return new TextField( + keyboardType: TextInputType.number, + decoration: new InputDecoration.collapsed( + hintText: getHintString(), + hintStyle: new TextStyle(color: greyTextColor, fontSize: 16.0)), + controller: _sumController, + onSubmitted: (String text) { + setState(() { + _sumController.text = _parseSum(text); + }); + }, + textAlign: TextAlign.center); + } + + String _cleanupNumber(String text) { + String tmp = text + .replaceAll(' ', '') + .replaceAll('-', '') + .replaceAll(',', '.') + .replaceAll('..', '.'); + + while (tmp.indexOf('..') != -1) { + tmp = tmp.replaceAll('..', '.'); + } + return tmp; + } + + String _parseSum(String input) { + num sumTotal = 0.0; + String text = _cleanupNumber(input); + + try { + sumTotal = num.parse(text); + } catch (exception) { + print(exception); + try { + int idx = text.indexOf('.'); + String integerPart = text.substring(0, idx); + String fractionalPart = text.substring(idx + 1, text.length); + if (fractionalPart.length > 2) { + fractionalPart = fractionalPart.substring(0, 2); + } + return '$integerPart.$fractionalPart'; + } catch (exception) { + print(exception); + } + } + print(sumTotal.toStringAsFixed(2)); + return sumTotal.toStringAsFixed(2); + } + + void _processResult(result) { + if (result is List) { + final String user = result[0] as String; + final String card = result[1] as String; + Navigator.of(context).push(new MaterialPageRoute(builder: (context) { + String sum = _parseSum(_sumController.text); + _sumController.text = ""; + return PurchaseScreen(helper, app, user, card, sum); + })); + } + } + + void _subscribe() async { + platform.setMethodCallHandler((MethodCall call) async { + if (call.method == 'findUser') { + try { + Response userResponse; + + switch (call.arguments[1]) { + case 'card': + userResponse = await getUserByCard(call.arguments[0], widget.token); + break; + case 'phone': + userResponse = await getUserByPhone(call.arguments[0], widget.token); + break; + } + + if (userResponse != null) { + print('I have user in method handler!'); + List users = json.decode(userResponse.body); + if (users.length > 0) { + return json.encode(users[0]); + } else { + throw new FlutterError("Users not found"); + } + } else { + throw new FlutterError("Users not found"); + } + } catch (error) { + print(error.toString()); + throw new FlutterError("Users not found"); + } + } else if (call.method == 'scanSuccess') { + _processResult(call.arguments); + } + }); + } +} diff --git a/lib/screens/splash.dart b/lib/screens/splash.dart index c678419..5fdec4e 100644 --- a/lib/screens/splash.dart +++ b/lib/screens/splash.dart @@ -11,6 +11,7 @@ import 'package:checker/resources.dart'; import 'package:checker/screens/faq.dart'; import 'package:checker/screens/finish_registration.dart'; import 'package:checker/screens/purchase.dart'; +import 'package:checker/screens/purchase_sum.dart'; import 'package:checker/screens/registration.dart'; import 'package:checker/screens/settings.dart'; import 'package:checker/strings.dart'; @@ -241,23 +242,13 @@ class _SplashScreenState extends BaseState { : json.encode(call.arguments[0]); print(userString); String card = call.arguments[1]; - showNextScreen(new PurchaseScreen(helper, app, userString, card)); + showNextScreen(new PurchaseScreen(helper, app, userString, card, null)); } }); - platform.invokeMethod('getEndpoint').then((url) { - platform.invokeMethod('getAppToken').then((appToken) { - Map args = StringsLocalization.strings; - args['token'] = token; - args['url'] = url; - args['appToken'] = appToken; - args['localeCode'] = StringsLocalization.localeCode; - args['color'] = Resources - .getPrimaryColor(app) - .value - .toString(); - platform.invokeMethod('startScanner', args); - }); - }); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => + PurchaseSumScreen(helper, app, token)) + ); } } diff --git a/lib/strings.dart b/lib/strings.dart index 026c2a7..ec5b3c4 100644 --- a/lib/strings.dart +++ b/lib/strings.dart @@ -111,17 +111,25 @@ class StringsLocalization { return [nominative, singular, plural]; } + static _normalizeDouble(String val) => val.substring(0, val.length - 3); static String confirmPurchase(String val, int code) { - String trimmedVal = val.substring(0, val.length - 3); + String trimmedVal = _normalizeDouble(val); return sprintf(strings['confirm_purchase'], [val, declineCurrency(int.parse(trimmedVal), code)]); } static String purchaseCompleted(String val, int code) { - String trimmedVal = val.substring(0, val.length - 3); + String trimmedVal = _normalizeDouble(val); return sprintf(strings['purchase_complite'], [val, declineCurrency(int.parse(trimmedVal), code)]); } + static String purchaseDetails(String total, String discountTotal, String discount, int code) { + final String normTotal = _normalizeDouble(total); + final String normDiscountTotal = _normalizeDouble(discountTotal); + return sprintf(strings['purchase_details'], [total, declineCurrency(int.parse(normTotal), code), + discountTotal, declineCurrency(int.parse(normDiscountTotal), code), discount]); + } + static String registration() => strings['registration']; static String usage() => strings['usage']; static String support() => strings['support']; @@ -214,4 +222,6 @@ class StringsLocalization { static String joysMinus() => strings['joys_minus']; static String joysHint() => strings['joys_hint']; static String phone() => strings['phone']; + static String cancel() => strings['cancel']; + static String cancelDialog() => strings['purchase_cancellation']; }