Merge branch 'develop' of github.com:invoiceninja/flutter-client into develop

This commit is contained in:
Hillel Coren 2020-11-08 04:59:35 +02:00
commit 27b71bdec3
9 changed files with 281 additions and 31 deletions

View File

@ -5,6 +5,9 @@
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" /> <uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="com.android.vending.BILLING" /> <uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
<application <application
android:label="Invoice Ninja" android:label="Invoice Ninja"

View File

@ -82,6 +82,43 @@ post_install do |installer|
installer.pods_project.targets.each do |target| installer.pods_project.targets.each do |target|
target.build_configurations.each do |config| target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO' config.build_settings['ENABLE_BITCODE'] = 'NO'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.calendar
'PERMISSION_EVENTS=0',
## dart: PermissionGroup.reminders
'PERMISSION_REMINDERS=0',
## dart: PermissionGroup.contacts
# 'PERMISSION_CONTACTS=0',
## dart: PermissionGroup.camera
'PERMISSION_CAMERA=0',
## dart: PermissionGroup.microphone
'PERMISSION_MICROPHONE=0',
## dart: PermissionGroup.speech
'PERMISSION_SPEECH_RECOGNIZER=0',
## dart: PermissionGroup.photos
'PERMISSION_PHOTOS=0',
## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
'PERMISSION_LOCATION=0',
## dart: PermissionGroup.notification
'PERMISSION_NOTIFICATIONS=0',
## dart: PermissionGroup.mediaLibrary
'PERMISSION_MEDIA_LIBRARY=0',
## dart: PermissionGroup.sensors
'PERMISSION_SENSORS=0'
]
end end
end end
end end

View File

@ -62,5 +62,7 @@
<true/> <true/>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>NSContactsUsageDescription</key>
<string>This app requires contacts access to function properly.</string>
</dict> </dict>
</plist> </plist>

View File

@ -11,6 +11,8 @@ import 'package:invoiceninja_flutter/ui/client/edit/client_edit_vm.dart';
import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/completers.dart';
import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart';
import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:contacts_service/contacts_service.dart';
class ClientEditContacts extends StatefulWidget { class ClientEditContacts extends StatefulWidget {
const ClientEditContacts({ const ClientEditContacts({
@ -174,6 +176,7 @@ class ContactEditDetailsState extends State<ContactEditDetails> {
final _debouncer = Debouncer(); final _debouncer = Debouncer();
List<TextEditingController> _controllers = []; List<TextEditingController> _controllers = [];
Contact _contact;
void _onDoneContactPressed() { void _onDoneContactPressed() {
if (widget.areButtonsVisible) { if (widget.areButtonsVisible) {
@ -250,6 +253,27 @@ class ContactEditDetailsState extends State<ContactEditDetails> {
}); });
} }
// Check contacts permission
Future<PermissionStatus> _getPermission() async {
final PermissionStatus permission = await Permission.contacts.status;
if (permission != PermissionStatus.granted &&
permission != PermissionStatus.denied) {
final Map<Permission, PermissionStatus> permissionStatus =
await [Permission.contacts].request();
return permissionStatus[Permission.contacts] ??
PermissionStatus.undetermined;
} else {
return permission;
}
}
void _setContactControllers(){
_firstNameController.text = _contact.givenName != null ? _contact.givenName: '';
_lastNameController.text = _contact.familyName != null ? _contact.familyName: '';
_emailController.text = _contact.emails.isNotEmpty ? _contact.emails.first.value : '';
_phoneController.text = _contact.phones.isNotEmpty ? _contact.phones.first.value : '';
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final localization = AppLocalization.of(context); final localization = AppLocalization.of(context);
@ -300,11 +324,35 @@ class ContactEditDetailsState extends State<ContactEditDetails> {
: Container(), : Container(),
DecoratedFormField( DecoratedFormField(
controller: _firstNameController, controller: _firstNameController,
label: localization.firstName,
validator: (String val) => !viewModel.client.hasNameSet validator: (String val) => !viewModel.client.hasNameSet
? AppLocalization.of(context).pleaseEnterAClientOrContactName ? AppLocalization.of(context).pleaseEnterAClientOrContactName
: null, : null,
onSavePressed: (_) => _onDoneContactPressed(), onSavePressed: (_) => _onDoneContactPressed(),
decoration: InputDecoration(
labelText: localization.firstName,
suffixIcon: IconButton(
alignment: Alignment.bottomCenter,
color: Theme.of(context).cardColor,
icon: Icon(
Icons.person,
color: Colors.grey,
),
onPressed: () async {
final PermissionStatus permissionStatus = await _getPermission();
if (permissionStatus == PermissionStatus.granted) {
try {
_contact = await ContactsService.openDeviceContactPicker();
setState(() {
_setContactControllers();
});
} catch (e) {
print(e.toString());
}
}
}
),
),
), ),
DecoratedFormField( DecoratedFormField(
controller: _lastNameController, controller: _lastNameController,

View File

@ -11,6 +11,8 @@ import 'package:invoiceninja_flutter/ui/client/edit/client_edit_vm.dart';
import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/completers.dart';
import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:invoiceninja_flutter/ui/app/form_card.dart'; import 'package:invoiceninja_flutter/ui/app/form_card.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:contacts_service/contacts_service.dart';
class ClientEditDetails extends StatefulWidget { class ClientEditDetails extends StatefulWidget {
const ClientEditDetails({ const ClientEditDetails({
@ -37,6 +39,7 @@ class ClientEditDetailsState extends State<ClientEditDetails> {
final _debouncer = Debouncer(); final _debouncer = Debouncer();
List<TextEditingController> _controllers; List<TextEditingController> _controllers;
Contact _contact;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
@ -101,6 +104,25 @@ class ClientEditDetailsState extends State<ClientEditDetails> {
}); });
} }
//Check contacts permission
Future<PermissionStatus> _getPermission() async {
final PermissionStatus permission = await Permission.contacts.status;
if (permission != PermissionStatus.granted &&
permission != PermissionStatus.denied) {
final Map<Permission, PermissionStatus> permissionStatus =
await [Permission.contacts].request();
return permissionStatus[Permission.contacts] ??
PermissionStatus.undetermined;
} else {
return permission;
}
}
void _setContactControllers(){
_nameController.text = _contact.displayName;
_phoneController.text = _contact.phones.first.value;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final localization = AppLocalization.of(context); final localization = AppLocalization.of(context);
@ -115,12 +137,35 @@ class ClientEditDetailsState extends State<ClientEditDetails> {
children: <Widget>[ children: <Widget>[
DecoratedFormField( DecoratedFormField(
autofocus: true, autofocus: true,
label: localization.name,
controller: _nameController, controller: _nameController,
validator: (String val) => !viewModel.client.hasNameSet validator: (String val) => !viewModel.client.hasNameSet
? AppLocalization.of(context).pleaseEnterAClientOrContactName ? AppLocalization.of(context).pleaseEnterAClientOrContactName
: null, : null,
onSavePressed: viewModel.onSavePressed, onSavePressed: viewModel.onSavePressed,
decoration: InputDecoration(
labelText: localization.name,
suffixIcon: IconButton(
alignment: Alignment.bottomCenter,
color: Theme.of(context).cardColor,
icon: Icon(
Icons.person,
color: Colors.grey,
),
onPressed: () async {
final PermissionStatus permissionStatus = await _getPermission();
if (permissionStatus == PermissionStatus.granted) {
try {
_contact = await ContactsService.openDeviceContactPicker();
setState(() {
_setContactControllers();
});
} catch (e) {
print(e.toString());
}
}
}
),
),
), ),
DynamicSelector( DynamicSelector(
entityType: EntityType.group, entityType: EntityType.group,

View File

@ -8,6 +8,8 @@ import 'package:invoiceninja_flutter/ui/vendor/edit/vendor_edit_contacts_vm.dart
import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/completers.dart';
import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart';
import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:contacts_service/contacts_service.dart';
class VendorEditContacts extends StatefulWidget { class VendorEditContacts extends StatefulWidget {
const VendorEditContacts({ const VendorEditContacts({
@ -159,6 +161,7 @@ class VendorContactEditDetailsState extends State<VendorContactEditDetails> {
final _debouncer = Debouncer(); final _debouncer = Debouncer();
List<TextEditingController> _controllers = []; List<TextEditingController> _controllers = [];
Contact _contact;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
@ -211,6 +214,27 @@ class VendorContactEditDetailsState extends State<VendorContactEditDetails> {
}); });
} }
void _setContactControllers(){
_firstNameController.text = _contact.givenName != null ? _contact.givenName: '';
_lastNameController.text = _contact.familyName != null ? _contact.familyName: '';
_emailController.text = _contact.emails.isNotEmpty ? _contact.emails.first.value : '';
_phoneController.text = _contact.phones.isNotEmpty ? _contact.phones.first.value : '';
}
// Check contacts permission
Future<PermissionStatus> _getPermission() async {
final PermissionStatus permission = await Permission.contacts.status;
if (permission != PermissionStatus.granted &&
permission != PermissionStatus.denied) {
final Map<Permission, PermissionStatus> permissionStatus =
await [Permission.contacts].request();
return permissionStatus[Permission.contacts] ??
PermissionStatus.undetermined;
} else {
return permission;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final localization = AppLocalization.of(context); final localization = AppLocalization.of(context);
@ -261,7 +285,30 @@ class VendorContactEditDetailsState extends State<VendorContactEditDetails> {
: Container(), : Container(),
DecoratedFormField( DecoratedFormField(
controller: _firstNameController, controller: _firstNameController,
label: localization.firstName, decoration: InputDecoration(
labelText: localization.firstName,
suffixIcon: IconButton(
alignment: Alignment.bottomCenter,
color: Theme.of(context).cardColor,
icon: Icon(
Icons.person,
color: Colors.grey,
),
onPressed: () async {
final PermissionStatus permissionStatus = await _getPermission();
if (permissionStatus == PermissionStatus.granted) {
try {
_contact = await ContactsService.openDeviceContactPicker();
setState(() {
_setContactControllers();
});
} catch (e) {
print(e.toString());
}
}
}
),
),
), ),
DecoratedFormField( DecoratedFormField(
controller: _lastNameController, controller: _lastNameController,

View File

@ -8,6 +8,8 @@ import 'package:invoiceninja_flutter/ui/app/forms/user_picker.dart';
import 'package:invoiceninja_flutter/ui/vendor/edit/vendor_edit_vm.dart'; import 'package:invoiceninja_flutter/ui/vendor/edit/vendor_edit_vm.dart';
import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/completers.dart';
import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/localization.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:contacts_service/contacts_service.dart';
class VendorEditDetails extends StatefulWidget { class VendorEditDetails extends StatefulWidget {
const VendorEditDetails({ const VendorEditDetails({
@ -34,6 +36,7 @@ class VendorEditDetailsState extends State<VendorEditDetails> {
final _debouncer = Debouncer(); final _debouncer = Debouncer();
List<TextEditingController> _controllers; List<TextEditingController> _controllers;
Contact _contact;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
@ -98,6 +101,25 @@ class VendorEditDetailsState extends State<VendorEditDetails> {
}); });
} }
void _setContactControllers(){
_nameController.text = _contact.displayName != null ? _contact.displayName : '';
_phoneController.text = _contact.phones.isNotEmpty ? _contact.phones.first.value : '';
}
// Check contacts permission
Future<PermissionStatus> _getPermission() async {
final PermissionStatus permission = await Permission.contacts.status;
if (permission != PermissionStatus.granted &&
permission != PermissionStatus.denied) {
final Map<Permission, PermissionStatus> permissionStatus =
await [Permission.contacts].request();
return permissionStatus[Permission.contacts] ??
PermissionStatus.undetermined;
} else {
return permission;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final localization = AppLocalization.of(context); final localization = AppLocalization.of(context);
@ -112,10 +134,33 @@ class VendorEditDetailsState extends State<VendorEditDetails> {
DecoratedFormField( DecoratedFormField(
autofocus: true, autofocus: true,
controller: _nameController, controller: _nameController,
label: localization.name,
validator: (String val) => val == null || val.isEmpty validator: (String val) => val == null || val.isEmpty
? AppLocalization.of(context).pleaseEnterAName ? AppLocalization.of(context).pleaseEnterAName
: null, : null,
decoration: InputDecoration(
labelText: localization.firstName,
suffixIcon: IconButton(
alignment: Alignment.bottomCenter,
color: Theme.of(context).cardColor,
icon: Icon(
Icons.person,
color: Colors.grey,
),
onPressed: () async {
final PermissionStatus permissionStatus = await _getPermission();
if (permissionStatus == PermissionStatus.granted) {
try {
_contact = await ContactsService.openDeviceContactPicker();
setState(() {
_setContactControllers();
});
} catch (e) {
print(e.toString());
}
}
}
),
),
), ),
UserPicker( UserPicker(
userId: vendor.assignedUserId, userId: vendor.assignedUserId,

View File

@ -35,14 +35,14 @@ packages:
name: async name: async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.5.0-nullsafety" version: "2.5.0-nullsafety.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
name: boolean_selector name: boolean_selector
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0-nullsafety" version: "2.1.0-nullsafety.1"
build: build:
dependency: transitive dependency: transitive
description: description:
@ -119,14 +119,14 @@ packages:
name: characters name: characters
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0-nullsafety.2" version: "1.1.0-nullsafety.3"
charcode: charcode:
dependency: transitive dependency: transitive
description: description:
name: charcode name: charcode
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0-nullsafety" version: "1.2.0-nullsafety.1"
charts_common: charts_common:
dependency: transitive dependency: transitive
description: description:
@ -161,7 +161,7 @@ packages:
name: clock name: clock
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0-nullsafety" version: "1.1.0-nullsafety.1"
code_builder: code_builder:
dependency: transitive dependency: transitive
description: description:
@ -175,7 +175,14 @@ packages:
name: collection name: collection
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.15.0-nullsafety.2" version: "1.15.0-nullsafety.3"
contacts_service:
dependency: "direct main"
description:
name: contacts_service
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.6"
convert: convert:
dependency: transitive dependency: transitive
description: description:
@ -252,7 +259,7 @@ packages:
name: fake_async name: fake_async
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0-nullsafety" version: "1.2.0-nullsafety.1"
faker: faker:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -273,7 +280,7 @@ packages:
name: file name: file
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.0-nullsafety.1" version: "6.0.0-nullsafety.2"
firebase: firebase:
dependency: transitive dependency: transitive
description: description:
@ -599,7 +606,7 @@ packages:
name: matcher name: matcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.10-nullsafety" version: "0.12.10-nullsafety.1"
material_design_icons_flutter: material_design_icons_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@ -620,7 +627,7 @@ packages:
name: meta name: meta
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0-nullsafety.2" version: "1.3.0-nullsafety.4"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@ -697,7 +704,7 @@ packages:
name: path name: path
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0-nullsafety" version: "1.8.0-nullsafety.1"
path_provider: path_provider:
dependency: "direct main" dependency: "direct main"
description: description:
@ -739,7 +746,21 @@ packages:
name: pedantic name: pedantic
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.10.0-nullsafety.2" version: "1.10.0-nullsafety"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.1+1"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@ -753,7 +774,7 @@ packages:
name: platform name: platform
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.0-nullsafety.1" version: "3.0.0-nullsafety.2"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -774,7 +795,7 @@ packages:
name: process name: process
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.0.0-nullsafety.1" version: "4.0.0-nullsafety.2"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@ -940,7 +961,7 @@ packages:
name: source_span name: source_span
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0-nullsafety" version: "1.8.0-nullsafety.2"
sqflite: sqflite:
dependency: transitive dependency: transitive
description: description:
@ -961,14 +982,14 @@ packages:
name: stack_trace name: stack_trace
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.10.0-nullsafety" version: "1.10.0-nullsafety.2"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
name: stream_channel name: stream_channel
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0-nullsafety" version: "2.1.0-nullsafety.1"
stream_transform: stream_transform:
dependency: transitive dependency: transitive
description: description:
@ -982,7 +1003,7 @@ packages:
name: string_scanner name: string_scanner
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.0-nullsafety" version: "1.1.0-nullsafety.1"
sync_http: sync_http:
dependency: transitive dependency: transitive
description: description:
@ -1003,28 +1024,28 @@ packages:
name: term_glyph name: term_glyph
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0-nullsafety" version: "1.2.0-nullsafety.1"
test: test:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: test name: test
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.16.0-nullsafety.4" version: "1.16.0-nullsafety.5"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.19-nullsafety" version: "0.2.19-nullsafety.2"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.12-nullsafety.4" version: "0.3.12-nullsafety.5"
timeago: timeago:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1045,7 +1066,7 @@ packages:
name: typed_data name: typed_data
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0-nullsafety.2" version: "1.3.0-nullsafety.3"
url_launcher: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1115,7 +1136,7 @@ packages:
name: vector_math name: vector_math
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0-nullsafety.2" version: "2.1.0-nullsafety.3"
version: version:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -58,6 +58,8 @@ dependencies:
flutter_launcher_icons: ^0.8.0 flutter_launcher_icons: ^0.8.0
overflow_view: ^0.2.1 overflow_view: ^0.2.1
flutter_styled_toast: ^1.4.0+1 flutter_styled_toast: ^1.4.0+1
permission_handler: ^5.0.1+1
contacts_service: ^0.4.6
dev_dependencies: dev_dependencies:
flutter_driver: flutter_driver: