Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,24 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

---

## [1.0.0] — 2025-03-30
## [1.1.0] — 2026-03-30

### Fixed
- **Week analytics showing 0** — "Week" tab now uses a rolling last-7-days window instead of the current Mon–Sun calendar week; transactions are no longer excluded when added before this week's Monday
- **Light mode home page blank top area** — greeting + balance card section now has a distinct accent-gradient header, eliminating the visually empty pale zone at the top
- **Onboarding keyboard overlap** — name input page now uses `SingleChildScrollView` so the `TextField` is never obscured when the keyboard opens
- **Onboarding no back navigation** — back button added; appears on pages 2 and 3 so users can return to previous steps
- **Notification not firing on some devices** — integrated `flutter_timezone` to resolve the correct local timezone at runtime; notifications now fire at the right local time on all devices and regions
- **Notification delivery on aggressive battery-saving ROMs** (OnePlus, Xiaomi, etc.) — upgraded to `Importance.max` / `Priority.max`, added `fullScreenIntent`, `category: reminder`, and `visibility: public`; switched to `inexactAllowWhileIdle` for broader Android compatibility
- **Hive null crash on older records** — `tags` and `knownSenderIds` adapter casts changed to `List?` to handle records written before those fields existed

### Added
- `USE_FULL_SCREEN_INTENT` Android permission for heads-up notification display
- `flutter_timezone` dependency for accurate timezone-aware scheduling

---

## [1.0.0] — 2026-03-30

### Added
- Transaction tracking — add income, expenses, and transfers with categories, notes, and tags
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Beautiful glassmorphism UI · Riverpod state · Hive local database · Zero data
| Typography | Google Fonts — Plus Jakarta Sans |
| Biometric Auth | `local_auth` |
| SMS Reading | `flutter_sms_inbox` |
| Notifications | `flutter_local_notifications` |
| Notifications | `flutter_local_notifications`, `flutter_timezone` |
| Background Tasks | `workmanager` |
| File Operations | `path_provider`, `share_plus`, `file_picker` |

Expand Down Expand Up @@ -137,6 +137,8 @@ lib/
- **Settings state** — `AppSettings` is cloned via `fromJson`/`toJson` on every update so Riverpod detects the change. Mutation-in-place won't work.
- **Tab indices** in `HomeShell`: `0`=Dashboard, `1`=Transactions, `2`=FAB (opens sheet), `3`=Analytics, `4`=Settings.
- **fl_chart v0.68** — does not accept `duration`/`curve` at the chart widget level.
- **Notifications** — `flutter_timezone` is used to resolve the device's local timezone at runtime via `FlutterTimezone.getLocalTimezone()`. This must be called during `NotificationService.init()` before any `zonedSchedule` call, otherwise notifications fire at UTC time on many devices.
- **Week analytics** — uses a rolling last-7-days window (`today − 6` to `today`), not a Mon–Sun calendar week, so data is always visible regardless of the day of the week.

---

Expand All @@ -149,6 +151,7 @@ lib/
| `POST_NOTIFICATIONS` | Daily reminder notifications |
| `CAMERA` / `READ_MEDIA_IMAGES` | Profile avatar picker |
| `RECEIVE_BOOT_COMPLETED` | Reschedule notifications after reboot |
| `USE_FULL_SCREEN_INTENT` | Show heads-up notification display |

All permissions are optional. Core tracking works without any of them.

Expand Down
20 changes: 20 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
Expand Down Expand Up @@ -41,6 +44,23 @@
</intent-filter>
</activity>

<!-- flutter_local_notifications: reschedule alarms after reboot -->
<receiver
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>

<!-- flutter_local_notifications: deliver scheduled notifications -->
<receiver
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
android:exported="false" />

<!-- WorkManager background task service -->
<service
android:name="androidx.work.impl.background.systemalarm.RescheduleReceiver"
Expand Down
5 changes: 3 additions & 2 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"url": "https://github.com/aswin-blix"
},
"license": "https://opensource.org/licenses/MIT",
"softwareVersion": "1.0.0",
"softwareVersion": "1.1.0",
"featureList": [
"Expense and income tracking",
"Bank SMS auto-reader",
Expand Down Expand Up @@ -473,7 +473,7 @@ <h3>Transaction Tracking</h3>
<article class="feature-card fade-up">
<div class="feature-icon" style="background:rgba(0,212,255,.15)">📊</div>
<h3>Analytics & Charts</h3>
<p>Bar charts, pie charts, and trend breakdowns for weekly, monthly, and yearly periods.</p>
<p>Bar charts, pie charts, and trend breakdowns. Week view shows a rolling last-7-days window so data is always visible.</p>
</article>
<article class="feature-card fade-up">
<div class="feature-icon" style="background:rgba(0,229,160,.15)">🎯</div>
Expand Down Expand Up @@ -584,6 +584,7 @@ <h2 class="section-title" id="tech-title">Built with modern Flutter</h2>
<div class="tech-badge"><span class="tech-dot" style="background:#FF4D6D"></span>local_auth</div>
<div class="tech-badge"><span class="tech-dot" style="background:#00CEC9"></span>flutter_sms_inbox</div>
<div class="tech-badge"><span class="tech-dot" style="background:#FFC107"></span>flutter_local_notifications</div>
<div class="tech-badge"><span class="tech-dot" style="background:#FDCB6E"></span>flutter_timezone</div>
<div class="tech-badge"><span class="tech-dot" style="background:#A29BFE"></span>Plus Jakarta Sans</div>
</div>
</section>
Expand Down
153 changes: 153 additions & 0 deletions lib/core/services/notification_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:timezone/data/latest_all.dart' as tz_data;
import 'package:timezone/timezone.dart' as tz;
import 'package:flutter_timezone/flutter_timezone.dart';
import '../constants/app_constants.dart';
import '../utils/app_logger.dart';

class NotificationService {
NotificationService._();
static final NotificationService instance = NotificationService._();

final FlutterLocalNotificationsPlugin _plugin =
FlutterLocalNotificationsPlugin();

bool _initialized = false;

// ── Initialise ──────────────────────────────────────────────────────────────
Future<void> init() async {
if (_initialized) return;
try {
tz_data.initializeTimeZones();
final timeZoneInfo = await FlutterTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation(timeZoneInfo.toString()));

const androidSettings =
AndroidInitializationSettings('@mipmap/ic_launcher');
const initSettings = InitializationSettings(android: androidSettings);

await _plugin.initialize(
initSettings,
onDidReceiveNotificationResponse: (details) {
AppLogger.i('NotificationService', 'Notification tapped: ${details.payload}');
},
);

_initialized = true;
AppLogger.i('NotificationService', 'Initialized');
} catch (e, stack) {
AppLogger.e('NotificationService', 'Failed to initialize', e, stack);
}
}

// ── Request POST_NOTIFICATIONS permission (Android 13+) ────────────────────
Future<bool> requestPermission() async {
try {
final status = await Permission.notification.request();
AppLogger.i('NotificationService', 'Notification permission: $status');
return status.isGranted;
} catch (e, stack) {
AppLogger.e('NotificationService', 'Permission request failed', e, stack);
return false;
}
}

Future<bool> hasPermission() async {
return Permission.notification.isGranted;
}

// ── Schedule daily reminder ─────────────────────────────────────────────────
Future<void> scheduleDailyReminder({
required int hour,
required int minute,
}) async {
if (!_initialized) await init();

try {
// Cancel existing before rescheduling
await _plugin.cancel(kDailyReminderNotifId);

// Request permission if not granted
final granted = await hasPermission();
if (!granted) {
final result = await requestPermission();
if (!result) {
AppLogger.w('NotificationService', 'Notification permission denied — reminder not scheduled');
return;
}
}

final now = tz.TZDateTime.now(tz.local);
var scheduled = tz.TZDateTime(
tz.local,
now.year,
now.month,
now.day,
hour,
minute,
);

// If the time has already passed today, schedule for tomorrow
if (scheduled.isBefore(now)) {
scheduled = scheduled.add(const Duration(days: 1));
}

const androidDetails = AndroidNotificationDetails(
'trackify_daily_reminder',
'Daily Reminder',
channelDescription: 'Reminds you to log your daily expenses',
importance: Importance.max,
priority: Priority.max,
icon: '@mipmap/ic_launcher',
enableVibration: true,
playSound: true,
fullScreenIntent: true,
category: AndroidNotificationCategory.reminder,
visibility: NotificationVisibility.public,
);

const details = NotificationDetails(android: androidDetails);

await _plugin.zonedSchedule(
kDailyReminderNotifId,
'💰 Time to log your expenses!',
'Keep your finances on track — it only takes a minute.',
scheduled,
details,
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle,
matchDateTimeComponents: DateTimeComponents.time, // repeats daily
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
);

AppLogger.i(
'NotificationService',
'Daily reminder scheduled at $hour:${minute.toString().padLeft(2, '0')} (next: $scheduled)',
);
} catch (e, stack) {
AppLogger.e('NotificationService', 'Failed to schedule daily reminder', e, stack);
}
}

// ── Cancel daily reminder ───────────────────────────────────────────────────
Future<void> cancelDailyReminder() async {
try {
await _plugin.cancel(kDailyReminderNotifId);
AppLogger.i('NotificationService', 'Daily reminder cancelled');
} catch (e, stack) {
AppLogger.e('NotificationService', 'Failed to cancel reminder', e, stack);
}
}

// ── Cancel all ──────────────────────────────────────────────────────────────
Future<void> cancelAll() async {
try {
await _plugin.cancelAll();
AppLogger.i('NotificationService', 'All notifications cancelled');
} catch (e, stack) {
AppLogger.e('NotificationService', 'Failed to cancel all notifications', e, stack);
}
}
}
27 changes: 27 additions & 0 deletions lib/core/utils/app_logger.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:flutter/foundation.dart';

/// Lightweight structured logger.
/// In debug builds: prints all levels.
/// In release builds: only prints warnings and errors.
class AppLogger {
const AppLogger._();

static void i(String tag, String message) {
if (kDebugMode) {
debugPrint('[INFO] [$tag] $message');
}
}

static void w(String tag, String message) {
if (kDebugMode) {
debugPrint('[WARN] [$tag] $message');
}
}

static void e(String tag, String message, [Object? error, StackTrace? stack]) {
// Always print errors — even in release (visible in logcat/crash logs)
debugPrint('[ERROR] [$tag] $message');
if (error != null) debugPrint('[ERROR] [$tag] $error');
if (stack != null && kDebugMode) debugPrint('[ERROR] [$tag] $stack');
}
}
2 changes: 1 addition & 1 deletion lib/data/models/app_settings.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/data/models/transaction_model.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading