From b69762ff1575a4a19dc7b2a6e6b9eb7b9bffb925 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 2 Feb 2021 20:37:54 +1100 Subject: [PATCH] API refactoring: - Error if the server version is *older* than the min required version - Display dialog boxes for different server errors --- lib/api.dart | 136 +++++++++++++++++++++++++++------------- lib/barcode.dart | 1 - lib/l10n | 2 +- lib/main.dart | 4 +- lib/preferences.dart | 9 +-- lib/settings/login.dart | 35 ++++++----- lib/widget/dialogs.dart | 15 ++++- lib/widget/home.dart | 7 +-- 8 files changed, 133 insertions(+), 76 deletions(-) diff --git a/lib/api.dart b/lib/api.dart index f947655f..29e2e749 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -2,11 +2,15 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:image/image.dart'; +import 'package:InvenTree/widget/dialogs.dart'; + import 'package:path/path.dart' as path; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; @@ -22,18 +26,56 @@ import 'package:shared_preferences/shared_preferences.dart'; class InvenTreeAPI { - // Minimum supported InvenTree server version is 0.1.1 - static const List MIN_SUPPORTED_VERSION = [0, 1, 1]; + // Minimum supported InvenTree server version is + static const List MIN_SUPPORTED_VERSION = [0, 1, 5]; + + String get _requiredVersionString => "${MIN_SUPPORTED_VERSION[0]}.${MIN_SUPPORTED_VERSION[1]}.${MIN_SUPPORTED_VERSION[2]}"; bool _checkServerVersion(String version) { - // TODO - Decode the provided version string and determine if the server is "new" enough - return false; + + // Provided version string should be of the format "x.y.z [...]" + List versionSplit = version.split(' ').first.split('.'); + + // Extract the version number .. from the string + if (versionSplit.length != 3) { + return false; + } + + // Cast the server version to an explicit integer + int server_version_code = 0; + + print("server version: ${version}"); + + server_version_code += (int.tryParse(versionSplit[0]) ?? 0) * 100 * 100; + server_version_code += (int.tryParse(versionSplit[1]) ?? 0) * 100; + server_version_code += (int.tryParse(versionSplit[2])); + + print("server version code: ${server_version_code}"); + + int required_version_code = 0; + + required_version_code += MIN_SUPPORTED_VERSION[0] * 100 * 100; + required_version_code += MIN_SUPPORTED_VERSION[1] * 100; + required_version_code += MIN_SUPPORTED_VERSION[2]; + + print("required version code: ${required_version_code}"); + + return server_version_code >= required_version_code; } // Endpoint for requesting an API token static const _URL_GET_TOKEN = "user/token/"; static const _URL_GET_VERSION = ""; + Future showServerError(BuildContext context, String description) async { + showErrorDialog( + context, + I18N.of(context).serverError, + description, + icon: FontAwesomeIcons.server + ); + } + // Base URL for InvenTree API e.g. http://192.168.120.10:8000 String _BASE_URL = ""; @@ -58,21 +100,13 @@ class InvenTreeAPI { return baseUrl + url; } - String get apiUrl { - return _makeUrl("/api/"); - } + String get apiUrl => _makeUrl("/api/"); - String get imageUrl { - return _makeUrl("/image/"); - } + String get imageUrl => _makeUrl("/image/"); - String makeApiUrl(String endpoint) { - return _makeUrl("/api/" + endpoint); - } + String makeApiUrl(String endpoint) => _makeUrl("/api/" + endpoint); - String makeUrl(String endpoint) { - return _makeUrl(endpoint); - } + String makeUrl(String endpoint) => _makeUrl(endpoint); String _username = ""; String _password = ""; @@ -80,9 +114,7 @@ class InvenTreeAPI { // Authentication token (initially empty, must be requested) String _token = ""; - bool isConnected() { - return _token.isNotEmpty; - } + bool isConnected() => _token.isNotEmpty; /* * Check server connection and display messages if not connected. @@ -106,9 +138,6 @@ class InvenTreeAPI { return false; } - // Is the server version too old? - // TODO - // Finally return true; } @@ -138,18 +167,17 @@ class InvenTreeAPI { InvenTreeAPI._internal(); - Future connect() async { + Future connect(BuildContext context) async { var prefs = await SharedPreferences.getInstance(); String server = prefs.getString("server"); String username = prefs.getString("username"); String password = prefs.getString("password"); - return connectToServer(server, username, password); + return connectToServer(context, server, username, password); } - Future connectToServer(String address, String username, - String password) async { + Future connectToServer(BuildContext context, String address, String username, String password) async { /* Address is the base address for the InvenTree server, * e.g. http://127.0.0.1:8000 @@ -161,9 +189,14 @@ class InvenTreeAPI { username = username.trim(); if (address.isEmpty || username.isEmpty || password.isEmpty) { - errorMessage = "Server Error: Empty details supplied"; - print(errorMessage); - throw errorMessage; + await showErrorDialog( + context, + I18N.of(context).error, + "Incomplete server details", + icon: FontAwesomeIcons.server + ); + + return false; } if (!address.endsWith('/')) { @@ -185,29 +218,34 @@ class InvenTreeAPI { print("Connecting to " + apiUrl + " -> " + username + ":" + password); - // TODO - Add connection timeout - var response = await get("").timeout(Duration(seconds: 10)).catchError((error) { if (error is SocketException) { - print("Could not connect to server"); + errorMessage = "Could not connect to server"; return null; } else if (error is TimeoutException) { - print("Server timeout"); + errorMessage = "Server timeout"; return null; } else { + // Unknown error type + errorMessage = error.toString(); // Unknown error type, re-throw error - print("Unknown error: ${error.toString()}"); - throw error; + return null; } }); if (response == null) { + // Null (or error) response: Show dialog and exit + + await showServerError(context, errorMessage); return false; } if (response.statusCode != 200) { - print("Invalid status code: " + response.statusCode.toString()); + // Any status code other than 200! + + // TODO: Interpret the error codes and show custom message? + await showServerError(context, "Invalid response code: ${response.statusCode.toString()}"); return false; } @@ -217,9 +255,9 @@ class InvenTreeAPI { // We expect certain response from the server if (!data.containsKey("server") || !data.containsKey("version")) { - errorMessage = "Server resonse contained incorrect data"; - print(errorMessage); - throw errorMessage; + + await showServerError(context, "Server response missing required fields"); + return false; } print("Server: " + data["server"]); @@ -228,35 +266,43 @@ class InvenTreeAPI { _version = data["version"]; if (!_checkServerVersion(_version)) { - // TODO - Something? + await showServerError(context, "Server version is too old.\n\nServer Version: ${_version}\n\nRequired version: ${_requiredVersionString}"); + return false; } // Record the instance name of the server instance = data['instance'] ?? ''; // Request token from the server if we do not already have one - if (_token.isNotEmpty) { + if (false && _token.isNotEmpty) { print("Already have token - $_token"); return true; } - // Clear out the token + // Clear the existing token value _token = ""; + print("Requesting token from server"); + response = await get(_URL_GET_TOKEN).timeout(Duration(seconds: 10)).catchError((error) { print("Error requesting token:"); print(error); - return false; + return null; }); + if (response == null) { + await showServerError(context, "Error requesting access token"); + return false; + } + if (response.statusCode != 200) { - print("Invalid status code: " + response.statusCode.toString()); + await showServerError(context, "Invalid status code: ${response.statusCode.toString()}"); return false; } else { var data = json.decode(response.body); if (!data.containsKey("token")) { - print("No token provided in response"); + await showServerError(context, "No token provided in response"); return false; } diff --git a/lib/barcode.dart b/lib/barcode.dart index f92c9ae7..a17b8091 100644 --- a/lib/barcode.dart +++ b/lib/barcode.dart @@ -32,7 +32,6 @@ class BarcodeHandler { QRViewController _controller; BuildContext _context; - Future onBarcodeMatched(Map data) { // Called when the server "matches" a barcode // Override this function diff --git a/lib/l10n b/lib/l10n index 6b50ee70..8f8809c1 160000 --- a/lib/l10n +++ b/lib/l10n @@ -1 +1 @@ -Subproject commit 6b50ee704cacfb3d9b45d89de31b1ce05e94959a +Subproject commit 8f8809c18776e8bfac050c35ad17e0416af96ad4 diff --git a/lib/main.dart b/lib/main.dart index 46b31b85..2c0e410a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -45,9 +45,6 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); - // Load login details - InvenTreePreferences().loadLoginDetails(); - runZoned>(() async { runApp(InvenTreeApp()); }, onError: (error, stackTrace) { @@ -63,6 +60,7 @@ class InvenTreeApp extends StatelessWidget { @override Widget build(BuildContext context) { + return MaterialApp( onGenerateTitle: (BuildContext context) => I18N.of(context).appTitle, theme: ThemeData( diff --git a/lib/preferences.dart b/lib/preferences.dart index 158224d6..91ced0b2 100644 --- a/lib/preferences.dart +++ b/lib/preferences.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'api.dart'; @@ -35,7 +36,7 @@ class InvenTreePreferences { InvenTreePreferences._internal(); // Load saved login details, and attempt connection - void loadLoginDetails() async { + void loadLoginDetails(BuildContext context) async { print("Loading login details"); @@ -45,10 +46,10 @@ class InvenTreePreferences { var username = prefs.getString(_USERNAME) ?? ''; var password = prefs.getString(_PASSWORD) ?? ''; - await InvenTreeAPI().connectToServer(server, username, password); + await InvenTreeAPI().connectToServer(context, server, username, password); } - void saveLoginDetails(String server, String username, String password) async { + void saveLoginDetails(BuildContext context, String server, String username, String password) async { SharedPreferences prefs = await SharedPreferences.getInstance(); @@ -57,6 +58,6 @@ class InvenTreePreferences { await prefs.setString(_PASSWORD, password); // Reconnect the API - await InvenTreeAPI().connectToServer(server, username, password); + await InvenTreeAPI().connectToServer(context, server, username, password); } } \ No newline at end of file diff --git a/lib/settings/login.dart b/lib/settings/login.dart index 15ae6db0..79aa2c89 100644 --- a/lib/settings/login.dart +++ b/lib/settings/login.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../api.dart'; import '../preferences.dart'; @@ -61,6 +62,15 @@ class _InvenTreeLoginSettingsState extends State { return null; } + void _save(BuildContext context) async { + if (_formKey.currentState.validate()) { + _formKey.currentState.save(); + + await InvenTreePreferences().saveLoginDetails(context, _server, _username, _password); + + } + } + @override Widget build(BuildContext context) { @@ -76,7 +86,7 @@ class _InvenTreeLoginSettingsState extends State { key: _formKey, child: new ListView( children: [ - Text("Server Address"), + Text(I18N.of(context).serverAddress), new TextFormField( initialValue: _server, decoration: InputDecoration( @@ -92,8 +102,8 @@ class _InvenTreeLoginSettingsState extends State { TextFormField( initialValue: _username, decoration: InputDecoration( - hintText: "Username", - labelText: "Username", + hintText: I18N.of(context).username, + labelText: I18N.of(context).username, ), validator: _validateUsername, onSaved: (String value) { @@ -104,8 +114,8 @@ class _InvenTreeLoginSettingsState extends State { initialValue: _password, obscureText: true, decoration: InputDecoration( - hintText: "Password", - labelText: "Password", + hintText: I18N.of(context).password, + labelText: I18N.of(context).password, ), validator: _validatePassword, onSaved: (String value) { @@ -115,8 +125,10 @@ class _InvenTreeLoginSettingsState extends State { Container( width: screenSize.width, child: RaisedButton( - child: Text("Save"), - onPressed: this.save, + child: Text(I18N.of(context).save), + onPressed: () { + _save(context); + } ) ) ], @@ -125,13 +137,4 @@ class _InvenTreeLoginSettingsState extends State { ) ); } - - void save() async { - if (_formKey.currentState.validate()) { - _formKey.currentState.save(); - - await InvenTreePreferences().saveLoginDetails(_server, _username, _password); - - } - } } \ No newline at end of file diff --git a/lib/widget/dialogs.dart b/lib/widget/dialogs.dart index 5efaa892..1816df20 100644 --- a/lib/widget/dialogs.dart +++ b/lib/widget/dialogs.dart @@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; void showMessage(BuildContext context, String message) { Scaffold.of(context).showSnackBar(SnackBar( @@ -9,7 +10,12 @@ void showMessage(BuildContext context, String message) { )); } -Future showInfoDialog(BuildContext context, String title, String description, {IconData icon = FontAwesomeIcons.info, String info = "Info", Function onDismissed}) async { +Future showInfoDialog(BuildContext context, String title, String description, {IconData icon = FontAwesomeIcons.info, String info, Function onDismissed}) async { + + if (info == null || info.isEmpty) { + info = I18N.of(context).info; + } + showDialog( context: context, child: SimpleDialog( @@ -31,7 +37,12 @@ Future showInfoDialog(BuildContext context, String title, String descripti }); } -Future showErrorDialog(BuildContext context, String title, String description, {IconData icon = FontAwesomeIcons.exclamationCircle, String error = "Error", Function onDismissed}) async { +Future showErrorDialog(BuildContext context, String title, String description, {IconData icon = FontAwesomeIcons.exclamationCircle, String error, Function onDismissed}) async { + + if (error == null || error.isEmpty) { + error = I18N.of(context).error; + } + showDialog( context: context, child: SimpleDialog( diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 73c28cb3..0a2d0129 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -25,7 +25,6 @@ class InvenTreeHomePage extends StatefulWidget { class _InvenTreeHomePageState extends State { _InvenTreeHomePageState() : super() { - _checkServerConnection(); } String _serverAddress = ""; @@ -63,7 +62,7 @@ class _InvenTreeHomePageState extends State { /* * Test the server connection */ - void _checkServerConnection() async { + void _checkServerConnection(BuildContext context) async { var prefs = await SharedPreferences.getInstance(); @@ -76,7 +75,7 @@ class _InvenTreeHomePageState extends State { _serverIcon = new FaIcon(FontAwesomeIcons.spinner); _serverStatusColor = Color.fromARGB(255, 50, 50, 250); - InvenTreeAPI().connect().then((bool result) { + InvenTreeAPI().connect(context).then((bool result) { if (result) { onConnectSuccess(""); @@ -322,7 +321,7 @@ class _InvenTreeHomePageState extends State { leading: _serverIcon, onTap: () { if (!_serverConnection) { - _checkServerConnection(); + _checkServerConnection(context); } }, ),