I'm trying to implement Firebase phone authorization using Flutter Bloc pattern.
I have the following code
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:firebase_auth/firebase_auth.dart';
import './bloc.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final FirebaseAuth _auth = FirebaseAuth.instance;
#override
AuthState get initialState => AuthNotStarted();
#override
Stream<AuthState> mapEventToState(
AuthEvent event,
) async* {
if (event is VerifyPhone) {
yield* _mapVerifyPhoneToState(event);
}
}
Stream<AuthState> _mapVerifyPhoneToState(VerifyPhone event) async* {
yield AuthStarted();
_auth.verifyPhoneNumber(
phoneNumber: "+" + event.phoneNumber,
timeout: Duration(seconds: 60),
verificationCompleted: (AuthCredential authCredential) {
print("verification completed: auth credential");
},
verificationFailed: (AuthException authException) {
print("verification failed: auth exception");
print(authException.message);
},
codeSent: (String verificationId, [int forceResendingToken]) {
print("code sent verification id" + verificationId);
},
codeAutoRetrievalTimeout: (String verificationId) {
print("auto time" + verificationId);
});
}
}
But i can't use yield inside verifyPhoneNumber callbacks.
The question is how to yield different states inside of callback functions?
You can add an event from your callback. For example, in verificationCompleted, you can do:
verificationCompleted: (AuthCredential authCredential) {
print("verification completed: auth credential");
add(AuthCompleted());
},
And you can handle the AuthCompleted() event on mapEventToState:
#override
Stream<AuthState> mapEventToState(
AuthEvent event,
) async* {
if (event is VerifyPhone) {
yield* _mapVerifyPhoneToState(event);
}
if (event is AuthCompleted){
//Here you can use yield and whathever you want
}
}
PhoneAuthenticationBloc
class PhoneAuthenticationBloc
extends Bloc<PhoneAuthenticationEvent, PhoneAuthenticationState> {
final AuthRepository _authRepository;
final AuthBloc _authBloc;
#override
Stream<PhoneAuthenticationState> mapEventToState(
PhoneAuthenticationEvent event,
) async* {
if (event is PhoneLoadingEvent) {
yield PhoneLoadingState();
} else if (event is PhoneVerificationFailedEvent) {
yield PhoneOTPFailureState(event.failure);
} else if (event is PhoneSmsCodeSentEvent) {
yield PhoneSmsCodeSentState(
verificationId: event.verificationId, resendCode: event.resendId);
} else if (event is PhoneVerifiedOtpEvent) {
yield* _mapToVerifyOtp(event.smsCode, event.verificationId);
}
}
void verifyPhoneNumber(String phoneNumber) async {
try {
add(PhoneLoadingEvent());
await _authRepository.verifyPhoneNumber(phoneNumber,
onRetrieval: (String retrievalCode) {
print("Time Out Retrieval Code: $retrievalCode");
}, onFailed: (Failure f) {
print("OnFailed: ${f.message}");
add(PhoneVerificationFailedEvent(f));
}, onCompleted: (Map<String, dynamic> data) {
print("verificationCompleted: $data");
}, onCodeSent: (String verificationId, int resendCode) {
print("verificationId:$verificationId & resendCode: $resendCode");
add(PhoneSmsCodeSentEvent(
verificationId: verificationId, resendId: resendCode));
});
} catch (e) {
add(PhoneVerificationFailedEvent(Failure(message: e.toString())));
}
}}
UI Screen
builder: (context, state) {
return AppButton(
isLoading: state is PhoneLoadingState,
onPressed: () async {
if (_formKey.currentState.validate()) {
BlocProvider.of<PhoneAuthenticationBloc>(context)
.verifyPhoneNumber(_phoneController.text);
}
},
title: "Continue",
textColor: Colors.white,
);
}
Related
I'm trying to create an auth service and I want to return the verificationId from the custom method. However, calling this method throws the null check exception because it doesn't wait for the Future to complete before returning.
Future<String> sendPhoneVerification({
required String phoneNumber,
}) async {
String? result;
await FirebaseAuth.instance.verifyPhoneNumber(
phoneNumber: '+1$phoneNumber',
verificationCompleted: (
PhoneAuthCredential credential,
) {
result = credential.verificationId;
},
verificationFailed: (e) {
if (e.code == 'invalid-phone-number') {
throw InvalidPhoneNumberAuthException();
} else if (e.code == 'too-many-requests') {
throw TooManyRequestsAuthException();
} else {
throw GenericAuthException();
}
},
codeSent: (verificationId, resendToken) {
print('ver_id $verificationId');
result = verificationId;
},
codeAutoRetrievalTimeout: (_) {},
);
print('This is the result $result');
return result!;
}
Here is the output in the terminal.
flutter: This is the result null
flutter: ver_id <ver_id>
Please add this property timeout: const Duration(seconds: 60), in the verifyPhoneNumber() method
I figured out the solution. I found out the verifyPhoneNumber method returns a future but the implementation doesn't await that async call. I used a [completer][1] to return a future.
Future<String> sendPhoneVerification({required String phoneNumber}) async {
Completer<String> result = Completer();
await FirebaseAuth.instance.verifyPhoneNumber(
phoneNumber: '+1$phoneNumber',
verificationCompleted: (
PhoneAuthCredential credential,
) {
result.complete(credential.verificationId);
},
verificationFailed: (e) {
if (e.code == 'invalid-phone-number') {
result.completeError(InvalidPhoneNumberAuthException());
} else if (e.code == 'too-many-requests') {
result.completeError(TooManyRequestsAuthException());
} else {
result.completeError(GenericAuthException());
}
},
codeSent: (verificationId, resendToken) {
result.complete(verificationId);
},
codeAutoRetrievalTimeout: (_) {},
);
return result.future;
}
i wanted to wait till firebase auth retrive verification_id and then return function
but in current code variable value newVerificationId is getting return first as error without updating from firebase
Future<String> phoneAuthontication(
phoneNumberController,
) async {
String newVerificationId = "error";
try {
await auth.verifyPhoneNumber(
phoneNumber: phoneNumberController.text,
verificationCompleted: (_) {},
verificationFailed: (e) {
print(e);
},
codeSent: (String verificationId, int? token) {
print("====" + verificationId);
newVerificationId = verificationId;
},
codeAutoRetrievalTimeout: (e) {
print(e);
});
} catch (e) {
print(e);
}
print("---" + newVerificationId);
return newVerificationId;
}
I am getting this error when I launch any kind of auth condition, is something related to emit states on bloc any help will be appreciated:
This is my code:
class SignInFormBloc extends Bloc<SignInFormEvent, SignInFormState> {
final IAuthFacade authFacade;
SignInFormBloc({required this.authFacade}) : super(SignInFormState.initial()) {
on<SignInFormEvent>((event, emit) {
event.map(
emailChanged: ((value) => emit(state.copyWith(
emailAddress: EmailAddress(value.emailStr),
authFailureOrSuccessOption: none(),
))),
passwordChanged: ((value) =>
emit(state.copyWith(password: Password(value.passwordStr), authFailureOrSuccessOption: none()))),
// Register with email and password
signUpWithEmailAndPasswordPressed: ((value) async =>
await _performActionOnAuthFacadeWithEmailAndPassword(emit, authFacade.signUpWithEmailAndPassword)),
// Login with email and password
signInWithEmailAndPasswordPressed: ((value) async =>
await _performActionOnAuthFacadeWithEmailAndPassword(emit, authFacade.signInWithEmailAndPassword)),
// login with google
signInWithGooglePressed: ((value) async {
emit(state.copyWith(isSubmitting: true, authFailureOrSuccessOption: none()));
final failureOrSuccess = await authFacade.signInWithGoogle();
emit(state.copyWith(isSubmitting: false, authFailureOrSuccessOption: some(failureOrSuccess)));
}));
});
}
Future<void> _performActionOnAuthFacadeWithEmailAndPassword(
Emitter<SignInFormState> emit,
Future<Either<AuthFailure, Unit>> Function({required EmailAddress emailAddress, required Password password})
forwardedCalled) async {
Either<AuthFailure, Unit>? failureOrSuccess;
final isValidEmail = state.emailAddress.isValid();
final isValidPassword = state.password.isValid();
if (isValidEmail && isValidPassword) {
emit(state.copyWith(isSubmitting: true, authFailureOrSuccessOption: none()));
failureOrSuccess = await forwardedCalled(emailAddress: state.emailAddress, password: state.password);
emit(state.copyWith(
isSubmitting: false,
showErrorMessages: true,
authFailureOrSuccessOption: optionOf(failureOrSuccess),
));
}
}
}
I am read a lot about this error but i can not get fixed with any possible solution
Flutter Doctor:
I have the below screen which contains user info and have a button Export CSV:
All I need when click Export CSV just export a file as the below format:
This is the CSV Controller file:
import 'package:csv/csv.dart';
import 'dart:io' as Io;
import 'package:path_provider/path_provider.dart';
import 'package:intl/intl.dart';
import 'package:simpleappauth/general.dart';
class CsvController {
static Future<Io.File> getCsvFromList(List<List<dynamic>> csvDataList) async {
try {
String csvDataString = const ListToCsvConverter().convert(csvDataList);
Io.File csvFile = await _saveFile(csvDataString);
return csvFile;
} catch (e) {
print(e.toString());
return null;
}
}
static Future<Io.File> getCsvFromString(String csvString) async {
try {
Io.File csvFile = await _saveFile(csvString);
return csvFile;
} catch (e) {
print(e.toString());
return null;
}
}
static Future<String> _getFilePath(String fileName) async {
Io.Directory appDocumentsDirectory = await getExternalStorageDirectory(); // 1
String appDocumentsPath = appDocumentsDirectory.path; // 2
String filePath = '$appDocumentsPath/$fileName.csv'; // 3
return filePath;
}
final DateFormat formatter = DateFormat('yyyy-MM-dd');
static Future<Io.File> _saveFile(String fileDataString, {index = 0}) async {
try {
Io.File file = Io.File(await _getFilePath(
"${DateTime.now().millisecondsSinceEpoch}" +
(index > 0 ? "($index)" : "")));
if (!file.existsSync()) {
// 1
file.writeAsStringSync(fileDataString); // 2
return file;
} else {
return _saveFile(fileDataString, index: index + 1);
}
} catch (e) {
print(e.toString());
return null;
}
}
}
and this is the below main.dart:
import 'package:flutter/material.dart';
import 'package:flutter_facebook_login/flutter_facebook_login.dart';
import 'package:http/http.dart' as http;
import 'dart:convert' as JSON;
import 'dart:io';
import 'package:simpleappauth/csv_controller.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
bool _isLoggedIn = false;
Map userProfile;
final facebookLogin = FacebookLogin();
_loginWithFB() async {
final result = await facebookLogin.logIn(['email']);
switch (result.status) {
case FacebookLoginStatus.loggedIn:
final token = result.accessToken.token;
final graphResponse = await http.get(Uri.parse(
'https://graph.facebook.com/v10.0/me?fields=id,name,picture,email,name_format,birthday,hometown&access_token=${token}'));
final profile = JSON.jsonDecode(graphResponse.body);
print(profile);
setState(() {
userProfile = profile;
_isLoggedIn = true;
});
break;
case FacebookLoginStatus.cancelledByUser:
setState(() => _isLoggedIn = false);
break;
case FacebookLoginStatus.error:
setState(() => _isLoggedIn = false);
break;
}
}
_logout() {
facebookLogin.logOut();
setState(() {
_isLoggedIn = false;
});
}
#override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
home: Scaffold(
body: Center(
child: _isLoggedIn
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Image.network(
userProfile["picture"]["data"]["url"],
height: 100.0,
width: 100.0,
),
Text(userProfile["id"]),
Text(userProfile["name"]),
Text(userProfile["email"]),
Text(userProfile["name_format"]),
Text(userProfile["birthday"] ?? 'Birthday: empty'),
Text(userProfile["hometown"] ?? 'Hometown: empty'),
OutlinedButton(
child: Text("Logout"),
onPressed: () {
_logout();
},
),
OutlinedButton(
child: Text("Export CSV"),
onPressed: () {
},
),
],
)
: Center(
child: OutlinedButton(
child: Text("Login with Facebook"),
onPressed: () {
_loginWithFB();
},
),
)),
),
);
}
}
So now I want to use the CSV controller file in the main class as to export the csv file which contains the user data.
Step 1: Just add these two functions in your Csvcontroller class
static List<List<dynamic>> getCsvListFromUserProfilesMap(
List<Map<String, dynamic>> userProfiles) {
List<List<dynamic>> csvDataRows = [];
List<dynamic> headerRow = ["id", "name", "email", "hometown"];
csvDataRows.add(headerRow);
userProfiles.forEach((userProfile) {
List<dynamic> dataRow = [
userProfile["id"],
userProfile["name"],
userProfile["email"],
userProfile["hometown"] ?? 'Hometown: empty'
];
csvDataRows.add(dataRow);
});
return csvDataRows;
}
static List<List<dynamic>> getCsvListFromUserProfileMap(
Map<String, dynamic> userProfile) {
List<List<dynamic>> csvDataRows = [];
List<dynamic> headerRow = ["id", "name", "email", "hometown"];
csvDataRows.add(headerRow);
List<dynamic> dataRow = [
userProfile["id"],
userProfile["name"],
userProfile["email"],
userProfile["hometown"] ?? 'Hometown: empty'
];
csvDataRows.add(dataRow);
return csvDataRows;
}
Step 2: Just add the following code to your export CSV button.
//don't forget to import the CsvController file in the main
For example or testing purpose,
//Initialize these variables in your code
var userProfile = [
{
"id": 123,
"name": "User 1",
"email": "user1#gmail.com",
"homeTown": "city1"
},
];
var userProfiles = [
{
"id": 123,
"name": "User 1",
"email": "user1#gmail.com",
"homeTown": "city1"
},
{
"id": 1234,
"name": "User 2",
"email": "user2#gmail.com",
"homeTown": "city2"
},
];
onPressed: () {
//if you just want to export only one profile
var userCsvData = CsvController.getCsvListFromUserProfileMap(userProfile);
var csvFile = await CsvController.getCsvFromList(userCsvData);
if(csvFile != null){
print("File created here :"+csvFile.path);
}else{
print("file not created");
}
//if you just want to export only multiple profiles
var userCsvData = CsvController.getCsvListFromUserProfilesMap(userProfiles);
var csvFile = await CsvController.getCsvFromList(userCsvData);
if(csvFile != null){
print("File created here :"+csvFile.path);
}else{
print("file not created");
}
},
Here I am doing login when I am logged in successfully it will store all the info in SQFLite database.
here is my login response where I don't know how to store string array and JSON in flutter SQFlite. I need to store all this response info in SQFLite.
Can anyone helps me to create a table based on this response?
{
"loginUser": {
"token": "eyJhbGciOiZTMzLTQ4NmItYmFhNS0wODRhMWI2Nzg3YjQiJhY2Nlc3MifQ.bLKz-y9W6VKXRXkG6fbxlrmcowRNupKL0g",
"user": {
"aclRoleId": [
"cmr",
"bsp"
],
"confirmationSentAt": "2019-12-30T09:12:08Z",
"confirmationToken": "$argon2id$v=19$m=131072,t=8,p=4$WQoFfhBUfabw7g1BgNuuxg$BmUEYNWTZHeCnY0xCqOT+nWEDNlXcrNIEWQZkA51oCk",
"confirmedAt": null,
"country": {
"capital": "New Delhi",
"code": "IND",
"contactInfo": null,
"currencyCode": "INR",
"currencySymbol": "₹",
"id": "3",
"isdCode": "+91",
"name": "India",
"officialName": "The Republic of India"
},
"currentSignInAt": null,
"email": "john#mailinator.com",
"failedAttempts": null,
"id": "1",
"language": {
"code": "EN",
"id": "1",
"isActive": true,
"name": "English"
},
"lockedAt": null,
"meta": null,
"mobile": "(999)-000-0040",
"profile": {
"first_name": "John",
"last_name": "Doe"
},
"resetPasswordSentAt": null,
"resetPasswordToken": null,
"scopes": null,
"signInCount": 12,
"statusId": "confirmed",
"token": null,
"unlockToken": null
}
}
}
Here is a one way of doing it:
import 'dart:io';
import 'package:path/path.dart';
import 'dart:async';
import 'package:path_provider/path_provider.dart';
import 'package:utility_demos/user_management/user.dart';
import 'package:sqflite/sqflite.dart';
class DatabaseHelper {
static final DatabaseHelper _instance = new DatabaseHelper.internal();
factory DatabaseHelper() => _instance;
static Database _db;
Future<Database> get db async {
if (_db != null) {
return _db;
}
_db = await initDb();
return _db;
}
DatabaseHelper.internal();
// create database
initDb() async {
Directory documentDirectory = await getApplicationDocumentsDirectory();
String path = join(documentDirectory.path, "main.db");
var ourDb = await openDatabase(path, version: 1, onCreate: _onCreate);
return ourDb;
}
// create tables
void _onCreate(Database db, int version) async {
await db.execute(
"CREATE TABLE User(uid INTEGER AUTO INCREMENT PRIMARY KEY, token TEXT, user TEXT)");
// note that Im inserting password as plain text. When you implement please store it as a hash for security purposes.
}
// insert user to db when login
Future<int> saveUser(User user) async {
Database dbClient = await db;
int res = await dbClient.insert("User", user.toMap());
return res;
}
// retrieve user from db
Future<User> getUser() async {
var dbClient = await db;
List<Map> list = await dbClient.rawQuery('SELECT * FROM User');
if (list.isNotEmpty) {
return User.fromJson(list.elementAt(0));
}
return null;
}
//delete use when logout
Future<int> deleteUser() async {
var dbClient = await db;
int res = await dbClient.delete("User");
return res;
}
// check if the user logged in when app launch or any other place
Future<bool> isLoggedIn() async {
var dbClient = await db;
var res = await dbClient.query("User");
return res.length > 0 ? true : false;
}
}
Here is the User model class:
class User {
final String user;
final String token;
User(this.user, this.token);
// here because of user object is too long Im storing it as string
// and that is actually not a good way to do this because of when you want to
// fetch user and handle information it will be very hard to do those and also when testing
// please map other properties in your json to model and that's the best way.
User.fromJson(dynamic obj)
: this.user = obj['user'].toString(),
this.token = obj['token'];
String get getUser => user;
String get getToken => token;
Map<String, dynamic> toMap() => {'user': user, 'token': token};
}
Here I have created a simple widget tree to show you. How to store retrieved json object properties. Please note that I'm storing json user object as String because of its pretty long. Its better to create your model for all variables and map them. See comment in User class.
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:utility_demos/user_management/database_helper.dart';
import 'package:utility_demos/user_management/user.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter SO answers sample snippet',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
Future<bool> doLogin() async {
String data = await DefaultAssetBundle.of(context).loadString("assets/login_mock_data.json");
final decoded = json.decode(data);
try {
if (decoded != null) {
final user = User.fromJson(decoded["loginUser"]);
DatabaseHelper dbHelper = new DatabaseHelper();
await dbHelper.saveUser(user);
return true;
}
} catch (e) {
debugPrint(e.toString());
}
return false;
}
// just to show you how to fetch from db
Future<String> getLoggedInUser() async {
bool isLoggedIn = await doLogin();
if (isLoggedIn) {
User user = await DatabaseHelper().getUser();
return user.getToken;
}
return null;
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getLoggedInUser(),// this is also not a good practice :D, since each and every time build method execute this will invoke too.
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return Center(child: Text("Something..."));
} else if (snapshot.hasData) {
// just to show you how to fetch from db
if (snapshot.data != null) {
return Center(child: Text("Logged in token: ${snapshot.data}"));
}
return Center(child: Text("Not logged in"));
}
return Center(child: CircularProgressIndicator());
},
);
}
}