diff --git a/android/app/build.gradle b/android/app/build.gradle index 9ced3334..ad105f02 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -25,12 +25,35 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +// Keys for the android play store def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('app/key.properties') if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } +// Key for wger.de REST API +def wgerProperties = new Properties() +def localMapsPropertiesFile = rootProject.file('app/wger.properties') +if (localMapsPropertiesFile.exists()) { + project.logger.info('Load maps properties from local file') + localMapsPropertiesFile.withReader('UTF-8') { reader -> + wgerProperties.load(reader) + } +} else { + project.logger.info('Load maps properties from environment') + try { + wgerProperties['WGER_API_KEY'] = System.getenv('WGER_API_KEY') + } catch(NullPointerException e) { + project.logger.warn('Failed to load WGER_API_KEY from environment.', e) + } +} +def wgerApiKey = wgerProperties.getProperty('WGER_API_KEY') +if(wgerApiKey == null){ + wgerApiKey = "" + project.logger.error('Wger Api Key not configured. Set it in `app/wger.properties` or in the environment variable `WGER_API_KEY`') +} + android { compileSdkVersion 29 @@ -49,6 +72,7 @@ android { targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName + manifestPlaceholders = [WGER_API_KEY: wgerApiKey] } signingConfigs { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7ae1c970..cb64563b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -11,6 +11,9 @@ android:name="io.flutter.app.FlutterApplication" android:label="wger" android:icon="@mipmap/ic_launcher"> + + + errorList = []; for (var key in exception.errors.keys) { // Error headers @@ -59,6 +58,7 @@ void showHttpExceptionErrorDialog(WgerHttpException exception, BuildContext cont for (var value in exception.errors[key]) { errorList.add(Text(value)); } + errorList.add(SizedBox(height: 8)); } showDialog( context: context, diff --git a/lib/models/http_exception.dart b/lib/models/http_exception.dart index 8dcd1c35..8bb02201 100644 --- a/lib/models/http_exception.dart +++ b/lib/models/http_exception.dart @@ -24,14 +24,14 @@ class WgerHttpException implements Exception { /// Custom http exception. /// Expects the response body of the REST call and will try to parse it to /// JSON. Will use the response as-is if it fails. - WgerHttpException(String responseBody) { + WgerHttpException(dynamic responseBody) { if (responseBody == null) { errors = {'unknown_error': 'An unknown error occurred, no further information available'}; } else { try { errors = json.decode(responseBody); } catch (e) { - errors = {'error': responseBody}; + errors = responseBody; } } this.errors = errors; diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index 0b5b3a6f..20e745dd 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -19,8 +19,11 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer'; +import 'dart:io'; +import 'package:android_metadata/android_metadata.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:http/http.dart' as http; import 'package:package_info/package_info.dart'; @@ -71,7 +74,46 @@ class Auth with ChangeNotifier { applicationVersion = packageInfo; } - Future _authenticate(String username, String password, String serverUrl) async { + /// Registers a new user + Future register({String username, String password, String email, String serverUrl}) async { + var url = '$serverUrl/api/v2/register/'; + Map metadata = Map(); + + // Read the api key from the manifest file + try { + metadata = await AndroidMetadata.metaDataAsMap; + } on PlatformException { + throw Exception('An error occurred reading the API key'); + } + + // Register + try { + Map data = {'username': username, 'password': password}; + if (email != '') { + data['email'] = email; + } + final response = await http.post( + url, + headers: { + HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', + HttpHeaders.authorizationHeader: "Token ${metadata['wger.api_key']}" + }, + body: json.encode(data), + ); + final responseData = json.decode(response.body); + + if (response.statusCode >= 400) { + throw WgerHttpException(responseData); + } + + login(username, password, serverUrl); + } catch (error) { + throw error; + } + } + + /// Authenticates a user + Future login(String username, String password, String serverUrl) async { var url = '$serverUrl/api/v2/login/'; try { @@ -80,7 +122,7 @@ class Auth with ChangeNotifier { final response = await http.post( url, headers: { - 'Content-Type': 'application/json; charset=UTF-8', + HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', }, body: json.encode({'username': username, 'password': password}), ); @@ -120,14 +162,6 @@ class Auth with ChangeNotifier { } } - Future signUp(String email, String password) async { - return _authenticate(email, password, 'signUp'); - } - - Future signIn(String username, String password, String serverUrl) async { - return _authenticate(username, password, serverUrl); - } - Future tryAutoLogin() async { final prefs = await SharedPreferences.getInstance(); if (!prefs.containsKey('userData')) { diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index 896f24f9..29ef735c 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -57,7 +57,9 @@ class Nutrition extends WgerBaseProvider with ChangeNotifier { /// Returns the current active nutritional plan. At the moment this is just /// the latest, but this might change in the future. NutritionalPlan get currentPlan { - return _plans.first; + if (_plans.length > 0) { + return _plans.first; + } } NutritionalPlan findById(int id) { diff --git a/lib/providers/workout_plans.dart b/lib/providers/workout_plans.dart index c1ed18d9..da07cc77 100644 --- a/lib/providers/workout_plans.dart +++ b/lib/providers/workout_plans.dart @@ -104,7 +104,9 @@ class WorkoutPlans extends WgerBaseProvider with ChangeNotifier { /// Returns the current active workout plan. At the moment this is just /// the latest, but this might change in the future. WorkoutPlan get activePlan { - return _workoutPlans.first; + if (_workoutPlans.length > 0) { + return _workoutPlans.first; + } } /* diff --git a/lib/screens/auth_screen.dart b/lib/screens/auth_screen.dart index 4d307fa0..bc3e6062 100644 --- a/lib/screens/auth_screen.dart +++ b/lib/screens/auth_screen.dart @@ -19,8 +19,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:wger/helpers/ui.dart'; +import 'package:wger/models/http_exception.dart'; -import '../models/http_exception.dart'; import '../providers/auth.dart'; enum AuthMode { @@ -116,18 +116,22 @@ class _AuthCardState extends State { _isLoading = true; }); try { + // Login existing user if (_authMode == AuthMode.Login) { - // Login existing user - await Provider.of(context, listen: false).signIn( + await Provider.of(context, listen: false).login( _authData['username'], _authData['password'], _authData['serverUrl'], ); - } else { - // Register new user - // await Provider.of(context, listen: false) - // .register(_authData['email'], _authData['password']); + // Register new user + } else { + await Provider.of(context, listen: false).register( + username: _authData['username'], + password: _authData['password'], + email: _authData['email'], + serverUrl: _authData['serverUrl'], + ); } } on WgerHttpException catch (error) { showHttpExceptionErrorDialog(error, context); @@ -197,8 +201,10 @@ class _AuthCardState extends State { controller: _emailController, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, + + // Email is not required validator: (value) { - if (value.isEmpty || !value.contains('@')) { + if (value.isNotEmpty && !value.contains('@')) { return 'Invalid email!'; } return null; diff --git a/lib/widgets/dashboard/widgets.dart b/lib/widgets/dashboard/widgets.dart index f88c2679..f6e426af 100644 --- a/lib/widgets/dashboard/widgets.dart +++ b/lib/widgets/dashboard/widgets.dart @@ -83,7 +83,11 @@ class _DashboardNutritionWidgetState extends State { ) ], ) - : Text('You have no nutritional plans'), + : Container( + alignment: Alignment.center, + height: 150, + child: Text('You have no nutritional plans'), + ), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -140,7 +144,11 @@ class _DashboardWeightWidgetState extends State { height: 180, child: WeightChartWidget(weightEntriesData.items), ) - : Text('You have no weight entries'), + : Container( + alignment: Alignment.center, + height: 150, + child: Text('You have no weight entries'), + ), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -199,51 +207,57 @@ class _DashboardWorkoutWidgetState extends State { style: Theme.of(context).textTheme.headline4, ), _workoutPlan != null - ? Column(children: [ - Text( - DateFormat.yMd().format(_workoutPlan.creationDate), - style: Theme.of(context).textTheme.headline6, - ), - TextButton( - child: Text( - _workoutPlan.description, - style: TextStyle(fontSize: 20), + ? Column( + children: [ + Text( + DateFormat.yMd().format(_workoutPlan.creationDate), + style: Theme.of(context).textTheme.headline6, ), - onPressed: () { - return Navigator.of(context) - .pushNamed(WorkoutPlanScreen.routeName, arguments: _workoutPlan); - }, - ), - ..._workoutPlan.days.map((workoutDay) { - return Column(children: [ - const SizedBox(height: 10), - showDetail == true - ? Text( - workoutDay.description, - style: TextStyle(fontWeight: FontWeight.bold), - ) - : Text(workoutDay.description), - if (showDetail) - ...workoutDay.sets - .map((set) => Text(set.exercisesObj.map((e) => e.name).join(','))) - .toList(), - ]); - }).toList(), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - child: Text(AppLocalizations.of(context).toggleDetails), - onPressed: () { - setState(() { - showDetail = !showDetail; - }); - }, + TextButton( + child: Text( + _workoutPlan.description, + style: TextStyle(fontSize: 20), ), - ], - ) - ]) - : Text('you have no workouts'), + onPressed: () { + return Navigator.of(context) + .pushNamed(WorkoutPlanScreen.routeName, arguments: _workoutPlan); + }, + ), + ..._workoutPlan.days.map((workoutDay) { + return Column(children: [ + const SizedBox(height: 10), + showDetail == true + ? Text( + workoutDay.description, + style: TextStyle(fontWeight: FontWeight.bold), + ) + : Text(workoutDay.description), + if (showDetail) + ...workoutDay.sets + .map((set) => Text(set.exercisesObj.map((e) => e.name).join(','))) + .toList(), + ]); + }).toList(), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + child: Text(AppLocalizations.of(context).toggleDetails), + onPressed: () { + setState(() { + showDetail = !showDetail; + }); + }, + ), + ], + ) + ], + ) + : Container( + alignment: Alignment.center, + height: 150, + child: Text('you have no workouts'), + ), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index 13246a1e..41420a25 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -15,6 +15,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.39.17" + android_metadata: + dependency: "direct main" + description: + name: android_metadata + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" archive: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2a3a4070..58a206f1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: flutter_typeahead: ^2.0.0 table_calendar: ^2.3.3 package_info: ^0.4.3+2 + android_metadata: ^0.1.3 dev_dependencies: flutter_test: