diff --git a/android/app/build.gradle b/android/app/build.gradle index 0b26478..1ea1db3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -25,7 +25,7 @@ android { defaultConfig { targetSdkVersion 21 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - applicationId "com.dinnect.checker" + applicationId "com.dinect.checker" } buildTypes { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2f27478..b28861b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ @@ -18,7 +18,7 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> - - 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 6564507..b61d425 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 @@ -1,4 +1,4 @@ -package com.dinnect.checker.activity; +package com.dinect.checker.activity; import android.app.Activity; import android.content.pm.ActivityInfo; @@ -29,8 +29,8 @@ import net.sourceforge.zbar.Symbol; import net.sourceforge.zbar.SymbolSet; import net.sourceforge.zbar.Config; -import com.dinnect.checker.R; -import com.dinnect.checker.view.CameraPreview; +import com.dinect.checker.R; +import com.dinect.checker.view.CameraPreview; public class CameraActivity extends Activity { 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 18a13cd..a57c639 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 @@ -1,10 +1,12 @@ -package com.dinnect.checker.activity; +package com.dinect.checker.activity; import android.os.Bundle; +import android.content.Context; import android.content.Intent; import android.util.Log; -import com.dinnect.checker.activity.CameraActivity; -import com.dinnect.checker.service.RegistrationIntentService; +import android.content.SharedPreferences; +import com.dinect.checker.activity.CameraActivity; +import com.dinect.checker.service.RegistrationIntentService; import io.flutter.app.FlutterActivity; import io.flutter.plugins.GeneratedPluginRegistrant; @@ -16,32 +18,56 @@ import io.flutter.plugin.common.MethodChannel.Result; import com.google.android.gms.iid.InstanceID; +import java.util.Map; + public class MainActivity extends FlutterActivity { - private static final String INSTANCE_ID_CHANNEL = "com.dinnect.checker/instance_id"; + private static final String INSTANCE_ID_CHANNEL = "com.dinect.checker/instance_id"; + private static final String PREF_POS_TOKEN = "pref_pos_token"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); GeneratedPluginRegistrant.registerWith(this); + final SharedPreferences preferences = getPreferences(Context.MODE_PRIVATE); + new MethodChannel(getFlutterView(), INSTANCE_ID_CHANNEL).setMethodCallHandler( new MethodCallHandler() { @Override public void onMethodCall(MethodCall call, Result result) { - if (call.method.equals("getInstanceID")) { + switch (call.method) { - InstanceID instanceID = InstanceID.getInstance(MainActivity.this); - String id = instanceID.getId(); + case "getInstanceID": - if (id != null) { - result.success(id); - } else { - result.error("UNAVAILABLE", "Can't get instanceID.", null); - } + InstanceID instanceID = InstanceID.getInstance(MainActivity.this); + String id = instanceID.getId(); - } else { - result.notImplemented(); + if (id != null) { + result.success(id); + } else { + result.error("UNAVAILABLE", "Can't get instanceID.", null); + } + break; + + case "saveToken": + Map arguments = call.arguments(); + String token = (String) arguments.get("token"); + Log.d("kifio", token); + preferences.edit().putString(PREF_POS_TOKEN, token).apply(); + break; + + case "getToken": + result.success(preferences.getString(PREF_POS_TOKEN, null)); + break; + + case "startScanner": + startActivity(new Intent(MainActivity.this, CameraActivity.class)); + break; + + default: + result.notImplemented(); + break; } } }); @@ -55,4 +81,12 @@ public class MainActivity extends FlutterActivity { } + public void saveToken() { + + } + + public void getToken() { + + } + } diff --git a/android/app/src/main/java/com/dinect/checker/service/RegistrationIntentService.java b/android/app/src/main/java/com/dinect/checker/service/RegistrationIntentService.java index 85e56d0..b801b26 100644 --- a/android/app/src/main/java/com/dinect/checker/service/RegistrationIntentService.java +++ b/android/app/src/main/java/com/dinect/checker/service/RegistrationIntentService.java @@ -1,4 +1,4 @@ -package com.dinnect.checker.service; +package com.dinect.checker.service; import android.app.IntentService; import android.content.Intent; diff --git a/android/app/src/main/java/com/dinect/checker/view/CameraPreview.java b/android/app/src/main/java/com/dinect/checker/view/CameraPreview.java index 68d0c22..040215e 100644 --- a/android/app/src/main/java/com/dinect/checker/view/CameraPreview.java +++ b/android/app/src/main/java/com/dinect/checker/view/CameraPreview.java @@ -1,4 +1,4 @@ -package com.dinnect.checker.view; +package com.dinect.checker.view; import java.io.IOException; diff --git a/assets/activate_token_message_background.png b/assets/activate_token_message_background.png new file mode 100644 index 0000000..e852596 Binary files /dev/null and b/assets/activate_token_message_background.png differ diff --git a/lib/activate_token.dart b/lib/activate_token.dart new file mode 100644 index 0000000..87bcebd --- /dev/null +++ b/lib/activate_token.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'main.dart'; + +/// Экран регистрации магазина и кассы. +class FinishRegistrationScreen extends StatefulWidget { + @override State createState() => new _RegistrationScreenState(); +} + +class _RegistrationScreenState extends BaseState { + + @override Widget build(BuildContext context) { + return new Scaffold(appBar: _getAppBar(), body: _getScreen(context)); + } + + AppBar _getAppBar() { + return new AppBar(title: new Text("Регистрация магазина"), + backgroundColor: const Color(0xff4272e7), actions: [ + new IconButton( + icon: new Icon(Icons.help_outline), + tooltip: 'Air it', + onPressed: faq, + ), + new IconButton( + icon: new Image(height: 24.0, width: 24.0, image: new AssetImage(logout_png)), + tooltip: 'Restitch it', + onPressed: logout, + ) + ]); + } + + Widget _getScreen(BuildContext context) { + return new Center(child: new Column(children: [ + _getLogo(), + _getDecoratedInputField(merchantIDHint), + _getDecoratedInputField(posIDHint), + _getMessage(), + _getButton(context) + ])); + } + + Container _getLogo() { + return new Container(padding: new EdgeInsets.only(top: 16.0, bottom: 16.0), + child: new Image.asset(logo_png, height: 24.0, width: 156.0)); + } + + Container _getDecoratedInputField(String hint) { + return new Container( + padding: new EdgeInsets.only(left: 28.0, right: 28.0, top: 8.0), + child: new Container(height: 48.0, + padding: new EdgeInsets.only(left: 16.0, right: 16.0), + decoration: _getDecoraionForInputField(), + child: _getInputField(hint))) ; + } + + TextField _getInputField(String hint) { + return new TextField(decoration: new InputDecoration(hintText: hint, + hideDivider: true, + hintStyle: new TextStyle(color: const Color(0xffa5a5a5), + fontSize: 16.0)), onChanged: null); + } + + Container _getMessage() { + return new Container(padding: new EdgeInsets.only(top: 20.0, left: 26.0, right: 26.0), + child: new Container(height: 128.0, decoration: _getDecoraionForMessageField(), + padding: new EdgeInsets.only(top: 16.0, bottom: 8.0, left: 28.0, right: 28.0), + child: new Text('Запрос на активацию программы отправлен, дождитесь подтверждения активации администратором', + textAlign: TextAlign.center, style: new TextStyle(fontWeight: FontWeight.bold, color: const Color(0xff4e3a19))))); + } + + Decoration _getDecoraionForMessageField() { + return new BoxDecoration(image: new DecorationImage( + image: new ExactAssetImage(activate_token_bg_png), fit: BoxFit.fill)); + } + + Decoration _getDecoraionForInputField() { + return new BoxDecoration(color: Colors.white, + border: new Border.all( + color: const Color(0xffcfd8dc), width: 1.0,), + borderRadius: new BorderRadius.all(new Radius.circular(4.0))); + } + + Container _getButton(BuildContext context) { + return new Container(padding: new EdgeInsets.only(top: 36.0), + child: new Container(height: 64.0, padding: new EdgeInsets.all(8.0), + child: new RaisedButton(child: new Text('Обновить статус активации', + style: new TextStyle(color: Colors.white)), + onPressed: _checkToken(context), + disabledColor: const Color(0xffbfbfbf), + color: const Color(0xff3078c0)))); + } +} + +_checkToken(BuildContext context) { + checkToken(context, new CheckTokenCallback()); +} + +class CheckTokenCallback extends Callback { + + call(BuildContext context) { + startScanner(); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 9d82383..2953d10 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,17 +1,75 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'splash.dart'; +import 'dart:async'; +import 'dart:convert'; -const String _name = 'Ivan Murashov'; +/// Главный класс приложения. +/// Здесь распоосложены константы и некоторые методы, которые могут вызываться с разных экранов приложения. + +// 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'; + final httpClient = createHttpClient(); void main() { runApp(new Checker()); } +/// Токен кассы. Инициализируется при регистрации. +String token; + +/// Проверка статуса токена. Токен может быть активирован, либо не активирован. +void checkToken(BuildContext context, Callback callback) { + + String url = intUrl + 'tokens/' + token + '?_dmapptoken=' + intToken; + print(url); + + httpClient.get(url).then((response) { + + print(response.body); + Map parsedMap = JSON.decode(response.body); + bool active = parsedMap['active']; + + if (!active) { + callback.call(context); + } else { + // Запускается экран сканера, токен кассы активирован, с его помощью можно делать запросы к pos-api. + startScanner(); + } + + }).catchError((error) { + print(error.toString()); + }); +} + +/// Запуск спецефичной для каждой платформы части приложения - сканера. +/// Может производиться с нескольких экранов (splash, finish_registration). +startScanner() async{ + const platform = const MethodChannel('com.dinect.checker/instance_id'); + await platform.invokeMethod('startScanner'); +} + +/// Навигация по приложению. +/// widget - следующий экран приложения. +pushRoute(BuildContext context, Widget widget) { + Navigator.of(context).push(new MaterialPageRoute( + builder: (BuildContext context) { + return widget; + })); +} + class Checker extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp(title: "DemoApp", home: new SplashScreen()); @@ -27,4 +85,8 @@ abstract class BaseState extends State { void logout() { } +} + +abstract class Callback { + void call(BuildContext context); } \ No newline at end of file diff --git a/lib/registration.dart b/lib/registration.dart index 0001a8d..144e12b 100644 --- a/lib/registration.dart +++ b/lib/registration.dart @@ -1,6 +1,8 @@ +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'main.dart'; +import 'activate_token.dart'; /// Экран регистрации магазина и кассы. class RegistrationScreen extends StatefulWidget { @@ -9,14 +11,11 @@ class RegistrationScreen extends StatefulWidget { class _RegistrationScreenState extends BaseState { - static const String _merchantIDHint = 'ID магазина'; - static const String _posIDHint = 'Номер кассы'; - String _merchantID = ""; String _posID = ""; @override Widget build(BuildContext context) { - return new Scaffold(appBar: _getAppBar(), body: _getScreen()); + return new Scaffold(appBar: _getAppBar(), body: _getScreen(context)); } AppBar _getAppBar() { @@ -35,25 +34,19 @@ class _RegistrationScreenState extends BaseState { ]); } - Widget _getScreen() { - return new Center(child: new Column(children: _getChildren())); + Widget _getScreen(BuildContext context) { + return new Center(child: new Column(children: [ + _getLogo(), + _getDecoratedInputField(merchantIDHint, 0.0), + _getDecoratedInputField(posIDHint, 36.0), + _getButton(context) + ])); } - List _getChildren() { - return[ - _getLogo(), - _getDecoratedInputField(_merchantIDHint, 0.0), - _getDecoratedInputField(_posIDHint, 36.0), - _getButton() - ]; - } Container _getLogo() { return new Container(height: 192.0, - child: new Center( - child: new Image.asset('assets/registration_logo.png', - height: 24.0, - width: 156.0))); + child: new Image.asset(logo_png, height: 24.0, width: 156.0)); } Container _getDecoratedInputField(String hint, double topPadding) { @@ -75,9 +68,9 @@ class _RegistrationScreenState extends BaseState { void _handleUserInput(String hint, String text) { if (text.length > 0) { setState(() { - if (hint == _merchantIDHint) + if (hint == merchantIDHint) _merchantID = text; - else if (hint == _posIDHint) + else if (hint == posIDHint) _posID = text; }); } @@ -90,13 +83,14 @@ class _RegistrationScreenState extends BaseState { borderRadius: new BorderRadius.all(new Radius.circular(4.0))); } - Container _getButton() { + Container _getButton(BuildContext context) { return new Container(padding: new EdgeInsets.only(top: 36.0), child: new Container(height: 64.0, padding: new EdgeInsets.all(8.0), child: new RaisedButton(child: new Text('ЗАРЕГИСТРИРОВАТЬ', style: new TextStyle(color: Colors.white)), - onPressed: _isFieldsAreFilled() ? () => _register(_merchantID) : null, - disabledColor: const Color(0xffbfbfbf)))); + onPressed: _isFieldsAreFilled() ? () => _registerShop(context, _merchantID) : null, + disabledColor: const Color(0xffbfbfbf), + color: const Color(0xff3078c0)))); } _isFieldsAreFilled() { @@ -105,9 +99,13 @@ class _RegistrationScreenState extends BaseState { return _merchantID.length == 5 && _posID.length > 0; } - _register(String merchantShop) async { + void _registerShop(BuildContext context, String merchantShop) { + _register(context, merchantShop); + } - const platform = const MethodChannel('com.dinnect.checker/instance_id'); + _register(BuildContext context, String merchantShop) async { + + const platform = const MethodChannel('com.dinect.checker/instance_id'); String url = intUrl + 'tokens/?_dmapptoken=' + intToken; String pos = await platform.invokeMethod('getInstanceID'); print(pos); @@ -127,6 +125,12 @@ class _RegistrationScreenState extends BaseState { httpClient.post(url, body: body).then((response) { print(response.body); + Map parsedMap = JSON.decode(response.body); + token = parsedMap['token']; + platform.invokeMethod('saveToken', {'token' : token}).then((value) { + print(value.toString()); + }); + pushRoute(context, new FinishRegistrationScreen()); }).catchError((error) { print(error.toString()); }); diff --git a/lib/splash.dart b/lib/splash.dart index 2a289a9..07a9411 100644 --- a/lib/splash.dart +++ b/lib/splash.dart @@ -1,24 +1,47 @@ -import 'dart:async'; -import 'registration.dart'; - +import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; +import 'main.dart'; +import 'registration.dart'; +import 'activate_token.dart'; +import 'dart:async'; class SplashScreen extends StatelessWidget { @override Widget build(BuildContext context) { + // Splash скрин зависает мимнимум на 1 секунду. + // После этого начинается проверка токена. new Future.delayed(const Duration(milliseconds: 1000), () { - _goToNextScreen(context); + _showNextScreen(context); }); - return new Image.asset('assets/splash.png', fit: BoxFit.cover); + return new Image.asset(logo_png, fit: BoxFit.cover); } - _goToNextScreen(BuildContext context) async { - Navigator.of(context).push(new MaterialPageRoute( - builder: (BuildContext context) { - return new RegistrationScreen(); - })); + /// Запуск следующего экрана приложения. + _showNextScreen(BuildContext context) async { + + const platform = const MethodChannel('com.dinect.checker/instance_id'); + token = await platform.invokeMethod('getToken'); + + // В случае, если в приложении отсутствует токен, + // необходимо запустить регистрацию кассы. + if (token == null) { + pushRoute(context, new RegistrationScreen()); + } else { + checkToken(context, new CheckTokenCallback()); + } + } + +} + +class CheckTokenCallback extends Callback { + + /// Запускается экран ожидания активации токена. + /// В реальности токен активируется в админке вручную, + /// на тестовом сервере токен активируется через несколько минут после создания. + call(BuildContext context) { + pushRoute(context, new FinishRegistrationScreen()); } } \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index c7b847f..ec73eea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,7 @@ flutter: - assets/registration_logo.png - assets/splash.png - assets/logout.png + - assets/activate_token_message_background.png # To add assets from package dependencies, first ensure the asset # is in the lib/ directory of the dependency. Then,