diff --git a/lib/main.dart b/lib/main.dart index d447902..51ee89b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,8 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_settings_screens/flutter_settings_screens.dart'; import 'package:le_kiosque_by_gcs/services/auth/main_auth.dart'; +import 'package:le_kiosque_by_gcs/services/firestore/firebase_user_service.dart'; +import 'package:le_kiosque_by_gcs/services/firestore/user_service.dart'; import 'package:le_kiosque_by_gcs/ui/view/landing.dart'; import 'package:provider/provider.dart'; @@ -26,9 +28,10 @@ class MyApp extends StatelessWidget { ), home: MultiProvider( providers: [ - Provider(create: (_) => MainAuth()) + Provider(create: (_) => MainAuth()), + Provider(create: (_) => FirebaseUserService()) ], - child: LandingPageView(), + builder: (context, widget) => LandingPageView(), ), ); } diff --git a/lib/model/user.dart b/lib/model/user.dart index 95e9520..39e1d53 100644 --- a/lib/model/user.dart +++ b/lib/model/user.dart @@ -1,14 +1,21 @@ +import 'package:firebase_auth/firebase_auth.dart'; + class KiosqueUser { - final String uid; - final String displayName; - final String city; - final String country; - final String phoneNumber; - final String emailAddress; - final String gender; - final String profileUrl; + String uid; + String displayName; + String city; + String country; + String phoneNumber; + String emailAddress; + String gender; + String profileUrl; + DateTime createdAt; + DateTime updatedAt; + DateTime birthDay; KiosqueUser({ + this.createdAt, + this.updatedAt, this.uid, this.displayName, this.city, @@ -17,5 +24,17 @@ class KiosqueUser { this.emailAddress, this.gender, this.profileUrl, + this.birthDay, }); } + +KiosqueUser toKiosqueUser(User firebaseUser) { + return KiosqueUser( + uid: firebaseUser.uid, + phoneNumber: firebaseUser.phoneNumber, + profileUrl: firebaseUser.photoURL ?? "https://www.ltc.lu/images/Contact/no_user_picture.jpg", + displayName: firebaseUser.displayName, + createdAt: firebaseUser.metadata.creationTime, + emailAddress: firebaseUser.email, + ); +} diff --git a/lib/services/auth/main_auth.dart b/lib/services/auth/main_auth.dart index b22451a..1704f57 100644 --- a/lib/services/auth/main_auth.dart +++ b/lib/services/auth/main_auth.dart @@ -1,11 +1,10 @@ - import 'package:le_kiosque_by_gcs/services/auth/auth.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter_login_facebook/flutter_login_facebook.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:le_kiosque_by_gcs/services/firestore/user_service.dart'; class MainAuth implements Auth { - FirebaseAuth _auth = FirebaseAuth.instance; @override @@ -17,7 +16,9 @@ class MainAuth implements Auth { final userAuth = await googleUser.authentication; if (userAuth.accessToken != null) { final authCredential = GoogleAuthProvider.credential( - accessToken: userAuth.accessToken, idToken: userAuth.idToken); + accessToken: userAuth.accessToken, + idToken: userAuth.idToken, + ); final authResult = await _auth.signInWithCredential(authCredential); return authResult.user; } else { @@ -66,4 +67,4 @@ class MainAuth implements Auth { throw UnimplementedError(); } } -} \ No newline at end of file +} diff --git a/lib/services/firestore/firebase_user_service.dart b/lib/services/firestore/firebase_user_service.dart new file mode 100644 index 0000000..48fc187 --- /dev/null +++ b/lib/services/firestore/firebase_user_service.dart @@ -0,0 +1,55 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:le_kiosque_by_gcs/model/user.dart'; +import 'package:le_kiosque_by_gcs/services/firestore/user_service.dart'; + +class FirebaseUserService implements UserService { + FirebaseFirestore _firestore = FirebaseFirestore.instance; + + @override + Stream getCurrentUser(String uid) { + final snapshots = _firestore.collection("users").doc(uid).snapshots(); + return snapshots.map( + (event) { + final data = event.data(); + return KiosqueUser( + uid: data["uid"], + displayName: data['displayName'], + emailAddress: data['emailAddress'], + city: data['city'], + country: data['country'], + gender: data['gender'], + profileUrl: data['profileUrl'], + phoneNumber: data['phoneNumber'], + createdAt: (data['createdAt'] as Timestamp).toDate(), + updatedAt : (data['updatedAt'] as Timestamp)?.toDate(), + birthDay : (data['birthDay'] as Timestamp)?.toDate(), + ); + }, + ); + } + + @override + Future save(KiosqueUser kiosqueUser) async { + final user = { + 'uid': kiosqueUser.uid, + 'displayName': kiosqueUser.displayName, + 'emailAddress': kiosqueUser.emailAddress, + 'profileUrl': kiosqueUser.profileUrl, + 'phoneNumber': kiosqueUser.phoneNumber, + 'city': kiosqueUser.city, + 'country': kiosqueUser.country, + 'gender': kiosqueUser.gender, + 'createdAt': kiosqueUser.createdAt, + 'updatedAt': kiosqueUser.updatedAt, + 'birthDay': kiosqueUser.birthDay, + }; + return await _firestore.collection("users").doc(user['uid']).set(user); + } + + @override + Future updateCurrentUser(data) async { + final userId = FirebaseAuth.instance.currentUser.uid; + return await _firestore.collection("users").doc(userId).set(data); + } +} diff --git a/lib/services/firestore/user_service.dart b/lib/services/firestore/user_service.dart new file mode 100644 index 0000000..20d9a45 --- /dev/null +++ b/lib/services/firestore/user_service.dart @@ -0,0 +1,8 @@ +import 'package:le_kiosque_by_gcs/model/user.dart'; + +abstract class UserService { + Future save(KiosqueUser kiosqueUser); + Future updateCurrentUser(dynamic data); + Stream getCurrentUser(String uid); +} + diff --git a/lib/ui/view/auth.dart b/lib/ui/view/auth.dart index 43d410b..45f2362 100644 --- a/lib/ui/view/auth.dart +++ b/lib/ui/view/auth.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:le_kiosque_by_gcs/model/user.dart'; import 'package:le_kiosque_by_gcs/services/auth/auth.dart'; +import 'package:le_kiosque_by_gcs/services/firestore/user_service.dart'; import 'package:provider/provider.dart'; import '../custom/custom_elevated_button.dart'; @@ -9,8 +11,13 @@ class AuthView extends StatelessWidget { Future _signWithGoogle(context) async { final auth = Provider.of(context, listen: false); + final userService = Provider.of(context, listen: false); try { - await auth.signInWithGoogle(); + final firebaseUser = await auth.signInWithGoogle(); + if (firebaseUser.metadata.creationTime == firebaseUser.metadata.lastSignInTime) { + final kiosqueUser = toKiosqueUser(firebaseUser); + await userService.save(kiosqueUser); + } } catch (e) { print("Error $e"); } @@ -18,8 +25,13 @@ class AuthView extends StatelessWidget { Future _signInWithFacebook(context) async { final auth = Provider.of(context, listen: false); + final userService = Provider.of(context, listen: false); try { - await auth.signInWithFacebook(); + final firebaseUser = await auth.signInWithFacebook(); + if (firebaseUser.metadata.creationTime == firebaseUser.metadata.lastSignInTime) { + final kiosqueUser = toKiosqueUser(firebaseUser); + await userService.save(kiosqueUser); + } } catch (e) { print("Error $e"); } diff --git a/lib/ui/view/edit_profil.dart b/lib/ui/view/edit_profil.dart deleted file mode 100644 index 5260092..0000000 --- a/lib/ui/view/edit_profil.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; - -class EditProfileView extends StatefulWidget { - @override - _EditProfileViewState createState() => _EditProfileViewState(); -} - -class _EditProfileViewState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Colors.white, - iconTheme: IconThemeData( - color: Color(0xFF545454), //change your color here - ) - ), - body: Container( - width: double.infinity, - - ), - ); - } -} diff --git a/lib/ui/view/editprofile/edit_profil.dart b/lib/ui/view/editprofile/edit_profil.dart new file mode 100644 index 0000000..41b2bb1 --- /dev/null +++ b/lib/ui/view/editprofile/edit_profil.dart @@ -0,0 +1,249 @@ +import 'package:country_picker/country_picker.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:le_kiosque_by_gcs/model/user.dart'; +import 'package:le_kiosque_by_gcs/services/firestore/user_service.dart'; +import 'package:le_kiosque_by_gcs/ui/custom/profile_picture.dart'; + +class EditProfileView extends StatefulWidget { + final UserService userService; + + const EditProfileView({ + Key key, + @required this.userService, + }) : super(key: key); + + @override + _EditProfileViewState createState() => _EditProfileViewState(); +} + +class _EditProfileViewState extends State { + TextEditingController _countryController = TextEditingController(); + TextEditingController _cityController = TextEditingController(); + TextEditingController _dateController = TextEditingController(); + TextEditingController _genderController = TextEditingController(); + + DateTime _selectedDate; + + final _formKey = GlobalKey(); + KiosqueUser currentUser; + + void _submitForm(KiosqueUser user) async { + if (_validateAndSaveForm()) { + user.country = _countryController.text.toString(); + user.city = _cityController.text.toString(); + user.gender = _genderController.text.toString(); + user.birthDay = _selectedDate; + + await widget.userService.save(user); + Navigator.of(context).pop(); + } + } + + bool _validateAndSaveForm() { + final formState = _formKey.currentState; + if (formState.validate()) { + formState.save(); + return true; + } else { + return false; + } + } + + Future _selectDate(BuildContext context) async { + final DateTime picked = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(1900, 8), + lastDate: DateTime(2101), + ); + + final stringDate = DateFormat.yMMMd().format(picked); + _selectedDate = picked; + _dateController.text = stringDate; + } + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: widget.userService + .getCurrentUser(FirebaseAuth.instance.currentUser.uid), + builder: (_, snapshot) { + if (snapshot.hasData) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.white, + iconTheme: IconThemeData( + color: Color(0xFF545454), //change your color here + ), + ), + body: SingleChildScrollView( + child: Column( + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: + ProfilePicture(imageUrl: snapshot.data.profileUrl), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + snapshot.data.displayName, + style: TextStyle( + fontSize: 26, + color: Colors.black, + ), + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + right: 10, + bottom: 10, + ), + child: Icon( + Icons.location_on, + color: Color(0xFFA5A5A5), + size: 17, + ), + ), + Text( + snapshot.data.city ?? "No location", + style: TextStyle(color: Colors.grey), + ) + ], + ) + ], + ) + ], + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextFormField( + controller: _countryController, + readOnly: true, + validator: (value) => value.isEmpty ? 'Ne doit pas être vide ! !' : null, + onTap: () => _showCountryPicker(context), + decoration: InputDecoration( + border: OutlineInputBorder(), + hintText: 'Pays', + labelText: "Pays", + hoverColor: Colors.black, + ), + ), + SizedBox(height: 16), + TextFormField( + controller: _cityController, + validator: (value) => value.isEmpty ? 'Ne doit pas être vide ! !' : null, + decoration: InputDecoration( + border: OutlineInputBorder(), + hintText: 'Ville', + labelText: "Ville", + hoverColor: Colors.black, + ), + ), + SizedBox(height: 16), + TextFormField( + validator: (value) => value.isEmpty ? 'Ne doit pas être vide ! !' : null, + controller: _genderController, + onTap: () => _showGenderDialog(), + readOnly: true, + decoration: InputDecoration( + border: OutlineInputBorder(), + hintText: 'Genre', + labelText: "Genre", + hoverColor: Colors.black, + ), + ), + SizedBox(height: 16), + TextFormField( + readOnly: true, + validator: (value) => value.isEmpty ? 'Ne doit pas être vide ! !' : null, + controller: _dateController, + onTap: () => _selectDate(context), + decoration: InputDecoration( + border: OutlineInputBorder(), + hintText: 'Date de naissance', + labelText: "Date de naissance", + hoverColor: Colors.black, + ), + ), + SizedBox(height: 16), + SizedBox( + height: 50, + child: ElevatedButton( + onPressed: () => _submitForm(snapshot.data), + child: Text("Enregistrer"), + ), + ) + ], + ), + ), + ) + ], + ), + ), + ); + } else { + return Scaffold( + body: Center( + child: Text("Problème de connexion !"), + ), + ); + } + }, + ); + } + + void _showGenderDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text("Genre"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text("Homme"), + onTap: () { + _genderController.text = "Homme"; + Navigator.of(context).pop(); + }, + ), + ListTile( + title: Text("Femme"), + onTap: () { + _genderController.text = "Femme"; + Navigator.of(context).pop(); + }, + ) + ], + ), + ); + }, + ); + } + + void _showCountryPicker(BuildContext context) { + showCountryPicker( + context: context, + showPhoneCode: false, + onSelect: (Country country) { + setState(() { + _countryController.text = country.displayNameNoCountryCode; + }); + }, + ); + } +} diff --git a/lib/ui/view/landing.dart b/lib/ui/view/landing.dart index c0e2551..1df1af8 100644 --- a/lib/ui/view/landing.dart +++ b/lib/ui/view/landing.dart @@ -6,29 +6,26 @@ import 'package:le_kiosque_by_gcs/ui/view/main.dart'; import 'package:provider/provider.dart'; class LandingPageView extends StatelessWidget { - @override Widget build(BuildContext context) { - final auth = Provider.of(context, listen: false); return StreamBuilder( - stream: auth.authStateChanges(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.active) { - if (snapshot.data == null) { - return AuthView(); - } else { - return MainView(); - } - } - else { - return Scaffold( - body: Center( - child: CircularProgressIndicator(), - ), - ); + stream: auth.authStateChanges(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.active) { + if (snapshot.data == null) { + return AuthView(); + } else { + return MainView(); } + } else { + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); } + }, ); } } diff --git a/lib/ui/view/main.dart b/lib/ui/view/main.dart index 75374e2..089d945 100644 --- a/lib/ui/view/main.dart +++ b/lib/ui/view/main.dart @@ -2,12 +2,15 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:le_kiosque_by_gcs/model/news.dart'; +import 'package:le_kiosque_by_gcs/services/auth/auth.dart'; +import 'package:le_kiosque_by_gcs/services/auth/main_auth.dart'; import 'package:le_kiosque_by_gcs/ui/view/home.dart'; import 'package:le_kiosque_by_gcs/ui/view/news/news.dart'; import 'package:le_kiosque_by_gcs/ui/view/profile.dart'; import 'package:le_kiosque_by_gcs/ui/view/search/search.dart'; import 'package:le_kiosque_by_gcs/ui/view/setting/setting.dart'; import 'package:le_kiosque_by_gcs/ui/view/subscription/subscription.dart'; +import 'package:provider/provider.dart'; class MainView extends StatefulWidget { @override @@ -33,9 +36,13 @@ class _MainViewState extends State { } _showSettingView(BuildContext context) { + final auth = Provider.of(context, listen: false); Navigator.of(context).push( MaterialPageRoute( - builder: (_) => SettingView(), + builder: (context) => Provider( + create: (context) => MainAuth(), + child: SettingView(auth: auth), + ), ), ); } @@ -43,7 +50,7 @@ class _MainViewState extends State { void _showSearchView(BuildContext context) { Navigator.of(context).push( MaterialPageRoute( - builder: (_) => SearchView(), + builder: (context) => SearchView(), ), ); } diff --git a/lib/ui/view/profile.dart b/lib/ui/view/profile.dart index 632e8e0..4f1fae9 100644 --- a/lib/ui/view/profile.dart +++ b/lib/ui/view/profile.dart @@ -1,9 +1,12 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:le_kiosque_by_gcs/model/magazine.dart'; +import 'package:le_kiosque_by_gcs/model/user.dart'; +import 'package:le_kiosque_by_gcs/services/firestore/user_service.dart'; import 'package:le_kiosque_by_gcs/ui/custom/item_mag_large.dart'; import 'package:le_kiosque_by_gcs/ui/custom/profile_picture.dart'; -import 'package:le_kiosque_by_gcs/ui/view/edit_profil.dart'; +import 'package:le_kiosque_by_gcs/ui/view/editprofile/edit_profil.dart'; +import 'package:provider/provider.dart'; class ProfileView extends StatefulWidget { @override @@ -92,48 +95,69 @@ class _ProfileViewState extends State { } Widget _buildProfileHeader() { - final imageUrl = FirebaseAuth.instance.currentUser.photoURL; - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: ProfilePicture(imageUrl: imageUrl), - ), - ), - Text( - "Eric Ampire", - style: TextStyle( - fontSize: 26, - color: Colors.black, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - Icons.location_on, - color: Color(0xFFA5A5A5), - size: 17, - ), + final userService = Provider.of(context, listen: false); + return StreamBuilder( + stream: userService.getCurrentUser(FirebaseAuth.instance.currentUser.uid), + builder: (context, snapshot) { + if (snapshot.hasError) { + return Scaffold( + body: Center( + child: Text("Une erreur s'est produite"), ), - Text( - "Lubumbashi", - style: TextStyle(color: Colors.grey), - ) - ], - ) - ], + ); + } + if (snapshot.hasData) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: ProfilePicture(imageUrl: snapshot.data.profileUrl), + ), + ), + Text( + snapshot.data.displayName, + style: TextStyle( + fontSize: 26, + color: Colors.black, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.location_on, + color: Color(0xFFA5A5A5), + size: 17, + ), + ), + Text( + snapshot.data.city ?? "No location", + style: TextStyle(color: Colors.grey), + ) + ], + ) + ], + ); + } else { + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + } + }, ); } _showEditProfileView(BuildContext context) { + final userService = Provider.of(context, listen: false); Navigator.of(context).push( MaterialPageRoute( - builder: (BuildContext context) => EditProfileView(), + builder: (BuildContext context) => EditProfileView(userService: userService), ), ); } diff --git a/pubspec.lock b/pubspec.lock index 78e0e0c..6843e4d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.3" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1" collection: dependency: transitive description: @@ -64,6 +85,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + country_picker: + dependency: "direct main" + description: + name: country_picker + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" crypto: dependency: transitive description: @@ -133,7 +161,7 @@ packages: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "0.5.1" + version: "0.5.2" firebase_core_platform_interface: dependency: transitive description: @@ -241,7 +269,7 @@ packages: source: hosted version: "1.1.0" intl: - dependency: transitive + dependency: "direct main" description: name: intl url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index 4b59253..4ee0665 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,8 +31,9 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 - firebase_core: 0.5.1 + firebase_core: 0.5.2 firebase_auth: 0.18.2 + cloud_firestore: 0.14.3 google_sign_in: ^4.5.6 flutter_login_facebook: 0.4.0+1 provider: ^4.0.0 @@ -40,6 +41,8 @@ dependencies: flutter_settings_screens: ^0.2.1+1 url_launcher: ^6.0.2 carousel_slider: ^3.0.0 + country_picker: ^2.0.3 + intl: ^0.16.1 dev_dependencies: flutter_test: