github issue

Created Diff never expires
149 removals
Words removed46
Total words1435
Words removed (%)3.21
409 lines
27 additions
Words added26
Total words1415
Words added (%)1.84
401 lines
import 'dart:async';
import 'dart:async';
import 'dart:convert';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_firebase/data_models/countries.dart';
import 'countries.dart';
import 'package:flutter_firebase/firebase/auth/phone_auth/code.dart';
import 'code.dart';
import 'package:flutter_firebase/firebase/auth/phone_auth/verify.dart';
import 'verify.dart';
import 'package:flutter_firebase/utils/constants.dart';
import 'code.dart' show FirebasePhoneAuth;
import 'code.dart' show FirebasePhoneAuth, phoneAuthState;
import 'widgets.dart';
import '../../../utils/widgets.dart';


/*
/*
* PhoneAuthUI - this file contains whole ui and controllers of ui
* PhoneAuthUI - this file contains whole ui and controllers of ui
* Background code will be in other class
* Background code will be in other class
* This code can be easily re-usable with any other service type, as UI part and background handling are completely from different sources
* This code can be easily re-usable with any other service type, as UI part and background handling are completely from different sources
* code.dart - Class to control background processes in phone auth verification using Firebase
* code.dart - Class to control background processes in phone auth verification using Firebase
*/
*/


// ignore: must_be_immutable
// ignore: must_be_immutable
class PhoneAuthGetPhone extends StatefulWidget {
class PhoneAuthGetPhone extends StatefulWidget {
/*
/*
* cardBackgroundColor & logo values will be passed to the constructor
* cardBackgroundColor & logo values will be passed to the constructor
* here we access these params in the _PhoneAuthState using "widget"
* here we access these params in the _PhoneAuthState using "widget"
*/
*/
Color cardBackgroundColor = Color(0xFF6874C2);
Color cardBackgroundColor = Color(0xFF6874C2);
String logo = Assets.firebase;
String appName = "Awesome app";
String appName = "Awesome app";


@override
@override
_PhoneAuthGetPhoneState createState() => _PhoneAuthGetPhoneState();
_PhoneAuthGetPhoneState createState() => _PhoneAuthGetPhoneState();
}
}


class _PhoneAuthGetPhoneState extends State<PhoneAuthGetPhone> {
class _PhoneAuthGetPhoneState extends State<PhoneAuthGetPhone> {
/*
/*
* _height & _width:
* _height & _width:
* will be calculated from the MediaQuery of widget's context
* will be calculated from the MediaQuery of widget's context
* countries:
* countries:
* will be a list of Country model, Country model contains name, dialCode, flag and code for various countries
* will be a list of Country model, Country model contains name, dialCode, flag and code for various countries
* and below params are all related to StreamBuilder
* and below params are all related to StreamBuilder
*/
*/
double _height, _width, _fixedPadding;
double _height, _width, _fixedPadding;


List<Country> countries = [];
List<Country> countries = [];
StreamController<List<Country>> _countriesStreamController;
StreamController<List<Country>> _countriesStreamController;
Stream<List<Country>> _countriesStream;
Stream<List<Country>> _countriesStream;
Sink<List<Country>> _countriesSink;
Sink<List<Country>> _countriesSink;


/*
/*
* _searchCountryController - This will be used as a controller for listening to the changes what the user is entering
* _searchCountryController - This will be used as a controller for listening to the changes what the user is entering
* and it's listener will take care of the rest
* and it's listener will take care of the rest
*/
*/
TextEditingController _searchCountryController = TextEditingController();
TextEditingController _searchCountryController = TextEditingController();
TextEditingController _phoneNumberController = TextEditingController();
TextEditingController _phoneNumberController = TextEditingController();


/*
/*
* This will be the index, we will modify each time the user selects a new country from the dropdown list(dialog),
* This will be the index, we will modify each time the user selects a new country from the dropdown list(dialog),
* As a default case, we are using India as default country, index = 31
* As a default case, we are using India as default country, index = 31
*/
*/
int _selectedCountryIndex = 100;
int _selectedCountryIndex = 31;


bool _isCountriesDataFormed = false;
bool _isCountriesDataFormed = false;


@override
@override
void initState() {
void initState() {
super.initState();
super.initState();
}
}


@override
@override
void dispose() {
void dispose() {
// While disposing the widget, we should close all the streams and controllers
// While disposing the widget, we should close all the streams and controllers


// Disposing Stream components
// Disposing Stream components
// _countriesSink.close();
// _countriesSink.close();
// _countriesStreamController.close();
// _countriesStreamController.close();


// Disposing _countriesSearchController
// Disposing _countriesSearchController
_searchCountryController.dispose();
_searchCountryController.dispose();
super.dispose();
super.dispose();
}
}


Future<List<Country>> loadCountriesJson() async {
Future<List<Country>> loadCountriesJson() async {
// Cleaning up the countries list before we put our data in it
// Cleaning up the countries list before we put our data in it
countries.clear();
countries.clear();


// Fetching the json file, decoding it and storing each object as Country in countries(list)
// Fetching the json file, decoding it and storing each object as Country in countries(list)
var value = await DefaultAssetBundle.of(context)
var value = await DefaultAssetBundle.of(context).loadString("country_phone_codes.json");
.loadString("data/country_phone_codes.json");
var countriesJson = json.decode(value);
var countriesJson = json.decode(value);
for (var country in countriesJson) {
for (var country in countriesJson) {
countries.add(Country.fromJson(country));
countries.add(Country.fromJson(country));
}
}


//Finally adding the initial data to the _countriesSink
//Finally adding the initial data to the _countriesSink
// _countriesSink.add(countries);
// _countriesSink.add(countries);
return countries;
return countries;
}
}


@override
@override
Widget build(BuildContext context) {
Widget build(BuildContext context) {
// Fetching height & width parameters from the MediaQuery
// Fetching height & width parameters from the MediaQuery
// _logoPadding will be a constant, scaling it according to device's size
// _logoPadding will be a constant, scaling it according to device's size
_height = MediaQuery.of(context).size.height;
_height = MediaQuery.of(context).size.height;
_width = MediaQuery.of(context).size.width;
_width = MediaQuery.of(context).size.width;
_fixedPadding = _height * 0.025;
_fixedPadding = _height * 0.025;


WidgetsBinding.instance.addPostFrameCallback((Duration d) {
WidgetsBinding.instance.addPostFrameCallback((Duration d) {
if (countries.length < 240) {
if (countries.length < 240) {
loadCountriesJson().whenComplete(() {
loadCountriesJson().whenComplete(() {
setState(() => _isCountriesDataFormed = true);
setState(() => _isCountriesDataFormed = true);
});
});
}
}
});
});


/* Scaffold: Using a Scaffold widget as parent
/* Scaffold: Using a Scaffold widget as parent
* SafeArea: As a precaution - wrapping all child descendants in SafeArea, so that even notched phones won't loose data
* SafeArea: As a precaution - wrapping all child descendants in SafeArea, so that even notched phones won't loose data
* Center: As we are just having Card widget - making it to stay in Center would really look good
* Center: As we are just having Card widget - making it to stay in Center would really look good
* SingleChildScrollView: There can be chances arising where
* SingleChildScrollView: There can be chances arising where
*/
*/
return Scaffold(
return Scaffold(
backgroundColor: Colors.white.withOpacity(0.95),
backgroundColor: Colors.white.withOpacity(0.95),
body: SafeArea(
body: SafeArea(
child: Center(
child: Center(
child: SingleChildScrollView(
child: SingleChildScrollView(
child: _getBody(),
child: _getBody(),
),
),
),
),
),
),
);
);
}
}


/*
/*
* Widget hierarchy ->
* Widget hierarchy ->
* Scaffold -> SafeArea -> Center -> SingleChildScrollView -> Card()
* Scaffold -> SafeArea -> Center -> SingleChildScrollView -> Card()
* Card -> FutureBuilder -> Column()
* Card -> FutureBuilder -> Column()
*/
*/
Widget _getBody() => Card(
Widget _getBody() => Card(
color: widget.cardBackgroundColor,
color: widget.cardBackgroundColor,
elevation: 2.0,
elevation: 2.0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
child: SizedBox(
child: SizedBox(
height: _height * 8 / 10,
height: _height * 8 / 10,
width: _width * 8 / 10,
width: _width * 8 / 10,


/*
/*
* Fetching countries data from JSON file and storing them in a List of Country model:
* Fetching countries data from JSON file and storing them in a List of Country model:
* ref:- List<Country> countries
* ref:- List<Country> countries
* Until the data is fetched, there will be CircularProgressIndicator showing, describing something is on it's way
* Until the data is fetched, there will be CircularProgressIndicator showing, describing something is on it's way
* (Previously there was a FutureBuilder rather that the below thing, which created unexpected exceptions and had to be removed)
* (Previously there was a FutureBuilder rather that the below thing, which created unexpected exceptions and had to be removed)
*/
*/
child: _isCountriesDataFormed
child: _isCountriesDataFormed
? _getColumnBody()
? _getColumnBody()
: Center(child: CircularProgressIndicator()),
: Center(child: CircularProgressIndicator()),
),
),
);
);


Widget _getColumnBody() => Column(
Widget _getColumnBody() => Column(
mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
children: <Widget>[
// Logo: scaling to occupy 2 parts of 10 in the whole height of device
// Logo: scaling to occupy 2 parts of 10 in the whole height of device
Padding(
padding: EdgeInsets.all(_fixedPadding),
child: PhoneAuthWidgets.getLogo(
logoPath: widget.logo, height: _height * 0.2),
),


// AppName:
// AppName:
Text(widget.appName,
Text(widget.appName,
textAlign: TextAlign.center,
textAlign: TextAlign.center,
style: TextStyle(
style: TextStyle(
color: Colors.white,
color: Colors.white,
fontSize: 24.0,
fontSize: 24.0,
fontWeight: FontWeight.w700)),
fontWeight: FontWeight.w700)),


Padding(
Padding(
padding: EdgeInsets.only(top: _fixedPadding, left: _fixedPadding),
padding: EdgeInsets.only(top: _fixedPadding, left: _fixedPadding),
child: PhoneAuthWidgets.subTitle('Select your country'),
child: PhoneAuthWidgets.subTitle('Select your country'),
),
),


/*
/*
* Select your country, this will be a custom DropDown menu, rather than just as a dropDown
* Select your country, this will be a custom DropDown menu, rather than just as a dropDown
* onTap of this, will show a Dialog asking the user to select country they reside,
* onTap of this, will show a Dialog asking the user to select country they reside,
* according to their selection, prefix will change in the PhoneNumber TextFormField
* according to their selection, prefix will change in the PhoneNumber TextFormField
*/
*/
Padding(
Padding(
padding: EdgeInsets.only(left: _fixedPadding, right: _fixedPadding),
padding: EdgeInsets.only(left: _fixedPadding, right: _fixedPadding),
child: PhoneAuthWidgets.selectCountryDropDown(
child: PhoneAuthWidgets.selectCountryDropDown(
countries[_selectedCountryIndex], showCountries),
countries[_selectedCountryIndex], showCountries),
),
),


// Subtitle for Enter your phone
// Subtitle for Enter your phone
Padding(
Padding(
padding: EdgeInsets.only(top: 10.0, left: _fixedPadding),
padding: EdgeInsets.only(top: 10.0, left: _fixedPadding),
child: PhoneAuthWidgets.subTitle('Enter your phone'),
child: PhoneAuthWidgets.subTitle('Enter your phone'),
),
),
// PhoneNumber TextFormFields
// PhoneNumber TextFormFields
Padding(
Padding(
padding: EdgeInsets.only(
padding: EdgeInsets.only(
left: _fixedPadding,
left: _fixedPadding,
right: _fixedPadding,
right: _fixedPadding,
bottom: _fixedPadding),
bottom: _fixedPadding),
child: PhoneAuthWidgets.phoneNumberField(_phoneNumberController,
child: PhoneAuthWidgets.phoneNumberField(_phoneNumberController,
countries[_selectedCountryIndex].dialCode),
countries[_selectedCountryIndex].dialCode),
),
),


/*
/*
* Some informative text
* Some informative text
*/
*/
Row(
Row(
mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
children: <Widget>[
SizedBox(width: _fixedPadding),
SizedBox(width: _fixedPadding),
Icon(Icons.info, color: Colors.white, size: 20.0),
Icon(Icons.info, color: Colors.white, size: 20.0),
SizedBox(width: 10.0),
SizedBox(width: 10.0),
Expanded(
Expanded(
child: RichText(
child: RichText(
text: TextSpan(children: [
text: TextSpan(children: [
TextSpan(
TextSpan(
text: 'We will send ',
text: 'We will send ',
style: TextStyle(
style: TextStyle(
color: Colors.white, fontWeight: FontWeight.w400)),
color: Colors.white, fontWeight: FontWeight.w400)),
TextSpan(
TextSpan(
text: 'One Time Password',
text: 'One Time Password',
style: TextStyle(
style: TextStyle(
color: Colors.white,
color: Colors.white,
fontSize: 16.0,
fontSize: 16.0,
fontWeight: FontWeight.w700)),
fontWeight: FontWeight.w700)),
TextSpan(
TextSpan(
text: ' to this mobile number',
text: ' to this mobile number',
style: TextStyle(
style: TextStyle(
color: Colors.white, fontWeight: FontWeight.w400)),
color: Colors.white, fontWeight: FontWeight.w400)),
])),
])),
),
SizedBox(width: _fixedPadding),
],
),
),
SizedBox(width: _fixedPadding),
],
),


/*
/*
* Button: OnTap of this, it appends the dial code and the phone number entered by the user to send OTP,
* Button: OnTap of this, it appends the dial code and the phone number entered by the user to send OTP,
* knowing once the OTP has been sent to the user - the user will be navigated to a new Screen,
* knowing once the OTP has been sent to the user - the user will be navigated to a new Screen,
* where is asked to enter the OTP he has received on his mobile (or) wait for the system to automatically detect the OTP
* where is asked to enter the OTP he has received on his mobile (or) wait for the system to automatically detect the OTP
*/
*/
SizedBox(height: _fixedPadding * 1.5),
SizedBox(height: _fixedPadding * 1.5),
RaisedButton(
RaisedButton(
elevation: 16.0,
elevation: 16.0,
onPressed: startPhoneAuth,
onPressed: startPhoneAuth,
child: Padding(
child: Padding(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0),
child: Text(
child: Text(
'SEND OTP',
'SEND OTP',
style: TextStyle(
style: TextStyle(
color: widget.cardBackgroundColor, fontSize: 18.0),
color: widget.cardBackgroundColor, fontSize: 18.0),
),
),
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0)),
),
),
],
),
);
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0)),
),
],
);


/*
/*
* This will trigger a dialog, that will let the user to select their country, so the dialcode
* This will trigger a dialog, that will let the user to select their country, so the dialcode
* of their country will be automatically added at the end
* of their country will be automatically added at the end
*/
*/
showCountries() {
showCountries() {
/*
/*
* Initialising components required for StreamBuilder
* Initialising components required for StreamBuilder
* We will not be using _countriesStreamController anywhere, but just to initialize Stream & Sink from that
* We will not be using _countriesStreamController anywhere, but just to initialize Stream & Sink from that
* _countriesStream will give us the data what we need(output) - that will be used in StreamBuilder widget
* _countriesStream will give us the data what we need(output) - that will be used in StreamBuilder widget
* _countriesSink is the place where we send the data(input)
* _countriesSink is the place where we send the data(input)
*/
*/
_countriesStreamController = StreamController();
_countriesStreamController = StreamController();
_countriesStream = _countriesStreamController.stream;
_countriesStream = _countriesStreamController.stream;
_countriesSink = _countriesStreamController.sink;
_countriesSink = _countriesStreamController.sink;
_countriesSink.add(countries);
_countriesSink.add(countries);


_searchCountryController.addListener(searchCountries);
_searchCountryController.addListener(searchCountries);


showDialog(
showDialog(
context: context,
context: context,
builder: (BuildContext context) => searchAndPickYourCountryHere(),
builder: (BuildContext context) => searchAndPickYourCountryHere(),
barrierDismissible: false);
barrierDismissible: false);
}
}


/*
/*
* This will be the listener for searching the query entered by user for their country, (dialog pop-up),
* This will be the listener for searching the query entered by user for their country, (dialog pop-up),
* searches for the query and returns list of countries matching the query by adding the results to the sink of _countriesStream
* searches for the query and returns list of countries matching the query by adding the results to the sink of _countriesStream
*/
*/
searchCountries() {
searchCountries() {
String query = _searchCountryController.text;
String query = _searchCountryController.text;
if (query.length == 0 || query.length == 1) {
if (query.length == 0 || query.length == 1) {
if(!_countriesStreamController.isClosed)
if(!_countriesStreamController.isClosed)
_countriesSink.add(countries);
_countriesSink.add(countries);
// print('added all countries again');
// print('added all countries again');
} else if (query.length >= 2 && query.length <= 5) {
} else if (query.length >= 2 && query.length <= 5) {
List<Country> searchResults = [];
List<Country> searchResults = [];
searchResults.clear();
searchResults.clear();
countries.forEach((Country c) {
countries.forEach((Country c) {
if (c.toString().toLowerCase().contains(query.toLowerCase()))
if (c.toString().toLowerCase().contains(query.toLowerCase()))
searchResults.add(c);
searchResults.add(c);
});
});
_countriesSink.add(searchResults);
_countriesSink.add(searchResults);
// print('added few countries based on search ${searchResults.length}');
// print('added few countries based on search ${searchResults.length}');
} else {
} else {
//No results
//No results
List<Country> searchResults = [];
List<Country> searchResults = [];
_countriesSink.add(searchResults);
_countriesSink.add(searchResults);
// print('no countries added');
// print('no countries added');
}
}
}
}


/*
/*
* Child for Dialog
* Child for Dialog
* Contents:
* Contents:
* SearchCountryTextFormField
* SearchCountryTextFormField
* StreamBuilder
* StreamBuilder
* - Shows a list of countries
* - Shows a list of countries
*/
*/
Widget searchAndPickYourCountryHere() => WillPopScope(
Widget searchAndPickYourCountryHere() => WillPopScope(
onWillPop: () => Future.value(false),
onWillPop: () => Future.value(false),
child: Dialog(
child: Dialog(
key: Key('SearchCountryDialog'),
key: Key('SearchCountryDialog'),
elevation: 8.0,
elevation: 8.0,
shape:
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0)),
child: Container(
child: Container(
margin: const EdgeInsets.all(5.0),
margin: const EdgeInsets.all(5.0),
child: Column(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
children: <Widget>[
// TextFormField for searching country
// TextFormField for searching country
PhoneAuthWidgets.searchCountry(_searchCountryController),
PhoneAuthWidgets.searchCountry(_searchCountryController),


// Returns a list of Countries that will change according to the search query
// Returns a list of Countries that will change according to the search query
SizedBox(
SizedBox(
height: 300.0,
height: 300.0,
child: StreamBuilder<List<Country>>(
child: StreamBuilder<List<Country>>(
//key: Key('Countries-StreamBuilder'),
//key: Key('Countries-StreamBuilder'),
stream: _countriesStream,
stream: _countriesStream,
builder: (context, snapshot) {
builder: (context, snapshot) {
if (snapshot.hasData) {
if (snapshot.hasData) {
// print(snapshot.data.length);
// print(snapshot.data.length);
return snapshot.data.length == 0
return snapshot.data.length == 0
? Center(
? Center(
child: Text('Your search found no results',
child: Text('Your search found no results',
style: TextStyle(fontSize: 16.0)),
style: TextStyle(fontSize: 16.0)),
)
)
: ListView.builder(
: ListView.builder(
itemCount: snapshot.data.length,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int i) =>
itemBuilder: (BuildContext context, int i) =>
PhoneAuthWidgets.selectableWidget(
PhoneAuthWidgets.selectableWidget(
snapshot.data[i],
snapshot.data[i],
(Country c) => selectThisCountry(c)),
(Country c) => selectThisCountry(c)),
);
);
} else if (snapshot.hasError)
} else if (snapshot.hasError)
return Center(
return Center(
child: Text('Seems, there is an error',
child: Text('Seems, there is an error',
style: TextStyle(fontSize: 16.0)),
style: TextStyle(fontSize: 16.0)),
);
);
return Center(child: CircularProgressIndicator());
return Center(child: CircularProgressIndicator());
}),
}),
)
)
],
],
),
),
),
),
);
),
),
);


/*
/*
* This callback is triggered when the user taps(selects) on any country from the available list in dialog
* This callback is triggered when the user taps(selects) on any country from the available list in dialog
* Resets the search value
* Resets the search value
* Close the stream & sink
* Close the stream & sink
* Updates the selected Country and adds dialCode as prefix according to the user's selection
* Updates the selected Country and adds dialCode as prefix according to the user's selection
*/
*/
void selectThisCountry(Country country) {
void selectThisCountry(Country country) {
print(country);
print(country);
_searchCountryController.clear();
_searchCountryController.clear();
Navigator.of(context).pop();
Navigator.of(context).pop();
Future.delayed(Duration(milliseconds: 10)).whenComplete(() {
Future.delayed(Duration(milliseconds: 10)).whenComplete(() {
_countriesStreamController.close();
_countriesStreamController.close();
_countriesSink.close();
_countriesSink.close();


setState(() {
setState(() {
_selectedCountryIndex = countries.indexOf(country);
_selectedCountryIndex = countries.indexOf(country);
});
});
});
});
}
}


startPhoneAuth() {
startPhoneAuth() {
FirebasePhoneAuth.instantiate(
FirebasePhoneAuth.instantiate(
phoneNumber: countries[_selectedCountryIndex].dialCode +
phoneNumber: countries[_selectedCountryIndex].dialCode +
_phoneNumberController.text);
_phoneNumberController.text);


Navigator.of(context).pushReplacement(CupertinoPageRoute(
Navigator.of(context).pushReplacement(CupertinoPageRoute(
builder: (BuildContext context) => PhoneAuthVerify()));
builder: (BuildContext context) => PhoneAuthVerify()));


// FirebasePhoneAuth.stateStream.listen((state) {
// FirebasePhoneAuth.stateStream.listen((state) {
//
//
// print(state);
// print(state);
//
//
// if (state == PhoneAuthState.CodeSent) {
// if (state == PhoneAuthState.CodeSent) {
// Navigator.of(context).pushReplacement(CupertinoPageRoute(
// Navigator.of(context).pushReplacement(CupertinoPageRoute(
// builder: (BuildContext context) => PhoneAuthVerify()));
// builder: (BuildContext context) => PhoneAuthVerify()));
// }
// }
// if (state == PhoneAuthState.Failed)
// if (state == PhoneAuthState.Failed)
// debugPrint("Seems there is an issue with it");
// debugPrint("Seems there is an issue with it");
// });
// });
}
}
}
}