Compare commits

...

16 Commits

21 changed files with 297 additions and 39 deletions

View File

@ -13,6 +13,6 @@ Discuss it on [Discord](https://discord.gg/u9vq7QE) or at [Home Assistant commun
#### Last release build status #### Last release build status
[![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build) [![Codemagic build status](https://api.codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/status_badge.svg)](https://codemagic.io/apps/5da8bdab9f20ef798f7c2c65/5db1862025dc3f0b0288a57a/latest_build)
#### Special thanks to #### Projects used
- [Crewski](https://github.com/Crewski) for his [HANotify](https://github.com/Crewski/HANotify) - [HANotify](https://github.com/Crewski/HANotify) by [Crewski](https://github.com/Crewski)
- [Home Assistant](https://github.com/home-assistant) for some support and [Home Assistant](https://www.home-assistant.io/) - [hassalarm](https://github.com/Johboh/hassalarm) by [Johboh](https://github.com/Johboh) distributed under [MIT License](https://github.com/Johboh/hassalarm/blob/master/LICENSE)

View File

@ -62,7 +62,12 @@
<action android:name="android.intent.action.INPUT_METHOD_CHANGED" /> <action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".NextAlarmBroadcastReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.app.action.NEXT_ALARM_CLOCK_CHANGED" />
</intent-filter>
</receiver>
<service <service
android:name="io.flutter.plugins.androidalarmmanager.AlarmService" android:name="io.flutter.plugins.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE" android:permission="android.permission.BIND_JOB_SERVICE"
@ -74,7 +79,7 @@
android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver" android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver"
android:enabled="false"> android:enabled="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"></action> <action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
</application> </application>

View File

@ -145,7 +145,7 @@ public class MessagingService extends FirebaseMessagingService {
connection.connect(); connection.connect();
InputStream input = connection.getInputStream(); InputStream input = connection.getInputStream();
return BitmapFactory.decodeStream(input); return BitmapFactory.decodeStream(input);
} catch (IOException e) { } catch (Exception e) {
return null; return null;
} }
} }

View File

@ -0,0 +1,51 @@
package com.keyboardcrumbs.hassclient;
import android.app.AlarmManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import androidx.work.BackoffPolicy;
import androidx.work.Constraints;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import androidx.work.WorkRequest;
import java.util.concurrent.TimeUnit;
public class NextAlarmBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "NextAlarmReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) {
return;
}
final boolean isBootIntent = Intent.ACTION_BOOT_COMPLETED.equalsIgnoreCase(intent.getAction());
final boolean isNextAlarmIntent = AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED.equalsIgnoreCase(intent.getAction());
if (!isBootIntent && !isNextAlarmIntent) {
return;
}
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
OneTimeWorkRequest uploadWorkRequest =
new OneTimeWorkRequest.Builder(UpdateNextAlarmWorker.class)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
10,
TimeUnit.SECONDS)
.setConstraints(constraints)
.build();
WorkManager
.getInstance(context)
.enqueueUniqueWork("NextAlarmUpdate", ExistingWorkPolicy.REPLACE, uploadWorkRequest);
}
}

View File

@ -1,7 +1,7 @@
package com.keyboardcrumbs.hassclient; package com.keyboardcrumbs.hassclient;
import android.app.AlarmManager;
import android.content.Context; import android.content.Context;
import androidx.annotation.NonNull;
import android.util.Log; import android.util.Log;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Intent; import android.content.Intent;
@ -15,10 +15,14 @@ import android.content.SharedPreferences;
public class NotificationActionReceiver extends BroadcastReceiver { public class NotificationActionReceiver extends BroadcastReceiver {
private static final String TAG = "NotificationActionReceiver"; private static final String TAG = "NotificationAction";
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (intent == null) {
return;
}
String rawActionData = intent.getStringExtra("actionData"); String rawActionData = intent.getStringExtra("actionData");
if (intent.hasExtra("tag")) { if (intent.hasExtra("tag")) {
String notificationTag = intent.getStringExtra("tag"); String notificationTag = intent.getStringExtra("tag");

View File

@ -0,0 +1,119 @@
package com.keyboardcrumbs.hassclient;
import android.app.AlarmManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import android.webkit.URLUtil;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
public class UpdateNextAlarmWorker extends Worker {
private Context currentContext;
private static final String TAG = "NextAlarmWorker";
private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:00", Locale.ENGLISH);
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:00", Locale.ENGLISH);
public UpdateNextAlarmWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
currentContext = context;
}
@NonNull
@Override
public Result doWork() {
final AlarmManager alarmManager;
if (android.os.Build.VERSION.SDK_INT >= 23) {
alarmManager = currentContext.getSystemService(AlarmManager.class);
} else {
alarmManager = (AlarmManager)currentContext.getSystemService(Context.ALARM_SERVICE);
}
final AlarmManager.AlarmClockInfo alarmClockInfo = alarmManager.getNextAlarmClock();
SharedPreferences prefs = currentContext.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE);
String webhookId = prefs.getString("flutter.app-webhook-id", null);
if (webhookId != null) {
try {
String requestUrl = prefs.getString("flutter.hassio-res-protocol", "") +
"://" +
prefs.getString("flutter.hassio-domain", "") +
":" +
prefs.getString("flutter.hassio-port", "") + "/api/webhook/" + webhookId;
JSONObject dataToSend = new JSONObject();
if (URLUtil.isValidUrl(requestUrl)) {
dataToSend.put("type", "update_sensor_states");
JSONArray dataArray = new JSONArray();
JSONObject sensorData = new JSONObject();
JSONObject sensorAttrs = new JSONObject();
sensorData.put("unique_id", "next_alarm");
sensorData.put("type", "sensor");
final long triggerTimestamp;
if (alarmClockInfo != null) {
triggerTimestamp = alarmClockInfo.getTriggerTime();
final Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(triggerTimestamp);
sensorData.put("state", DATE_TIME_FORMAT.format(calendar.getTime()));
sensorAttrs.put("date", DATE_FORMAT.format(calendar.getTime()));
sensorAttrs.put("time", TIME_FORMAT.format(calendar.getTime()));
sensorAttrs.put("timestamp", triggerTimestamp);
} else {
sensorData.put("state", "");
sensorAttrs.put("date", "");
sensorAttrs.put("time", "");
sensorAttrs.put("timestamp", 0);
}
sensorData.put("icon", "mdi:alarm");
sensorData.put("attributes", sensorAttrs);
dataArray.put(0, sensorData);
dataToSend.put("data", dataArray);
String stringRequest = dataToSend.toString();
try {
URL url = new URL(requestUrl);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setRequestProperty("Content-Type", "application/json");
urlConnection.setDoOutput(true);
byte[] outputBytes = stringRequest.getBytes("UTF-8");
OutputStream os = urlConnection.getOutputStream();
os.write(outputBytes);
int responseCode = urlConnection.getResponseCode();
urlConnection.disconnect();
if (responseCode >= 300) {
return Result.retry();
}
} catch (Exception e) {
Log.e(TAG, "Error sending data", e);
return Result.retry();
}
} else {
Log.w(TAG, "Invalid HA url");
return Result.failure();
}
} catch (Exception e) {
Log.e(TAG, "Error setting next alarm", e);
return Result.failure();
}
} else {
Log.w(TAG, "Webhook id not found");
return Result.failure();
}
return Result.success();
}
}

View File

@ -1,6 +1,4 @@
org.gradle.jvmargs=-Xmx2g org.gradle.jvmargs=-Xmx512m
org.gradle.daemon=true
org.gradle.caching=true
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
android.enableR8=true android.enableR8=true

View File

@ -90,6 +90,12 @@ class CardData {
return BadgesData(rawData); return BadgesData(rawData);
break; break;
default: default:
if (rawData.containsKey('entity')) {
rawData['entities'] = [rawData['entity']];
}
if (rawData.containsKey('entities') && rawData['entities'] is List) {
return EntitiesCardData(rawData);
}
return CardData(null); return CardData(null);
} }
} catch (error, stacktrace) { } catch (error, stacktrace) {
@ -103,7 +109,11 @@ class CardData {
type = rawData['type']; type = rawData['type'];
conditions = rawData['conditions'] ?? []; conditions = rawData['conditions'] ?? [];
showEmpty = rawData['show_empty'] ?? true; showEmpty = rawData['show_empty'] ?? true;
stateFilter = rawData['state_filter'] ?? []; if (rawData.containsKey('state_filter') && rawData['state_filter'] is List) {
stateFilter = rawData['state_filter'];
} else {
stateFilter = [];
}
} else { } else {
type = CardType.UNKNOWN; type = CardType.UNKNOWN;
conditions = []; conditions = [];
@ -374,7 +384,13 @@ class LightCardData extends CardData {
@override @override
Widget buildCardWidget() { Widget buildCardWidget() {
return LightCard(card: this); if (this.entity != null && this.entity.entity is LightEntity) {
return LightCard(card: this);
}
return ErrorCard(
errorText: 'Specify an entity from within the light domain.',
showReportButton: false,
);
} }
LightCardData(rawData) : super(rawData) { LightCardData(rawData) : super(rawData) {

View File

@ -2,12 +2,21 @@ part of '../main.dart';
class ErrorCard extends StatelessWidget { class ErrorCard extends StatelessWidget {
final ErrorCardData card; final ErrorCardData card;
final String errorText;
final bool showReportButton;
const ErrorCard({Key key, this.card}) : super(key: key); const ErrorCard({Key key, this.card, this.errorText, this.showReportButton: true}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String error;
if (errorText == null) {
error = 'There was an error showing ${card?.type}';
} else {
error = errorText;
}
return CardWrapper( return CardWrapper(
color: Theme.of(context).errorColor,
child: Padding( child: Padding(
padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding), padding: EdgeInsets.fromLTRB(Sizes.leftWidgetPadding, Sizes.rowPadding, Sizes.rightWidgetPadding, Sizes.rowPadding),
child: Column( child: Column(
@ -15,21 +24,25 @@ class ErrorCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Text( Text(
'There was an error rendering card: ${card.type}. Please copy card config to clipboard and report this issue. Thanks!', error,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
card != null ?
RaisedButton( RaisedButton(
onPressed: () { onPressed: () {
Clipboard.setData(new ClipboardData(text: card.cardConfig)); Clipboard.setData(new ClipboardData(text: card.cardConfig));
}, },
child: Text('Copy card config'), child: Text('Copy card config'),
), ) :
Container(width: 0, height: 0),
showReportButton ?
RaisedButton( RaisedButton(
onPressed: () { onPressed: () {
Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title="); Launcher.launchURLInBrowser("https://github.com/estevez-dev/ha_client/issues/new?assignees=&labels=&template=bug_report.md&title=");
}, },
child: Text('Report issue'), child: Text('Report issue'),
) ) :
Container(width: 0, height: 0)
], ],
), ),
) )

View File

@ -7,6 +7,6 @@ class UnsupportedCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container(); return Container(height: 20);
} }
} }

View File

@ -4,12 +4,14 @@ class CardWrapper extends StatelessWidget {
final Widget child; final Widget child;
final EdgeInsets padding; final EdgeInsets padding;
final Color color;
const CardWrapper({Key key, this.child, this.padding: const EdgeInsets.all(0)}) : super(key: key); const CardWrapper({Key key, this.child, this.color, this.padding: const EdgeInsets.all(0)}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
color: color,
child: Padding( child: Padding(
padding: padding, padding: padding,
child: child child: child

View File

@ -40,8 +40,8 @@ class CoverEntity extends Entity {
CoverEntity.SUPPORT_SET_TILT_POSITION); CoverEntity.SUPPORT_SET_TILT_POSITION);
double get currentPosition => _getDoubleAttributeValue('current_position'); double get currentPosition => _getDoubleAttributeValue('current_position') ?? 0;
double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position'); double get currentTiltPosition => _getDoubleAttributeValue('current_tilt_position') ?? 0;
bool get canBeOpened => ((state != EntityState.opening) && (state != EntityState.open)) || (state == EntityState.open && currentPosition != null && currentPosition > 0.0 && currentPosition < 100.0); bool get canBeOpened => ((state != EntityState.opening) && (state != EntityState.open)) || (state == EntityState.open && currentPosition != null && currentPosition > 0.0 && currentPosition < 100.0);
bool get canBeClosed => ((state != EntityState.closing) && (state != EntityState.closed)); bool get canBeClosed => ((state != EntityState.closing) && (state != EntityState.closed));
bool get canTiltBeOpened => currentTiltPosition < 100; bool get canTiltBeOpened => currentTiltPosition < 100;

View File

@ -8,8 +8,8 @@ class TimerEntity extends Entity {
@override @override
void update(Map rawData, String webHost) { void update(Map rawData, String webHost) {
super.update(rawData, webHost); super.update(rawData, webHost);
String durationSource = "${attributes["duration"]}"; if (attributes.containsKey('duration')) {
if (durationSource != null && durationSource.isNotEmpty) { String durationSource = "${attributes["duration"]}";
try { try {
List<String> durationList = durationSource.split(":"); List<String> durationList = durationSource.split(":");
if (durationList.length == 1) { if (durationList.length == 1) {

View File

@ -149,7 +149,7 @@ class EntityCollection {
} }
bool isExist(String entityId) { bool isExist(String entityId) {
return _allEntities[entityId] != null; return _allEntities.containsKey(entityId);
} }
List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) { List<Entity> getByDomains({List<String> includeDomains: const [], List<String> excludeDomains: const [], List<String> stateFiler}) {

View File

@ -221,7 +221,7 @@ class HomeAssistant {
var data = json.decode(prefs.getString('cached_services')); var data = json.decode(prefs.getString('cached_services'));
_parseServices(data ?? {}); _parseServices(data ?? {});
} catch (e, stacktrace) { } catch (e, stacktrace) {
Logger.e(e, stacktrace: stacktrace); Logger.e(e, stacktrace: stacktrace, skipCrashlytics: true);
} }
} }
await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) { await ConnectionManager().sendSocketMessage(type: "get_services").then((data) => _parseServices(data)).catchError((e) {
@ -261,7 +261,7 @@ class HomeAssistant {
var data = json.decode(sharedPrefs.getString('cached_panels')); var data = json.decode(sharedPrefs.getString('cached_panels'));
_parsePanels(data ?? {}); _parsePanels(data ?? {});
} catch (e, stacktrace) { } catch (e, stacktrace) {
Logger.e(e, stacktrace: stacktrace); Logger.e(e, stacktrace: stacktrace, skipCrashlytics: true);
panels.clear(); panels.clear();
} }
} else { } else {

View File

@ -156,11 +156,9 @@ part 'cards/badges.dart';
part 'managers/app_settings.dart'; part 'managers/app_settings.dart';
EventBus eventBus = new EventBus(); EventBus eventBus = new EventBus();
//final FirebaseMessaging _firebaseMessaging = FirebaseMessaging();
//FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
const String appName = 'HA Client'; const String appName = 'HA Client';
const String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0'); const String appVersion = String.fromEnvironment('versionName', defaultValue: '0.0.0');
const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.1.0-b2.md'; const whatsNewUrl = 'http://ha-client.app/service/whats_new_1.2.0.md';
Future<void> _reportError(dynamic error, dynamic stackTrace) async { Future<void> _reportError(dynamic error, dynamic stackTrace) async {
// Print the exception to the console. // Print the exception to the console.
@ -249,6 +247,7 @@ class _HAClientAppState extends State<HAClientApp> {
positiveText: "Ok" positiveText: "Ok"
) )
)); ));
InAppPurchaseConnection.instance.completePurchase(purchase[0]);
} else { } else {
Logger.d("Purchase change handler: ${purchase[0].status}"); Logger.d("Purchase change handler: ${purchase[0].status}");
} }

View File

@ -28,6 +28,7 @@ class AppSettings {
String webhookId; String webhookId;
double haVersion; double haVersion;
bool scrollBadges; bool scrollBadges;
bool nextAlarmSensorCreated = false;
DisplayMode displayMode; DisplayMode displayMode;
AppTheme appTheme; AppTheme appTheme;
final int defaultLocationUpdateIntervalMinutes = 20; final int defaultLocationUpdateIntervalMinutes = 20;
@ -61,6 +62,7 @@ class AppSettings {
locationUpdateInterval = Duration(minutes: prefs.getInt("location-interval") ?? locationUpdateInterval = Duration(minutes: prefs.getInt("location-interval") ??
defaultLocationUpdateIntervalMinutes); defaultLocationUpdateIntervalMinutes);
locationTrackingEnabled = prefs.getBool("location-enabled") ?? false; locationTrackingEnabled = prefs.getBool("location-enabled") ?? false;
nextAlarmSensorCreated = prefs.getBool("next-alarm-sensor-created") ?? false;
longLivedToken = Hive.box(DEFAULT_HIVE_BOX).get(AUTH_TOKEN_KEY); longLivedToken = Hive.box(DEFAULT_HIVE_BOX).get(AUTH_TOKEN_KEY);
oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent( oauthUrl = "$httpWebHost/auth/authorize?client_id=${Uri.encodeComponent(
'https://ha-client.app')}&redirect_uri=${Uri 'https://ha-client.app')}&redirect_uri=${Uri

View File

@ -5,9 +5,9 @@ class MobileAppIntegrationManager {
static final _appRegistrationData = { static final _appRegistrationData = {
"device_name": "", "device_name": "",
"app_version": "$appVersion", "app_version": "$appVersion",
"manufacturer": DeviceInfoManager().manufacturer, "manufacturer": DeviceInfoManager().manufacturer ?? "unknown",
"model": DeviceInfoManager().model, "model": DeviceInfoManager().model ?? "unknown",
"os_version": DeviceInfoManager().osVersion, "os_version": DeviceInfoManager().osVersion ?? "0",
"app_data": { "app_data": {
"push_token": "", "push_token": "",
"push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV3" "push_url": "https://us-central1-ha-client-c73c4.cloudfunctions.net/pushNotifyV3"
@ -62,12 +62,13 @@ class MobileAppIntegrationManager {
includeAuthHeader: true, includeAuthHeader: true,
data: json.encode(registrationData) data: json.encode(registrationData)
).then((response) { ).then((response) {
Logger.d("Processing registration responce..."); Logger.d("Processing registration response...");
var responseObject = json.decode(response); var responseObject = json.decode(response);
AppSettings().webhookId = responseObject["webhook_id"]; AppSettings().webhookId = responseObject["webhook_id"];
AppSettings().save({ AppSettings().save({
'app-webhook-id': responseObject["webhook_id"] 'app-webhook-id': responseObject["webhook_id"]
}).then((prefs) { }).then((_) {
_createNextAlarmSensor(true);
completer.complete(); completer.complete();
eventBus.fire(ShowPopupEvent( eventBus.fire(ShowPopupEvent(
popup: Popup( popup: Popup(
@ -112,6 +113,7 @@ class MobileAppIntegrationManager {
_askToRegisterApp(); _askToRegisterApp();
} else { } else {
Logger.d('App registration works fine'); Logger.d('App registration works fine');
_createNextAlarmSensor(false);
} }
completer.complete(); completer.complete();
}).catchError((e) { }).catchError((e) {
@ -131,6 +133,42 @@ class MobileAppIntegrationManager {
return completer.future; return completer.future;
} }
static _createNextAlarmSensor(bool force) {
if (AppSettings().nextAlarmSensorCreated && !force) {
Logger.d("Next alarm sensor was previously created");
return;
}
Logger.d("Creating next alarm sensor...");
ConnectionManager().sendHTTPPost(
endPoint: "/api/webhook/${AppSettings().webhookId}",
includeAuthHeader: false,
data: json.encode(
{
"data": {
"device_class": "timestamp",
"icon": "mdi:alarm",
"name": "Next Alarm",
"state": "",
"type": "sensor",
"unique_id": "next_alarm"
},
"type": "register_sensor"
}
)
).then((_){
AppSettings().nextAlarmSensorCreated = true;
AppSettings().save({
'next-alarm-sensor-created': true
});
}).catchError((e) {
if (e is http.Response) {
Logger.e("Error creating next alarm sensor: ${e.statusCode}: ${e.body}");
} else {
Logger.e("Error creating next alarm sensor: ${e?.toString()}");
}
});
}
static void _showError() { static void _showError() {
eventBus.fire(ShowPopupEvent( eventBus.fire(ShowPopupEvent(
popup: Popup( popup: Popup(

View File

@ -63,7 +63,18 @@ class _PurchasePageState extends State<PurchasePage> {
} }
List<Widget> _buildProducts() { List<Widget> _buildProducts() {
List<Widget> productWidgets = []; List<Widget> productWidgets = [
Card(
child: Padding(
padding: EdgeInsets.all(15),
child: Text(
'This will not unlock any additional functionality. This is only a donation to the HA Client open source project.',
style: Theme.of(context).textTheme.headline5,
textAlign: TextAlign.center,
)
)
)
];
for (ProductDetails product in _products) { for (ProductDetails product in _products) {
productWidgets.add( productWidgets.add(
ProductPurchase( ProductPurchase(

View File

@ -15,7 +15,7 @@ class ProductPurchase extends StatelessWidget {
String buttonText = ''; String buttonText = '';
String buttonTextInactive = ''; String buttonTextInactive = '';
if (product.id.contains("year")) { if (product.id.contains("year")) {
period += "/ year"; period += "once a year";
buttonText = "Subscribe"; buttonText = "Subscribe";
buttonTextInactive = "Already"; buttonTextInactive = "Already";
priceColor = Colors.amber; priceColor = Colors.amber;

View File

@ -1,7 +1,7 @@
name: hass_client name: hass_client
description: Home Assistant Android Client description: Home Assistant Android Client
version: 1.1.0+1156 version: 1.1.2+1160
environment: environment:
@ -19,13 +19,13 @@ dependencies:
date_format: ^1.0.8 date_format: ^1.0.8
charts_flutter: ^0.8.1 charts_flutter: ^0.8.1
flutter_markdown: ^0.3.3 flutter_markdown: ^0.3.3
in_app_purchase: ^0.3.0+3 in_app_purchase: ^0.3.4
flutter_custom_tabs: ^0.6.0 flutter_custom_tabs: ^0.6.0
flutter_webview_plugin: ^0.3.10+1 flutter_webview_plugin: ^0.3.10+1
webview_flutter: ^0.3.19+7 webview_flutter: ^0.3.19+7
hive: ^1.4.1+1 hive: ^1.4.1+1
hive_flutter: ^0.3.0+2 hive_flutter: ^0.3.0+2
device_info: ^0.4.1+4 device_info: ^0.4.2+4
geolocator: ^5.3.1 geolocator: ^5.3.1
workmanager: ^0.2.2 workmanager: ^0.2.2
battery: ^1.0.0 battery: ^1.0.0