Resolves #412 Add 'Support app development' option
This commit is contained in:
parent
916d0b7e3c
commit
5126c54914
@ -121,7 +121,7 @@ class HomeAssistant {
|
||||
prefs.setString("app-webhook-id", responseObject["webhook_id"]);
|
||||
Connection().webhookId = responseObject["webhook_id"];
|
||||
completer.complete();
|
||||
eventBus.fire(ShowDialogEvent(
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Mobile app Integration was created",
|
||||
body: "HA Client was registered as MobileApp in your Home Assistant. To start using notifications you need to restart your Home Assistant",
|
||||
positiveText: "Restart now",
|
||||
@ -149,7 +149,7 @@ class HomeAssistant {
|
||||
).then((response) {
|
||||
Logger.d("App registration works fine");
|
||||
if (showOkDialog) {
|
||||
eventBus.fire(ShowDialogEvent(
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "All good",
|
||||
body: "HA Client integration with your Home Assistant server works fine",
|
||||
positiveText: "Nice!",
|
||||
@ -160,7 +160,7 @@ class HomeAssistant {
|
||||
}).catchError((e) {
|
||||
if (e['code'] != null && e['code'] == 410) {
|
||||
Logger.e("MobileApp integration was removed");
|
||||
eventBus.fire(ShowDialogEvent(
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "App integration was removed",
|
||||
body: "Looks like app integration was removed from your Home Assistant. HA Client needs to be registered on your Home Assistant server to make it possible to use notifications and other useful stuff.",
|
||||
positiveText: "Register now",
|
||||
@ -175,7 +175,7 @@ class HomeAssistant {
|
||||
));
|
||||
} else {
|
||||
Logger.e("Error updating app registration: ${e.toString()}");
|
||||
eventBus.fire(ShowDialogEvent(
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "App integration is not working properly",
|
||||
body: "Something wrong with HA Client integration on your Home Assistant server. Try to remove current app integration from Configuration -> Integrationds using web UI, restart your Home Assistant and go back to the app. NOTE that after clicking 'Ok' current integration data will be removed from the app and new integration wll be created on Home Assistant side on next app launch.",
|
||||
positiveText: "Ok",
|
||||
|
@ -86,7 +86,10 @@ part 'entity_widgets/controls/media_player_widgets.dart';
|
||||
part 'entity_widgets/controls/fan_controls.dart';
|
||||
part 'entity_widgets/controls/alarm_control_panel_controls.dart';
|
||||
part 'settings.page.dart';
|
||||
part 'purchase.page.dart';
|
||||
part 'pages/purchase.page.dart';
|
||||
part 'pages/widgets/product_purchase.widget.dart';
|
||||
part 'pages/widgets/page_loading_indicator.dart';
|
||||
part 'pages/widgets/page_loading_error.dart';
|
||||
part 'panel.page.dart';
|
||||
part 'home_assistant.class.dart';
|
||||
part 'log.page.dart';
|
||||
@ -185,7 +188,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
StreamSubscription _showEntityPageSubscription;
|
||||
StreamSubscription _showErrorSubscription;
|
||||
StreamSubscription _startAuthSubscription;
|
||||
StreamSubscription _showDialogSubscription;
|
||||
StreamSubscription _showPopupDialogSubscription;
|
||||
StreamSubscription _showPopupMessageSubscription;
|
||||
StreamSubscription _reloadUISubscription;
|
||||
int _previousViewCount;
|
||||
bool _showLoginButton = false;
|
||||
@ -304,8 +308,18 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePurchaseUpdates(purchases) {
|
||||
Logger.d('Handling purchases...');
|
||||
void _handlePurchaseUpdates(purchase) {
|
||||
if (purchase is List<PurchaseDetails>) {
|
||||
if (purchase[0].status == PurchaseStatus.purchased) {
|
||||
eventBus.fire(ShowPopupMessageEvent(
|
||||
title: "Thanks a lot!",
|
||||
body: "Thank you for supporting HA Client development!",
|
||||
buttonText: "Ok"
|
||||
));
|
||||
}
|
||||
} else {
|
||||
Logger.e("Something wrong with purchase handling. Got: $purchase");
|
||||
}
|
||||
}
|
||||
|
||||
Future _subscribe() {
|
||||
@ -325,9 +339,9 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
_quickLoad();
|
||||
});
|
||||
}
|
||||
if (_showDialogSubscription == null) {
|
||||
_showDialogSubscription = eventBus.on<ShowDialogEvent>().listen((event){
|
||||
_showDialog(
|
||||
if (_showPopupDialogSubscription == null) {
|
||||
_showPopupDialogSubscription = eventBus.on<ShowPopupDialogEvent>().listen((event){
|
||||
_showPopupDialog(
|
||||
title: event.title,
|
||||
body: event.body,
|
||||
onPositive: event.onPositive,
|
||||
@ -337,6 +351,17 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
);
|
||||
});
|
||||
}
|
||||
if (_showPopupMessageSubscription == null) {
|
||||
_showPopupMessageSubscription = eventBus.on<ShowPopupMessageEvent>().listen((event){
|
||||
_showPopupDialog(
|
||||
title: event.title,
|
||||
body: event.body,
|
||||
onPositive: event.onButtonClick,
|
||||
positiveText: event.buttonText,
|
||||
negativeText: null
|
||||
);
|
||||
});
|
||||
}
|
||||
if (_serviceCallSubscription == null) {
|
||||
_serviceCallSubscription =
|
||||
eventBus.on<ServiceCallEvent>().listen((event) {
|
||||
@ -393,7 +418,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
}
|
||||
}
|
||||
|
||||
void _showDialog({String title, String body, var onPositive, var onNegative, String positiveText, String negativeText}) {
|
||||
void _showPopupDialog({String title, String body, var onPositive, var onNegative, String positiveText, String negativeText}) {
|
||||
List<Widget> buttons = [];
|
||||
buttons.add(FlatButton(
|
||||
child: new Text("$positiveText"),
|
||||
@ -535,7 +560,7 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
),
|
||||
Divider(),
|
||||
new ListTile(
|
||||
leading: Icon(Icons.insert_drive_file),
|
||||
leading: Icon(MaterialDesignIcons.getIconDataFromIconName("mdi:food")),
|
||||
title: Text("Support app development"),
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
@ -879,7 +904,8 @@ class _MainPageState extends State<MainPage> with WidgetsBindingObserver, Ticker
|
||||
_stateSubscription?.cancel();
|
||||
_settingsSubscription?.cancel();
|
||||
_serviceCallSubscription?.cancel();
|
||||
_showDialogSubscription?.cancel();
|
||||
_showPopupDialogSubscription?.cancel();
|
||||
_showPopupMessageSubscription?.cancel();
|
||||
_showEntityPageSubscription?.cancel();
|
||||
_showErrorSubscription?.cancel();
|
||||
_startAuthSubscription?.cancel();
|
||||
|
107
lib/pages/purchase.page.dart
Normal file
107
lib/pages/purchase.page.dart
Normal file
@ -0,0 +1,107 @@
|
||||
part of '../main.dart';
|
||||
|
||||
class PurchasePage extends StatefulWidget {
|
||||
PurchasePage({Key key, this.title}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
_PurchasePageState createState() => new _PurchasePageState();
|
||||
}
|
||||
|
||||
class _PurchasePageState extends State<PurchasePage> {
|
||||
|
||||
bool _loaded = false;
|
||||
String _error = "";
|
||||
List<ProductDetails> _products;
|
||||
List<PurchaseDetails> _purchases;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProducts();
|
||||
}
|
||||
|
||||
_loadProducts() async {
|
||||
final bool available = await InAppPurchaseConnection.instance.isAvailable();
|
||||
if (!available) {
|
||||
setState(() {
|
||||
_error = "Error connecting to store";
|
||||
});
|
||||
} else {
|
||||
const Set<String> _kIds = {'just_few_bucks', 'flat_white_a_month', 'lunch_a_year', 'lunch_a_month'};
|
||||
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
|
||||
if (!response.notFoundIDs.isEmpty) {
|
||||
Logger.d("Products not found: ${response.notFoundIDs}");
|
||||
}
|
||||
_products = response.productDetails;
|
||||
_loadPreviousPurchases();
|
||||
}
|
||||
}
|
||||
|
||||
_loadPreviousPurchases() async {
|
||||
final QueryPurchaseDetailsResponse response = await InAppPurchaseConnection.instance.queryPastPurchases();
|
||||
if (response.error != null) {
|
||||
setState(() {
|
||||
_error = "Error loading previous purchases";
|
||||
});
|
||||
} else {
|
||||
_purchases = response.pastPurchases;
|
||||
for (PurchaseDetails purchase in _purchases) {
|
||||
Logger.d("Previous purchase: ${purchase.status}");
|
||||
}
|
||||
if (_products.isEmpty) {
|
||||
setState(() {
|
||||
_error = "No data found in store";
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_loaded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildProducts() {
|
||||
List<Widget> productWidgets = [];
|
||||
for (ProductDetails product in _products) {
|
||||
productWidgets.add(
|
||||
ProductPurchase(
|
||||
product: product,
|
||||
onBuy: (product) => _buyProduct(product),
|
||||
purchased: _purchases.any((purchase) { return purchase.productID == product.id;}),)
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
scrollDirection: Axis.vertical,
|
||||
children: productWidgets
|
||||
);
|
||||
}
|
||||
|
||||
void _buyProduct(ProductDetails product) {
|
||||
Logger.d("Starting purchase of ${product.id}");
|
||||
final PurchaseParam purchaseParam = PurchaseParam(productDetails: product);
|
||||
InAppPurchaseConnection.instance.buyNonConsumable(purchaseParam: purchaseParam);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
if (!_loaded) {
|
||||
body = _error.isEmpty ? PageLoadingIndicator() : PageLoadingError(errorText: _error);
|
||||
} else {
|
||||
body = _buildProducts();
|
||||
}
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
title: new Text(widget.title),
|
||||
),
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
32
lib/pages/widgets/page_loading_error.dart
Normal file
32
lib/pages/widgets/page_loading_error.dart
Normal file
@ -0,0 +1,32 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class PageLoadingError extends StatelessWidget {
|
||||
|
||||
final String errorText;
|
||||
|
||||
const PageLoadingError({Key key, this.errorText: "Error"}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
|
||||
child: Icon(
|
||||
Icons.error,
|
||||
color: Colors.redAccent,
|
||||
size: 48.0
|
||||
)
|
||||
),
|
||||
Text(this.errorText, style: TextStyle(color: Colors.black45))
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
23
lib/pages/widgets/page_loading_indicator.dart
Normal file
23
lib/pages/widgets/page_loading_indicator.dart
Normal file
@ -0,0 +1,23 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class PageLoadingIndicator extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 40.0, bottom: 20.0),
|
||||
child: CircularProgressIndicator()
|
||||
),
|
||||
Text("Loading...", style: TextStyle(color: Colors.black45))
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
68
lib/pages/widgets/product_purchase.widget.dart
Normal file
68
lib/pages/widgets/product_purchase.widget.dart
Normal file
@ -0,0 +1,68 @@
|
||||
part of '../../main.dart';
|
||||
|
||||
class ProductPurchase extends StatelessWidget {
|
||||
|
||||
final ProductDetails product;
|
||||
final onBuy;
|
||||
final purchased;
|
||||
|
||||
const ProductPurchase({Key key, @required this.product, @required this.onBuy, this.purchased}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String period = "/ ";
|
||||
Color priceColor;
|
||||
if (product.id.contains("month")) {
|
||||
period += "month";
|
||||
priceColor = Colors.deepOrangeAccent;
|
||||
} else {
|
||||
period += "year";
|
||||
priceColor = Colors.amber;
|
||||
}
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(Sizes.leftWidgetPadding),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: Sizes.rightWidgetPadding),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"${product.title}",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16.0
|
||||
),
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text(
|
||||
"${product.description}",
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 4,
|
||||
softWrap: true,
|
||||
),
|
||||
Container(height: Sizes.rowPadding,),
|
||||
Text("${product.price} $period", style: TextStyle(color: priceColor)),
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: RaisedButton(
|
||||
child: Text(this.purchased ? "Bought" : "Buy", style: TextStyle(color: Colors.white)),
|
||||
color: Colors.blue,
|
||||
onPressed: this.purchased ? null : () => this.onBuy(this.product),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
part of 'main.dart';
|
||||
|
||||
class PurchasePage extends StatefulWidget {
|
||||
PurchasePage({Key key, this.title}) : super(key: key);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
_PurchasePageState createState() => new _PurchasePageState();
|
||||
}
|
||||
|
||||
class _PurchasePageState extends State<PurchasePage> {
|
||||
|
||||
bool _loaded = false;
|
||||
List<ProductDetails> _products;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProducts();
|
||||
}
|
||||
|
||||
_loadProducts() async {
|
||||
const Set<String> _kIds = {'flat_white_a_month', 'lunch_a_month'};
|
||||
final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
|
||||
if (!response.notFoundIDs.isEmpty) {
|
||||
Logger.d("Not found products: ${response.notFoundIDs}");
|
||||
}
|
||||
_products = response.productDetails;
|
||||
for (ProductDetails product in _products) {
|
||||
Logger.d("Product: ${product}");
|
||||
}
|
||||
setState(() {
|
||||
_loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildLoading() {
|
||||
return Text("Loading...");
|
||||
}
|
||||
|
||||
Widget _buildProducts() {
|
||||
List<Widget> productWidgets = [];
|
||||
for (ProductDetails product in _products) {
|
||||
productWidgets.add(
|
||||
_buildProduct(product)
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
scrollDirection: Axis.vertical,
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
children: productWidgets
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProduct(ProductDetails product) {
|
||||
return Card(
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
Text("${product.title}"),
|
||||
Text("${product.price}")
|
||||
],
|
||||
),
|
||||
FlatButton(
|
||||
child: Text("Buy"),
|
||||
onPressed: () => Logger.d("Buy!"),
|
||||
)
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget body;
|
||||
if (!_loaded) {
|
||||
body = _buildLoading();
|
||||
} else {
|
||||
body = _buildProducts();
|
||||
}
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(
|
||||
leading: IconButton(icon: Icon(Icons.arrow_back), onPressed: (){
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
title: new Text(widget.title),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(Icons.content_copy),
|
||||
onPressed: () {
|
||||
//
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
body: body,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -256,7 +256,7 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
|
||||
}
|
||||
|
||||
restart() {
|
||||
eventBus.fire(ShowDialogEvent(
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Are you sure you want to restart Home Assistant?",
|
||||
body: "This will restart your Home Assistant server.",
|
||||
positiveText: "Sure. Make it so",
|
||||
@ -268,7 +268,7 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
|
||||
}
|
||||
|
||||
stop() {
|
||||
eventBus.fire(ShowDialogEvent(
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Are you sure you wanr to STOP Home Assistant?",
|
||||
body: "This will STOP your Home Assistant server. It means that your web interface as well as HA Client will not work untill you'll find a way to start your server using ssh or something.",
|
||||
positiveText: "Sure. Make it so",
|
||||
@ -284,7 +284,7 @@ class _ConfigPanelWidgetState extends State<ConfigPanelWidget> {
|
||||
}
|
||||
|
||||
resetRegistration() {
|
||||
eventBus.fire(ShowDialogEvent(
|
||||
eventBus.fire(ShowPopupDialogEvent(
|
||||
title: "Waaaait",
|
||||
body: "If you don't whant to have duplicate integrations and entities in your HA for your current device, first you need to remove MobileApp integration from Integration settings in HA and restart server.",
|
||||
positiveText: "Done it already",
|
||||
|
@ -173,7 +173,7 @@ class ServiceCallEvent {
|
||||
ServiceCallEvent(this.domain, this.service, this.entityId, this.additionalParams);
|
||||
}
|
||||
|
||||
class ShowDialogEvent {
|
||||
class ShowPopupDialogEvent {
|
||||
final String title;
|
||||
final String body;
|
||||
final String positiveText;
|
||||
@ -181,7 +181,16 @@ class ShowDialogEvent {
|
||||
final onPositive;
|
||||
final onNegative;
|
||||
|
||||
ShowDialogEvent({this.title, this.body, this.positiveText: "Ok", this.negativeText: "Cancel", this.onPositive, this.onNegative});
|
||||
ShowPopupDialogEvent({this.title, this.body, this.positiveText: "Ok", this.negativeText: "Cancel", this.onPositive, this.onNegative});
|
||||
}
|
||||
|
||||
class ShowPopupMessageEvent {
|
||||
final String title;
|
||||
final String body;
|
||||
final String buttonText;
|
||||
final onButtonClick;
|
||||
|
||||
ShowPopupMessageEvent({this.title, this.body, this.buttonText: "Ok", this.onButtonClick});
|
||||
}
|
||||
|
||||
class ShowEntityPageEvent {
|
||||
|
Reference in New Issue
Block a user