Skip to content

Commit

Permalink
feat: ✨ one notification per product
Browse files Browse the repository at this point in the history
- only one notification per price change
- notification tap handling for closed and in-memory states of the app
- introduced 3 types of notifications
  • Loading branch information
lucafluri committed Jul 14, 2020
1 parent 6d45037 commit d17c715
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 81 deletions.
6 changes: 5 additions & 1 deletion lib/models/product.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class Product {
bool get parseSuccess => _parseSuccess;
set parseSuccess(bool newVal) => this._parseSuccess = newVal;

double get latestPrice => _prices[_prices.length - 1];

var formatter = new DateFormat('yyyy-MM-dd');

@override
Expand Down Expand Up @@ -185,6 +187,8 @@ class Product {
return roundToPlace(last - secondLast, 2);
}

// Returns the saved price percentage
// e.g. 200 -> 100 = 50.0
double percentageToYesterday() {
int length = prices.length;

Expand All @@ -193,7 +197,7 @@ class Product {
double last = prices[length - 1];
double secondLast = prices[length - 2];

return roundToPlace((1 - (last / secondLast)) * -100, 2);
return (roundToPlace((1 - (last / secondLast)).abs() * 100, 2));
}

List<double> prices2List(String prices) {
Expand Down
4 changes: 1 addition & 3 deletions lib/screens/home/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:price_tracker/components/widget_view/widget_view.dart';
import 'package:price_tracker/screens/home/components/product_list_tile.dart';
import 'package:price_tracker/screens/home/home_controller.dart';
import 'package:price_tracker/services/notifications.dart';
import 'package:price_tracker/services/product_utils.dart';
import 'package:toast/toast.dart';
import 'package:workmanager/workmanager.dart';
Expand Down Expand Up @@ -33,8 +32,7 @@ class HomeScreenView extends WidgetView<HomeScreen, HomeScreenController> {
if (NOTIFICATION_TEST_BUTTON)
IconButton(
icon: Icon(Icons.speaker_notes),
onPressed: () => NotificationService.sendPushNotification(
0, "test", "test body"),
onPressed: () => state.testNotification(),
color: Colors.redAccent,
),
if (BACKGROUND_TEST_BUTTON)
Expand Down
33 changes: 30 additions & 3 deletions lib/screens/home/home_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:flutter_clipboard_manager/flutter_clipboard_manager.dart';
import 'package:price_tracker/models/product.dart';
import 'package:price_tracker/screens/home/home.dart';
import 'package:price_tracker/services/database.dart';
import 'package:price_tracker/services/notifications.dart';
import 'package:price_tracker/services/product_utils.dart';
import 'package:price_tracker/services/scraper.dart';
import 'package:price_tracker/services/share_intent.dart';
Expand All @@ -32,6 +33,8 @@ class HomeScreenController extends State<HomeScreen> {
void init() async {
await _loadProducts();
await checkInternet();
await _checkForNotificationTap();

if (iConnectivity) await _checkForSharedText();
}

Expand All @@ -42,14 +45,20 @@ class HomeScreenController extends State<HomeScreen> {
}

_checkForSharedText() async {
if (ShareIntentService.sharedText != null) {
String input = ShareIntentService.sharedText;
String input = ShareIntentService.sharedText;
if (input != null) {
ShareIntentService.sharedText = null;

await addProduct(input);
}
}

_checkForNotificationTap() async {
String payload = NotificationService.currentPayload;
if (payload != null) {
notificationTapCallback(payload);
}
}

Future<bool> checkInternet() async {
try {
final result = await InternetAddress.lookup('google.com')
Expand Down Expand Up @@ -194,6 +203,24 @@ class HomeScreenController extends State<HomeScreen> {
curve: Curves.easeInOut, duration: Duration(milliseconds: 1000));
}

testNotification() {
Product p = Product.fromMap({
"_id": 5,
"name": "Apple iPad (10.2-inch, WiFi, 32GB) - Gold (latest model)",
"productUrl": "testUrl.com",
"prices": "[-1.0, 269.0, 260.0]",
"dates":
"[2020-07-02 00:00:00.000, 2020-07-03 15:43:12.345, 2020-07-04 04:00:45.000]",
"targetPrice": "261.0",
"imageUrl": "",
"parseSuccess": "true",
});
// Only one can be send at a time (same id)
sendPriceFallNotification(p);
// sendUnderTargetNotification(p);
// sendAvailableAgainNotification(p);
}

@override
Widget build(BuildContext context) => HomeScreenView(this);
}
38 changes: 33 additions & 5 deletions lib/services/notifications.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:price_tracker/services/init.dart';
import 'package:price_tracker/services/product_utils.dart';

import 'package:rxdart/subjects.dart';

class NotificationService {
static FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin;

static String currentPayload;
NotificationService._privateConstructor();

static final NotificationService _instance =
Expand Down Expand Up @@ -35,22 +38,47 @@ class NotificationService {
onSelectNotification: (String payload) async {
if (payload != null) {
debugPrint('notification payload: ' + payload);
currentPayload = payload;

// If state is mounted, the app is in memory,
// else the app starts and the callback gets triggered in the home view
if (navigatorKey.currentState.mounted) notificationTapCallback(payload);
}
selectNotificationSubject.add(payload);
});
}

static Future<void> sendPushNotification(
int id, String title, String body) async {
static Future<void> sendAlertPushNotificationSmall(
int id, String title, String body,
{String payload = ""}) async {
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'your channel id', 'your channel name', 'your channel description',
'0', 'Price Alerts', 'Price change alerts for tracked products',
importance: Importance.Max, priority: Priority.High, ticker: 'ticker');
var iOSPlatformChannelSpecifics = IOSNotificationDetails();
var platformChannelSpecifics = NotificationDetails(
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);

await _flutterLocalNotificationsPlugin
.show(id, title, body, platformChannelSpecifics, payload: 'item x');
.show(id, title, body, platformChannelSpecifics, payload: payload);

print("Push notifications was sent!");
}

static Future<void> sendAlertPushNotificationBig(
int id, String title, String body,
{String payload = ""}) async {
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'0', 'Price Alerts', 'Price change alerts for tracked products',
importance: Importance.Max,
priority: Priority.High,
ticker: 'ticker',
styleInformation: BigTextStyleInformation(body));
var iOSPlatformChannelSpecifics = IOSNotificationDetails();
var platformChannelSpecifics = NotificationDetails(
androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);

await _flutterLocalNotificationsPlugin
.show(id, title, body, platformChannelSpecifics, payload: payload);

print("Push notifications was sent!");
}
Expand Down
124 changes: 58 additions & 66 deletions lib/services/product_utils.dart
Original file line number Diff line number Diff line change
@@ -1,46 +1,12 @@
import 'package:price_tracker/models/product.dart';
import 'package:price_tracker/screens/product_detail/product_detail.dart';
import 'package:price_tracker/services/database.dart';
import 'package:flutter/material.dart';
import 'package:price_tracker/services/init.dart';
import 'package:price_tracker/services/notifications.dart';

double reloadProgress;

Future<int> countPriceFall() async {
final _db = await DatabaseService.getInstance();

List<Product> products = await _db.getAllProducts();

int count = 0;

for (int i = 0; i < products.length; i++) {
if (products[i].priceFall()) {
count++;

debugPrint(products[i].name);
debugPrint(
"Price diff: " + products[i].priceDifferenceToYesterday().toString());
debugPrint(
"Percentage: " + products[i].percentageToYesterday().toString());
debugPrint("Available again: " + products[i].availableAgain().toString());
}
}
return count;
}

//Returns number of products that fell under the set target
Future<int> countPriceUnderTarget() async {
final _db = await DatabaseService.getInstance();

List<Product> products = await _db.getAllProducts();

int count = 0;

for (int i = 0; i < products.length; i++) {
if (products[i].underTarget()) count++;
}
return count;
}

//Returns number of products that fell under the set target
Future<int> countFailedParsing() async {
final _db = await DatabaseService.getInstance();
Expand All @@ -63,42 +29,68 @@ Future<void> updatePrices(Function perUpdate, {test: false}) async {
List<Product> products = await _db.getAllProducts();

for (int i = 0; i < products.length; i++) {
double lastPrice = products[i].latestPrice;
await products[i].update(test: test);
await _db.update(products[i]);
reloadProgress = i / products.length;
perUpdate();
}

products = await _db.getAllProducts();
int countFall = await countPriceFall();
int countTarget = await countPriceUnderTarget();

if (countFall > 0) {
if (countFall == 1) {
NotificationService.sendPushNotification(
0,
'$countFall product is cheaper',
'We detected that $countFall product is cheaper today!'); //Display Notification
} else {
NotificationService.sendPushNotification(
0,
'$countFall products are cheaper',
'We detected that $countFall products are cheaper today!'); //Display Notification
}
}
if (countTarget > 0) {
if (countTarget == 1) {
NotificationService.sendPushNotification(
1,
'$countTarget product is under their target!',
'We detected that $countTarget product is under the set target today!'); //Display Notification
} else {
NotificationService.sendPushNotification(
1,
'$countTarget products are under their target!',
'We detected that $countTarget products are under the set targets today!'); //Display Notification
}
// Send notifications only when price changed
// otherwise the app notified twice per day.
if (lastPrice != products[i].latestPrice)
checkAndSendNotifications(products[i]);
}

reloadProgress = null;
}

// Opens the corresponding details view of the product by product id in payload
Future<void> notificationTapCallback(String payload) async {
// get Product by id
Product product = await (await DatabaseService.getInstance())
.getProduct(payload != "" ? int.parse(payload) : null);

// Clear payload variable
NotificationService.currentPayload = null;

if (product != null)
navigatorKey.currentState.push(MaterialPageRoute(
builder: (context) => ProductDetail(
product: product,
)));
}

sendPriceFallNotification(Product p) {
NotificationService.sendAlertPushNotificationBig(
p.id,
"A Product is cheaper by ${p.percentageToYesterday()}%!",
p.getShortName() +
"\nThe price fell by ${-p.priceDifferenceToYesterday()} and it costs now ${p.latestPrice}",
payload: p.id.toString());
}

sendUnderTargetNotification(Product p) {
NotificationService.sendAlertPushNotificationBig(
p.id,
"A Product is under their Target!",
p.getShortName() + " is under the set target and costs ${p.latestPrice}",
payload: p.id.toString());
}

sendAvailableAgainNotification(Product p) {
NotificationService.sendAlertPushNotificationBig(
p.id,
"A Product is available again!",
p.getShortName() + " is available again and costs ${p.latestPrice}",
payload: p.id.toString());
}

// Checks theproduct and sends the relevant Notification if any
checkAndSendNotifications(Product p) {
if (p.priceFall()) {
if (p.underTarget())
sendUnderTargetNotification(p);
else
sendPriceFallNotification(p);
} else if (p.availableAgain()) sendAvailableAgainNotification(p);
}
6 changes: 3 additions & 3 deletions test/product_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -403,15 +403,15 @@ void main() {
productMap2["dates"] = "[2020-07-02, 2020-07-03]";
p = Product.fromMap(productMap2);

expect(p.percentageToYesterday(), -50.0);
expect(p.percentageToYesterday(), 50.0);
});

test('100 -> 200 -> 100', () {
productMap2["prices"] = "[100, 200, 100]";
productMap2["dates"] = "[2020-07-01, 2020-07-02, 2020-07-03]";
p = Product.fromMap(productMap2);

expect(p.percentageToYesterday(), -50.0);
expect(p.percentageToYesterday(), 50.0);
});

test('100 -> 200 -> 220', () {
Expand All @@ -427,7 +427,7 @@ void main() {
productMap2["dates"] = "[2020-07-02, 2020-07-03]";
p = Product.fromMap(productMap2);

expect(p.percentageToYesterday(), -21.45);
expect(p.percentageToYesterday(), 21.45);
});
});
});
Expand Down

0 comments on commit d17c715

Please sign in to comment.