Browse Source

refactored api and added localstorage and handle auto-relogin

master
TheNils 3 months ago
parent
commit
28ac632820
  1. 12
      android/app/src/main/res/drawable-v21/launch_background.xml
  2. 18
      android/app/src/main/res/values-night/styles.xml
  3. 109
      lib/api/api.dart
  4. 2
      lib/lang/en.dart
  5. 2
      lib/lang/fr.dart
  6. 11
      lib/main.dart
  7. 0
      lib/store/projectStore.dart
  8. 75
      lib/store/user.dart
  9. 38
      lib/views/account.dart
  10. 9
      lib/views/login/signInForm.dart
  11. 139
      pubspec.lock
  12. 6
      pubspec.yaml
  13. BIN
      web/favicon.png
  14. BIN
      web/icons/Icon-192.png
  15. BIN
      web/icons/Icon-512.png
  16. 45
      web/index.html
  17. 23
      web/manifest.json

12
android/app/src/main/res/drawable-v21/launch_background.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

18
android/app/src/main/res/values-night/styles.xml

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

109
lib/api/api.dart

@ -1,9 +1,6 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:youtribe_lib/globals.dart' as globals;
import 'package:alice/alice.dart';
Alice alice = Alice();
class Api {
// Singleton
@ -13,7 +10,7 @@ class Api {
return _instance;
}
Map<String, dynamic> credentials = {'isSet': false};
Map<String, dynamic> credentials = {};
Map<String, String> apiHeaders = {
'Content-Type': 'application/json; charset=UTF-8'
@ -23,18 +20,22 @@ class Api {
var client = http.Client();
void setHeaders(String jwt, String refreshToken, int expires) {
apiHeaders = {
print('setting new refresh token');
print(refreshToken);
this.apiHeaders = {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer $jwt',
};
credentials = {
this.credentials = {
'jwt': jwt,
'refreshToken': refreshToken,
'expiresAt': new DateTime.now().add(new Duration(minutes: 1)),
'expiresAt': expires >= 0
? new DateTime.now().add(new Duration(seconds: expires))
: new DateTime.now().add(new Duration(seconds: expires)),
};
}
Future<Map<String, dynamic>> handleResponse(
Future<Map<dynamic, dynamic>> handleResponse(
Future<dynamic> response,
String path,
Map<String, dynamic> body,
@ -42,18 +43,18 @@ class Api {
Function(String path, Map<String, dynamic> body, int tryCount)
callback) async {
return await response.then((res) {
var json = jsonDecode(res.body);
if (res.statusCode == 200) {
var json = jsonDecode(res.body);
return json['data'];
} else {
if (tryCount <= maxRetryCount) {
if (tryCount < maxRetryCount) {
return callback(path, body, tryCount + 1);
} else {
return json;
}
}
}).catchError((err) {
if (tryCount <= maxRetryCount) {
if (tryCount < maxRetryCount) {
return callback(path, body, tryCount + 1);
} else {
globals.isApiReachabe = false;
@ -66,8 +67,8 @@ class Api {
[int tryCount = 1]) async {
await refreshJwt();
var res =
client.post('${globals.apiUrl}$path', headers: apiHeaders, body: body);
var res = client.post('${globals.apiUrl}$path',
headers: this.apiHeaders, body: body);
return await handleResponse(res, path, body, tryCount, post);
}
@ -75,18 +76,18 @@ class Api {
[int tryCount = 1]) async {
await refreshJwt();
var res =
client.patch('${globals.apiUrl}$path', headers: apiHeaders, body: body);
var res = client.patch('${globals.apiUrl}$path',
headers: this.apiHeaders, body: body);
return await handleResponse(res, path, body, tryCount, patch);
}
Future<Map<String, dynamic>> get(String path, Map<String, dynamic> body,
[int tryCount = 1]) async {
Future<Map<dynamic, dynamic>> get(String path,
[Map<String, dynamic> body = const {}, int tryCount = 1]) async {
await refreshJwt();
var res = http.get(
'${globals.apiUrl}$path',
headers: apiHeaders,
headers: this.apiHeaders,
);
return await handleResponse(res, path, body, tryCount, get);
}
@ -95,8 +96,8 @@ class Api {
[int tryCount = 1]) async {
await refreshJwt();
var res =
client.put('${globals.apiUrl}$path', headers: apiHeaders, body: body);
var res = client.put('${globals.apiUrl}$path',
headers: this.apiHeaders, body: body);
return await handleResponse(res, path, body, tryCount, put);
}
@ -106,36 +107,32 @@ class Api {
var res = client.delete(
'${globals.apiUrl}$path',
headers: apiHeaders,
headers: this.apiHeaders,
);
return await handleResponse(res, path, body, tryCount, delete);
}
Future<Map<String, dynamic>> authenticate(Map<String, String> body,
Future<Map<dynamic, dynamic>> authenticate(Map<String, String> body,
[int tryCount = 1]) async {
print(jsonEncode(body));
this.apiHeaders = {'Content-Type': 'application/json; charset=UTF-8'};
var res = await http
.post('${globals.apiUrl}/auth/login',
headers: this.apiHeaders, body: jsonEncode(body))
.then((res) {
alice.onHttpResponse(res);
if (res.statusCode == 200) {
var json = jsonDecode(res.body);
setHeaders(json['data']['token'], json['data']['refresh_token'],
setHeaders(json['data']['access_token'], json['data']['refresh_token'],
json['data']['expires']);
globals.isLoggedIn = true;
return json['data']['user'];
return {
"success": "logged in",
...json['data'],
};
} else {
print('statuscode bad');
if (tryCount < maxRetryCount) {
print('trycount next returning callback');
return authenticate(body, tryCount + 1);
} else {
print('trycount bad returning error json');
print(res.body);
return {"error": "http", "code": "${res.statusCode}"};
// return json;
@ -143,14 +140,10 @@ class Api {
}
}).catchError((err) {
if (tryCount <= maxRetryCount) {
print('error trycount next returning callback');
return authenticate(body, tryCount + 1);
} else {
globals.isApiReachabe = false;
print('error trycount bad returning error json');
return {"error": err, "code": "000"};
// return jsonDecode('{"code": 0, "message": "$err"}');
}
});
@ -158,21 +151,45 @@ class Api {
}
Future<bool> refreshJwt() async {
if (credentials['expiresAt'].isAfter(new DateTime.now())) {
if (!this.credentials.containsKey('expiresAt')) {
return false;
}
if (this.credentials['expiresAt'].isAfter(new DateTime.now())) {
return true;
} else {
try {
var res = await http.post('${globals.apiUrl}/auth/refresh',
body: {'refresh_token': credentials['refreshToken']});
var response = await http
.post('${globals.apiUrl}/auth/refresh',
headers: {'Content-Type': 'application/json; charset=UTF-8'},
body: jsonEncode(
{'refresh_token': this.credentials['refreshToken']}))
.then((res) {
if (res.statusCode == 200) {
var json = jsonDecode(res.body);
setHeaders(
json['data']['access_token'], credentials['refreshToken'], 3600);
print(res.body);
setHeaders(json['data']['access_token'],
json['data']['refresh_token'], json['data']['expires']);
print('refreshed : ');
print(json['data']['refresh_token']);
return true;
} else {
return false;
}
return true;
} catch (err) {
return true;
}
}).catchError((err) {
return false;
});
return response;
}
}
Future<bool> resumeSession(jwt, refreshToken) async {
this.setHeaders(jwt, refreshToken, -1000);
return await this.refreshJwt().then((res) {
return res;
}).catchError((err) {
return false;
});
}
}

2
lib/lang/en.dart

@ -2,4 +2,6 @@ Map<String, String> en = {
'hello': 'Hello',
'signin': 'Sign in',
'signup': 'Sign up',
'my-account': 'My Account',
'my-projects': 'My Projects',
};

2
lib/lang/fr.dart

@ -2,4 +2,6 @@ Map<String, String> fr = {
'hello': 'Bonjour',
'signin': 'Connexion',
'signup': 'Créer un compte',
'my-account': 'Mon compte',
'my-projects': 'Mes Projets',
};

11
lib/main.dart

@ -8,8 +8,7 @@ import 'package:youtribe_lib/lang/en.dart';
import 'package:youtribe_lib/lang/fr.dart';
import 'package:youtribe_lib/views/login/login.dart';
import 'package:alice/alice.dart';
import 'package:youtribe_lib/views/account.dart';
// translation
class Messages extends Translations {
@ -17,8 +16,6 @@ class Messages extends Translations {
Map<String, Map<String, String>> get keys => {'en_US': en, 'fr_FR': fr};
}
Alice alice = Alice();
// main runtime
void main() {
runApp(NavMap());
@ -31,12 +28,14 @@ class NavMap extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
navigatorKey: alice.getNavigatorKey(),
title: globals.appName,
locale: globals.locale,
fallbackLocale: globals.fallbackLocale,
translations: Messages(),
routes: {'/': (context) => Login()},
routes: {
'/': (context) => Login(),
'/account': (context) => Account(),
},
);
}
}

0
lib/store/projectStore.dart

75
lib/store/user.dart

@ -1,41 +1,58 @@
import 'dart:convert';
import 'package:get/state_manager.dart';
import 'package:youtribe_lib/api/api.dart';
class User {
User({
this.isLogged = false,
this.id = '',
this.jwt = '',
this.firstName = '',
this.lastName = '',
this.email = '',
this.phone = '',
});
String id;
bool isLogged;
String jwt;
String firstName;
String lastName;
String email;
String phone;
}
import 'package:localstorage/localstorage.dart';
class UserController extends GetxController {
final user = User(jwt: 'blabal').obs;
final localStorage = new LocalStorage('user-store');
final api = Api();
Future<Map<String, dynamic>> authenticate(Map<String, String> payload) async {
print('before await api');
final user = Rx<Map<dynamic, dynamic>>({}).obs;
set user(payload) {
user(Rx({...user().value, ...payload}));
localStorage.setItem('user', jsonEncode(payload));
}
Future<Map<dynamic, dynamic>> authenticate(
Map<String, String> payload) async {
var res = await api.authenticate(payload);
print('after await api');
if (!res.containsKey('error')) {
user(User(
isLogged: true,
firstName: res['first_name'],
lastName: res['last_name'],
email: res['email'],
phone: res['phone']));
this.user = res;
this.fetch();
}
return res;
}
@override
void onInit() async {
await localStorage.ready;
var localUser = await localStorage.getItem('user');
if (localUser != null) {
this.user = jsonDecode(localUser);
var canResume = await api.resumeSession(
user().value['access_token'], user().value['refresh_token']);
if (canResume) {
this.user = {'refresh_token': this.api.credentials['refreshToken']};
this.fetch();
} else {
await this.localStorage.deleteItem('user');
}
}
// fetchApi();
super.onInit();
}
Future<Map<dynamic, dynamic>> fetch() async {
var res = await api.get('/users/me');
if (!res.containsKey('error')) {
this.user = {...user().value, ...res};
}
return user().value;
}
// Future<
}

38
lib/views/account.dart

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youtribe_lib/store/user.dart';
class Account extends StatelessWidget {
final UserController userController = Get.put(UserController());
void test() {
print(userController.user());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Account'.tr),
),
body: Center(
child: Obx(
() => (Column(
children: [
Text('${userController.user().value['id']}'),
Text('${userController.user().value['first_name']}'),
// Obx(() => Text('${userController.user().lastName}')),
// Obx(() => Text('${userController.user().email}')),
// Obx(() => Text('${userController.user().avatar}')),
Text('my-projects'.tr),
OutlinedButton(
onPressed: test,
child: Text('test button'),
)
],
)),
),
),
);
}
}

9
lib/views/login/signInForm.dart

@ -64,10 +64,10 @@ class SignInFormState extends State<SignInForm> {
),
),
Padding(padding: EdgeInsets.all(50)),
RaisedButton(
ElevatedButton(
onPressed: () async {
if (_formKey.currentState.validate()) {
Map<String, dynamic> res = await userController
Map<dynamic, dynamic> res = await userController
.authenticate({'email': email, 'password': password});
if (res.containsKey('error')) {
setState(() {
@ -77,12 +77,15 @@ class SignInFormState extends State<SignInForm> {
setState(() {
serverError = '';
});
Get.toNamed('/account');
}
}
},
child: Text('Sign In'),
),
Text(serverError)
Text(serverError),
Obx(() => Text('${userController.user().value['first_name']}'))
// Text('${userController.get().firstName}')
],
),
);

139
pubspec.lock

@ -14,42 +14,42 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.0-nullsafety.1"
version: "2.5.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
version: "2.1.0"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.3"
version: "1.1.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.2.0"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
version: "1.1.0"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0-nullsafety.3"
version: "1.15.0"
console_log_handler:
dependency: transitive
description:
@ -70,7 +70,21 @@ packages:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.2.0"
ffi:
dependency: transitive
description:
name: ffi
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.0"
flutter:
dependency: "direct main"
description: flutter
@ -96,7 +110,7 @@ packages:
name: flutter_map_marker_popup
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.4"
version: "0.2.0"
flutter_test:
dependency: "direct dev"
description: flutter
@ -108,7 +122,7 @@ packages:
name: get
url: "https://pub.dartlang.org"
source: hosted
version: "3.13.2"
version: "3.25.4"
http:
dependency: "direct main"
description:
@ -129,7 +143,7 @@ packages:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.1"
version: "0.17.0"
latlong:
dependency: transitive
description:
@ -144,6 +158,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.6"
localstorage:
dependency: "direct main"
description:
name: localstorage
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.6+9"
logging:
dependency: transitive
description:
@ -157,14 +178,14 @@ packages:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10-nullsafety.1"
version: "0.12.10"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
version: "1.3.0"
mgrs_dart:
dependency: transitive
description:
@ -178,14 +199,63 @@ packages:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.1"
version: "1.8.0"
path_provider:
dependency: transitive
description:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.27"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.1+2"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4+8"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4+3"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.0"
version: "1.10.0"
platform:
dependency: transitive
description:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
positioned_tap_detector:
dependency: transitive
description:
@ -193,6 +263,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
process:
dependency: transitive
description:
name: process
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
proj4dart:
dependency: transitive
description:
@ -218,42 +295,42 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.2"
version: "1.8.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0-nullsafety.1"
version: "1.10.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
version: "2.1.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
version: "1.1.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
version: "1.2.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.19-nullsafety.2"
version: "0.2.19"
transparent_image:
dependency: transitive
description:
@ -274,7 +351,7 @@ packages:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
version: "1.3.0"
unicode:
dependency: transitive
description:
@ -295,7 +372,14 @@ packages:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.3"
version: "2.1.0"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.4+1"
wkt_parser:
dependency: transitive
description:
@ -303,6 +387,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.7"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
sdks:
dart: ">=2.10.0-110 <2.11.0"
flutter: ">=1.10.15 <2.0.0"
dart: ">=2.12.0-0.0 <3.0.0"
flutter: ">=1.12.13+hotfix.5"

6
pubspec.yaml

@ -24,12 +24,10 @@ dependencies:
flutter:
sdk: flutter
get:
http:
http: ^0.12.2
flutter_map: any # or the latest version on Pub
flutter_map_marker_popup: any
localstorage: ^3.0.6+9
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.

BIN
web/favicon.png

After

Width: 16  |  Height: 16  |  Size: 917 B

BIN
web/icons/Icon-192.png

After

Width: 192  |  Height: 192  |  Size: 5.2 KiB

BIN
web/icons/Icon-512.png

After

Width: 512  |  Height: 512  |  Size: 8.1 KiB

45
web/index.html

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
Fore more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
-->
<base href="/">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="youtribe_lib">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>youtribe_lib</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('flutter-first-frame', function () {
navigator.serviceWorker.register('flutter_service_worker.js');
});
}
</script>
<script src="main.dart.js" type="application/javascript"></script>
</body>
</html>

23
web/manifest.json

@ -0,0 +1,23 @@
{
"name": "youtribe_lib",
"short_name": "youtribe_lib",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Loading…
Cancel
Save