├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── LICENSE
├── README.md
├── analysis_options.yaml
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── my_expense_tracker
│ │ │ │ └── MainActivity.kt
│ │ └── res
│ │ │ ├── drawable-v21
│ │ │ └── launch_background.xml
│ │ │ ├── drawable
│ │ │ └── launch_background.xml
│ │ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── values-night
│ │ │ └── styles.xml
│ │ │ └── values
│ │ │ └── styles.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── settings.gradle
├── assets
├── images
│ ├── 2.0x
│ │ └── flutter_logo.png
│ ├── 3.0x
│ │ └── flutter_logo.png
│ └── flutter_logo.png
└── sample_transactions.json
├── ios
├── .gitignore
├── Flutter
│ ├── AppFrameworkInfo.plist
│ ├── Debug.xcconfig
│ └── Release.xcconfig
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
├── Runner
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ ├── Icon-App-20x20@1x.png
│ │ │ ├── Icon-App-20x20@2x.png
│ │ │ ├── Icon-App-20x20@3x.png
│ │ │ ├── Icon-App-29x29@1x.png
│ │ │ ├── Icon-App-29x29@2x.png
│ │ │ ├── Icon-App-29x29@3x.png
│ │ │ ├── Icon-App-40x40@1x.png
│ │ │ ├── Icon-App-40x40@2x.png
│ │ │ ├── Icon-App-40x40@3x.png
│ │ │ ├── Icon-App-60x60@2x.png
│ │ │ ├── Icon-App-60x60@3x.png
│ │ │ ├── Icon-App-76x76@1x.png
│ │ │ ├── Icon-App-76x76@2x.png
│ │ │ └── Icon-App-83.5x83.5@2x.png
│ │ └── LaunchImage.imageset
│ │ │ ├── Contents.json
│ │ │ ├── LaunchImage.png
│ │ │ ├── LaunchImage@2x.png
│ │ │ ├── LaunchImage@3x.png
│ │ │ └── README.md
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Info.plist
│ └── Runner-Bridging-Header.h
└── RunnerTests
│ └── RunnerTests.swift
├── l10n.yaml
├── lib
├── main.dart
└── src
│ ├── app.dart
│ ├── data
│ ├── data.dart
│ └── tz.json
│ ├── localization
│ ├── app_am.arb
│ └── app_en.arb
│ ├── models
│ ├── db.dart
│ └── transaction.dart
│ ├── sample_feature
│ ├── sample_item.dart
│ ├── sample_item_details_view.dart
│ └── sample_item_list_view.dart
│ ├── screen
│ ├── error.dart
│ ├── home
│ │ ├── home.dart
│ │ ├── main_screen.dart
│ │ └── recent_transactions.dart
│ └── transactions
│ │ ├── transactions_details.dart
│ │ ├── transactions_list copy.dart
│ │ ├── transactions_list.dart
│ │ └── transactions_list_item.dart
│ ├── services
│ ├── sms
│ │ ├── cbe_sms_parser.dart
│ │ ├── sms_service.dart
│ │ └── telebirr_receipt_parser.dart
│ └── transactions
│ │ └── transaction_service.dart
│ ├── settings
│ ├── settings_controller.dart
│ ├── settings_service.dart
│ └── settings_view.dart
│ ├── theme.dart
│ ├── theme
│ └── theme_type.dart
│ └── utils
│ ├── background_service.dart
│ ├── cbe_client.dart
│ ├── constants.dart
│ ├── helpers.dart
│ ├── populate_sms.dart
│ └── types.dart
├── linux
├── .gitignore
├── CMakeLists.txt
├── flutter
│ ├── CMakeLists.txt
│ ├── generated_plugin_registrant.cc
│ ├── generated_plugin_registrant.h
│ └── generated_plugins.cmake
└── runner
│ ├── CMakeLists.txt
│ ├── main.cc
│ ├── my_application.cc
│ └── my_application.h
├── macos
├── .gitignore
├── Flutter
│ ├── Flutter-Debug.xcconfig
│ ├── Flutter-Release.xcconfig
│ └── GeneratedPluginRegistrant.swift
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── Runner
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ └── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── app_icon_1024.png
│ │ │ ├── app_icon_128.png
│ │ │ ├── app_icon_16.png
│ │ │ ├── app_icon_256.png
│ │ │ ├── app_icon_32.png
│ │ │ ├── app_icon_512.png
│ │ │ └── app_icon_64.png
│ ├── Base.lproj
│ │ └── MainMenu.xib
│ ├── Configs
│ │ ├── AppInfo.xcconfig
│ │ ├── Debug.xcconfig
│ │ ├── Release.xcconfig
│ │ └── Warnings.xcconfig
│ ├── DebugProfile.entitlements
│ ├── Info.plist
│ ├── MainFlutterWindow.swift
│ └── Release.entitlements
└── RunnerTests
│ └── RunnerTests.swift
├── plan.json
├── plan.md
├── pubspec.lock
├── pubspec.yaml
├── schema.md
├── test
├── unit_test.dart
└── widget_test.dart
├── web
├── favicon.png
├── icons
│ ├── Icon-192.png
│ ├── Icon-512.png
│ ├── Icon-maskable-192.png
│ └── Icon-maskable-512.png
├── index.html
└── manifest.json
└── windows
├── .gitignore
├── CMakeLists.txt
├── flutter
├── CMakeLists.txt
├── generated_plugin_registrant.cc
├── generated_plugin_registrant.h
└── generated_plugins.cmake
└── runner
├── CMakeLists.txt
├── Runner.rc
├── flutter_window.cpp
├── flutter_window.h
├── main.cpp
├── resource.h
├── resources
└── app_icon.ico
├── runner.exe.manifest
├── utils.cpp
├── utils.h
├── win32_window.cpp
└── win32_window.h
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build and Release APK
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 |
14 | - name: Setup Java
15 | uses: actions/setup-java@v3
16 | with:
17 | distribution: 'zulu'
18 | java-version: '17'
19 |
20 | - name: Setup Flutter
21 | uses: subosito/flutter-action@v2
22 | with:
23 | channel: stable
24 | flutter-version-file: pubspec.yaml
25 |
26 | - name: Get dependencies
27 | run: flutter pub get
28 |
29 | - name: Build APK
30 | run: flutter build apk --release
31 |
32 | - name: Create Release
33 | id: create_release
34 | uses: softprops/action-gh-release@v1
35 | with:
36 | files: build/app/outputs/flutter-apk/app-release.apk
37 | draft: false
38 | prerelease: false
39 | generate_release_notes: true
40 | env:
41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .build/
9 | .buildlog/
10 | .history
11 | .svn/
12 | .swiftpm/
13 | migrate_working_dir/
14 |
15 | # IntelliJ related
16 | *.iml
17 | *.ipr
18 | *.iws
19 | .idea/
20 |
21 | # The .vscode folder contains launch configuration and tasks you configure in
22 | # VS Code which you may wish to be included in version control, so this line
23 | # is commented out by default.
24 | #.vscode/
25 |
26 | # Flutter/Dart/Pub related
27 | **/doc/api/
28 | **/ios/Flutter/.last_build_id
29 | .dart_tool/
30 | .flutter-plugins
31 | .flutter-plugins-dependencies
32 | .pub-cache/
33 | .pub/
34 | /build/
35 |
36 | # Symbolication related
37 | app.*.symbols
38 |
39 | # Obfuscation related
40 | app.*.map.json
41 |
42 | # Android Studio will place build artifacts here
43 | /android/app/debug
44 | /android/app/profile
45 | /android/app/release
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Temkin Mengsitu
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Muday - Ethiopian Expense Tracker
2 |
3 |
4 |
5 |
6 |
7 | [](https://github.com/yourusername/muday/actions)
8 | [](LICENSE)
9 | [](https://android-arsenal.com/api?level=24)
10 |
11 |
12 |
13 | Muday is a comprehensive expense tracking application designed specifically for Ethiopian users. It automatically tracks your expenses by parsing SMS notifications from major Ethiopian banks and mobile money services, including Telebirr, Commercial Bank of Ethiopia (CBE), and Bank of Abyssinia.
14 |
15 | ## Features
16 |
17 | ### 🔄 Automatic Transaction Tracking
18 | - Real-time SMS parsing for automatic expense tracking
19 | - Support for multiple Ethiopian banks and payment services:
20 | - Telebirr
21 | - Commercial Bank of Ethiopia (CBE)
22 | - Bank of Abyssinia
23 | - More banks coming soon
24 | - Automatic parsing of transaction details including:
25 | - Transaction amount
26 | - Balance
27 | - Reference numbers
28 | - Sender/receiver information
29 | - Commission and VAT charges
30 |
31 | ### 📊 Smart Analytics
32 | - Detailed transaction analytics and visualization
33 | - AI-powered expense categorization using Google's Gemini AI
34 | - Custom categorization options
35 | - Flexible date range filtering:
36 | - Daily
37 | - Weekly
38 | - Monthly
39 | - Yearly
40 | - Custom date ranges
41 |
42 | ### 💰 Budget Management
43 | - Set and track budgets for different time periods
44 | - Budget alerts and notifications
45 | - Category-based budget allocation
46 |
47 | ### ☁️ Cloud Features
48 | - Secure cloud backup with Firebase
49 | - Cross-device synchronization
50 | - Data export in multiple formats:
51 | - CSV
52 | - JSON
53 | - PDF reports
54 | - Share transactions with other Muday users
55 |
56 | ## Getting Started
57 |
58 | ### Prerequisites
59 | - VS Code or Android Studio
60 | - Android SDK 24 or higher
61 | - Firebase account for cloud features
62 | - Google Cloud account for AI features
63 |
64 | ### Installation
65 | 1. Clone the repository:
66 | ```bash
67 | git clone https://github.com/chapimenge3/Muday.git
68 | ```
69 |
70 | 2. Open the project in Android Studio or VS Code
71 |
72 | 3. Create a Firebase project and add the `google-services.json` file to the app directory(NOT IMPLEMENTED SKIP this)
73 |
74 | 4. Configure your Google Cloud credentials for Gemini AI integration(NOT IMPLEMENTED SKIP this)
75 |
76 | 5. Build and run the project
77 |
78 | ### Configuration
79 | 1. Enable SMS permissions when prompted
80 | 2. Set up your preferred banks and payment services
81 | 3. Configure cloud backup settings (optional)
82 | 4. Set up your budget preferences
83 |
84 | ## Architecture (TO BE IMPLEMENTED)
85 | Muday follows the MVVM architecture pattern and is built with modern Android development practices:
86 |
87 | - **UI Layer**: Jetpack Compose
88 | - **Business Logic**: ViewModel + Use Cases
89 | - **Data Layer**: Repository Pattern
90 | - **Local Storage**: Room Database
91 | - **Cloud Storage**: Firebase
92 | - **Dependencies**: Hilt for dependency injection
93 |
94 | ## Contributing
95 |
96 | We welcome contributions! Please read our [Contributing Guidelines](CONTRIBUTING.md) before submitting pull requests.
97 |
98 | ### Development Setup
99 | 1. Fork the repository
100 | 2. Create a new branch for your feature
101 | 3. Implement your changes
102 | 4. Submit a pull request
103 |
104 | ## Privacy & Security
105 |
106 | Muday takes your financial privacy seriously:
107 | - All sensitive data is encrypted
108 | - SMS parsing happens locally on your device
109 | - Cloud sync is optional and secured with Firebase Authentication
110 | - No sensitive data is shared without explicit user consent
111 |
112 | ## Support
113 |
114 | For support, please:
115 | - Check our [Documentation](docs/README.md)
116 | - Visit our [Issues](https://github.com/chapimenge3/muday/issues) page
117 | - Join our [Telegram Community](https://t.me/chapidevtalks)
118 |
119 | ## License
120 |
121 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
122 |
123 | ## Acknowledgments
124 |
125 | - Thanks to all contributors and users
126 | - Special thanks to the Ethiopian developer community
127 | - Icons and graphics from [Material Design Icons](https://material.io/icons)
128 |
129 | ---
130 |
131 |
132 | Made with ❤️ in Ethiopia
133 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at https://dart.dev/lints.
17 | #
18 | # Instead of disabling a lint rule for the entire project in the
19 | # section below, it can also be suppressed for a single line of code
20 | # or a specific dart file by using the `// ignore: name_of_lint` and
21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
22 | # producing the lint.
23 | rules:
24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
26 |
27 | # Additional information about this file can be found at
28 | # https://dart.dev/guides/language/analysis-options
29 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/to/reference-keystore
11 | key.properties
12 | **/*.keystore
13 | **/*.jks
14 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "com.android.application"
3 | id "kotlin-android"
4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
5 | id "dev.flutter.flutter-gradle-plugin"
6 | }
7 |
8 | android {
9 | namespace = "com.chapimenge.muday"
10 | compileSdk = flutter.compileSdkVersion
11 | ndkVersion = flutter.ndkVersion
12 |
13 | compileOptions {
14 | sourceCompatibility = JavaVersion.VERSION_1_8
15 | targetCompatibility = JavaVersion.VERSION_1_8
16 | }
17 |
18 | kotlinOptions {
19 | jvmTarget = JavaVersion.VERSION_1_8
20 | }
21 |
22 | defaultConfig {
23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
24 | applicationId = "com.chapimenge.muday"
25 | // You can update the following values to match your application needs.
26 | // For more information, see: https://flutter.dev/to/review-gradle-config.
27 | minSdk = flutter.minSdkVersion
28 | targetSdk = flutter.targetSdkVersion
29 | versionCode = flutter.versionCode
30 | versionName = flutter.versionName
31 | }
32 |
33 | buildTypes {
34 | release {
35 | // TODO: Add your own signing config for the release build.
36 | // Signing with the debug keys for now, so `flutter run --release` works.
37 | signingConfig = signingConfigs.debug
38 | }
39 | }
40 | }
41 |
42 | flutter {
43 | source = "../.."
44 | }
45 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
17 |
21 |
25 |
26 |
27 |
28 |
29 |
30 |
32 |
35 |
36 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/example/my_expense_tracker/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.my_expense_tracker
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity()
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | allprojects {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | }
7 |
8 | rootProject.buildDir = "../build"
9 | subprojects {
10 | project.buildDir = "${rootProject.buildDir}/${project.name}"
11 | }
12 | subprojects {
13 | project.evaluationDependsOn(":app")
14 | }
15 |
16 | tasks.register("clean", Delete) {
17 | delete rootProject.buildDir
18 | }
19 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
6 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | def flutterSdkPath = {
3 | def properties = new Properties()
4 | file("local.properties").withInputStream { properties.load(it) }
5 | def flutterSdkPath = properties.getProperty("flutter.sdk")
6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
7 | return flutterSdkPath
8 | }()
9 |
10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
11 |
12 | repositories {
13 | google()
14 | mavenCentral()
15 | gradlePluginPortal()
16 | }
17 | }
18 |
19 | plugins {
20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0"
21 | id "com.android.application" version "8.2.1" apply false
22 | id "org.jetbrains.kotlin.android" version "1.8.22" apply false
23 | }
24 |
25 | include ":app"
26 |
--------------------------------------------------------------------------------
/assets/images/2.0x/flutter_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/assets/images/2.0x/flutter_logo.png
--------------------------------------------------------------------------------
/assets/images/3.0x/flutter_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/assets/images/3.0x/flutter_logo.png
--------------------------------------------------------------------------------
/assets/images/flutter_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/assets/images/flutter_logo.png
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | **/dgph
2 | *.mode1v3
3 | *.mode2v3
4 | *.moved-aside
5 | *.pbxuser
6 | *.perspectivev3
7 | **/*sync/
8 | .sconsign.dblite
9 | .tags*
10 | **/.vagrant/
11 | **/DerivedData/
12 | Icon?
13 | **/Pods/
14 | **/.symlinks/
15 | profile
16 | xcuserdata
17 | **/.generated/
18 | Flutter/App.framework
19 | Flutter/Flutter.framework
20 | Flutter/Flutter.podspec
21 | Flutter/Generated.xcconfig
22 | Flutter/ephemeral/
23 | Flutter/app.flx
24 | Flutter/app.zip
25 | Flutter/flutter_assets/
26 | Flutter/flutter_export_environment.sh
27 | ServiceDefinitions.json
28 | Runner/GeneratedPluginRegistrant.*
29 |
30 | # Exceptions to above rules.
31 | !default.mode1v3
32 | !default.mode2v3
33 | !default.pbxuser
34 | !default.perspectivev3
35 |
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 12.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Generated.xcconfig"
2 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Generated.xcconfig"
2 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
43 |
49 |
50 |
51 |
52 |
53 |
63 |
65 |
71 |
72 |
73 |
74 |
80 |
82 |
88 |
89 |
90 |
91 |
93 |
94 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 |
4 | @main
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9 | ) -> Bool {
10 | GeneratedPluginRegistrant.register(with: self)
11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chapimenge3/Muday/628b24f9da5b0decf6b942fcba261ad6551c1cdd/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | My Expense Tracker
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | my_expense_tracker
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(FLUTTER_BUILD_NAME)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(FLUTTER_BUILD_NUMBER)
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 | Main
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | CADisableMinimumFrameDurationOnPhone
45 |
46 | UIApplicationSupportsIndirectInputEvents
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/ios/RunnerTests/RunnerTests.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 | import XCTest
4 |
5 | class RunnerTests: XCTestCase {
6 |
7 | func testExample() {
8 | // If you add code to the Runner application, consider adding tests here.
9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/l10n.yaml:
--------------------------------------------------------------------------------
1 | arb-dir: lib/src/localization
2 | template-arb-file: app_en.arb
3 | output-localization-file: app_localizations.dart
4 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter/widgets.dart';
3 | import 'package:muday/src/models/db.dart';
4 | import 'package:muday/src/utils/background_service.dart';
5 |
6 | // import 'package:muday/src/utils/sms_parsers.dart';
7 |
8 | import 'src/app.dart';
9 | import 'src/settings/settings_controller.dart';
10 | import 'src/settings/settings_service.dart';
11 |
12 | void main() async {
13 | // Set up the SettingsController, which will glue user settings to multiple
14 | // Flutter Widgets.
15 | final settingsController = SettingsController(SettingsService());
16 |
17 | // Load the user's preferred theme while the splash screen is displayed.
18 | // This prevents a sudden theme change when the app is first displayed.
19 | await settingsController.loadSettings();
20 |
21 | print('Settings loaded!');
22 |
23 | WidgetsFlutterBinding.ensureInitialized();
24 |
25 | // Run background service
26 | await initializeService();
27 |
28 | // // create a database
29 | final db = DatabaseService.instance;
30 | // print('Database initialized: $db');
31 | // print('Database getTables: ${await db.getTables()}');
32 |
33 | // Run the app and pass in the SettingsController. The app listens to the
34 | // SettingsController for changes, then passes it further down to the
35 | // SettingsView.
36 | runApp(MyApp(settingsController: settingsController, db: db));
37 | }
38 |
--------------------------------------------------------------------------------
/lib/src/app.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_gen/gen_l10n/app_localizations.dart';
3 | import 'package:flutter_localizations/flutter_localizations.dart';
4 | import 'package:muday/src/models/db.dart';
5 | import 'package:muday/src/models/transaction.dart';
6 | import 'package:muday/src/screen/error.dart';
7 | import 'package:muday/src/screen/home/home.dart';
8 | import 'package:muday/src/screen/transactions/transactions_details.dart';
9 | import 'package:muday/src/screen/transactions/transactions_list.dart';
10 | import 'package:permission_handler/permission_handler.dart';
11 |
12 | import 'theme.dart';
13 |
14 | import 'sample_feature/sample_item_details_view.dart';
15 | import 'sample_feature/sample_item_list_view.dart';
16 | import 'settings/settings_controller.dart';
17 | import 'settings/settings_view.dart';
18 |
19 | class Routes {
20 | static const String settings = '/settings';
21 | static const String sampleItemDetails = '/item-details';
22 | static const String sampleItemList = '/item-list';
23 | static const String transactionDetails = '/transaction-details';
24 | static const String transactionList = '/transaction-list';
25 | }
26 |
27 | /// The Widget that configures your application.
28 | class MyApp extends StatelessWidget {
29 | const MyApp({super.key, required this.settingsController, required this.db});
30 |
31 | final SettingsController settingsController;
32 | final DatabaseService db;
33 |
34 | Future _checkPermissionAndReadSMS() async {
35 | var permission = await Permission.sms.status;
36 | if (!permission.isGranted) {
37 | permission = await Permission.sms.request();
38 | if (!permission.isGranted) return false;
39 | }
40 | return true;
41 | }
42 |
43 | @override
44 | Widget build(BuildContext context) {
45 | final materialTheme = MaterialTheme(Theme.of(context).textTheme);
46 | // Glue the SettingsController to the MaterialApp.
47 | //
48 | // The ListenableBuilder Widget listens to the SettingsController for changes.
49 | // Whenever the user updates their settings, the MaterialApp is rebuilt.
50 |
51 | return ListenableBuilder(
52 | listenable: settingsController,
53 | builder: (BuildContext context, Widget? child) {
54 | return MaterialApp(
55 | // Providing a restorationScopeId allows the Navigator built by the
56 | // MaterialApp to restore the navigation stack when a user leaves and
57 | // returns to the app after it has been killed while running in the
58 | // background.
59 | restorationScopeId: 'app',
60 |
61 | debugShowCheckedModeBanner: false,
62 |
63 | // Provide the generated AppLocalizations to the MaterialApp. This
64 | // allows descendant Widgets to display the correct translations
65 | // depending on the user's locale.
66 | localizationsDelegates: const [
67 | AppLocalizations.delegate,
68 | GlobalMaterialLocalizations.delegate,
69 | GlobalWidgetsLocalizations.delegate,
70 | GlobalCupertinoLocalizations.delegate,
71 | ],
72 | supportedLocales: const [
73 | Locale('en', ''), // English, no country code
74 | Locale('am', ''), // Amharic, no country code
75 | ],
76 |
77 | // Use AppLocalizations to configure the correct application title
78 | // depending on the user's locale.
79 | //
80 | // The appTitle is defined in .arb files found in the localization
81 | // directory.
82 | onGenerateTitle: (BuildContext context) =>
83 | AppLocalizations.of(context)!.appTitle,
84 |
85 | // Define a light and dark color theme. Then, read the user's
86 | // preferred ThemeMode (light, dark, or system default) from the
87 | // SettingsController to display the correct theme.
88 | // theme: ThemeData(
89 | // // colorScheme: ColorScheme.light()
90 | // colorScheme: ColorScheme.light(
91 | // primary: Color(0xFF36618e),
92 | // secondary: Color(0xFF535f70),
93 | // tertiary: Color(0xFF6b5778),
94 | // error: Color(0xFFba1a1a),
95 | // ),
96 | // ),
97 | // darkTheme: ThemeData(
98 | // colorScheme: ColorScheme.dark(
99 | // primary: Color(0xFFa0cafd),
100 | // secondary: Color(0xFFbbc7db),
101 | // tertiary: Color(0xFFd6bee4),
102 | // error: Color(0xFFffb4ab),
103 | // ),
104 | // ),
105 | theme: materialTheme.light(),
106 | darkTheme: materialTheme.dark(),
107 | themeMode: settingsController.themeMode,
108 | home: FutureBuilder(
109 | future: _checkPermissionAndReadSMS(),
110 | builder: (context, snapshot) {
111 | if (snapshot.connectionState == ConnectionState.waiting) {
112 | return const Scaffold(
113 | body: Center(
114 | child: CircularProgressIndicator(),
115 | ),
116 | );
117 | }
118 |
119 | if (snapshot.hasError || snapshot.data == false) {
120 | return const Scaffold(
121 | body: Center(
122 | child: Text(
123 | 'SMS permission is required to use this app',
124 | style: TextStyle(color: Colors.red),
125 | ),
126 | ),
127 | );
128 | }
129 |
130 | return const HomeScreen();
131 | },
132 | ),
133 | // home: const HomeScreen(),
134 |
135 | // Define a function to handle named routes in order to support
136 | // Flutter web url navigation and deep linking.
137 | onGenerateRoute: (RouteSettings routeSettings) {
138 | return MaterialPageRoute(
139 | settings: routeSettings,
140 | builder: (BuildContext context) {
141 | switch (routeSettings.name) {
142 | case Routes.settings:
143 | return SettingsView(controller: settingsController);
144 |
145 | case Routes.sampleItemDetails:
146 | return const SampleItemDetailsView();
147 |
148 | case Routes.transactionDetails:
149 | // Extract the transaction from arguments
150 | final args =
151 | routeSettings.arguments as Map?;
152 | final transaction = args?['transaction'];
153 |
154 | if (transaction == null) {
155 | return const ErrorView(message: 'Transaction not found');
156 | }
157 |
158 | return TransactionDetail(
159 | transaction: transaction as TransactionCls);
160 | case Routes.transactionList:
161 | return const TransactionListView();
162 |
163 | case Routes.sampleItemList:
164 | default:
165 | return const SampleItemListView();
166 | }
167 | },
168 | );
169 | },
170 | );
171 | },
172 | );
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/lib/src/data/data.dart:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | import 'package:muday/src/utils/types.dart';
5 |
6 | final List sampleTransactions = [
7 | Transaction(
8 | id: '1',
9 | title: 'Shopping',
10 | description: 'Buy some grocery',
11 | amount: 120.00,
12 | dateTime: DateTime.now(),
13 | category: 'Shopping',
14 | icon: '🛍️',
15 | isExpense: true,
16 | ),
17 | Transaction(
18 | id: '2',
19 | title: 'Subscription',
20 | description: 'Disney+ Annual Plan',
21 | amount: 80.00,
22 | dateTime: DateTime.now().subtract(Duration(hours: 1)),
23 | category: 'Entertainment',
24 | icon: '📱',
25 | isExpense: true,
26 | ),
27 | Transaction(
28 | id: '3',
29 | title: 'Buy Phone',
30 | description: 'Buy a Samsung S24 Ultra',
31 | amount: 145_000.00,
32 | dateTime: DateTime.now().subtract(Duration(hours: 3)),
33 | category: 'Utilities',
34 | icon: '📱',
35 | isExpense: true,
36 | ),
37 | Transaction(
38 | id: '4',
39 | title: 'Salary',
40 | description: 'Salary for July',
41 | amount: 5000.00,
42 | dateTime: DateTime.now().subtract(Duration(hours: 12)),
43 | category: 'Income',
44 | icon: '💰',
45 | isExpense: false,
46 | ),
47 | Transaction(
48 | id: '5',
49 | title: 'Transportation',
50 | description: 'Charging Tesla',
51 | amount: 18.00,
52 | dateTime: DateTime.now().subtract(Duration(days: 1)),
53 | category: 'Transport',
54 | icon: '🚗',
55 | isExpense: true,
56 | ),
57 | Transaction(
58 | id: '6',
59 | title: 'Buy House',
60 | description: 'Buy a new house',
61 | amount: 1_500_000.00,
62 | dateTime: DateTime.now().subtract(Duration(days: 4)),
63 | category: 'Housing',
64 | icon: '🏠',
65 | isExpense: true,
66 | ),
67 | // ... Add more sample transactions
68 | ] + List.generate(14, (index) => Transaction(
69 | id: (index + 7).toString(),
70 | title: ['Coffee', 'Lunch', 'Dinner', 'Movies', 'Shopping', 'Gas', 'Internet'][index % 7],
71 | description: 'Transaction description',
72 | amount: (index + 1) * 10.0,
73 | dateTime: DateTime.now().subtract(Duration(days: (index + 1) * 7)),
74 | category: ['Food', 'Entertainment', 'Transport', 'Utilities'][index % 4],
75 | icon: ['☕', '🍽️', '🎬', '🛒', '⛽', '🌐'][index % 6],
76 | isExpense: true,
77 | ));
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/lib/src/localization/app_am.arb:
--------------------------------------------------------------------------------
1 | {
2 | "appTitle": "ወጪ መቆጣጠሪያ | ኢትዮጵያ",
3 | "@appTitle": {
4 | "description": "ወጪ መቆጣጠሪያ መተግበሪያ"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/lib/src/localization/app_en.arb:
--------------------------------------------------------------------------------
1 | {
2 | "appTitle": "Expense Tracker | ET",
3 | "@appTitle": {
4 | "description": "Tracking expenses made easy"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/lib/src/models/db.dart:
--------------------------------------------------------------------------------
1 | import 'package:muday/src/models/transaction.dart';
2 | import 'package:path/path.dart';
3 | import 'package:sqflite/sqflite.dart';
4 |
5 | class DatabaseService {
6 | static const _databaseName = "expense_tracker.db";
7 | static const _databaseVersion = 1;
8 | static Database? _database;
9 |
10 | // Singleton pattern
11 | DatabaseService._privateConstructor();
12 | static final DatabaseService instance = DatabaseService._privateConstructor();
13 |
14 | Future get database async {
15 | _database ??= await _initDatabase();
16 | return _database!;
17 | }
18 |
19 | Future _initDatabase() async {
20 | print('Initializing database');
21 | String path = join(await getDatabasesPath(), _databaseName);
22 | return await openDatabase(
23 | path,
24 | version: _databaseVersion,
25 | onCreate: _onCreate,
26 | onUpgrade: _onUpgrade,
27 | );
28 | }
29 |
30 | Future _onCreate(Database db, int version) async {
31 | // User table
32 | await db.execute('''
33 | CREATE TABLE users (
34 | id TEXT PRIMARY KEY,
35 | name TEXT,
36 | email TEXT,
37 | phone TEXT,
38 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP
39 | )
40 | ''');
41 |
42 | // Transaction table
43 | await db.execute(transactionDbCreateQuery);
44 |
45 | // checkpoint table with automatic date and time
46 | await db.execute('''
47 | CREATE TABLE checkpoints (
48 | id TEXT PRIMARY KEY,
49 | name TEXT NOT NULL,
50 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP
51 | )
52 | ''');
53 |
54 | print('Database created');
55 | }
56 |
57 | Future _onUpgrade(Database db, int oldVersion, int newVersion) async {
58 | // Handle database upgrades here
59 | print('Database upgraded from $oldVersion to $newVersion');
60 | }
61 |
62 | // Generic CRUD operations
63 | Future insert(String table, Map row) async {
64 | Database db = await database;
65 | return await db.insert(table, row);
66 | }
67 |
68 | Future>> queryAll(String table) async {
69 | Database db = await database;
70 | return await db.query(table);
71 | }
72 |
73 | Future>> query(
74 | String table, {
75 | String? where,
76 | List? whereArgs,
77 | String? groupBy,
78 | String? having,
79 | String? orderBy,
80 | int? limit,
81 | int? offset,
82 | }) async {
83 | Database db = await database;
84 | return await db.query(
85 | table,
86 | where: where,
87 | whereArgs: whereArgs,
88 | orderBy: orderBy,
89 | limit: limit,
90 | offset: offset,
91 | );
92 | }
93 |
94 | Future update(
95 | String table,
96 | Map row,
97 | String where,
98 | List whereArgs,
99 | ) async {
100 | Database db = await database;
101 | return await db.update(table, row, where: where, whereArgs: whereArgs);
102 | }
103 |
104 | Future delete(
105 | String table, String where, List whereArgs) async {
106 | Database db = await database;
107 | return await db.delete(table, where: where, whereArgs: whereArgs);
108 | }
109 |
110 | // Transaction specific operations
111 | Future>> getTransactions({
112 | String? userId,
113 | DateTime? startDate,
114 | DateTime? endDate,
115 | String? type,
116 | }) async {
117 | Database db = await database;
118 | List whereConditions = [];
119 | List whereArgs = [];
120 |
121 | if (userId != null) {
122 | whereConditions.add('user_id = ?');
123 | whereArgs.add(userId);
124 | }
125 | if (startDate != null) {
126 | whereConditions.add('date >= ?');
127 | whereArgs.add(startDate.toIso8601String());
128 | }
129 | if (endDate != null) {
130 | whereConditions.add('date <= ?');
131 | whereArgs.add(endDate.toIso8601String());
132 | }
133 | if (type != null) {
134 | whereConditions.add('type = ?');
135 | whereArgs.add(type);
136 | }
137 |
138 | String? where =
139 | whereConditions.isEmpty ? null : whereConditions.join(' AND ');
140 |
141 | return await db.query(
142 | 'transactions',
143 | where: where,
144 | whereArgs: whereArgs,
145 | orderBy: 'date DESC',
146 | );
147 | }
148 |
149 | // return all tables in the database
150 | Future>> getTables() async {
151 | Database db = await database;
152 | return await db
153 | .query('sqlite_master', where: 'type = ?', whereArgs: ['table']);
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/lib/src/models/transaction.dart:
--------------------------------------------------------------------------------
1 | class TransactionCls {
2 | final String id;
3 | final TransactionWallet wallet; // enum: CBE, TELEBIRR
4 | final TransactionType type; // enum: DEBITED, CREDITED
5 | final double amount;
6 | final double? serviceFee;
7 | final double? vat;
8 | final DateTime? date;
9 | final String? referenceNumber;
10 |
11 | // Common optional fields from PDF
12 | final String? payer;
13 | final String? payerAccount;
14 | final String? receiver;
15 | final String? receiverAccount;
16 | final String? reason;
17 |
18 | // TeleBirr specific fields
19 | final String? payerAccountType;
20 | final String? payerTinNumber;
21 | final TransactionStatus? status;
22 | final double? stampDuty;
23 | final double? discount;
24 | final String? channel;
25 |
26 | // Computed fields
27 | double get total =>
28 | amount +
29 | (serviceFee ?? 0) +
30 | (vat ?? 0) +
31 | (stampDuty ?? 0) -
32 | (discount ?? 0);
33 |
34 | TransactionCls({
35 | required this.id,
36 | required this.wallet,
37 | required this.type,
38 | required this.amount,
39 | required this.date,
40 | this.serviceFee,
41 | this.vat,
42 | this.referenceNumber,
43 | this.payer,
44 | this.payerAccount,
45 | this.receiver,
46 | this.receiverAccount,
47 | this.reason,
48 | this.payerAccountType,
49 | this.payerTinNumber,
50 | this.status,
51 | this.stampDuty,
52 | this.discount,
53 | this.channel,
54 | });
55 |
56 | // SQLite converter
57 | Map toMap() {
58 | return {
59 | 'id': id,
60 | 'wallet': wallet.toString(),
61 | 'type': type.toString(),
62 | 'amount': amount,
63 | 'service_fee': serviceFee,
64 | 'vat': vat,
65 | 'date': date?.toIso8601String(),
66 | 'reference_number': referenceNumber,
67 | 'payer': payer,
68 | 'payer_account': payerAccount,
69 | 'receiver': receiver,
70 | 'receiver_account': receiverAccount,
71 | 'reason': reason,
72 | 'payer_account_type': payerAccountType,
73 | 'payer_tin_number': payerTinNumber,
74 | 'status': status?.toString(),
75 | 'stamp_duty': stampDuty,
76 | 'discount': discount,
77 | 'channel': channel,
78 | };
79 | }
80 |
81 | // Firebase converter
82 | Map toJson() => toMap();
83 |
84 | // Factory constructor for SQLite
85 | factory TransactionCls.fromMap(Map map) {
86 | return TransactionCls(
87 | id: map['id'] ?? '',
88 | wallet: TransactionWallet.values.firstWhere(
89 | (e) => e.toString() == map['type'],
90 | orElse: () => TransactionWallet.CBE,
91 | ),
92 | type: TransactionType.values.firstWhere(
93 | (e) => e.toString() == map['type'],
94 | orElse: () => TransactionType.DEBITED,
95 | ),
96 | amount: map['amount'] ?? 0.0,
97 | serviceFee: map['service_fee'] ?? 0.0,
98 | vat: map['vat'] ?? 0.0,
99 | date: map['date'] != null ? DateTime.parse(map['date']) : DateTime.now(),
100 | referenceNumber: map['reference_number'] ?? '',
101 | payer: map['payer'] ?? '',
102 | payerAccount: map['payer_account'] ?? '',
103 | receiver: map['receiver'] ?? '',
104 | receiverAccount: map['receiver_account'] ?? '',
105 | reason: map['reason'] ?? '',
106 | payerAccountType: map['payer_account_type'] ?? '',
107 | payerTinNumber: map['payer_tin_number'] ?? '',
108 | status: map['status'] != null
109 | ? TransactionStatus.values.firstWhere(
110 | (e) => e.toString() == map['status'],
111 | orElse: () => TransactionStatus.PENDING,
112 | )
113 | : TransactionStatus.PENDING,
114 | stampDuty: map['stamp_duty'] ?? 0.0,
115 | discount: map['discount'] ?? 0.0,
116 | channel: map['channel'] ?? '',
117 | );
118 | }
119 |
120 | // Factory constructor for Firebase
121 | factory TransactionCls.fromJson(Map json) =>
122 | TransactionCls.fromMap(json);
123 |
124 | // write me a method to calculate the PDF link of the transaction based on the reference number
125 | // if the transaction is CBE, the link should be https://apps.cbe.com.et:100/?id=referenceNumber
126 | // if the transaction is TeleBirr, the link should be https://transactionsinfo.ethiotelecom.et/receipt/referenceNumber
127 | String get pdfLink {
128 | if (wallet == TransactionWallet.CBE) {
129 | return 'https://apps.cbe.com.et:100/?id=$referenceNumber';
130 | } else {
131 | return 'https://transactionsinfo.ethiotelecom.et/receipt/$referenceNumber';
132 | }
133 | }
134 | }
135 |
136 | final transactionDbCreateQuery = '''CREATE TABLE transactions (
137 | id TEXT PRIMARY KEY,
138 | type TEXT NOT NULL,
139 | wallet TEXT NOT NULL,
140 | amount REAL NOT NULL,
141 | service_fee REAL,
142 | vat REAL,
143 | date DATETIME NOT NULL,
144 | reference_number TEXT,
145 | payer TEXT,
146 | payer_account TEXT,
147 | receiver TEXT,
148 | receiver_account TEXT,
149 | reason TEXT,
150 | payer_account_type TEXT,
151 | payer_tin_number TEXT,
152 | status TEXT,
153 | stamp_duty REAL,
154 | discount REAL,
155 | channel TEXT,
156 | user_id TEXT,
157 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
158 | FOREIGN KEY (user_id) REFERENCES users (id)
159 | )
160 | ''';
161 |
162 | enum TransactionWallet { CBE, TELEBIRR }
163 |
164 | enum TransactionStatus { PENDING, COMPLETED, FAILED, CANCELLED }
165 |
166 | enum TransactionType { DEBITED, CREDITED }
167 |
--------------------------------------------------------------------------------
/lib/src/sample_feature/sample_item.dart:
--------------------------------------------------------------------------------
1 | /// A placeholder class that represents an entity or model.
2 | class SampleItem {
3 | const SampleItem(this.id);
4 |
5 | final int id;
6 | }
7 |
--------------------------------------------------------------------------------
/lib/src/sample_feature/sample_item_details_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | /// Displays detailed information about a SampleItem.
4 | class SampleItemDetailsView extends StatelessWidget {
5 | const SampleItemDetailsView({super.key});
6 |
7 | static const routeName = '/sample_item';
8 |
9 | @override
10 | Widget build(BuildContext context) {
11 | return Scaffold(
12 | appBar: AppBar(
13 | title: const Text('Item Details'),
14 | ),
15 | body: const Center(
16 | child: Text('More Information Here'),
17 | ),
18 | );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lib/src/sample_feature/sample_item_list_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../settings/settings_view.dart';
4 | import 'sample_item.dart';
5 | import 'sample_item_details_view.dart';
6 |
7 | /// Displays a list of SampleItems.
8 | class SampleItemListView extends StatelessWidget {
9 | const SampleItemListView({
10 | super.key,
11 | this.items = const [SampleItem(1), SampleItem(2), SampleItem(3)],
12 | });
13 |
14 | static const routeName = '/';
15 |
16 | final List items;
17 |
18 | @override
19 | Widget build(BuildContext context) {
20 | return Scaffold(
21 | appBar: AppBar(
22 | title: const Text('Sample Items'),
23 | actions: [
24 | IconButton(
25 | icon: const Icon(Icons.settings),
26 | onPressed: () {
27 | // Navigate to the settings page. If the user leaves and returns
28 | // to the app after it has been killed while running in the
29 | // background, the navigation stack is restored.
30 | Navigator.restorablePushNamed(context, SettingsView.routeName);
31 | },
32 | ),
33 | ],
34 | ),
35 | bottomNavigationBar: BottomNavigationBar(
36 | backgroundColor: Theme.of(context).navigationBarTheme.backgroundColor,
37 | items: [
38 | BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
39 | BottomNavigationBarItem(
40 | icon: Icon(Icons.compare_arrows), label: 'Transactions'),
41 | BottomNavigationBarItem(icon: Icon(Icons.add), label: 'Add'),
42 | // BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
43 | ],
44 | ),
45 | // add a center floating action button
46 | floatingActionButton: FloatingActionButton(
47 | onPressed: () {
48 | // Add your onPressed code here!
49 | // show pop-up dialog
50 | ScaffoldMessenger.of(context).showSnackBar(
51 | const SnackBar(content: Text('Add Button Pressed')),
52 | );
53 | },
54 | shape: const CircleBorder(),
55 | child: const Icon(Icons.add),
56 | ),
57 | floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
58 | // To work with lists that may contain a large number of items, it’s best
59 | // to use the ListView.builder constructor.
60 | //
61 | // In contrast to the default ListView constructor, which requires
62 | // building all Widgets up front, the ListView.builder constructor lazily
63 | // builds Widgets as they’re scrolled into view.
64 | body: ListView.builder(
65 | // Providing a restorationId allows the ListView to restore the
66 | // scroll position when a user leaves and returns to the app after it
67 | // has been killed while running in the background.
68 | restorationId: 'sampleItemListView',
69 | itemCount: items.length,
70 | itemBuilder: (BuildContext context, int index) {
71 | final item = items[index];
72 |
73 | return ListTile(
74 | title: Text('SampleItem ${item.id}'),
75 | leading: const CircleAvatar(
76 | // Display the Flutter Logo image asset.
77 | foregroundImage: AssetImage('assets/images/flutter_logo.png'),
78 | ),
79 | onTap: () {
80 | // Navigate to the details page. If the user leaves and returns to
81 | // the app after it has been killed while running in the
82 | // background, the navigation stack is restored.
83 | Navigator.restorablePushNamed(
84 | context,
85 | SampleItemDetailsView.routeName,
86 | );
87 | });
88 | },
89 | ),
90 | );
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/lib/src/screen/error.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class ErrorView extends StatelessWidget {
4 | final String message;
5 |
6 | const ErrorView({
7 | super.key,
8 | required this.message,
9 | });
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | return Scaffold(
14 | appBar: AppBar(
15 | title: Text('Error'),
16 | ),
17 | body: Center(
18 | child: Text(message),
19 | ),
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib/src/screen/home/home.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:muday/src/app.dart';
3 | import 'package:muday/src/screen/home/main_screen.dart';
4 | import 'package:muday/src/settings/settings_view.dart';
5 |
6 | class HomeScreen extends StatefulWidget {
7 | const HomeScreen({super.key});
8 |
9 | @override
10 | State createState() => _HomeScreenState();
11 | }
12 |
13 | class _HomeScreenState extends State {
14 | int _selectedIndex = 0;
15 |
16 | void _onItemTapped(int index) {
17 | print('Selected Index: $index');
18 | // Adjust index for FAB
19 | // if (index >= 2) {
20 | // index += 1; // Skip the FAB index
21 | // }
22 |
23 | if (index == 1) {
24 | Navigator.restorablePushNamed(context, Routes.transactionList);
25 | } else if (index == 3) {
26 | Navigator.restorablePushNamed(context, SettingsView.routeName);
27 | }
28 |
29 | setState(() {
30 | _selectedIndex = index;
31 | });
32 | }
33 |
34 | @override
35 | Widget build(BuildContext context) {
36 | return Scaffold(
37 | body: MainScreen(),
38 | floatingActionButton: FloatingActionButton(
39 | onPressed: () {
40 | // Handle add button press
41 | // ScaffoldMessenger.of(context).showSnackBar(
42 | // const SnackBar(content: Text('Add Button Pressed')),
43 | // );
44 | },
45 | shape: const CircleBorder(),
46 | child: Container(
47 | width: 60,
48 | height: 60,
49 | decoration: BoxDecoration(
50 | shape: BoxShape.circle,
51 | // color: Color(0xFF29756F),
52 | ),
53 | child: const Icon(Icons.add, size: 40),
54 | )),
55 | floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
56 | bottomNavigationBar: BottomNavigationBar(
57 | type: BottomNavigationBarType.fixed,
58 | // backgroundColor: Colors.grey.shade300,
59 | selectedItemColor: Theme.of(context).colorScheme.tertiary,
60 | items: [
61 | BottomNavigationBarItem(
62 | icon: Icon(
63 | Icons.home_filled,
64 | // depending on the selected index change the color
65 | // color: _selectedIndex == 0 ? Colors.blue : Colors.grey,
66 | ),
67 | label: 'Home',
68 | ),
69 | const BottomNavigationBarItem(
70 | icon: Icon(Icons.account_balance_wallet),
71 | label: 'Transaction',
72 | ),
73 | const BottomNavigationBarItem(
74 | icon: Icon(Icons.pie_chart),
75 | label: 'Budget',
76 | ),
77 | const BottomNavigationBarItem(
78 | icon: Icon(Icons.person),
79 | label: 'Profile',
80 | ),
81 | ],
82 | currentIndex: _selectedIndex,
83 | onTap: _onItemTapped,
84 | ),
85 | );
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/lib/src/screen/home/recent_transactions.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:muday/src/app.dart';
3 | import 'package:muday/src/models/transaction.dart';
4 | import 'package:muday/src/screen/transactions/transactions_list_item.dart';
5 | import 'package:muday/src/services/transactions/transaction_service.dart';
6 | import 'package:muday/src/utils/helpers.dart';
7 |
8 | class RecentTransactions extends StatefulWidget {
9 | const RecentTransactions({super.key});
10 |
11 | @override
12 | State createState() => _RecentTransactionsState();
13 | }
14 |
15 | class _RecentTransactionsState extends State {
16 | final _transactionService = TransactionService();
17 | List transactions = [];
18 | bool _isLoading = true;
19 | Map> groupedTransactions = {};
20 |
21 | @override
22 | void initState() {
23 | super.initState();
24 | _loadTransactions();
25 | }
26 |
27 | Future _loadTransactions() async {
28 | try {
29 | final fetchedTransactions =
30 | await _transactionService.getAllTransactions(count: 5);
31 | setState(() {
32 | transactions = fetchedTransactions;
33 | _isLoading = false;
34 | _groupTransactions();
35 | });
36 | } catch (e) {
37 | setState(() => _isLoading = false);
38 | // Handle error appropriately
39 | print('Error loading transactions: $e');
40 | }
41 | }
42 |
43 | void _groupTransactions() {
44 | groupedTransactions = {};
45 | for (var transaction in transactions) {
46 | final date = humanReadableDate(transaction.date);
47 | groupedTransactions.putIfAbsent(date, () => []).add(transaction);
48 | }
49 | }
50 |
51 | @override
52 | Widget build(BuildContext context) {
53 | return Column(
54 | children: [
55 | // _buildTransactionHeader(context),
56 | const SizedBox(height: 20),
57 | _isLoading
58 | ? const Center(child: CircularProgressIndicator())
59 | : Expanded(
60 | child: ListView.builder(
61 | itemCount: groupedTransactions.length,
62 | itemBuilder: (context, index) {
63 | final date = groupedTransactions.keys.elementAt(index);
64 | final dateTransactions = groupedTransactions[date]!;
65 |
66 | return Column(
67 | crossAxisAlignment: CrossAxisAlignment.start,
68 | children: [
69 | Padding(
70 | padding: const EdgeInsets.symmetric(vertical: 8),
71 | child: Row(
72 | crossAxisAlignment: CrossAxisAlignment.start,
73 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
74 | children: [
75 | Text(
76 | date,
77 | style: Theme.of(context)
78 | .textTheme
79 | .titleSmall
80 | ?.copyWith(
81 | fontWeight: FontWeight.bold,
82 | ),
83 | ),
84 | if (index == 0)
85 | GestureDetector(
86 | onTap: () => Navigator.pushNamed(
87 | context, '/transactions'),
88 | child: GestureDetector(
89 | onTap: () => Navigator.restorablePushNamed(
90 | context, Routes.transactionList),
91 | child: Text(
92 | 'View All',
93 | style: TextStyle(
94 | fontSize: 18,
95 | fontWeight: FontWeight.normal,
96 | color: Theme.of(context)
97 | .colorScheme
98 | .outline,
99 | ),
100 | ),
101 | ),
102 | ),
103 | ],
104 | ),
105 | ),
106 | ...dateTransactions
107 | .map((transaction) => TransactionListItem(
108 | transaction: transaction,
109 | onTap: () => Navigator.pushNamed(
110 | context,
111 | '/transaction-details',
112 | arguments: {'transaction': transaction},
113 | ),
114 | )),
115 | ],
116 | );
117 | },
118 | ),
119 | ),
120 | ],
121 | );
122 | }
123 |
124 | Widget _buildTransactionHeader(BuildContext context) {
125 | return Row(
126 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
127 | children: [
128 | Text(
129 | 'Transactions History',
130 | style: TextStyle(
131 | fontSize: 18,
132 | fontWeight: FontWeight.bold,
133 | color: Theme.of(context).colorScheme.secondary,
134 | ),
135 | ),
136 | GestureDetector(
137 | onTap: () => Navigator.pushNamed(context, '/transactions'),
138 | child: GestureDetector(
139 | onTap: () =>
140 | Navigator.restorablePushNamed(context, Routes.transactionList),
141 | child: Text(
142 | 'View All',
143 | style: TextStyle(
144 | fontSize: 18,
145 | fontWeight: FontWeight.normal,
146 | color: Theme.of(context).colorScheme.outline,
147 | ),
148 | ),
149 | ),
150 | ),
151 | ],
152 | );
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/lib/src/screen/transactions/transactions_list copy.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:muday/src/data/data.dart';
3 | import 'package:muday/src/utils/helpers.dart';
4 | import 'package:muday/src/utils/types.dart';
5 |
6 | class TransactionListView extends StatelessWidget {
7 | const TransactionListView({super.key});
8 |
9 | String _getGroupTitle(DateTime date) {
10 | final now = DateTime.now();
11 | final difference = now.difference(date);
12 |
13 | // if (difference.inDays == 0) return 'Today';
14 | // if (difference.inDays == 1) return 'Yesterday';
15 | if (difference.inDays < 7) return 'This week';
16 | if (difference.inDays < 30) {
17 | return '${(difference.inDays / 7).floor()} weeks ago';
18 | }
19 | if (difference.inDays < 365) {
20 | return '${(difference.inDays / 30).floor()} months ago';
21 | }
22 | return '${(difference.inDays / 365).floor()} years ago';
23 | }
24 |
25 | @override
26 | Widget build(BuildContext context) {
27 | // Group transactions by date
28 | final groupedTransactions = >{};
29 |
30 | for (var transaction in sampleTransactions) {
31 | final group = _getGroupTitle(transaction.dateTime);
32 | groupedTransactions.putIfAbsent(group, () => []);
33 | groupedTransactions[group]!.add(transaction);
34 | }
35 |
36 | return Scaffold(
37 | appBar: AppBar(
38 | title: Text('Transactions'),
39 | actions: [
40 | IconButton(
41 | icon: Icon(Icons.search),
42 | onPressed: () {
43 | // Implement search functionality
44 | },
45 | ),
46 | ],
47 | ),
48 | body: Column(
49 | children: [
50 | // Financial Report Card
51 | Container(
52 | margin: EdgeInsets.all(16),
53 | padding: EdgeInsets.all(16),
54 | decoration: BoxDecoration(
55 | color: Colors.purple.shade50,
56 | borderRadius: BorderRadius.circular(12),
57 | ),
58 | child: Row(
59 | children: [
60 | Expanded(
61 | child: Text(
62 | 'See your financial report',
63 | style: TextStyle(
64 | color: Colors.purple,
65 | fontWeight: FontWeight.w500,
66 | ),
67 | ),
68 | ),
69 | Icon(
70 | Icons.chevron_right,
71 | color: Colors.purple,
72 | ),
73 | ],
74 | ),
75 | ),
76 | // Transactions List
77 | Expanded(
78 | child: ListView.builder(
79 | itemCount: groupedTransactions.length,
80 | itemBuilder: (context, index) {
81 | final group = groupedTransactions.keys.elementAt(index);
82 | final transactions = groupedTransactions[group]!;
83 |
84 | return Column(
85 | crossAxisAlignment: CrossAxisAlignment.start,
86 | children: [
87 | Padding(
88 | padding:
89 | EdgeInsets.symmetric(horizontal: 16, vertical: 8),
90 | child: Text(
91 | group,
92 | style: Theme.of(context).textTheme.titleSmall?.copyWith(
93 | fontWeight: FontWeight.bold,
94 | ),
95 | ),
96 | ),
97 | ...transactions.map((transaction) => TransactionListItem(
98 | transaction: transaction,
99 | onTap: () {
100 | Navigator.pushNamed(
101 | context,
102 | '/transaction-details',
103 | arguments: {'transaction': transaction},
104 | );
105 | },
106 | )),
107 | ],
108 | );
109 | },
110 | ),
111 | ),
112 | ],
113 | ),
114 | );
115 | }
116 | }
117 |
118 | class TransactionListItem extends StatelessWidget {
119 | final Transaction transaction;
120 | final VoidCallback onTap;
121 |
122 | const TransactionListItem({
123 | super.key,
124 | required this.transaction,
125 | required this.onTap,
126 | });
127 |
128 | @override
129 | Widget build(BuildContext context) {
130 | return InkWell(
131 | onTap: onTap,
132 | child: Padding(
133 | padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
134 | child: Row(
135 | children: [
136 | // Category Icon
137 | Container(
138 | width: 48,
139 | height: 48,
140 | decoration: BoxDecoration(
141 | color: transaction.isExpense
142 | ? Colors.red.shade50
143 | : Colors.green.shade50,
144 | borderRadius: BorderRadius.circular(12),
145 | ),
146 | child: Center(
147 | child: Text(
148 | transaction.icon,
149 | style: TextStyle(fontSize: 24),
150 | ),
151 | ),
152 | ),
153 | SizedBox(width: 12),
154 | // Transaction Details
155 | Expanded(
156 | child: Column(
157 | crossAxisAlignment: CrossAxisAlignment.start,
158 | children: [
159 | Text(
160 | transaction.title,
161 | style: Theme.of(context).textTheme.titleMedium,
162 | ),
163 | Text(
164 | transaction.description,
165 | style: Theme.of(context).textTheme.bodySmall?.copyWith(
166 | color: Colors.grey,
167 | ),
168 | ),
169 | ],
170 | ),
171 | ),
172 | // Amount and Time
173 | Column(
174 | crossAxisAlignment: CrossAxisAlignment.end,
175 | children: [
176 | Text(
177 | '${transaction.isExpense ? "-" : "+"} \$${transaction.amount.toStringAsFixed(2)}',
178 | style: TextStyle(
179 | color: transaction.isExpense ? Colors.red : Colors.green,
180 | fontWeight: FontWeight.bold,
181 | ),
182 | ),
183 | Text(
184 | humanReadableDate(transaction.dateTime),
185 | style: Theme.of(context).textTheme.bodySmall?.copyWith(
186 | color: Colors.grey,
187 | ),
188 | ),
189 | ],
190 | ),
191 | ],
192 | ),
193 | ),
194 | );
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/lib/src/screen/transactions/transactions_list.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:muday/src/models/transaction.dart';
3 | import 'package:muday/src/screen/transactions/transactions_list_item.dart';
4 | import 'package:muday/src/services/transactions/transaction_service.dart';
5 |
6 | class TransactionListView extends StatefulWidget {
7 | const TransactionListView({super.key});
8 |
9 | @override
10 | State createState() => _TransactionListViewState();
11 | }
12 |
13 | class _TransactionListViewState extends State {
14 | final _transactionService = TransactionService();
15 | final ScrollController _scrollController = ScrollController();
16 | List _transactions = [];
17 | bool _isLoading = true;
18 | int _currentOffset = 0;
19 | static const int _pageSize = 20;
20 | int _currentPage = 1;
21 | bool _isLoadingMore = false;
22 | final bool _hasMore = true;
23 |
24 | @override
25 | void initState() {
26 | super.initState();
27 | _loadTransactions();
28 | _scrollController.addListener(_onScroll);
29 | }
30 |
31 | @override
32 | void dispose() {
33 | _scrollController.dispose();
34 | super.dispose();
35 | }
36 |
37 | void _onScroll() {
38 | if (_scrollController.position.pixels >=
39 | _scrollController.position.maxScrollExtent) {
40 | _loadMoreTransactions();
41 | }
42 | }
43 |
44 | Future _loadTransactions() async {
45 | try {
46 | print('Loading initial transactions');
47 | // if last transaction
48 | final transactions = await _transactionService.getAllTransactions(
49 | offset: _currentOffset,
50 | count: _pageSize,
51 | );
52 | print('Loaded transactions: ${transactions.length}');
53 | setState(() {
54 | _transactions = transactions;
55 | _isLoading = false;
56 | _currentOffset += (_currentPage * _pageSize) + 1;
57 | _currentPage++;
58 | });
59 | print('Current Offset: $_currentOffset and Current Page: $_currentPage');
60 | } catch (e) {
61 | print('Error: $e');
62 | setState(() => _isLoading = false);
63 | // Handle error
64 | }
65 | }
66 |
67 | Future _loadMoreTransactions() async {
68 | if (_isLoadingMore) {
69 | print('Already loading more transactions');
70 | return;
71 | }
72 |
73 | try {
74 | setState(() {
75 | _isLoadingMore = true;
76 | });
77 |
78 | // Create a temporary list to hold new transactions
79 | final moreTransactions = await _transactionService.getAllTransactions(
80 | offset: _currentOffset,
81 | count: _pageSize,
82 | );
83 | print('Got: ${moreTransactions.length} transactions');
84 |
85 | // Only update state once with all changes
86 | if (mounted) {
87 | setState(() {
88 | // Use List.addAll() on a new list to avoid concurrent modification
89 | final newTransactions = List.from(_transactions)
90 | ..addAll(moreTransactions);
91 |
92 | // Update the state variables
93 | _transactions.clear();
94 | _transactions.addAll(newTransactions);
95 | _currentOffset += moreTransactions.length;
96 | _isLoadingMore = false;
97 | print('Current Offset: $_currentOffset');
98 | print('Current Transactions: ${_transactions.length}');
99 | });
100 | }
101 | } catch (e) {
102 | print('_loadMoreTransactions Error: ${e.toString()}');
103 | if (mounted) {
104 | setState(() {
105 | _isLoadingMore = false;
106 | });
107 | }
108 | }
109 | }
110 |
111 | String _getGroupTitle(DateTime? date) {
112 | if (date == null) return 'All transactions';
113 | final now = DateTime.now();
114 | final difference = now.difference(date);
115 |
116 | if (difference.inDays < 7) return 'This week';
117 | if (difference.inDays < 30) {
118 | return '${(difference.inDays / 7).floor()} weeks ago';
119 | }
120 | if (difference.inDays < 365) {
121 | return '${(difference.inDays / 30).floor()} months ago';
122 | }
123 | return '${(difference.inDays / 365).floor()} years ago';
124 | }
125 |
126 | @override
127 | Widget build(BuildContext context) {
128 | if (_isLoading) {
129 | return const Center(child: CircularProgressIndicator());
130 | }
131 |
132 | final groupedTransactions = >{};
133 | for (var transaction in _transactions) {
134 | final group = _getGroupTitle(transaction.date);
135 | groupedTransactions.putIfAbsent(group, () => []);
136 | groupedTransactions[group]!.add(transaction);
137 | }
138 |
139 | return Scaffold(
140 | appBar: AppBar(
141 | title: const Text('Transactions'),
142 | actions: [
143 | IconButton(
144 | icon: const Icon(Icons.search),
145 | onPressed: () {
146 | // Implement search
147 | },
148 | ),
149 | ],
150 | ),
151 | body: Column(
152 | children: [
153 | Container(
154 | margin: const EdgeInsets.all(16),
155 | padding: const EdgeInsets.all(16),
156 | decoration: BoxDecoration(
157 | color: Colors.purple.shade50,
158 | borderRadius: BorderRadius.circular(12),
159 | ),
160 | child: const Row(
161 | children: [
162 | Expanded(
163 | child: Text(
164 | 'See your financial report',
165 | style: TextStyle(
166 | color: Colors.purple,
167 | fontWeight: FontWeight.w500,
168 | ),
169 | ),
170 | ),
171 | Icon(Icons.chevron_right, color: Colors.purple),
172 | ],
173 | ),
174 | ),
175 | Expanded(
176 | child: ListView.builder(
177 | controller: _scrollController,
178 | itemCount: groupedTransactions.length,
179 | itemBuilder: (context, index) {
180 | if (index == groupedTransactions.length) {
181 | return Padding(
182 | padding: const EdgeInsets.all(16.0),
183 | child: Center(
184 | child: _isLoadingMore
185 | ? const CircularProgressIndicator()
186 | : TextButton(
187 | onPressed: _loadMoreTransactions,
188 | child: const Text('Load More'),
189 | ),
190 | ),
191 | );
192 | }
193 |
194 | final group = groupedTransactions.keys.elementAt(index);
195 | final transactions = groupedTransactions[group]!;
196 |
197 | return Column(
198 | crossAxisAlignment: CrossAxisAlignment.start,
199 | children: [
200 | Padding(
201 | padding: const EdgeInsets.symmetric(
202 | horizontal: 16, vertical: 8),
203 | child: Text(
204 | group,
205 | style: Theme.of(context).textTheme.titleSmall?.copyWith(
206 | fontWeight: FontWeight.bold,
207 | ),
208 | ),
209 | ),
210 | ...transactions.map((transaction) => TransactionListItem(
211 | transaction: transaction,
212 | onTap: () {
213 | Navigator.pushNamed(
214 | context,
215 | '/transaction-details',
216 | arguments: {'transaction': transaction},
217 | );
218 | },
219 | )),
220 | ],
221 | );
222 | },
223 | ),
224 | ),
225 | ],
226 | ),
227 | );
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/lib/src/screen/transactions/transactions_list_item.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:muday/src/models/transaction.dart';
3 | import 'package:muday/src/utils/helpers.dart';
4 |
5 | class TransactionListItem extends StatelessWidget {
6 | final TransactionCls transaction;
7 | final VoidCallback onTap;
8 |
9 | const TransactionListItem({
10 | super.key,
11 | required this.transaction,
12 | required this.onTap,
13 | });
14 |
15 | @override
16 | Widget build(BuildContext context) {
17 | final bool isExpense = transaction.type == TransactionType.DEBITED;
18 | String title;
19 | if (transaction.type == TransactionType.CREDITED) {
20 | title = transaction.payer ?? 'Unknown Sender';
21 | if (title.isEmpty) {
22 | title = 'Unknown Sender';
23 | }
24 | } else {
25 | title = transaction.receiver ?? 'Unknown Receiver';
26 | if (title.isEmpty) {
27 | title = 'Unknown Receiver';
28 | }
29 | }
30 |
31 | return InkWell(
32 | onTap: onTap,
33 | child: Padding(
34 | padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
35 | child: Row(
36 | children: [
37 | Container(
38 | width: 25,
39 | height: 25,
40 | decoration: BoxDecoration(
41 | color: isExpense ? Colors.red.shade50 : Colors.green.shade50,
42 | borderRadius: BorderRadius.circular(12),
43 | ),
44 | child: Center(
45 | child: Icon(
46 | isExpense
47 | ? Icons.arrow_circle_up_outlined
48 | : Icons.arrow_circle_down_outlined,
49 | color: isExpense ? Colors.red : Colors.green,
50 | ),
51 | ),
52 | ),
53 | const SizedBox(width: 12),
54 | Expanded(
55 | child: Column(
56 | crossAxisAlignment: CrossAxisAlignment.start,
57 | children: [
58 | Text(
59 | title,
60 | style: Theme.of(context).textTheme.titleMedium,
61 | ),
62 | Text(
63 | transaction.reason ?? 'Transaction',
64 | style: Theme.of(context).textTheme.bodySmall?.copyWith(
65 | color: Colors.grey,
66 | ),
67 | ),
68 | ],
69 | ),
70 | ),
71 | Column(
72 | crossAxisAlignment: CrossAxisAlignment.end,
73 | children: [
74 | Text(
75 | 'ETB ${humanReadableNumber(transaction.amount)}',
76 | style: TextStyle(
77 | color: isExpense ? Colors.red : Colors.green,
78 | fontWeight: FontWeight.bold,
79 | ),
80 | ),
81 | Text(
82 | transaction.date.toString().substring(0, 16),
83 | style: Theme.of(context).textTheme.bodySmall?.copyWith(
84 | color: Colors.grey,
85 | ),
86 | ),
87 | ],
88 | ),
89 | ],
90 | ),
91 | ),
92 | );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/lib/src/services/sms/sms_service.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_sms_inbox/flutter_sms_inbox.dart';
2 | import 'package:muday/src/models/transaction.dart';
3 | import 'package:muday/src/services/sms/cbe_sms_parser.dart';
4 | import 'package:permission_handler/permission_handler.dart';
5 |
6 | class SMSService {
7 | final SmsQuery _query = SmsQuery();
8 | final List _targetSenders = ['127', 'CBE'];
9 |
10 | // count is the number of messages to read if -1 read all messages
11 | Future> readMessages(String address,
12 | {int count = -1, start = 0}) async {
13 | var permission = await Permission.sms.status;
14 | if (!permission.isGranted) {
15 | permission = await Permission.sms.request();
16 | if (!permission.isGranted) return [];
17 | }
18 | if (count == -1) {
19 | print('Reading all messages');
20 | return await _query.querySms(
21 | kinds: [SmsQueryKind.inbox],
22 | address: address,
23 | start: start,
24 | count: count == -1 ? 100 : count,
25 | );
26 | } else {
27 | return await _query.querySms(
28 | kinds: [SmsQueryKind.inbox],
29 | address: address,
30 | count: count,
31 | start: start,
32 | );
33 | }
34 | }
35 |
36 | // read all CBE messages and parse them and convert them to TransactionCls and return it as a list
37 | Future> getParsedCBETransaction(
38 | {int count = -1, offset = 0, bool includeReceipt = false}) async {
39 | // print('Reading CBE messages with count: $count');
40 | var messages = await readMessages('CBE', count: count, start: offset);
41 | // print('Total messages: ${messages.length}');
42 | List transactions = [];
43 | for (var message in messages) {
44 | try {
45 | var msg = await CBEReceiptParser.parseMessage(
46 | message.body ?? '', message.date,
47 | includeReceipt: includeReceipt);
48 | if (msg == null) {
49 | print('ERROR: Failed to parse message: ${message.body}');
50 | continue;
51 | }
52 |
53 | transactions.add(TransactionCls(
54 | id: msg['id'] ?? '',
55 | wallet: msg['wallet'] == 'CBE'
56 | ? TransactionWallet.CBE
57 | : TransactionWallet.TELEBIRR,
58 | type: msg['type'] == 'debited'
59 | ? TransactionType.DEBITED
60 | : TransactionType.CREDITED,
61 | amount: double.parse(msg['amount'] ?? '0'),
62 | serviceFee: double.parse(msg['serviceFee'] ?? '0'),
63 | vat: double.parse(msg['vat'] ?? '0'),
64 | date: message.date,
65 | referenceNumber: msg['referenceNumber'],
66 | payer: msg['payer'],
67 | payerAccount: msg['payerAccount'],
68 | receiver: msg['receiver'],
69 | receiverAccount: msg['receiverAccount'],
70 | reason: msg['reason'],
71 | status: TransactionStatus.COMPLETED,
72 | channel: msg['channel'],
73 | ));
74 | } catch (e) {
75 | print('Error parsing message: $e and msg: ${message.body}');
76 | }
77 | }
78 | print('Total transactions: ${transactions.length}');
79 | return transactions;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/lib/src/services/sms/telebirr_receipt_parser.dart:
--------------------------------------------------------------------------------
1 | class TelebirrReceiptParser {
2 | @override
3 | Future