mirror of
https://gitlab.com/comunic/comunicmobile
synced 2024-11-25 06:19:22 +00:00
Can sign in user
This commit is contained in:
parent
b315b5ad77
commit
23f25e7704
@ -1,3 +1,8 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:comunic/models/login_tokens.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
/// Accounts credentials helper
|
/// Accounts credentials helper
|
||||||
///
|
///
|
||||||
/// Stores current account tokens
|
/// Stores current account tokens
|
||||||
@ -8,7 +13,27 @@ class AccountCredentialsHelper {
|
|||||||
|
|
||||||
/// Checkout whether current user is signed in or not
|
/// Checkout whether current user is signed in or not
|
||||||
Future<bool> signedIn() async {
|
Future<bool> signedIn() async {
|
||||||
return false; // TODO : implement
|
return await get() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set new login tokens
|
||||||
|
Future<void> set(LoginTokens tokens) async {
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString("login_tokens", tokens.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get current [LoginTokens]. Returns null if none or in case of failure
|
||||||
|
Future<LoginTokens> get() async {
|
||||||
|
try {
|
||||||
|
|
||||||
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||||
|
final string = prefs.getString("login_tokens");
|
||||||
|
if(string == null) return null;
|
||||||
|
return LoginTokens.fromJSON(jsonDecode(string));
|
||||||
|
|
||||||
|
} on Exception catch(e){
|
||||||
|
print(e.toString());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
41
lib/helpers/account_helper.dart
Normal file
41
lib/helpers/account_helper.dart
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import 'package:comunic/helpers/account_credentials_helper.dart';
|
||||||
|
import 'package:comunic/helpers/api_helper.dart';
|
||||||
|
import 'package:comunic/models/api_request.dart';
|
||||||
|
import 'package:comunic/models/authentication_details.dart';
|
||||||
|
import 'package:comunic/models/login_tokens.dart';
|
||||||
|
|
||||||
|
/// Account helper
|
||||||
|
///
|
||||||
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
|
enum AuthResult {
|
||||||
|
SUCCESS,
|
||||||
|
TOO_MANY_ATTEMPTS,
|
||||||
|
NETWORK_ERROR,
|
||||||
|
INVALID_CREDENTIALS
|
||||||
|
}
|
||||||
|
|
||||||
|
class AccountHelper {
|
||||||
|
/// Sign in user
|
||||||
|
Future<AuthResult> signIn(AuthenticationDetails auth) async {
|
||||||
|
final request = APIRequest(uri: "account/login");
|
||||||
|
request.addString("userMail", auth.email);
|
||||||
|
request.addString("userPassword", auth.password);
|
||||||
|
|
||||||
|
final response = await APIHelper().exec(request);
|
||||||
|
|
||||||
|
// Handle errors
|
||||||
|
if (response.code == 401)
|
||||||
|
return AuthResult.INVALID_CREDENTIALS;
|
||||||
|
else if (response.code == 429)
|
||||||
|
return AuthResult.TOO_MANY_ATTEMPTS;
|
||||||
|
else if (response.code != 200) return AuthResult.NETWORK_ERROR;
|
||||||
|
|
||||||
|
//Save login tokens
|
||||||
|
final tokensObj = response.getObject()["tokens"];
|
||||||
|
await AccountCredentialsHelper()
|
||||||
|
.set(LoginTokens(tokensObj["token1"], tokensObj["token2"]));
|
||||||
|
|
||||||
|
return AuthResult.SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
68
lib/helpers/api_helper.dart
Normal file
68
lib/helpers/api_helper.dart
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:comunic/models/api_request.dart';
|
||||||
|
import 'package:comunic/models/api_response.dart';
|
||||||
|
import 'package:comunic/models/config.dart';
|
||||||
|
|
||||||
|
/// API Helper
|
||||||
|
///
|
||||||
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
|
class APIHelper {
|
||||||
|
final _httpClient = HttpClient();
|
||||||
|
|
||||||
|
/// Execute a [request] on the server and returns a [APIResponse]
|
||||||
|
///
|
||||||
|
/// This method should never throw but the response code of the [APIResponse]
|
||||||
|
/// should be verified before accessing response content
|
||||||
|
Future<APIResponse> exec(APIRequest request) async {
|
||||||
|
try {
|
||||||
|
//Add API tokens
|
||||||
|
request.addString("serviceName", config().serviceName);
|
||||||
|
request.addString("serviceToken", config().serviceToken);
|
||||||
|
|
||||||
|
//Add user tokens (if required)
|
||||||
|
if (request.needLogin) throw "Can add user tokens right now !";
|
||||||
|
|
||||||
|
// Prepare request body
|
||||||
|
String requestBody = "";
|
||||||
|
request.args.forEach((key, value) => requestBody +=
|
||||||
|
Uri.encodeQueryComponent(key) +
|
||||||
|
"=" +
|
||||||
|
Uri.encodeQueryComponent(value) +
|
||||||
|
"&");
|
||||||
|
|
||||||
|
List<int> bodyBytes = utf8.encode(requestBody);
|
||||||
|
|
||||||
|
// Determine server URL
|
||||||
|
final path = config().apiServerUri + request.uri;
|
||||||
|
Uri url;
|
||||||
|
if (!config().apiServerSecure)
|
||||||
|
url = Uri.http(config().apiServerName, path);
|
||||||
|
else
|
||||||
|
url = Uri.https(config().apiServerName, path);
|
||||||
|
|
||||||
|
//Connect to server
|
||||||
|
final connection = await _httpClient.postUrl(url);
|
||||||
|
connection.headers.set("Content-Length", bodyBytes.length.toString());
|
||||||
|
connection.headers
|
||||||
|
.set("Content-Type", "application/x-www-form-urlencoded");
|
||||||
|
connection.add(bodyBytes);
|
||||||
|
|
||||||
|
final response = await connection.close();
|
||||||
|
|
||||||
|
if (response.statusCode != HttpStatus.ok)
|
||||||
|
return APIResponse(response.statusCode, null);
|
||||||
|
|
||||||
|
//Save & return response
|
||||||
|
final responseBody = await response.transform(utf8.decoder).join();
|
||||||
|
|
||||||
|
return APIResponse(response.statusCode, responseBody);
|
||||||
|
} on Exception catch (e) {
|
||||||
|
print(e.toString());
|
||||||
|
print("Could not execute a request!");
|
||||||
|
return APIResponse(-1, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
import 'package:comunic/helpers/account_credentials_helper.dart';
|
import 'package:comunic/helpers/account_credentials_helper.dart';
|
||||||
import 'package:comunic/ui/login_route.dart';
|
import 'package:comunic/models/config.dart';
|
||||||
|
import 'package:comunic/ui/routes/home_route.dart';
|
||||||
|
import 'package:comunic/ui/routes/login_route.dart';
|
||||||
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Main file of the application
|
/// Main file of the application
|
||||||
@ -7,6 +10,16 @@ import 'package:flutter/material.dart';
|
|||||||
/// @author Pierre HUBERT
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
/// Initialize application configuration
|
||||||
|
///
|
||||||
|
// TODO : Adapt to production
|
||||||
|
Config.set(Config(
|
||||||
|
apiServerName: "devweb.local",
|
||||||
|
apiServerUri: "/comunic/api/",
|
||||||
|
apiServerSecure: false,
|
||||||
|
serviceName: "ComunicFlutter",
|
||||||
|
serviceToken: "G9sZCBmb3IgVWJ1bnR1CkNvbW1lbnRbbmVdPeCkieCkrOCkq"));
|
||||||
|
|
||||||
runApp(ComunicApplication());
|
runApp(ComunicApplication());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,20 +28,37 @@ class ComunicApplication extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
// We need to know whether the user is signed in or not to continue
|
home: ComunicApplicationHome(),
|
||||||
home: FutureBuilder(
|
|
||||||
future: AccountCredentialsHelper().signedIn(),
|
|
||||||
builder: (b, AsyncSnapshot<bool> c){
|
|
||||||
if(!c.hasData)
|
|
||||||
return CircularProgressIndicator();
|
|
||||||
|
|
||||||
else
|
|
||||||
if(c.data)
|
|
||||||
throw "Not supported yet !";
|
|
||||||
else
|
|
||||||
return LoginRoute();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ComunicApplicationHome extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _ComunicApplicationHomeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ComunicApplicationHomeState extends State<ComunicApplicationHome> {
|
||||||
|
bool _signedIn;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
AccountCredentialsHelper().signedIn().then((v) {
|
||||||
|
setState(() {
|
||||||
|
_signedIn = v;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_signedIn == null) return buildLoadingPage();
|
||||||
|
|
||||||
|
if (_signedIn)
|
||||||
|
return HomeRoute();
|
||||||
|
else
|
||||||
|
return LoginRoute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
27
lib/models/api_request.dart
Normal file
27
lib/models/api_request.dart
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
/// API Request model
|
||||||
|
///
|
||||||
|
/// Contains all the information associated to an API request
|
||||||
|
///
|
||||||
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
|
class APIRequest {
|
||||||
|
final String uri;
|
||||||
|
final bool needLogin;
|
||||||
|
Map<String, String> args;
|
||||||
|
|
||||||
|
APIRequest({
|
||||||
|
@required this.uri,
|
||||||
|
this.needLogin = false,
|
||||||
|
}) : assert(uri != null),
|
||||||
|
assert(needLogin != null),
|
||||||
|
args = Map();
|
||||||
|
|
||||||
|
void addString(String name, String value) => args[name] = value;
|
||||||
|
|
||||||
|
void addInt(String name, int value) => args[name] = value.toString();
|
||||||
|
|
||||||
|
void addBool(String name, bool value) =>
|
||||||
|
args[name] = value ? "true" : "false";
|
||||||
|
}
|
16
lib/models/api_response.dart
Normal file
16
lib/models/api_response.dart
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
/// API response
|
||||||
|
///
|
||||||
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
|
class APIResponse {
|
||||||
|
final int code;
|
||||||
|
final String content;
|
||||||
|
|
||||||
|
const APIResponse(this.code, this.content) : assert(code != null);
|
||||||
|
|
||||||
|
List<dynamic> getArray() => jsonDecode(this.content);
|
||||||
|
|
||||||
|
Map<String, dynamic> getObject() => jsonDecode(this.content);
|
||||||
|
}
|
14
lib/models/authentication_details.dart
Normal file
14
lib/models/authentication_details.dart
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
/// Authentication details
|
||||||
|
///
|
||||||
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
|
class AuthenticationDetails {
|
||||||
|
final String email;
|
||||||
|
final String password;
|
||||||
|
|
||||||
|
const AuthenticationDetails({@required this.email, @required this.password})
|
||||||
|
: assert(email != null),
|
||||||
|
assert(password != null);
|
||||||
|
}
|
42
lib/models/config.dart
Normal file
42
lib/models/config.dart
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
/// Application configuration model
|
||||||
|
///
|
||||||
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
|
/// Configuration class
|
||||||
|
class Config {
|
||||||
|
final String apiServerName;
|
||||||
|
final String apiServerUri;
|
||||||
|
final bool apiServerSecure;
|
||||||
|
final String serviceName;
|
||||||
|
final String serviceToken;
|
||||||
|
|
||||||
|
const Config(
|
||||||
|
{@required this.apiServerName,
|
||||||
|
@required this.apiServerUri,
|
||||||
|
@required this.apiServerSecure,
|
||||||
|
@required this.serviceName,
|
||||||
|
@required this.serviceToken})
|
||||||
|
: assert(apiServerName != null),
|
||||||
|
assert(apiServerUri != null),
|
||||||
|
assert(apiServerSecure != null),
|
||||||
|
assert(serviceName != null),
|
||||||
|
assert(serviceToken != null);
|
||||||
|
|
||||||
|
/// Get and set static configuration
|
||||||
|
static Config _config;
|
||||||
|
|
||||||
|
static Config get() {
|
||||||
|
return _config;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set(Config conf) {
|
||||||
|
_config = conf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current configuration of the application
|
||||||
|
Config config() {
|
||||||
|
return Config.get();
|
||||||
|
}
|
23
lib/models/login_tokens.dart
Normal file
23
lib/models/login_tokens.dart
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
/// Login tokens model
|
||||||
|
///
|
||||||
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
|
class LoginTokens {
|
||||||
|
final String tokenOne;
|
||||||
|
final String tokenTwo;
|
||||||
|
|
||||||
|
const LoginTokens(this.tokenOne, this.tokenTwo)
|
||||||
|
: assert(tokenOne != null),
|
||||||
|
assert(tokenTwo != null);
|
||||||
|
|
||||||
|
LoginTokens.fromJSON(Map<String, dynamic> json)
|
||||||
|
: tokenOne = json["token_one"],
|
||||||
|
tokenTwo = json["token_two"];
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return jsonEncode({"token_one": tokenOne, "token_two": tokenTwo});
|
||||||
|
}
|
||||||
|
}
|
12
lib/ui/routes/home_route.dart
Normal file
12
lib/ui/routes/home_route.dart
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Main route of the application
|
||||||
|
///
|
||||||
|
/// @author Pierre HUBERT
|
||||||
|
|
||||||
|
class HomeRoute extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Text("Home route");
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,9 @@
|
|||||||
|
import 'package:comunic/helpers/account_helper.dart';
|
||||||
|
import 'package:comunic/models/authentication_details.dart';
|
||||||
|
import 'package:comunic/ui/routes/home_route.dart';
|
||||||
import 'package:comunic/utils/input_utils.dart';
|
import 'package:comunic/utils/input_utils.dart';
|
||||||
import 'package:comunic/utils/intl_utils.dart';
|
import 'package:comunic/utils/intl_utils.dart';
|
||||||
|
import 'package:comunic/utils/ui_utils.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Login route
|
/// Login route
|
||||||
@ -14,6 +18,7 @@ class LoginRoute extends StatefulWidget {
|
|||||||
class _LoginRouteState extends State<LoginRoute> {
|
class _LoginRouteState extends State<LoginRoute> {
|
||||||
String _currEmail;
|
String _currEmail;
|
||||||
String _currPassword;
|
String _currPassword;
|
||||||
|
AuthResult _authResult;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -35,26 +40,51 @@ class _LoginRouteState extends State<LoginRoute> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Call this whenever the user request to perform login
|
/// Call this whenever the user request to perform login
|
||||||
void _submitForm() {
|
Future<void> _submitForm(BuildContext context) async {
|
||||||
|
final loginResult = await AccountHelper().signIn(
|
||||||
|
AuthenticationDetails(email: _currEmail, password: _currPassword));
|
||||||
|
|
||||||
|
if (loginResult == AuthResult.SUCCESS)
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context, MaterialPageRoute(builder: (b) => HomeRoute()));
|
||||||
|
else
|
||||||
|
setState(() {
|
||||||
|
_authResult = loginResult;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build error card
|
||||||
|
Widget _buildErrorCard() {
|
||||||
|
if (_authResult == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
//Determine the right message
|
||||||
|
final message = (_authResult == AuthResult.INVALID_CREDENTIALS ? tr(
|
||||||
|
"Invalid credentials!") : (_authResult == AuthResult.TOO_MANY_ATTEMPTS
|
||||||
|
? tr("Too many unsuccessfull login attempts! Please try again later...")
|
||||||
|
: tr("A network error occured!")));
|
||||||
|
|
||||||
|
return buildErrorCard(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build login form
|
/// Build login form
|
||||||
Widget _buildLoginForm(){
|
Widget _buildLoginForm() {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(tr("Please sign into your Comunic account: ")),
|
Text(tr("Please sign into your Comunic account: ")),
|
||||||
|
Container(
|
||||||
|
child: _buildErrorCard(),
|
||||||
|
),
|
||||||
//Email address
|
//Email address
|
||||||
TextField(
|
TextField(
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: tr("Email address"),
|
labelText: tr("Email address"),
|
||||||
alignLabelWithHint: true,
|
alignLabelWithHint: true,
|
||||||
errorText:
|
errorText: _currEmail.length > 0 && !validateEmail(_currEmail)
|
||||||
_currEmail.length > 0 && !validateEmail(_currEmail)
|
|
||||||
? tr("Invalid email address!")
|
? tr("Invalid email address!")
|
||||||
: null),
|
: null),
|
||||||
onChanged: _emailChanged,
|
onChanged: _emailChanged,
|
||||||
@ -72,7 +102,9 @@ class _LoginRouteState extends State<LoginRoute> {
|
|||||||
|
|
||||||
RaisedButton(
|
RaisedButton(
|
||||||
child: Text(tr("Sign in")),
|
child: Text(tr("Sign in")),
|
||||||
onPressed: !validateEmail(_currEmail) || _currPassword.length < 3 ? null : _submitForm,
|
onPressed: !validateEmail(_currEmail) || _currPassword.length < 3
|
||||||
|
? null
|
||||||
|
: () => _submitForm(context),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -83,10 +115,9 @@ class _LoginRouteState extends State<LoginRoute> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text("Comunic"),
|
title: Text("Comunic"),
|
||||||
),
|
),
|
||||||
body: _buildLoginForm()
|
body: _buildLoginForm());
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
41
lib/utils/ui_utils.dart
Normal file
41
lib/utils/ui_utils.dart
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// User interface utilities
|
||||||
|
|
||||||
|
/// Build and return a full loading page
|
||||||
|
Widget buildLoadingPage() {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build and return an error card
|
||||||
|
Widget buildErrorCard(String message) {
|
||||||
|
return Card(
|
||||||
|
elevation: 2.0,
|
||||||
|
color: Colors.red,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: Icon(
|
||||||
|
Icons.error,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
maxLines: null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
@ -81,6 +81,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
|
shared_preferences:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: shared_preferences
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.5.2"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -144,3 +151,4 @@ packages:
|
|||||||
version: "2.0.8"
|
version: "2.0.8"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.1.0 <3.0.0"
|
dart: ">=2.1.0 <3.0.0"
|
||||||
|
flutter: ">=0.1.4 <2.0.0"
|
||||||
|
@ -24,6 +24,9 @@ dependencies:
|
|||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^0.1.2
|
cupertino_icons: ^0.1.2
|
||||||
|
|
||||||
|
# Preferences are useful for a lot of things (ex: login tokens)
|
||||||
|
shared_preferences: ^0.5.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
Loading…
Reference in New Issue
Block a user