From d0b5b173342c536f6fa1971d4f667e2d7682aba1 Mon Sep 17 00:00:00 2001 From: kifio Date: Thu, 27 Jul 2017 08:45:47 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=8B=D0=BD=D0=B5=D1=81=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=83=20=D1=81=20=D1=81=D0=B5=D1=82=D1=8C?= =?UTF-8?q?=D1=8E=20=D0=B8=20=D0=BE=D0=B1=D1=89=D0=B8=D0=B5=20=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D0=BE=D0=B4=D1=8B=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../checker/activity/CameraActivity.java | 21 ++- .../dinect/checker/activity/MainActivity.java | 20 ++- lib/activate_token.dart | 6 +- lib/base_state.dart | 16 +- lib/common.dart | 111 ++++++++++++ lib/consts.dart | 35 ++++ lib/faq.dart | 2 +- lib/main.dart | 163 +----------------- lib/network.dart | 39 +++++ lib/registration.dart | 10 +- lib/splash.dart | 39 +++-- 11 files changed, 256 insertions(+), 206 deletions(-) create mode 100644 lib/common.dart create mode 100644 lib/consts.dart create mode 100644 lib/network.dart diff --git a/android/app/src/main/java/com/dinect/checker/activity/CameraActivity.java b/android/app/src/main/java/com/dinect/checker/activity/CameraActivity.java index 5f7a84c..77adbcd 100644 --- a/android/app/src/main/java/com/dinect/checker/activity/CameraActivity.java +++ b/android/app/src/main/java/com/dinect/checker/activity/CameraActivity.java @@ -47,6 +47,10 @@ import android.view.SurfaceView; import java.io.IOException; import java.lang.Exception; import java.util.List; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; import com.dinect.checker.R; public class CameraActivity extends AppCompatActivity implements SurfaceHolder.Callback { @@ -57,7 +61,7 @@ public class CameraActivity extends AppCompatActivity implements SurfaceHolder.C private HashMap mContours = new HashMap<>(); private boolean mBarcodeScanned = false; - private boolean previewing = true; + private boolean previewing = false; private Handler autoFocusHandler; private int mOffset; @@ -117,12 +121,14 @@ public class CameraActivity extends AppCompatActivity implements SurfaceHolder.C protected void onResume() { super.onResume(); mCamera.startPreview(); + previewing = true; } @Override protected void onPause() { super.onPause(); mCamera.stopPreview(); + previewing = false; } @Override @@ -138,6 +144,7 @@ public class CameraActivity extends AppCompatActivity implements SurfaceHolder.C try { mCamera.setPreviewDisplay(holder); mCamera.startPreview(); + previewing = true; try { mCamera.autoFocus(autoFocusCallback); } catch(Exception e) { @@ -207,8 +214,15 @@ public class CameraActivity extends AppCompatActivity implements SurfaceHolder.C @Override public boolean onOptionsItemSelected(MenuItem item) { + Intent intent = new Intent(); if (item.getItemId() == R.id.logout) { - setResult(RESULT_OK); + intent.putExtra("item", "logout"); + setResult(RESULT_OK, intent); + finish(); + return true; + } else if (item.getItemId() == R.id.faq) { + intent.putExtra("item", "faq"); + setResult(RESULT_OK, intent); finish(); return true; } @@ -253,8 +267,9 @@ public class CameraActivity extends AppCompatActivity implements SurfaceHolder.C private Runnable doAutoFocus = new Runnable() { public void run() { - if (previewing) + if (previewing) { mCamera.autoFocus(autoFocusCallback); + } } }; diff --git a/android/app/src/main/java/com/dinect/checker/activity/MainActivity.java b/android/app/src/main/java/com/dinect/checker/activity/MainActivity.java index 0d1f568..95a0c33 100644 --- a/android/app/src/main/java/com/dinect/checker/activity/MainActivity.java +++ b/android/app/src/main/java/com/dinect/checker/activity/MainActivity.java @@ -108,19 +108,25 @@ public class MainActivity extends FlutterActivity { if (requestCode == START_SCANNER_REQUEST_CODE && resultCode == RESULT_CANCELED) { finish(); } else if (requestCode == START_SCANNER_REQUEST_CODE && resultCode == RESULT_OK) { - if (data == null) { - mChannel.invokeMethod("foo", null); - } else { + if (data != null) { String code = data.getExtras().getString("code", null); - if (code == null) { - mChannel.invokeMethod("foo", null); - } else { - mChannel.invokeMethod("purchase", code); + if (code != null) { + mChannel.invokeMethod("purchase", code); + } else { + String menuItem = data.getExtras().getString("item", null); + Log.d("item", menuItem); + if (menuItem != null) { + mChannel.invokeMethod(menuItem, null); } + } } } } + private void handleItemClick() { + + } + private void getDocID() { } diff --git a/lib/activate_token.dart b/lib/activate_token.dart index f6aa3b2..42ca4df 100644 --- a/lib/activate_token.dart +++ b/lib/activate_token.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'main.dart'; +import 'common.dart'; +import 'consts.dart'; +import 'network.dart'; import 'dart:convert'; // Пакет для обработки json с ответом от сервера. import 'base_state.dart'; @@ -40,7 +42,7 @@ class _RegistrationScreenState extends BaseState { handleTap() { if (_tokenActive) { - startScanner(context); + startScanner(context); } else { checkTokenStatus(context).then((response) { diff --git a/lib/base_state.dart b/lib/base_state.dart index 4350068..3c156eb 100644 --- a/lib/base_state.dart +++ b/lib/base_state.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'dart:convert'; -import 'dart:async'; -import 'main.dart'; -import 'faq.dart'; +import 'common.dart'; +import 'network.dart'; +import 'consts.dart'; abstract class BaseState extends State { bool loading = false; String error = null; - String textFieldValue = ""; + String textFieldValue = ''; TextEditingController controller = new TextEditingController(); @@ -28,7 +27,7 @@ abstract class BaseState extends State { } getFaqButton() { - return new IconButton(icon: new Icon(Icons.help_outline), onPressed: () => faq()); + return new IconButton(icon: new Icon(Icons.help_outline), onPressed: () => faq(context)); } getLogoutButton() { @@ -45,11 +44,6 @@ abstract class BaseState extends State { String getHint(); - faq() { - var route = new MaterialPageRoute(builder: (BuildContext context) => new FAQScreen()); - Navigator.of(context).push(route); - } - /// Метод возвращает контейнер с отступами, который содержит картинку с логотипом. getLogo() { double containerHeight = 92.0; diff --git a/lib/common.dart b/lib/common.dart new file mode 100644 index 0000000..c4e83c7 --- /dev/null +++ b/lib/common.dart @@ -0,0 +1,111 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/material.dart'; + +import 'network.dart'; +import 'purchase.dart'; +import 'faq.dart'; + +// Канал для взаимодействия с кодом платформы. +const platform = const MethodChannel('com.dinect.checker/instance_id'); + +/// Токен кассы. Инициализируется при регистрации. +String token; + +// Метод обеспечивает замену текущего объекта route новым. +pushRoute(BuildContext context, Widget widget) { + var route = new MaterialPageRoute(builder: (BuildContext context) => widget); + Navigator.of(context).pushReplacement(route); +} + +// Добавление route, с возможностью вернуться к предыдущему экрану. +faq(BuildContext context) { + var route = new MaterialPageRoute(builder: (BuildContext context) => new FAQScreen()); + Navigator.of(context).push(route); +} + +// В методе отправляется запрос на удаление токена кассы, очищаются SharedPreferences приложения. +logout(BuildContext context) { + + VoidCallback positiveCalback = () { + if (token != null) { + deleteToken(token).then((response) async { + print(response.body); + await platform.invokeMethod('removeKeys'); + pushRoute(context, new RegistrationScreen()); // Запускаем регистрацию + }).catchError((error) { + print(error.toString()); + }); + } else { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + } + }; + + showYesNoDialog(context, 'Подтверждение', 'Вы действительно хотите выйти и ввести другой номер магазина?', positiveCalback); +} + +/// Запуск спецефичной для каждой платформы части приложения - сканера. +/// Может производиться с нескольких экранов (splash, finish_registration). +startScanner(BuildContext context) async { + + // Канал ловит вызовы методов из "нативной" части приложения. + // Могут быть вызваны либо logaut либо faq, либо purchase. + if (token != null) { + platform.setMethodCallHandler((MethodCall call) async { + + print('call.method: ${call.method}'); + if (call.method == 'logout') { + logout(context); + } else if (call.method == 'faq') { + faq(context); + } else { + + List usersList = JSON.decode(call.arguments); + print('usersList.length: ${usersList.length}'); + if (usersList.length > 0) { + pushRoute(context, new PurchaseScreen(usersList[0], card)); + } + + + // var card = ; + + // String url = 'http://pos-api-int.dinect.com/20130701/users/?auto=${card}'; + // print('url: ' + url); + + // var headers = { + // 'DM-Authorization': 'dmapptoken 9fec83cdca38c357e6b65dbb17514cdd36bf2a08', + // 'Authorization': 'dmtoken ${token}' + // }; + + // httpClient.get(url, headers: headers).then((response) { + + // print(response.body); + + + // }).catchError((error) { + // print(error.toString()); + // }); + + } + }); + } + + await platform.invokeMethod('startScanner'); +} + +// Запуск диалога с двумя кнопками +showYesNoDialog(BuildContext context, String title, String content, VoidCallback positiveCallback) { + showDialog(context: context, child: new AlertDialog( + title: new Text(title), + content: new Text(content), + actions: [ + new FlatButton( + child: new Text('Нет'), + onPressed: () { + Navigator.of(context).pop(); + } + ), + new FlatButton( + child: new Text('Да'), + onPressed: positiveCallback)])); +} \ No newline at end of file diff --git a/lib/consts.dart b/lib/consts.dart new file mode 100644 index 0000000..59033d5 --- /dev/null +++ b/lib/consts.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +// Serious constants +const String intUrl = 'https://pos-api-int.dinect.com/20130701/'; +const String intToken = '9fec83cdca38c357e6b65dbb17514cdd36bf2a08'; + +// Hints +const String merchantIDHint = 'ID магазина'; +const String posIDHint = 'Номер кассы'; + +// Assets +const String logo_png = 'assets/registration_logo.png'; +const String splash_png = 'assets/splash.png'; +const String logout_png = 'assets/logout.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'; +const String expansion_icon_png = 'assets/expansion_icon.png'; +const String powered_by_dinect_splash_png = 'assets/powered_by_dinect_splash.png'; +const String powered_by_dinect_png = 'assets/powered_by_dinect.png'; + +// Colors +const Color primaryColor = const Color(0xffeb0004); +const Color greyTextColor = const Color(0xffa5a5a5); +const Color textBorderColor = const Color(0xffcfd8dc); +const Color tokenActiveTextColor = const Color(0xff1f5a1f); +const Color tokenActivateTextColor = const Color(0xff4e3a19); +const Color greenBackground = const Color(0xff8ae28a); +const Color faqGrey = const Color(0xff5b5b5b); +const Color faqTitlesColor = const Color(0xff404040); + +// Dimens +const double verticalMargin = 28.0; +const double buttonVerticalMargin = 42.0; +const double buttonHeight = 48.0; +const double iconHeight = 20.0; \ No newline at end of file diff --git a/lib/faq.dart b/lib/faq.dart index f429d47..8bba250 100644 --- a/lib/faq.dart +++ b/lib/faq.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'main.dart'; import 'base_state.dart'; +import 'consts.dart'; /// Класс содержит заголовки и текст блоков FAQ. class Entry { diff --git a/lib/main.dart b/lib/main.dart index 5a4552f..98574cd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,169 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'splash.dart'; -import 'registration.dart'; -import 'dart:async'; -import 'dart:convert'; -import 'purchase.dart'; - -/// Главный класс приложения. -/// Здесь распоосложены константы и некоторые методы, которые могут вызываться с разных экранов приложения. - -// Serious constants -const String intUrl = 'https://pos-api-int.dinect.com/20130701/'; -const String intToken = '9fec83cdca38c357e6b65dbb17514cdd36bf2a08'; - -// Hints -const String merchantIDHint = 'ID магазина'; -const String posIDHint = 'Номер кассы'; - -// Assets -const String logo_png = 'assets/registration_logo.png'; -const String splash_png = 'assets/splash.png'; -const String logout_png = 'assets/logout.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'; -const String expansion_icon_png = 'assets/expansion_icon.png'; -const String powered_by_dinect_splash_png = 'assets/powered_by_dinect_splash.png'; -const String powered_by_dinect_png = 'assets/powered_by_dinect.png'; -// Colors -const Color primaryColor = const Color(0xffeb0004); -const Color greyTextColor = const Color(0xffa5a5a5); -const Color textBorderColor = const Color(0xffcfd8dc); -const Color tokenActiveTextColor = const Color(0xff1f5a1f); -const Color tokenActivateTextColor = const Color(0xff4e3a19); -const Color greenBackground = const Color(0xff8ae28a); -const Color faqGrey = const Color(0xff5b5b5b); -const Color faqTitlesColor = const Color(0xff404040); -// Dimens -const double verticalMargin = 28.0; -const double buttonVerticalMargin = 42.0; -const double buttonHeight = 48.0; -const double iconHeight = 20.0; - -const platform = const MethodChannel('com.dinect.checker/instance_id'); - -// HttpClient -final httpClient = createHttpClient(); - -/// Токен кассы. Инициализируется при регистрации. -String token; +import 'consts.dart'; /// Точка входа в приложение. void main() { runApp(new Checker()); } -/// Проверка статуса токена. Токен может быть активирован, либо не активирован. -checkTokenStatus(BuildContext context) async { - return httpClient.get(intUrl + 'tokens/' + token + '?_dmapptoken=' + intToken); -} - -createToken(String merchantId) async { - - String url = intUrl + 'tokens/?_dmapptoken=' + intToken; - - String posID = await platform.invokeMethod('getPosID'); - print('posID: ${posID}'); - - print('merchantId: ${merchantId}'); - - // Поле description - необязательное. - var body = { - 'merchant_shop': merchantId, - 'pos': posID, - }; - - return httpClient.post(url, body: body); -} - -/// Запуск спецефичной для каждой платформы части приложения - сканера. -/// Может производиться с нескольких экранов (splash, finish_registration). -startScanner(BuildContext context) async { - - // Канал ловит вызовы методов из "нативной" части приложения. - // Могут быть вызваны либо logaut либо faq, либо purchase. - if (token != null) { - platform.setMethodCallHandler((MethodCall call) async { - - if (call.method == 'foo') { - logout(context); - } else { - - var card = call.arguments; - - print('card: ' + card); - String url = 'http://pos-api-int.dinect.com/20130701/users/?auto=${card}'; - print('url: ' + url); - - var headers = { - 'DM-Authorization': 'dmapptoken 9fec83cdca38c357e6b65dbb17514cdd36bf2a08', - 'Authorization': 'dmtoken ${token}' - }; - - httpClient.get(url, headers: headers).then((response) { - - print(response.body); - - List usersList = JSON.decode(response.body); - print('usersList.length: ${usersList.length}'); - if (usersList.length > 0) { - pushRoute(context, new PurchaseScreen(usersList[0], card)); - } - - }).catchError((error) { - print(error.toString()); - }); - - } - }); - } - - await platform.invokeMethod('startScanner'); -} - -logout(BuildContext context) { - showDialog(context: context, child: new AlertDialog( - title: new Text('Подтверждение'), - content: new Text('Вы действительно хотите выйти и ввести другой номер магазина?'), - actions: [ - new FlatButton( - child: new Text('Нет'), - onPressed: () { - Navigator.of(context).pop(); - } - ), - new FlatButton( - child: new Text('Да'), - onPressed: () { - if (token != null) { - String url = intUrl + 'tokens/' + token + '?_dmapptoken=' + intToken; - print(url); - httpClient.delete(url).then((response) async { - print(response.body); - await platform.invokeMethod('removeKeys'); - Navigator.of(context).pop(); - Navigator.of(context).pop(); - pushRoute(context, new RegistrationScreen()); - }).catchError((error) { - print(error.toString()); - }); - } else { - Navigator.of(context).pop(); - Navigator.of(context).pop(); - } - } - )])); - -} - -/// Навигация по приложению. -/// widget - следующий экран приложения. -pushRoute(BuildContext context, Widget widget) { - var route = new MaterialPageRoute(builder: (BuildContext context) => widget); - Navigator.of(context).pushReplacement(route); -} - class Checker extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp(title: "AutoClub", @@ -173,6 +16,4 @@ class Checker extends StatelessWidget { accentColor: primaryColor )); } -} - - +} \ No newline at end of file diff --git a/lib/network.dart b/lib/network.dart new file mode 100644 index 0000000..bd41282 --- /dev/null +++ b/lib/network.dart @@ -0,0 +1,39 @@ +import 'package:flutter/services.dart'; +import 'dart:async'; + +import 'consts.dart'; + +// Клиент http приложения +final httpClient = createHttpClient(); + +// Попытка создать токен для кассы. +// В случае если токен для кассы уже существует, вернется ошибка 409. +// На сервере есть ограничение в 40 токенов. +createToken(String merchantId, String posID) async { + + String url = intUrl + 'tokens/?_dmapptoken=' + intToken; + + // Поле description - необязательное. + var body = { + 'merchant_shop': merchantId, + 'pos': posID, + }; + + return httpClient.post(url, body: body); +} + +// Проверка статуса токена. В ответе приходит параметр active, который может быть либо true, либо false,. +checkTokenStatus(String token) async { + return httpClient.get(intUrl + 'tokens/' + token + '?_dmapptoken=' + intToken); +} + +// Удаление токена на сервере. +deleteToken(String token) async { + String url = intUrl + 'tokens/' + token + '?_dmapptoken=' + intToken; + print(url); + return httpClient.delete(url).then(); +} + +// Удалить токены +// 57e0d09aa935252d2a3463ebc1d61501608a6af9 +// f1355ea87375c173695b57afa72f78fedbe5b6c3 \ No newline at end of file diff --git a/lib/registration.dart b/lib/registration.dart index fe61128..d972bea 100644 --- a/lib/registration.dart +++ b/lib/registration.dart @@ -1,14 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'dart:convert'; // Пакет для обработки json с ответом от сервера. -import 'dart:async'; -import 'main.dart'; +import 'common.dart'; +import 'network.dart'; +import 'consts.dart'; import 'activate_token.dart'; import 'base_state.dart'; -/// На фото мой сын, большой любитель голых констант. - /// Экран регистрации магазина и кассы. class RegistrationScreen extends StatefulWidget { @override State createState() => new _RegistrationScreenState(); @@ -54,7 +52,7 @@ class _RegistrationScreenState extends BaseState { /// Получение от платформы id установки, формирование запроса на получение токена, сохранение токена. _register(BuildContext context) async { - createToken(textFieldValue).then((response) { + createToken(textFieldValue, await platform.invokeMethod('getPosID')).then((response) { setState(() { error = null; diff --git a/lib/splash.dart b/lib/splash.dart index 6c1bd5f..69adcdb 100644 --- a/lib/splash.dart +++ b/lib/splash.dart @@ -1,12 +1,12 @@ -import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; import 'dart:async'; import 'dart:convert'; -import 'main.dart'; +import 'common.dart'; +import 'network.dart'; +import 'consts.dart'; import 'registration.dart'; import 'activate_token.dart'; -import 'purchase.dart'; class SplashScreen extends StatelessWidget { @@ -20,14 +20,18 @@ class SplashScreen extends StatelessWidget { // pushRoute(context, new PurchaseScreen(null)); }); - return new Stack(children: [new Container(padding: new EdgeInsets.only(left: 48.0, right: 48.0), decoration: getSplashBg()), + return new Stack(children: [getBackgroundContainer(), new Align(alignment: FractionalOffset.bottomRight, child: new Container(margin: new EdgeInsets.only(right: 11.0, bottom: 5.0), child: new Image.asset(powered_by_dinect_splash_png, height: 16.0, width: 122.0)))]); } - Decoration getSplashBg() { - return new BoxDecoration(image: new DecorationImage( - image: new ExactAssetImage(splash_png), fit: BoxFit.cover)); + getBackgroundContainer() { + const margin = 48.0; + return new Container(padding: new EdgeInsets.only(left: margin, right: margin), decoration: getSplashBackground()); + } + + getSplashBackground() { + return new BoxDecoration(image: new DecorationImage(image: new ExactAssetImage(splash_png), fit: BoxFit.cover)); } /// Запуск следующего экрана приложения. @@ -42,20 +46,18 @@ class SplashScreen extends StatelessWidget { pushRoute(context, new RegistrationScreen()); } else { - checkTokenStatus(context).then((statusResponse) { + checkTokenStatus(token).then((statusResponse) { handleStatusResponse(context, statusResponse); }).catchError((error) { - print('Handle exception!'); print(error.toString()); return false; }); } } - startRegistration() async { - - } - + // Обработка ответа. + // В случае, если токен был удален может прийти active: false, либо 404. + // Если токен не активен, попробовать создать его еще раз. В случае успешного создания токена удалить его и перейти на экран регистрации handleStatusResponse(BuildContext context, var statusResponse) async { int code = statusResponse.statusCode; print('resp: ${code}'); @@ -73,13 +75,20 @@ class SplashScreen extends StatelessWidget { if (active) { startScanner(context); } else { - createToken(await platform.invokeMethod('getMerchantID')).then((response) { + createToken(await platform.invokeMethod('getMerchantID'), await platform.invokeMethod('getPosID')).then((response) { print('response.body: ${response.body}'); if (response.statusCode == 409) { pushRoute(context, new FinishRegistrationScreen()); } else { - startRegistration(); + deleteToken(token).then((response) async { + print(response.body); + await platform.invokeMethod('removeKeys'); + Navigator.of(context).pop(); // Убираем текущий route + pushRoute(context, new RegistrationScreen()); // Запускаем регистрацию + }).catchError((error) { + print(error.toString()); + }); } }).catchError((error) {