From 7dae53d30489bb54279b791dff6fc61847dfcc93 Mon Sep 17 00:00:00 2001
From: niranjannlc <77341018@nou.edu.np>
Date: Wed, 14 Feb 2024 08:36:30 +0545
Subject: [PATCH] trigger viewmodel image to solve profile pic bug on nav
drawer
---
.gitignore | 47 ++-
README.md | 60 +++-
app/build.gradle | 210 ------------
app/build.gradle.kts | 174 ++++++++++
app/proguard-rules.pro | 2 +-
.../java/org/mifos/mobile/api/BaseURL.kt | 2 +-
.../mobile/api/SelfServiceInterceptor.kt | 4 +-
app/src/main/AndroidManifest.xml | 2 +-
.../org/mifos/mobile/MifosSelfServiceApp.kt | 8 +-
.../mifos/mobile/ui/about/AboutUsScreen.kt | 3 -
.../mobile/ui/activities/HomeActivity.kt | 6 +-
.../mifos/mobile/ui/adapters/FAQAdapter.kt | 97 ------
.../mifos/mobile/ui/fragments/HelpFragment.kt | 212 ------------
.../mobile/ui/fragments/SettingsFragment.kt | 19 +-
.../ui/{activities => help}/HelpActivity.kt | 8 +-
.../org/mifos/mobile/ui/help/HelpFragment.kt | 130 ++++++++
.../org/mifos/mobile/ui/help/HelpScreen.kt | 126 +++++++
.../org/mifos/mobile/ui/help/HelpViewModel.kt | 56 ++++
.../org/mifos/mobile/ui/home/HomeContent.kt | 288 ++++++++++++++++
.../ui/{fragments => home}/HomeOldFragment.kt | 312 +++++-------------
.../org/mifos/mobile/ui/home/HomeScreen.kt | 74 +++++
.../org/mifos/mobile/ui/home/HomeViewModel.kt | 176 ++++++++++
.../org/mifos/mobile/ui/login/LoginScreen.kt | 2 +
.../ui/registration/RegistrationScreen.kt | 2 +
.../ui/user_profile/UserProfileScreen.kt | 8 +-
.../org/mifos/mobile/utils/FaqDiffUtil.kt | 38 ---
.../org/mifos/mobile/utils/HelpUiState.kt | 5 +-
.../org/mifos/mobile/utils/HomeUiState.kt | 13 -
.../org/mifos/mobile/utils/LanguageHelper.kt | 17 +-
.../utils/fcm/RegistrationIntentService.kt | 6 +-
.../mifos/mobile/viewModels/HelpViewModel.kt | 41 ---
.../mifos/mobile/viewModels/HomeViewModel.kt | 124 -------
app/src/main/res/layout/row_faq.xml | 43 ---
.../layout/row_saving_account_transaction.xml | 2 +-
app/src/main/res/values/strings.xml | 4 +
app/src/main/res/xml/settings_preference.xml | 2 +-
.../mobile/viewModels/HelpViewModelTest.kt | 1 +
.../mobile/viewModels/HomeViewModelTest.kt | 1 +
build-logic/README.md | 38 +++
build-logic/convention/build.gradle.kts | 86 +++++
...droidApplicationComposeConventionPlugin.kt | 16 +
.../AndroidApplicationConventionPlugin.kt | 32 ++
...roidApplicationFirebaseConventionPlugin.kt | 39 +++
.../kotlin/AndroidFeatureConventionPlugin.kt | 32 ++
.../kotlin/AndroidHiltConventionPlugin.kt | 25 ++
.../AndroidLibraryComposeConventionPlugin.kt | 17 +
.../kotlin/AndroidLibraryConventionPlugin.kt | 37 +++
.../kotlin/AndroidLintConventionPlugin.kt | 30 ++
.../kotlin/AndroidRoomConventionPlugin.kt | 29 ++
.../kotlin/AndroidTestConventionPlugin.kt | 21 ++
.../main/kotlin/JvmLibraryConventionPlugin.kt | 15 +
.../kotlin/org/mifos/mobile/AndroidCompose.kt | 69 ++++
.../mifos/mobile/AndroidInstrumentedTests.kt | 19 ++
.../main/kotlin/org/mifos/mobile/Badging.kt | 144 ++++++++
.../kotlin/org/mifos/mobile/KotlinAndroid.kt | 75 +++++
.../kotlin/org/mifos/mobile/PrintTestApks.kt | 87 +++++
.../org/mifos/mobile/ProjectExtensions.kt | 9 +
build-logic/gradle.properties | 4 +
build-logic/settings.gradle.kts | 30 ++
build.gradle | 98 ------
build.gradle.kts | 32 ++
config/quality/quality.gradle | 67 ++--
core/build.gradle | 59 ----
.../core/ui/component/MifosUserImage.kt | 41 ---
gradle.properties | 42 ++-
gradle/libs.versions.toml | 148 +++++++++
gradle/wrapper/gradle-wrapper.properties | 2 +-
settings.gradle | 2 -
settings.gradle.kts | 24 ++
{core => ui}/.gitignore | 0
ui/build.gradle.kts | 30 ++
{core => ui}/consumer-rules.pro | 0
{core => ui}/proguard-rules.pro | 2 +-
.../mifos/core/ExampleInstrumentedTest.kt | 0
{core => ui}/src/main/AndroidManifest.xml | 0
.../core/ui/component/AboutUsItemCard.kt | 0
.../mobile/core/ui/component/EmptyDataView.kt | 47 +++
.../mobile/core/ui/component/FaqItemHolder.kt | 86 +++++
.../core/ui/component/MifosHiddenTextRow.kt | 65 ++++
.../mobile/core/ui/component/MifosItemCard.kt | 0
.../mobile/core/ui/component/MifosLinkText.kt | 33 ++
.../core/ui/component/MifosMobileIcon.kt | 0
.../ui/component/MifosOutlinedTextField.kt | 0
.../ui/component/MifosProgressIndicator.kt | 23 ++
.../core/ui/component/MifosSearchTextField.kt | 71 ++++
.../MifosTextButtonWithTopDrawable.kt | 60 ++++
.../core/ui/component/MifosTextUserImage.kt | 44 +++
.../core/ui/component/MifosTitleSearchCard.kt | 81 +++++
.../mobile/core/ui/component/MifosTopBar.kt | 44 +++
.../core/ui/component/MifosUserImage.kt | 40 +++
.../mobile/core/ui/component/NoInternet.kt | 4 +-
.../core/ui/component/UserProfileField.kt | 4 +-
.../core/ui/component/UserProfileTopBar.kt | 0
.../org/mifos/mobile/core/ui/theme/Color.kt | 5 +-
.../org/mifos/mobile/core/ui/theme/Theme.kt | 22 +-
.../org/mifos/mobile/core/ExampleUnitTest.kt | 0
96 files changed, 3036 insertions(+), 1319 deletions(-)
delete mode 100644 app/build.gradle
create mode 100644 app/build.gradle.kts
delete mode 100644 app/src/main/java/org/mifos/mobile/ui/adapters/FAQAdapter.kt
delete mode 100644 app/src/main/java/org/mifos/mobile/ui/fragments/HelpFragment.kt
rename app/src/main/java/org/mifos/mobile/ui/{activities => help}/HelpActivity.kt (78%)
create mode 100644 app/src/main/java/org/mifos/mobile/ui/help/HelpFragment.kt
create mode 100644 app/src/main/java/org/mifos/mobile/ui/help/HelpScreen.kt
create mode 100644 app/src/main/java/org/mifos/mobile/ui/help/HelpViewModel.kt
create mode 100644 app/src/main/java/org/mifos/mobile/ui/home/HomeContent.kt
rename app/src/main/java/org/mifos/mobile/ui/{fragments => home}/HomeOldFragment.kt (50%)
create mode 100644 app/src/main/java/org/mifos/mobile/ui/home/HomeScreen.kt
create mode 100644 app/src/main/java/org/mifos/mobile/ui/home/HomeViewModel.kt
delete mode 100644 app/src/main/java/org/mifos/mobile/utils/FaqDiffUtil.kt
delete mode 100644 app/src/main/java/org/mifos/mobile/utils/HomeUiState.kt
delete mode 100644 app/src/main/java/org/mifos/mobile/viewModels/HelpViewModel.kt
delete mode 100644 app/src/main/java/org/mifos/mobile/viewModels/HomeViewModel.kt
delete mode 100644 app/src/main/res/layout/row_faq.xml
create mode 100644 build-logic/README.md
create mode 100644 build-logic/convention/build.gradle.kts
create mode 100644 build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt
create mode 100644 build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
create mode 100644 build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
create mode 100644 build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt
create mode 100644 build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt
create mode 100644 build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt
create mode 100644 build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
create mode 100644 build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt
create mode 100644 build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt
create mode 100644 build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt
create mode 100644 build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt
create mode 100644 build-logic/convention/src/main/kotlin/org/mifos/mobile/AndroidCompose.kt
create mode 100644 build-logic/convention/src/main/kotlin/org/mifos/mobile/AndroidInstrumentedTests.kt
create mode 100644 build-logic/convention/src/main/kotlin/org/mifos/mobile/Badging.kt
create mode 100644 build-logic/convention/src/main/kotlin/org/mifos/mobile/KotlinAndroid.kt
create mode 100644 build-logic/convention/src/main/kotlin/org/mifos/mobile/PrintTestApks.kt
create mode 100644 build-logic/convention/src/main/kotlin/org/mifos/mobile/ProjectExtensions.kt
create mode 100644 build-logic/gradle.properties
create mode 100644 build-logic/settings.gradle.kts
delete mode 100644 build.gradle
create mode 100644 build.gradle.kts
delete mode 100644 core/build.gradle
delete mode 100644 core/src/main/java/org/mifos/mobile/core/ui/component/MifosUserImage.kt
create mode 100644 gradle/libs.versions.toml
delete mode 100644 settings.gradle
create mode 100644 settings.gradle.kts
rename {core => ui}/.gitignore (100%)
create mode 100644 ui/build.gradle.kts
rename {core => ui}/consumer-rules.pro (100%)
rename {core => ui}/proguard-rules.pro (94%)
rename {core => ui}/src/androidTest/java/org/mifos/mifos/core/ExampleInstrumentedTest.kt (100%)
rename {core => ui}/src/main/AndroidManifest.xml (100%)
rename {core => ui}/src/main/java/org/mifos/mobile/core/ui/component/AboutUsItemCard.kt (100%)
create mode 100644 ui/src/main/java/org/mifos/mobile/core/ui/component/EmptyDataView.kt
create mode 100644 ui/src/main/java/org/mifos/mobile/core/ui/component/FaqItemHolder.kt
create mode 100644 ui/src/main/java/org/mifos/mobile/core/ui/component/MifosHiddenTextRow.kt
rename {core => ui}/src/main/java/org/mifos/mobile/core/ui/component/MifosItemCard.kt (100%)
create mode 100644 ui/src/main/java/org/mifos/mobile/core/ui/component/MifosLinkText.kt
rename {core => ui}/src/main/java/org/mifos/mobile/core/ui/component/MifosMobileIcon.kt (100%)
rename {core => ui}/src/main/java/org/mifos/mobile/core/ui/component/MifosOutlinedTextField.kt (100%)
create mode 100644 ui/src/main/java/org/mifos/mobile/core/ui/component/MifosProgressIndicator.kt
create mode 100644 ui/src/main/java/org/mifos/mobile/core/ui/component/MifosSearchTextField.kt
create mode 100644 ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTextButtonWithTopDrawable.kt
create mode 100644 ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTextUserImage.kt
create mode 100644 ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTitleSearchCard.kt
create mode 100644 ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTopBar.kt
create mode 100644 ui/src/main/java/org/mifos/mobile/core/ui/component/MifosUserImage.kt
rename {core => ui}/src/main/java/org/mifos/mobile/core/ui/component/NoInternet.kt (95%)
rename {core => ui}/src/main/java/org/mifos/mobile/core/ui/component/UserProfileField.kt (96%)
rename {core => ui}/src/main/java/org/mifos/mobile/core/ui/component/UserProfileTopBar.kt (100%)
rename {core => ui}/src/main/java/org/mifos/mobile/core/ui/theme/Color.kt (72%)
rename {core => ui}/src/main/java/org/mifos/mobile/core/ui/theme/Theme.kt (61%)
rename {core => ui}/src/test/java/org/mifos/mobile/core/ExampleUnitTest.kt (100%)
diff --git a/.gitignore b/.gitignore
index 8f91d5adb..3f9ffd26f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,46 @@
.DS_Store
/build
/captures
-.idea/
-app/src/main/res/raw/third_party_licenses
-app/src/main/res/raw/third_party_license_metadata
-app/release/
+.externalNativeBuild
+.idea
+/*.iml
+
+# files for the dex VM
+*.dex
+
+# Java class files
+*.class
+
+# generated files
+bin/
+gen/
+out/
+build/
+
+# Eclipse project files
+.classpath
+.project
+
+# Windows thumbnail db
+.DS_Store
+
+# IDEA/Android Studio project files, because
+# the project can be imported from settings.gradle.kts
+*.iml
+.idea/*
+!.idea/copyright
+# Keep the code styles.
+!/.idea/codeStyles
+/.idea/codeStyles/*
+!/.idea/codeStyles/Project.xml
+!/.idea/codeStyles/codeStyleConfig.xml
+
+# Gradle cache
+.gradle
+
+# Android Studio captures folder
+captures/
+
+/app/app-release.apk
+app/app.iml
+app/manifest-merger-release-report.txt
diff --git a/README.md b/README.md
index 94b9a920a..89ab3e60f 100644
--- a/README.md
+++ b/README.md
@@ -4,11 +4,21 @@
An Android Application built on top of the MifosX Self-Service platform for end-user customers to view/transact on the accounts and loans they hold. Data visible to customers will be a sub-set of what staff can see. This is a native Android Application written in Kotlin.
+## Notice
+
+:warning: We are fully committed to implement [Jetpack Compose](https://developer.android.com/jetpack/compose) and moving ourself to support
+`kotlin multi-platform`. **If you are sending any PR regarding `XML changes` we will `not` consider at this moment but converting XML to jetpack compose are most welcome.** If you sending any PR regarding logical changes in Activity/Fragment you are most welcome.
+
### Status
| Master | Development | Chat |
|------------|-----------------|-----------------|
-| ![Mifos-Mobile CI[Master]](https://github.com/openMF/mifos-mobile/workflows/Workflow%20for%20master/development%20branches/badge.svg?branch=master) | ![Mifos-Mobile CI[Development]](https://github.com/openMF/mifos-mobile/workflows/Workflow%20for%20master/development%20branches/badge.svg?branch=development) |[![Join the chat at https://gitter.im/openMF/self-service-app](https://badges.gitter.im/openMF/self-service-app.svg)](https://gitter.im/openMF/self-service-app?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)|
+| ![Mifos-Mobile CI[Master]](https://github.com/openMF/mifos-mobile/workflows/Workflow%20for%20master/development%20branches/badge.svg?branch=master) | ![Mifos-Mobile CI[Development]](https://github.com/openMF/mifos-mobile/workflows/Workflow%20for%20master/development%20branches/badge.svg?branch=development) |[![Join the chat at https://mifos.slack.com/](https://img.shields.io/badge/Join%20Our%20Community-Slack-blue)](https://mifos.slack.com/)|
+
+
+## Join Us on Slack
+
+Mifos boasts an active and vibrant contributor community, Please join us on [slack](https://mifos.slack.com/). Once you've joined the mifos slack community, please join the `#mifos-mobile` channel to engage with mifos-mobile development. If you encounter any difficulties joining our Slack channel, please don't hesitate to open an issue. This will allow us to assist you promptly or send you an invitation.
## Screenshots
@@ -20,34 +30,41 @@ An Android Application built on top of the MifosX Self-Service platform for end-
This is an OpenSource project and we would be happy to see new contributors. The issues should be raised via the GitHub issue tracker.
For Issue tracker guidelines please click here. All fixes should be proposed via pull requests.
-For pull request guidelines please click here. For commit style guidelines please click here.
+For pull request guidelines please click here. For commit style guidelines please click here.
### Branch Policy
We have the following branches :
- * **development**
- All the contributions should be pushed to this branch. If you're making a contribution,
- you are supposed to make a pull request to _development_.
- Please make sure it passes a build check on Github Workflows CI.
+* **development**
+ All the contributions should be pushed to this branch. If you're making a contribution,
+ you are supposed to make a pull request to _development_.
+ Please make sure it passes a build check on Github Workflows CI.
+
+ It is advisable to clone only the development branch using the following command:
- It is advisable to clone only the development branch using the following command:
+ `git clone -b `
- `git clone -b `
+ With Git 1.7.10 and later, add --single-branch to prevent fetching of all branches. Example, with development branch:
- With Git 1.7.10 and later, add --single-branch to prevent fetching of all branches. Example, with development branch:
+ `git clone -b development --single-branch https://github.com/username/mifos-mobile.git`
- `git clone -b development --single-branch https://github.com/username/mifos-mobile.git`
+* **ui-redesign**
+ All the contributions related to redesigning of the app should be pushed to this branch. If you're making a contribution,
+ you are supposed to make a pull request to _ui-redesign_.
+ Please make sure it passes a build check on Github Workflows CI.
- * **ui-redesign**
- All the contributions related to redesigning of the app should be pushed to this branch. If you're making a contribution,
- you are supposed to make a pull request to _ui-redesign_.
- Please make sure it passes a build check on Github Workflows CI.
+ This branch will be merged with the development branch once the redesign is complete.
- This branch will be merged with the development branch once the redesign is complete.
+* **master**
+ The master branch contains all the stable and bug-free working code. The development branch once complete will be merged with this branch.
- * **master**
- The master branch contains all the stable and bug-free working code. The development branch once complete will be merged with this branch.
+### Demo credentials
+Fineract Instance: gsoc.mifos.community
+
+Username: `mifos`
+
+Password: `password`
### Instruction to get the latest APK
@@ -90,3 +107,12 @@ For Payment Hub usecases, check this [documentation](https://mifos.gitbook.io/do
## Note
The UI design is currently being revamped. New design can be found [here](https://docs.google.com/presentation/d/1yFR19vGlKW-amxzGms8TgPzd1jWkrALPFcaC85EyYpw/edit#slide=id.g6c6ccd991d_0_42)
+
+## Contributors
+
+Special thanks to the incredible code contributors who continue to drive this project forward.
+
+
+
+
+
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index 6c97f265a..000000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,210 +0,0 @@
-apply plugin: 'com.android.application'
-apply plugin: 'com.google.firebase.crashlytics'
-apply plugin: 'com.google.android.gms.oss-licenses-plugin'
-apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-parcelize'
-apply plugin: 'kotlin-kapt'
-apply plugin: 'com.google.dagger.hilt.android'
-
-apply from: '../config/quality/quality.gradle'
-
-android {
- compileSdkVersion rootProject.ext.compileSdkVersion
-
- defaultConfig {
- applicationId "org.mifos.mobile"
- minSdkVersion rootProject.ext.minSdkVersion
- targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 1
- versionName "1.0"
- // A test runner provided by https://code.google.com/p/android-test-kit/
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- vectorDrawables.useSupportLibrary = true
-
- ndk {
- abiFilters "armeabi-v7a", "x86", "x86_64", "arm64-v8a"
- }
-
- multiDexEnabled true
- }
-
- signingConfigs {
- release {
- storeFile file("../default_key_store.jks")
- storePassword "mifos1234"
- keyAlias "mifos-mobile"
- keyPassword "mifos1234"
- }
- }
-
- buildTypes {
- release {
- minifyEnabled false
- signingConfig signingConfigs.release
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
-
-
- sourceSets {
- def commonTestDir = 'src/commonTest/java'
- main {
- java.srcDir commonTestDir
- }
- androidTest {
- java.srcDir commonTestDir
- }
- test {
- java.srcDir commonTestDir
- }
- }
-
- compileOptions {
- incremental = false
- coreLibraryDesugaringEnabled true
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = "1.8"
- }
-
- lintOptions {
- abortOnError false
- disable 'InvalidPackage'
- }
-
- buildFeatures {
- compose true
- }
-
- composeOptions {
- kotlinCompilerExtensionVersion "1.4.4"
- }
-
- buildFeatures {
- viewBinding = true
- }
-
- kapt {
- correctErrorTypes = true
- }
-}
-
-dependencies {
-
- implementation project(':core')
- implementation fileTree(dir: 'libs', include: ['*.jar'])
-
- implementation 'androidx.legacy:legacy-support-v4:1.0.0'
- implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.lifecycleExtensionsVersion"
- implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.lifecycleVersion"
- kapt "com.github.Raizlabs.DBFlow:dbflow-processor:$rootProject.dbflowVersion"
- implementation "com.github.Raizlabs.DBFlow:dbflow-core:$rootProject.dbflowVersion"
- implementation "com.github.Raizlabs.DBFlow:dbflow:$rootProject.dbflowVersion"
- implementation "androidx.appcompat:appcompat:$rootProject.supportLibraryVersion"
- implementation "com.google.android.material:material:$rootProject.designLibraryVersion"
- implementation "androidx.preference:preference:1.0.0"
- implementation "com.google.android.gms:play-services-maps:$rootProject.playServicesVersion"
- implementation "com.google.firebase:firebase-messaging:$rootProject.firebaseMessagingVersion"
- implementation "androidx.recyclerview:recyclerview:$rootProject.recyclerViewVersion"
- implementation "androidx.vectordrawable:vectordrawable:$rootProject.vectorDrawablesVersion"
- implementation "com.google.android.gms:play-services-oss-licenses:$rootProject.oss_licenses"
- implementation "com.isseiaoki:simplecropview:$rootProject.cropviewVersion"
- implementation "androidx.activity:activity-ktx:$activity_version"
- implementation "androidx.fragment:fragment-ktx:$fragment_version"
-
- //Country Code picker
- implementation "com.hbb20:ccp:$rootProject.countryCodePicker"
- implementation 'com.github.ParveshSandila:CountryCodeChooser:1.0'
-
- //Square dependencies
- implementation("com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion") {
- // exclude Retrofit’s OkHttp peer-dependency module and define your own module import
- exclude module: 'okhttp'
- }
- implementation "com.squareup.retrofit2:converter-gson:$rootProject.retrofitVersion"
- implementation "com.squareup.retrofit2:adapter-rxjava2:$rootProject.retrofitVersion"
- implementation "com.squareup.okhttp3:okhttp:$rootProject.okHttp3Version"
- implementation "com.squareup.okhttp3:logging-interceptor:$rootProject.okHttp3Version"
-
- //rxjava Dependencies
- implementation "io.reactivex.rxjava2:rxandroid:$rootProject.rxandroidVersion"
- implementation "io.reactivex.rxjava2:rxjava:$rootProject.rxjavaVersion"
-
- //Butter Knife
- implementation "com.jakewharton:butterknife:$butterKnifeVersion"
- kapt "com.jakewharton:butterknife-compiler:$butterKnifeVersion"
-
- // Firebase Crashlytics dependency
- implementation "com.google.firebase:firebase-crashlytics:$firebaseCrashlyticsVersion"
-
- //Annotation library
- implementation "androidx.annotation:annotation:$rootProject.annotationLibraryVersion"
-
- //qr code
- implementation "com.google.zxing:core:$rootProject.zxingcoreVersion"
- implementation "me.dm7.barcodescanner:zxing:$rootProject.zxingbarcodescannerVersion"
-
- //sweet error dependency
- implementation "com.github.therajanmaurya:Sweet-Error:$rootProject.sweeterrorVersion"
-
- //mifos passcode
- implementation "com.mifos.mobile:mifos-passcode:$mifosPasscodeVersion"
-
- //multidex
- implementation "androidx.multidex:multidex:$rootProject.multiDexVersion"
-
- //TableView
- implementation "com.evrencoskun.library:tableview:$rootProject.tableViewVersion"
-
- //Biometric Authentication
- implementation "androidx.biometric:biometric:$rootProject.biometric"
-
- // Coroutines
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"
- testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$rootProject.coroutinesTest"
-
- // Unit tests dependencies
- testImplementation "junit:junit:$rootProject.jUnitVersion"
- testImplementation "org.mockito:mockito-core:$rootProject.mockitoVersion"
- implementation "org.mockito:mockito-core:$rootProject.mockitoVersion"
- implementation "org.mockito:mockito-android:$rootProject.mockitoVersion"
- androidTestImplementation "junit:junit:$rootProject.jUnitVersion"
- androidTestImplementation "org.mockito:mockito-core:$rootProject.mockitoVersion"
- androidTestImplementation "org.mockito:mockito-android:$rootProject.mockitoVersion"
- androidTestImplementation "androidx.annotation:annotation:1.0.0"
- implementation "androidx.arch.core:core-testing:$rootProject.archCoreVersion"
- androidTestImplementation("androidx.test.espresso:espresso-contrib:$rootProject.espressoVersion") {
- exclude group: 'com.android.support', module: 'appcompat'
- exclude group: 'com.android.support', module: 'support-v4'
- exclude group: 'com.android.support', module: 'recyclerview-v7'
- exclude group: 'com.android.support', module: 'design'
- exclude group: 'com.android.support', module: 'support-annotations'
- }
- androidTestImplementation "androidx.test.espresso:espresso-core:$rootProject.espressoVersion"
- androidTestImplementation "androidx.test:runner:$rootProject.runnerVersion"
- androidTestImplementation "androidx.test:rules:$rootProject.rulesVersion"
-
- implementation 'com.github.rahul-gill.mifos-ui-library:uihouse:alpha-2.1'
- coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
-
- // Hilt
- implementation("com.google.dagger:hilt-android:2.48")
- kapt("com.google.dagger:hilt-android-compiler:2.47")
-
- // Compose BOM
- implementation platform('androidx.compose:compose-bom:2023.08.00')
-
- // Jetpack Compose
- implementation "androidx.compose.material:material:$rootProject.composeVersion"
- implementation "androidx.compose.compiler:compiler:$rootProject.composeCompiler"
- implementation "androidx.compose.ui:ui-tooling-preview:$rootProject.composeVersion"
- implementation "androidx.activity:activity-compose:$rootProject.composeActivity"
- debugImplementation "androidx.compose.ui:ui-tooling:$rootProject.composeVersion"
- implementation "androidx.compose.material3:material3:$rootProject.materialVersion"
- implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$rootProject.lifecycleVersion"
- implementation "androidx.compose.material:material-icons-extended:$rootProject.composeVersion"
-
-}
-apply plugin: 'com.google.gms.google-services'
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 000000000..242a1cf0c
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,174 @@
+plugins {
+ alias(libs.plugins.mifos.android.application)
+ alias(libs.plugins.mifos.android.application.compose)
+ alias(libs.plugins.mifos.android.hilt)
+ alias(libs.plugins.mifos.android.application.firebase)
+ id("com.google.android.gms.oss-licenses-plugin")
+ id("kotlin-parcelize")
+ alias(libs.plugins.roborazzi)
+}
+
+apply(from = "../config/quality/quality.gradle")
+
+android {
+ namespace = "org.mifos.mobile"
+ defaultConfig {
+ applicationId = "org.mifos.mobile"
+ versionCode = 1
+ versionName = "1.0"
+ vectorDrawables.useSupportLibrary = true
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ ndk {
+ abiFilters.addAll(arrayOf("armeabi-v7a", "x86", "x86_64", "arm64-v8a"))
+ }
+ multiDexEnabled = true
+ }
+
+ signingConfigs {
+ create("release") {
+ storeFile = file("../default_key_store.jks")
+ storePassword = "mifos1234"
+ keyAlias = "mifos-mobile"
+ keyPassword = "mifos1234"
+ }
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ isDebuggable = false
+ signingConfig = signingConfigs.getByName("release")
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+
+ sourceSets {
+ val commonTestDir = "src/commonTest/java"
+ getByName("main"){
+ java.srcDir(commonTestDir)
+ }
+ getByName("androidTest"){
+ java.srcDir(commonTestDir)
+ }
+ getByName("test"){
+ java.srcDir(commonTestDir)
+ }
+ }
+
+ buildFeatures {
+ dataBinding = true
+ viewBinding = true
+ compose = true
+ buildConfig = true
+ }
+
+ lint {
+ abortOnError = false
+ disable.add("InvalidPackage")
+ }
+}
+
+dependencies {
+ implementation(projects.ui)
+
+ implementation("androidx.legacy:legacy-support-v4:1.0.0")
+ implementation(libs.androidx.lifecycle.ktx)
+ implementation(libs.androidx.lifecycle.extensions)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ implementation(libs.androidx.preference)
+ implementation(libs.play.services.maps)
+
+ // DBFlow
+ implementation(libs.dbflow)
+ kapt(libs.dbflow.processor)
+ implementation(libs.dbflow.core)
+
+ implementation("androidx.recyclerview:recyclerview:1.2.1")
+ implementation("androidx.vectordrawable:vectordrawable:1.1.0")
+ implementation(libs.google.oss.licenses)
+ implementation("com.isseiaoki:simplecropview:1.1.8")
+ implementation(libs.androidx.activity.ktx)
+ implementation(libs.androidx.fragment.ktx)
+
+ //Country Code picker
+ implementation("com.hbb20:ccp:2.7.2")
+ implementation("com.github.ParveshSandila:CountryCodeChooser:1.0")
+
+ //Square dependencies
+ implementation(libs.squareup.retrofit2) {
+ // exclude Retrofit’s OkHttp peer-dependency module and define your own module import
+ exclude(module = "okhttp")
+ }
+ implementation(libs.squareup.retrofit.adapter.rxjava)
+ implementation(libs.squareup.retrofit.converter.gson)
+ implementation(libs.squareup.okhttp)
+ implementation(libs.squareup.logging.interceptor)
+
+ //rxjava Dependencies
+ implementation(libs.reactivex.rxjava2.android)
+ implementation(libs.reactivex.rxjava2)
+
+ //Butter Knife
+ implementation(libs.jakewharton.butterknife)
+ implementation(libs.jakewharton.compiler)
+
+ //Annotation library
+ implementation("androidx.annotation:annotation:1.1.0")
+
+ //qr code
+ implementation("com.google.zxing:core:3.5.2")
+ implementation("me.dm7.barcodescanner:zxing:1.9.13")
+
+ //sweet error dependency
+ implementation("com.github.therajanmaurya:Sweet-Error:1.0.0")
+
+ //mifos passcode
+ implementation("com.mifos.mobile:mifos-passcode:1.0.0")
+
+ //multidex
+ implementation("androidx.multidex:multidex:2.0.1")
+
+ //TableView
+ implementation("com.evrencoskun.library:tableview:0.8.9.4")
+
+ //Biometric Authentication
+ implementation("androidx.biometric:biometric:1.1.0")
+
+ // Coroutines
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
+
+ // Unit tests dependencies
+ testImplementation(libs.junit)
+ testImplementation("org.mockito:mockito-core:5.4.0")
+ implementation("org.mockito:mockito-core:5.4.0")
+ implementation("org.mockito:mockito-android:5.4.0")
+ androidTestImplementation((libs.junit))
+ androidTestImplementation("org.mockito:mockito-core:5.4.0")
+ androidTestImplementation("org.mockito:mockito-android:5.4.0")
+ androidTestImplementation("androidx.annotation:annotation:1.0.0")
+ implementation("androidx.arch.core:core-testing:2.2.0")
+ androidTestImplementation("androidx.test.espresso:espresso-contrib:3.5.1") {
+
+ }
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+ androidTestImplementation("androidx.test:runner:1.6.0-alpha04")
+ androidTestImplementation("androidx.test:rules:1.6.0-alpha01")
+
+ implementation("com.github.rahul-gill.mifos-ui-library:uihouse:alpha-2.1")
+
+ // Jetpack Compose
+ api(libs.androidx.activity.compose)
+ api(libs.androidx.compose.material3)
+ api(libs.androidx.compose.foundation)
+ api(libs.androidx.compose.foundation.layout)
+ api(libs.androidx.compose.material.iconsExtended)
+ api(libs.androidx.compose.runtime)
+ api(libs.androidx.compose.ui.tooling.preview)
+ api(libs.androidx.compose.ui.util)
+
+ debugApi(libs.androidx.compose.ui.tooling)
+}
+
+
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 0178ce76d..83bf0ff34 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -2,7 +2,7 @@
# By default, the flags in this file are appended to flags specified
# in /Users/ishan/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
-# directive in build.gradle.
+# directive in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
diff --git a/app/src/debug/java/org/mifos/mobile/api/BaseURL.kt b/app/src/debug/java/org/mifos/mobile/api/BaseURL.kt
index 6198ddb07..e95823088 100644
--- a/app/src/debug/java/org/mifos/mobile/api/BaseURL.kt
+++ b/app/src/debug/java/org/mifos/mobile/api/BaseURL.kt
@@ -16,7 +16,7 @@ class BaseURL {
}
companion object {
- const val API_ENDPOINT = "demo.mifos.community"
+ const val API_ENDPOINT = "gsoc.mifos.community"
const val API_PATH = "/fineract-provider/api/v1/"
const val PROTOCOL_HTTPS = "https://"
}
diff --git a/app/src/debug/java/org/mifos/mobile/api/SelfServiceInterceptor.kt b/app/src/debug/java/org/mifos/mobile/api/SelfServiceInterceptor.kt
index 739003e09..bb645bfd5 100644
--- a/app/src/debug/java/org/mifos/mobile/api/SelfServiceInterceptor.kt
+++ b/app/src/debug/java/org/mifos/mobile/api/SelfServiceInterceptor.kt
@@ -20,10 +20,10 @@ class SelfServiceInterceptor(private val preferencesHelper: PreferencesHelper) :
override fun intercept(chain: Interceptor.Chain): Response {
val chainRequest = chain.request()
val builder = chainRequest.newBuilder()
- .header(HEADER_TENANT, preferencesHelper.tenant)
+ .header(HEADER_TENANT, preferencesHelper.tenant!!)
.header(CONTENT_TYPE, "application/json")
if (!TextUtils.isEmpty(preferencesHelper.token)) {
- builder.header(HEADER_AUTH, preferencesHelper.token)
+ builder.header(HEADER_AUTH, preferencesHelper.token!!)
}
val request = builder.build()
return chain.proceed(request)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 533e1b8ba..86895c66d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -119,7 +119,7 @@
android:windowSoftInputMode="adjustResize" />
() {
-
- private var faqArrayList: ArrayList?
- private var alreadySelectedPosition = 0
- private val context: Context
- private lateinit var binding: RowFaqBinding
-
- init {
- faqArrayList = ArrayList()
- this.context = context
- }
-
- fun setFaqArrayList(faqArrayList: ArrayList?) {
- this.faqArrayList = faqArrayList
- alreadySelectedPosition = -1
- }
-
- fun updateList(faqArrayList: java.util.ArrayList?) {
- val diffResult = DiffUtil.calculateDiff(FaqDiffUtil(this.faqArrayList, faqArrayList))
- diffResult.dispatchUpdatesTo(this)
- setFaqArrayList(faqArrayList)
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
- binding = RowFaqBinding.inflate(LayoutInflater.from(parent.context), parent, false)
- return ViewHolder(binding)
- }
-
- override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
- val (question, answer, isSelected) = faqArrayList?.get(position)!!
- (holder as ViewHolder).bind(question, answer, isSelected)
- }
-
- private fun updateView(position: Int) {
- if (alreadySelectedPosition == position) {
- faqArrayList?.get(alreadySelectedPosition)?.isSelected = false
- notifyItemChanged(alreadySelectedPosition)
- alreadySelectedPosition = -1
- return
- }
- if (alreadySelectedPosition != -1) {
- faqArrayList?.get(alreadySelectedPosition)?.isSelected = false
- notifyItemChanged(alreadySelectedPosition)
- }
- faqArrayList?.get(position)?.isSelected = true
- notifyItemChanged(position)
- alreadySelectedPosition = position
- }
-
- override fun getItemCount(): Int {
- return faqArrayList?.size ?: 0
- }
-
- inner class ViewHolder(private val binding: RowFaqBinding) :
- RecyclerView.ViewHolder(binding.root) {
-
- fun bind(question: String?, answer: String?, isSelected: Boolean) {
- binding.tvQs.text = question
- binding.tvAns.text = answer
-
- if (isSelected) {
- binding.tvAns.visibility = View.VISIBLE
- binding.ivArrow.setImageResource(R.drawable.ic_arrow_up)
- } else {
- binding.tvAns.visibility = View.GONE
- binding.ivArrow.setImageResource(R.drawable.ic_arrow_drop_down)
- }
- }
-
- init {
- binding.llFaq.setOnClickListener {
- updateView(adapterPosition)
- }
- }
- }
-}
diff --git a/app/src/main/java/org/mifos/mobile/ui/fragments/HelpFragment.kt b/app/src/main/java/org/mifos/mobile/ui/fragments/HelpFragment.kt
deleted file mode 100644
index c3b6d46f3..000000000
--- a/app/src/main/java/org/mifos/mobile/ui/fragments/HelpFragment.kt
+++ /dev/null
@@ -1,212 +0,0 @@
-package org.mifos.mobile.ui.fragments
-
-import android.app.SearchManager
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.os.Parcelable
-import android.view.LayoutInflater
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Toast
-import androidx.appcompat.widget.SearchView
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import androidx.recyclerview.widget.LinearLayoutManager
-import com.github.therajanmaurya.sweeterror.SweetUIErrorHandler
-import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.launch
-import org.mifos.mobile.R
-import org.mifos.mobile.databinding.FragmentHelpBinding
-import org.mifos.mobile.models.FAQ
-import org.mifos.mobile.ui.activities.base.BaseActivity
-import org.mifos.mobile.ui.adapters.FAQAdapter
-import org.mifos.mobile.ui.fragments.base.BaseFragment
-import org.mifos.mobile.utils.Constants
-import org.mifos.mobile.utils.DividerItemDecoration
-import org.mifos.mobile.utils.HelpUiState
-import org.mifos.mobile.viewModels.HelpViewModel
-import javax.inject.Inject
-
-/*
-~This project is licensed under the open source MPL V2.
-~See https://github.com/openMF/self-service-app/blob/master/LICENSE.md
-*/
-@AndroidEntryPoint
-class HelpFragment : BaseFragment() {
- private var _binding: FragmentHelpBinding? = null
- private val binding get() = _binding!!
-
- @JvmField
- @Inject
- var faqAdapter: FAQAdapter? = null
-
- private val viewModel: HelpViewModel by viewModels()
-
- private var faqArrayList: ArrayList? = null
- private lateinit var sweetUIErrorHandler: SweetUIErrorHandler
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View {
- _binding = FragmentHelpBinding.inflate(inflater, container, false)
- val rootView = binding.root
- setHasOptionsMenu(true)
- setToolbarTitle(getString(R.string.help))
- sweetUIErrorHandler = SweetUIErrorHandler(activity, rootView)
- showUserInterface()
- if (savedInstanceState == null) {
- viewModel.loadFaq(
- context?.resources?.getStringArray(R.array.faq_qs),
- context?.resources?.getStringArray(R.array.faq_ans)
- )
- }
- return rootView
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- viewLifecycleOwner.lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.helpUiState.collect() {
- when (it) {
-
- is HelpUiState.ShowFaq -> {
- showFaq(it.faqArrayList)
- }
-
- HelpUiState.Initial -> {}
- }
- }
- }
- }
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- outState.putParcelableArrayList(Constants.HELP, ArrayList(faqArrayList))
- }
-
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
- if (savedInstanceState != null) {
- val faqs: ArrayList =
- savedInstanceState.getParcelableArrayList(Constants.HELP) ?: arrayListOf()
- showFaq(faqs)
- }
- }
-
- private fun showUserInterface() {
- val layoutManager = LinearLayoutManager(activity)
- layoutManager.orientation = LinearLayoutManager.VERTICAL
- binding.rvFaq.layoutManager = layoutManager
- binding.rvFaq.addItemDecoration(
- DividerItemDecoration(
- activity,
- layoutManager.orientation,
- ),
- )
- binding.callButton.setOnClickListener {
- val intent = Intent(Intent.ACTION_DIAL)
- intent.data = Uri.parse("tel:" + getString(R.string.help_line_number))
- startActivity(intent)
- }
- binding.mailButton.setOnClickListener {
- val intent = Intent(Intent.ACTION_SENDTO).apply {
- data = Uri.parse("mailto:")
- putExtra(Intent.EXTRA_EMAIL, arrayOf(getString(R.string.contact_email)))
- putExtra(Intent.EXTRA_SUBJECT, getString(R.string.user_query))
- }
- try {
- startActivity(intent)
- } catch (e: Exception) {
- Toast.makeText(
- requireContext(),
- getString(R.string.no_app_to_support_action),
- Toast.LENGTH_SHORT,
- ).show()
- }
- }
- binding.locationsButton.setOnClickListener {
- (activity as BaseActivity?)?.replaceFragment(
- LocationsFragment.newInstance(),
- true,
- R.id.container,
- )
- }
- }
-
- private fun showFaq(faqArrayList: ArrayList?) {
- faqAdapter?.setFaqArrayList(faqArrayList)
- binding.rvFaq.adapter = faqAdapter
- this.faqArrayList = faqArrayList
- }
-
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- inflater.inflate(R.menu.menu_help, menu)
- val searchManager = activity?.getSystemService(Context.SEARCH_SERVICE) as SearchManager
- val searchView = menu.findItem(R.id.menu_search_faq).actionView as SearchView
- searchView.setSearchableInfo(searchManager.getSearchableInfo(activity?.componentName))
- searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(query: String): Boolean {
- return false
- }
-
- override fun onQueryTextChange(newText: String): Boolean {
- val filteredFAQList = viewModel.filterList(faqArrayList, newText)
- filteredFAQList.let {
- if (it.isNotEmpty()) {
- sweetUIErrorHandler.hideSweetErrorLayoutUI(
- binding.rvFaq,
- binding.layoutError.clErrorLayout,
- )
- faqAdapter?.updateList(it)
- } else {
- showEmptyFAQUI()
- }
- } ?: showEmptyFAQUI()
- return false
- }
- })
- }
-
- private fun showEmptyFAQUI() {
- sweetUIErrorHandler.showSweetEmptyUI(
- getString(R.string.questions),
- R.drawable.ic_help_black_24dp,
- binding.rvFaq,
- binding.layoutError.clErrorLayout,
- )
- }
-
- fun showProgress() {
- showProgressBar()
- }
-
- fun hideProgress() {
- hideProgressBar()
- }
-
- companion object {
- @JvmStatic
- fun newInstance(): HelpFragment {
- val fragment = HelpFragment()
- val args = Bundle()
- fragment.arguments = args
- return fragment
- }
- }
-
- override fun onDestroyView() {
- _binding = null
- super.onDestroyView()
- }
-}
diff --git a/app/src/main/java/org/mifos/mobile/ui/fragments/SettingsFragment.kt b/app/src/main/java/org/mifos/mobile/ui/fragments/SettingsFragment.kt
index 352a370a9..fc8574053 100644
--- a/app/src/main/java/org/mifos/mobile/ui/fragments/SettingsFragment.kt
+++ b/app/src/main/java/org/mifos/mobile/ui/fragments/SettingsFragment.kt
@@ -21,6 +21,7 @@ import org.mifos.mobile.utils.ConfigurationDialogFragmentCompat
import org.mifos.mobile.utils.ConfigurationPreference
import org.mifos.mobile.utils.Constants
import org.mifos.mobile.utils.LanguageHelper
+import java.util.Locale
/**
* Created by dilpreet on 02/10/17.
@@ -132,7 +133,23 @@ class SettingsFragment : PreferenceFragmentCompat(), OnSharedPreferenceChangeLis
override fun onSharedPreferenceChanged(p0: SharedPreferences?, p1: String?) {
val preference = findPreference(p1)
if (preference is ListPreference) {
- LanguageHelper.setLocale(context, preference.value)
+ val isSystemLanguage =
+ (preference.value == resources.getStringArray(R.array.languages_value)[0])
+ prefsHelper.putBoolean(
+ context?.getString(R.string.default_system_language),
+ isSystemLanguage
+ )
+ if (!isSystemLanguage) {
+ LanguageHelper.setLocale(context, preference.value)
+ } else {
+ if (!resources.getStringArray(R.array.languages_value)
+ .contains(Locale.getDefault().language)
+ ) {
+ LanguageHelper.setLocale(context, "en")
+ } else {
+ LanguageHelper.setLocale(context, Locale.getDefault().language)
+ }
+ }
val intent = Intent(activity, activity?.javaClass)
intent.putExtra(Constants.HAS_SETTINGS_CHANGED, true)
startActivity(intent)
diff --git a/app/src/main/java/org/mifos/mobile/ui/activities/HelpActivity.kt b/app/src/main/java/org/mifos/mobile/ui/help/HelpActivity.kt
similarity index 78%
rename from app/src/main/java/org/mifos/mobile/ui/activities/HelpActivity.kt
rename to app/src/main/java/org/mifos/mobile/ui/help/HelpActivity.kt
index 36f3029c7..788e84ebb 100644
--- a/app/src/main/java/org/mifos/mobile/ui/activities/HelpActivity.kt
+++ b/app/src/main/java/org/mifos/mobile/ui/help/HelpActivity.kt
@@ -1,10 +1,11 @@
-package org.mifos.mobile.ui.activities
+package org.mifos.mobile.ui.help
import android.os.Bundle
+import android.view.View
import org.mifos.mobile.R
import org.mifos.mobile.databinding.ActivityContainerBinding
import org.mifos.mobile.ui.activities.base.BaseActivity
-import org.mifos.mobile.ui.fragments.HelpFragment
+import org.mifos.mobile.ui.help.HelpFragment
/**
* @author Rajan Maurya
@@ -17,8 +18,7 @@ class HelpActivity : BaseActivity() {
super.onCreate(savedInstanceState)
binding = ActivityContainerBinding.inflate(layoutInflater)
setContentView(binding.root)
- setToolbarTitle(getString(R.string.help))
- showBackButton()
+ toolbar?.visibility = View.GONE
replaceFragment(HelpFragment.newInstance(), false, R.id.container)
}
}
diff --git a/app/src/main/java/org/mifos/mobile/ui/help/HelpFragment.kt b/app/src/main/java/org/mifos/mobile/ui/help/HelpFragment.kt
new file mode 100644
index 000000000..994f0f2e7
--- /dev/null
+++ b/app/src/main/java/org/mifos/mobile/ui/help/HelpFragment.kt
@@ -0,0 +1,130 @@
+package org.mifos.mobile.ui.help
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+import org.mifos.mobile.R
+import org.mifos.mobile.core.ui.theme.MifosMobileTheme
+import org.mifos.mobile.models.FAQ
+import org.mifos.mobile.ui.activities.base.BaseActivity
+import org.mifos.mobile.ui.fragments.LocationsFragment
+import org.mifos.mobile.ui.fragments.base.BaseFragment
+import org.mifos.mobile.utils.HelpUiState
+
+/*
+~This project is licensed under the open source MPL V2.
+~See https://github.com/openMF/self-service-app/blob/master/LICENSE.md
+*/
+@AndroidEntryPoint
+class HelpFragment : BaseFragment() {
+
+ private val viewModel: HelpViewModel by viewModels()
+ private var faqArrayList: MutableState> = mutableStateOf(arrayListOf())
+ private var selectedFaqPosition: MutableState = mutableStateOf(-1)
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?,
+ ): View {
+ if (savedInstanceState == null) {
+ viewModel.loadFaq(
+ context?.resources?.getStringArray(R.array.faq_qs),
+ context?.resources?.getStringArray(R.array.faq_ans)
+ )
+ }
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ MifosMobileTheme {
+ HelpScreen(
+ faqArrayList = faqArrayList.value,
+ callNow = { callHelpline() },
+ leaveEmail = { mailHelpline() },
+ findLocations = { findLocations() },
+ navigateBack = { activity?.finish() },
+ onSearchDismiss = { viewModel.loadFaq(qs = null, ans = null) },
+ searchQuery = { viewModel.filterList(it) },
+ selectedFaqPosition = selectedFaqPosition.value,
+ updateFaqPosition = { viewModel.updateSelectedFaqPosition(it) }
+ )
+ }
+ }
+ }
+ }
+
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.helpUiState.collect() {
+ when (it) {
+
+ is HelpUiState.ShowFaq -> {
+ faqArrayList.value = it.faqArrayList
+ selectedFaqPosition.value = it.selectedFaqPosition
+ }
+
+ HelpUiState.Initial -> {}
+ }
+ }
+ }
+ }
+ }
+
+ private fun callHelpline() {
+ val intent = Intent(Intent.ACTION_DIAL)
+ intent.data = Uri.parse("tel:" + getString(R.string.help_line_number))
+ startActivity(intent)
+ }
+
+ private fun findLocations() {
+ (activity as BaseActivity?)?.replaceFragment(
+ LocationsFragment.newInstance(),
+ true,
+ R.id.container,
+ )
+ }
+
+ private fun mailHelpline() {
+ val intent = Intent(Intent.ACTION_SENDTO).apply {
+ data = Uri.parse("mailto:")
+ putExtra(Intent.EXTRA_EMAIL, arrayOf(getString(R.string.contact_email)))
+ putExtra(Intent.EXTRA_SUBJECT, getString(R.string.user_query))
+ }
+ try {
+ startActivity(intent)
+ } catch (e: Exception) {
+ Toast.makeText(
+ requireContext(),
+ getString(R.string.no_app_to_support_action),
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ fun newInstance(): HelpFragment {
+ val fragment = HelpFragment()
+ val args = Bundle()
+ fragment.arguments = args
+ return fragment
+ }
+ }
+}
diff --git a/app/src/main/java/org/mifos/mobile/ui/help/HelpScreen.kt b/app/src/main/java/org/mifos/mobile/ui/help/HelpScreen.kt
new file mode 100644
index 000000000..ca1c9388a
--- /dev/null
+++ b/app/src/main/java/org/mifos/mobile/ui/help/HelpScreen.kt
@@ -0,0 +1,126 @@
+package org.mifos.mobile.ui.help
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.material.icons.filled.Phone
+import androidx.compose.material.icons.outlined.Mail
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import org.mifos.mobile.R
+import org.mifos.mobile.core.ui.component.EmptyDataView
+import org.mifos.mobile.core.ui.component.FaqItemHolder
+import org.mifos.mobile.core.ui.component.MifosTextButtonWithTopDrawable
+import org.mifos.mobile.core.ui.component.MifosTitleSearchCard
+import org.mifos.mobile.core.ui.component.MifosTopBar
+import org.mifos.mobile.models.FAQ
+
+
+@Composable
+fun HelpScreen(
+ faqArrayList: List?,
+ selectedFaqPosition: Int = -1,
+ callNow: () -> Unit,
+ leaveEmail: () -> Unit,
+ findLocations: () -> Unit,
+ navigateBack: () -> Unit,
+ searchQuery: (String) -> Unit,
+ onSearchDismiss: () -> Unit,
+ updateFaqPosition: (Int) -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ ) {
+ MifosTopBar(
+ navigateBack = navigateBack,
+ title = {
+ MifosTitleSearchCard(
+ searchQuery = searchQuery,
+ titleResourceId = R.string.help,
+ onSearchDismiss = onSearchDismiss
+ )
+ }
+ )
+
+ Text(
+ text = stringResource(id = R.string.faq),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 8.dp),
+ style = MaterialTheme.typography.titleMedium,
+ color = if (isSystemInDarkTheme()) Color.White else Color.Black
+ )
+
+ if (!faqArrayList.isNullOrEmpty()) {
+ LazyColumn(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxWidth()
+ ) {
+ itemsIndexed(items = faqArrayList) { index, faqItem ->
+ FaqItemHolder(
+ question = faqItem?.question,
+ answer = faqItem?.answer,
+ isSelected = selectedFaqPosition == index,
+ onItemSelected = { updateFaqPosition(it) },
+ index = index
+ )
+ }
+ }
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp)
+ ) {
+ MifosTextButtonWithTopDrawable(
+ modifier = Modifier
+ .weight(1f),
+ onClick = callNow,
+ textResourceId = R.string.call_now,
+ icon = Icons.Default.Phone,
+ contentDescription = "Phone Icon"
+ )
+ MifosTextButtonWithTopDrawable(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ onClick = leaveEmail,
+ textResourceId = R.string.leave_email,
+ icon = Icons.Outlined.Mail,
+ contentDescription = "Mail Icon"
+ )
+ MifosTextButtonWithTopDrawable(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ onClick = findLocations,
+ textResourceId = R.string.find_locations,
+ icon = Icons.Default.LocationOn,
+ contentDescription = "Location Icon"
+ )
+ }
+ } else {
+ EmptyDataView(
+ modifier = Modifier.fillMaxSize(),
+ icon = R.drawable.ic_help_black_24dp,
+ error = R.string.no_questions_found
+ )
+ }
+
+ }
+}
+
diff --git a/app/src/main/java/org/mifos/mobile/ui/help/HelpViewModel.kt b/app/src/main/java/org/mifos/mobile/ui/help/HelpViewModel.kt
new file mode 100644
index 000000000..b4bb2e1d2
--- /dev/null
+++ b/app/src/main/java/org/mifos/mobile/ui/help/HelpViewModel.kt
@@ -0,0 +1,56 @@
+package org.mifos.mobile.ui.help
+
+import androidx.lifecycle.ViewModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import org.mifos.mobile.models.FAQ
+import org.mifos.mobile.utils.HelpUiState
+import java.util.Locale
+import javax.inject.Inject
+
+@HiltViewModel
+class HelpViewModel @Inject constructor() : ViewModel() {
+
+ private val _helpUiState = MutableStateFlow(HelpUiState.Initial)
+ val helpUiState: StateFlow get() = _helpUiState
+ private var allFaqList: ArrayList? = null
+
+ fun loadFaq(qs: Array?, ans: Array?) {
+ if (allFaqList.isNullOrEmpty()) {
+ val faqArrayList = ArrayList()
+ if (qs != null) {
+ for (i in qs.indices) {
+ faqArrayList.add(FAQ(qs[i], ans?.get(i)))
+ }
+ }
+ allFaqList = faqArrayList
+ _helpUiState.value = HelpUiState.ShowFaq(faqArrayList)
+ } else {
+ _helpUiState.value = HelpUiState.ShowFaq(allFaqList!!)
+ }
+ }
+
+ fun filterList(query: String) {
+ val filteredList = ArrayList()
+ allFaqList?.let { faqList ->
+ for (faq in faqList) {
+ if (faq?.question?.lowercase(Locale.ROOT)
+ ?.contains(query.lowercase(Locale.ROOT)) == true
+ ) {
+ filteredList.add(faq)
+ }
+ }
+ }
+ _helpUiState.value = HelpUiState.ShowFaq(filteredList)
+ }
+
+ fun updateSelectedFaqPosition(position: Int) {
+ val currentState = _helpUiState.value
+ if (currentState is HelpUiState.ShowFaq) {
+ val selectFaqPosition =
+ if (currentState.selectedFaqPosition == position) -1 else position
+ _helpUiState.value = currentState.copy(selectedFaqPosition = selectFaqPosition)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/mifos/mobile/ui/home/HomeContent.kt b/app/src/main/java/org/mifos/mobile/ui/home/HomeContent.kt
new file mode 100644
index 000000000..3c68999ef
--- /dev/null
+++ b/app/src/main/java/org/mifos/mobile/ui/home/HomeContent.kt
@@ -0,0 +1,288 @@
+package org.mifos.mobile.ui.home
+
+import android.graphics.Bitmap
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Divider
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import org.mifos.mobile.R
+import org.mifos.mobile.core.ui.component.MifosHiddenTextRow
+import org.mifos.mobile.core.ui.component.MifosLinkText
+import org.mifos.mobile.core.ui.component.MifosUserImage
+import org.mifos.mobile.core.ui.theme.MifosMobileTheme
+import org.mifos.mobile.utils.CurrencyUtil
+
+@Composable
+fun HomeContent(
+ username: String,
+ totalLoanAmount: Double,
+ totalSavingsAmount: Double,
+ userBitmap: Bitmap?,
+ homeCards: List,
+ userProfile: () -> Unit,
+ totalSavings: () -> Unit,
+ totalLoan: () -> Unit,
+ callHelpline: (String) -> Unit,
+ mailHelpline: (String) -> Unit,
+ homeCardClicked: (HomeCardItem) -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp)
+ .verticalScroll(scrollState)
+ ) {
+ UserDetailsRow(
+ userBitmap = userBitmap,
+ username = username,
+ userProfile = userProfile
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ AccountOverviewCard(
+ totalLoanAmount = totalLoanAmount,
+ totalSavingsAmount = totalSavingsAmount,
+ totalLoan = totalLoan,
+ totalSavings = totalSavings
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ HomeCards(homeCardClicked = homeCardClicked, homeCards = homeCards)
+
+ ContactUsRow(callHelpline = callHelpline, mailHelpline = mailHelpline)
+ }
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+private fun HomeCards(
+ homeCardClicked: (HomeCardItem) -> Unit,
+ homeCards: List
+) {
+ FlowRow(
+ modifier = Modifier.fillMaxWidth(),
+ maxItemsInEachRow = 3,
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ homeCards.forEach { card ->
+ HomeCard(
+ modifier = Modifier
+ .weight(1f)
+ .padding(bottom = 8.dp),
+ titleId = card.titleId,
+ drawableResId = card.drawableResId,
+ onClick = { homeCardClicked(card) }
+ )
+ }
+ }
+}
+
+@Composable
+fun UserDetailsRow(
+ userBitmap: Bitmap?,
+ username: String,
+ userProfile: () -> Unit
+) {
+ val interactionSource = remember { MutableInteractionSource() }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ MifosUserImage(
+ modifier = Modifier
+ .size(84.dp)
+ .clickable(
+ indication = null,
+ interactionSource = interactionSource,
+ ) { userProfile.invoke() },
+ bitmap = userBitmap,
+ username = username
+ )
+ Text(
+ text = stringResource(R.string.hello_client, username),
+ style = MaterialTheme.typography.headlineSmall,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier
+ .padding(horizontal = 20.dp)
+ .fillMaxWidth(1f)
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun HomeCard(
+ modifier: Modifier,
+ titleId: Int,
+ drawableResId: Int,
+ onClick: () -> Unit
+) {
+ Card(
+ modifier = modifier,
+ onClick = { onClick.invoke() }
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ painter = painterResource(id = drawableResId),
+ contentDescription = null,
+ modifier = Modifier.size(56.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Text(
+ text = stringResource(id = titleId),
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ }
+ }
+}
+
+@Composable
+private fun AccountOverviewCard(
+ totalLoanAmount: Double,
+ totalSavingsAmount: Double,
+ totalSavings: () -> Unit,
+ totalLoan: () -> Unit
+) {
+ val context = LocalContext.current
+
+ Row {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth(),
+ colors = CardDefaults.cardColors()
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp)
+ ) {
+ Text(
+ text = stringResource(id = R.string.accounts_overview),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ Divider(modifier = Modifier.padding(top = 8.dp, bottom = 4.dp))
+
+ MifosHiddenTextRow(
+ title = stringResource(id = R.string.total_saving),
+ hiddenText = CurrencyUtil.formatCurrency(context, totalSavingsAmount),
+ hiddenColor = colorResource(id = R.color.deposit_green),
+ hidingText = stringResource(id = R.string.hidden_amount),
+ visibilityIconId = R.drawable.ic_visibility_24px,
+ visibilityOffIconId = R.drawable.ic_visibility_off_24px,
+ onClick = totalSavings
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ MifosHiddenTextRow(
+ title = stringResource(id = R.string.total_loan),
+ hiddenText = CurrencyUtil.formatCurrency(context, totalLoanAmount),
+ hiddenColor = colorResource(id = R.color.red),
+ hidingText = stringResource(id = R.string.hidden_amount),
+ visibilityIconId = R.drawable.ic_visibility_24px,
+ visibilityOffIconId = R.drawable.ic_visibility_off_24px,
+ onClick = totalLoan
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun ContactUsRow(
+ callHelpline: (String) -> Unit,
+ mailHelpline: (String) -> Unit
+) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(20.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = stringResource(id = R.string.need_help),
+ modifier = Modifier.weight(1f),
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.bodyMedium
+ )
+
+ Column {
+ MifosLinkText(
+ text = stringResource(id = R.string.help_line_number),
+ modifier = Modifier.align(Alignment.End),
+ onClick = callHelpline,
+ isUnderlined = false
+ )
+
+ MifosLinkText(
+ text = stringResource(id = R.string.contact_email),
+ modifier = Modifier.align(Alignment.End),
+ onClick = mailHelpline
+ )
+ }
+ }
+}
+
+@Preview(showSystemUi = true)
+@Composable
+fun PreviewHomeContent() {
+ MifosMobileTheme {
+ HomeContent(
+ username = stringResource(id = R.string.app_name),
+ totalLoanAmount = 32.32,
+ totalSavingsAmount = 34.43,
+ userBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888),
+ callHelpline = {},
+ mailHelpline = {},
+ totalSavings = {},
+ totalLoan = {},
+ userProfile = {},
+ homeCardClicked = {},
+ homeCards = listOf()
+ )
+ }
+}
diff --git a/app/src/main/java/org/mifos/mobile/ui/fragments/HomeOldFragment.kt b/app/src/main/java/org/mifos/mobile/ui/home/HomeOldFragment.kt
similarity index 50%
rename from app/src/main/java/org/mifos/mobile/ui/fragments/HomeOldFragment.kt
rename to app/src/main/java/org/mifos/mobile/ui/home/HomeOldFragment.kt
index d1eef0368..2c238c5ce 100644
--- a/app/src/main/java/org/mifos/mobile/ui/fragments/HomeOldFragment.kt
+++ b/app/src/main/java/org/mifos/mobile/ui/home/HomeOldFragment.kt
@@ -1,35 +1,45 @@
-package org.mifos.mobile.ui.fragments
-
-import android.animation.LayoutTransition
-import android.content.*
-import android.graphics.Bitmap
-import android.os.Build
+package org.mifos.mobile.ui.home
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.Uri
import android.os.Bundle
-import android.os.Parcelable
-import android.view.*
-import android.widget.ImageButton
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.View
+import android.view.ViewGroup
import android.widget.TextView
+import android.widget.Toast
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import dagger.hilt.android.AndroidEntryPoint
import org.mifos.mobile.R
import org.mifos.mobile.api.local.PreferencesHelper
-import org.mifos.mobile.databinding.FragmentHomeOldBinding
-import org.mifos.mobile.models.client.Client
+import org.mifos.mobile.core.ui.theme.MifosMobileTheme
import org.mifos.mobile.ui.activities.HomeActivity
import org.mifos.mobile.ui.activities.LoanApplicationActivity
import org.mifos.mobile.ui.activities.NotificationActivity
-import org.mifos.mobile.ui.user_profile.UserProfileActivity
import org.mifos.mobile.ui.activities.base.BaseActivity
import org.mifos.mobile.ui.enums.AccountType
import org.mifos.mobile.ui.enums.ChargeType
+import org.mifos.mobile.ui.fragments.BeneficiaryListFragment
+import org.mifos.mobile.ui.fragments.ClientAccountsFragment
+import org.mifos.mobile.ui.fragments.ClientChargeFragment
+import org.mifos.mobile.ui.fragments.SavingsMakeTransferFragment
+import org.mifos.mobile.ui.fragments.ThirdPartyTransferFragment
import org.mifos.mobile.ui.fragments.base.BaseFragment
-import org.mifos.mobile.ui.getThemeAttributeColor
-import org.mifos.mobile.utils.*
-import org.mifos.mobile.viewModels.HomeViewModel
+import org.mifos.mobile.ui.user_profile.UserProfileActivity
+import org.mifos.mobile.utils.Constants
+import org.mifos.mobile.utils.MaterialDialog
+import org.mifos.mobile.utils.Toaster
import javax.inject.Inject
/**
@@ -37,20 +47,15 @@ import javax.inject.Inject
*/
@AndroidEntryPoint
class HomeOldFragment : BaseFragment(), OnRefreshListener {
- private var _binding: FragmentHomeOldBinding? = null
- private val binding get() = _binding!!
private val viewModel: HomeViewModel by viewModels()
@JvmField
@Inject
var preferencesHelper: PreferencesHelper? = null
- private var totalLoanAmount = 0.0
- private var totalSavingAmount = 0.0
- private var client: Client? = null
+
private var clientId: Long? = 0
private var toolbarView: View? = null
- private var isDetailVisible: Boolean? = false
private var isReceiverRegistered = false
private var tvNotificationCount: TextView? = null
@@ -59,27 +64,40 @@ class HomeOldFragment : BaseFragment(), OnRefreshListener {
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
- _binding = FragmentHomeOldBinding.inflate(inflater, container, false)
- val rootView = binding.root
clientId = preferencesHelper?.clientId
setHasOptionsMenu(true)
- binding.swipeHomeContainer.setColorSchemeResources(
- R.color.blue_light,
- R.color.green_light,
- R.color.orange_light,
- R.color.red_light,
- )
- binding.swipeHomeContainer.setOnRefreshListener(this)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
- binding.llContainer.layoutTransition
- ?.enableTransitionType(LayoutTransition.CHANGING)
- }
- if (savedInstanceState == null) {
- loadClientData()
- }
setToolbarTitle(getString(R.string.home))
showUserInterface()
- return rootView
+ loadClientData()
+
+ return ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ MifosMobileTheme {
+ HomeScreen(
+ homeUiState = viewModel.homeUiState.value,
+ homeCards = viewModel.getHomeCardItems(),
+ callHelpline = { callHelpline() },
+ mailHelpline = { mailHelpline() },
+ totalSavings = { onClickSavings() },
+ totalLoan = { onClickLoan() },
+ userProfile = { userImageClicked() },
+ homeCardClicked = { handleHomeCardClick(it) }
+ )
+ }
+ }
+ }
+ }
+
+ private fun handleHomeCardClick(homeCardItem: HomeCardItem) {
+ when (homeCardItem) {
+ is HomeCardItem.AccountCard -> accountsClicked()
+ is HomeCardItem.BeneficiariesCard -> beneficiaries()
+ is HomeCardItem.ChargesCard -> chargesClicked()
+ is HomeCardItem.LoanCard -> applyForLoan()
+ is HomeCardItem.SurveyCard -> surveys()
+ is HomeCardItem.TransferCard -> transferClicked()
+ }
}
private val notificationReceiver: BroadcastReceiver = object : BroadcastReceiver() {
@@ -128,31 +146,14 @@ class HomeOldFragment : BaseFragment(), OnRefreshListener {
}
}
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- outState.putDouble(Constants.TOTAL_LOAN, totalLoanAmount)
- outState.putDouble(Constants.TOTAL_SAVINGS, totalSavingAmount)
- outState.putParcelable(Constants.USER_DETAILS, client)
- }
-
- override fun onActivityCreated(savedInstanceState: Bundle?) {
- super.onActivityCreated(savedInstanceState)
- if (savedInstanceState != null) {
- showUserDetails(savedInstanceState.getParcelable(Constants.USER_DETAILS) as? Client)
- viewModel.setUserProfile(preferencesHelper?.userProfileImage)
- showLoanAccountDetails(savedInstanceState.getDouble(Constants.TOTAL_LOAN))
- showSavingAccountDetails(savedInstanceState.getDouble(Constants.TOTAL_SAVINGS))
- }
- }
-
override fun onRefresh() {
loadClientData()
}
private fun loadClientData() {
viewModel.loadClientAccountDetails()
- viewModel.userDetails
- viewModel.userImage
+ viewModel.getUserDetails()
+ viewModel.getUserImage()
}
fun showUserInterface() {
@@ -172,16 +173,6 @@ class HomeOldFragment : BaseFragment(), OnRefreshListener {
)
}
- /**
- * Provides `totalLoanAmount` fetched from server
- *
- * @param totalLoanAmount Total Loan amount
- */
- fun showLoanAccountDetails(totalLoanAmount: Double) {
- this.totalLoanAmount = totalLoanAmount
- binding.tvLoanTotalAmount.text = CurrencyUtil.formatCurrency(context, totalLoanAmount)
- }
-
/**
* Open LOAN tab under ClientAccountsFragment
*/
@@ -190,16 +181,6 @@ class HomeOldFragment : BaseFragment(), OnRefreshListener {
(activity as HomeActivity?)?.setNavigationViewSelectedItem(R.id.item_accounts)
}
- /**
- * Provides `totalSavingAmount` fetched from server
- *
- * @param totalSavingAmount Total Saving amount
- */
- fun showSavingAccountDetails(totalSavingAmount: Double) {
- this.totalSavingAmount = totalSavingAmount
- binding.tvSavingTotalAmount.text = CurrencyUtil.formatCurrency(context, totalSavingAmount)
- }
-
/**
* Open SAVINGS tab under ClientAccountsFragment
*/
@@ -208,143 +189,35 @@ class HomeOldFragment : BaseFragment(), OnRefreshListener {
(activity as HomeActivity?)?.setNavigationViewSelectedItem(R.id.item_accounts)
}
- /**
- * Fetches Client details and display clientName
- *
- * @param client Details about client
- */
- fun showUserDetails(client: Client?) {
- this.client = client
- binding.tvUserName.text = getString(R.string.hello_client, client?.displayName)
- }
-
- /**
- * Provides with Client image fetched from server
- *
- * @param bitmap Client Image
- */
- fun showUserImage(bitmap: Bitmap?) {
- activity?.runOnUiThread {
- if (bitmap != null) {
- binding.ivCircularUserImage.visibility = View.VISIBLE
- binding.ivCircularUserImage.setImageBitmap(bitmap)
- } else {
- val userName = preferencesHelper?.clientName.let { savedName ->
- if (savedName.isNullOrBlank()) {
- getString(R.string.app_name)
- } else {
- savedName
- }
- }
-
- val drawable = TextDrawable.builder()
- .beginConfig()
- .toUpperCase()
- .endConfig()
- .buildRound(
- userName.substring(0, 1),
- requireContext().getThemeAttributeColor(R.attr.colorPrimary),
- )
- binding.ivCircularUserImage.setImageDrawable(drawable)
- }
- }
- }
-
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
-
lifecycleScope.launchWhenStarted {
- viewModel.homeUiState.collect {
- when (it) {
- is HomeUiState.Loading -> showProgress()
- is HomeUiState.UserImage -> {
- hideProgress()
- showUserImage(it.image)
- }
- is HomeUiState.ClientAccountDetails -> {
- hideProgress()
- showLoanAccountDetails(it.loanAccounts)
- showSavingAccountDetails(it.savingsAccounts)
- }
- is HomeUiState.Error -> {
- hideProgress()
- showError(getString(it.errorMessage))
- }
- is HomeUiState.UserDetails -> {
- hideProgress()
- showUserDetails(it.client)
- }
- is HomeUiState.UnreadNotificationsCount -> {
- hideProgress()
- showNotificationCount(it.count)
- }
- }
+ viewModel.notificationsCount.collect { count ->
+ showNotificationCount(count)
}
}
+ }
- toggleVisibilityButton(
- binding.btnSavingTotalAmountVisibility,
- binding.tvSavingTotalAmount,
- binding.tvSavingTotalAmountHidden,
- )
- toggleVisibilityButton(
- binding.btnLoanAmountVisibility,
- binding.tvLoanTotalAmount,
- binding.tvLoanTotalAmountHidden,
- )
-
- binding.llTotalLoan.setOnClickListener {
- onClickLoan()
- }
-
- binding.llTotalSavings.setOnClickListener {
- onClickSavings()
- }
-
- binding.ivCircularUserImage.setOnClickListener {
- userImageClicked()
- }
-
- binding.llAccounts.setOnClickListener {
- accountsClicked()
- }
-
- binding.llTransfer.setOnClickListener {
- transferClicked()
- }
-
- binding.llCharges.setOnClickListener {
- chargesClicked()
- }
-
- binding.llApplyForLoan.setOnClickListener {
- applyForLoan()
- }
-
- binding.llBeneficiaries.setOnClickListener {
- beneficiaries()
- }
-
- binding.llSurveys.setOnClickListener {
- surveys()
- }
+ private fun callHelpline() {
+ val intent = Intent(Intent.ACTION_DIAL)
+ intent.data = Uri.parse("tel:" + getString(R.string.help_line_number))
+ startActivity(intent)
}
- private fun toggleVisibilityButton(
- button: ImageButton,
- visibleView: View,
- hiddenView: View,
- ) {
- button.setOnClickListener {
- if (visibleView.visibility == View.VISIBLE) {
- visibleView.visibility = View.GONE
- hiddenView.visibility = View.VISIBLE
- button.setImageResource(R.drawable.ic_visibility_24px)
- } else {
- visibleView.visibility = View.VISIBLE
- hiddenView.visibility = View.GONE
- button.setImageResource(R.drawable.ic_visibility_off_24px)
- }
+ private fun mailHelpline() {
+ val intent = Intent(Intent.ACTION_SENDTO).apply {
+ data = Uri.parse("mailto:")
+ putExtra(Intent.EXTRA_EMAIL, arrayOf(getString(R.string.contact_email)))
+ putExtra(Intent.EXTRA_SUBJECT, getString(R.string.user_query))
+ }
+ try {
+ startActivity(intent)
+ } catch (e: Exception) {
+ Toast.makeText(
+ requireContext(),
+ getString(R.string.no_app_to_support_action),
+ Toast.LENGTH_SHORT,
+ ).show()
}
}
@@ -445,30 +318,7 @@ class HomeOldFragment : BaseFragment(), OnRefreshListener {
if (checkedItem == R.id.item_about_us || checkedItem == R.id.item_help || checkedItem == R.id.item_settings) {
return
}
- Toaster.show(binding.root, errorMessage)
- }
-
- /**
- * Shows [SwipeRefreshLayout]
- */
- fun showProgress() {
- binding.swipeHomeContainer.isRefreshing = true
- }
-
- /**
- * Hides [SwipeRefreshLayout]
- */
- fun hideProgress() {
- binding.swipeHomeContainer.isRefreshing = false
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- if (binding.swipeHomeContainer.isRefreshing) {
- binding.swipeHomeContainer.isRefreshing = false
- binding.swipeHomeContainer.removeAllViews()
- }
- _binding = null
+ Toaster.show(view, errorMessage)
}
companion object {
diff --git a/app/src/main/java/org/mifos/mobile/ui/home/HomeScreen.kt b/app/src/main/java/org/mifos/mobile/ui/home/HomeScreen.kt
new file mode 100644
index 000000000..1b256fd8f
--- /dev/null
+++ b/app/src/main/java/org/mifos/mobile/ui/home/HomeScreen.kt
@@ -0,0 +1,74 @@
+package org.mifos.mobile.ui.home
+
+import android.graphics.Bitmap
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import org.mifos.mobile.R
+import org.mifos.mobile.core.ui.component.EmptyDataView
+import org.mifos.mobile.core.ui.component.MifosProgressIndicator
+import org.mifos.mobile.core.ui.theme.MifosMobileTheme
+
+
+@Composable
+fun HomeScreen(
+ homeUiState: HomeUiState,
+ userProfile: () -> Unit,
+ totalSavings: () -> Unit,
+ totalLoan: () -> Unit,
+ callHelpline: (String) -> Unit,
+ mailHelpline: (String) -> Unit,
+ homeCardClicked: (HomeCardItem) -> Unit,
+ homeCards: List
+) {
+ when (homeUiState) {
+ is HomeUiState.Success -> {
+ HomeContent(
+ username = homeUiState.homeState.username ?: "",
+ totalLoanAmount = homeUiState.homeState.loanAmount,
+ totalSavingsAmount = homeUiState.homeState.savingsAmount,
+ userBitmap = homeUiState.homeState.image,
+ homeCards = homeCards,
+ userProfile = userProfile,
+ totalSavings = totalSavings,
+ totalLoan = totalLoan,
+ callHelpline = callHelpline,
+ mailHelpline = mailHelpline,
+ homeCardClicked = homeCardClicked
+ )
+ }
+
+ is HomeUiState.Loading -> {
+ MifosProgressIndicator(modifier = Modifier.fillMaxSize())
+ }
+
+ is HomeUiState.Error -> {
+ EmptyDataView(icon = R.drawable.ic_error_black_24dp, error = homeUiState.errorMessage)
+ }
+ }
+
+}
+
+@Preview(showSystemUi = true)
+@Composable
+fun HomeScreenPreview() {
+ val homeState = HomeState(
+ username = "",
+ savingsAmount = 34.43,
+ loanAmount = 34.45,
+ image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888),
+ )
+ MifosMobileTheme {
+ HomeScreen(
+ homeUiState = HomeUiState.Success(homeState),
+ callHelpline = {},
+ mailHelpline = {},
+ totalSavings = {},
+ totalLoan = {},
+ userProfile = {},
+ homeCardClicked = {},
+ homeCards = listOf()
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/mifos/mobile/ui/home/HomeViewModel.kt b/app/src/main/java/org/mifos/mobile/ui/home/HomeViewModel.kt
new file mode 100644
index 000000000..b4ecbeee7
--- /dev/null
+++ b/app/src/main/java/org/mifos/mobile/ui/home/HomeViewModel.kt
@@ -0,0 +1,176 @@
+package org.mifos.mobile.ui.home
+
+import android.graphics.Bitmap
+import android.util.Base64
+import android.util.Log
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.launch
+import org.mifos.mobile.R
+import org.mifos.mobile.api.local.PreferencesHelper
+import org.mifos.mobile.models.accounts.loan.LoanAccount
+import org.mifos.mobile.models.accounts.savings.SavingAccount
+import org.mifos.mobile.repositories.HomeRepository
+import org.mifos.mobile.utils.ImageUtil
+import javax.inject.Inject
+
+@HiltViewModel
+class HomeViewModel @Inject constructor(private val homeRepositoryImp: HomeRepository) :
+ ViewModel() {
+
+ @Inject
+ lateinit var preferencesHelper: PreferencesHelper
+
+ private val _homeUiState = mutableStateOf(HomeUiState.Loading)
+ val homeUiState: State get() = _homeUiState
+
+ private val _notificationsCount = MutableStateFlow(0)
+ val notificationsCount: StateFlow get() = _notificationsCount
+
+ fun loadClientAccountDetails() {
+ viewModelScope.launch {
+ homeRepositoryImp.clientAccounts().catch {
+ _homeUiState.value = HomeUiState.Error(R.string.no_internet_connection)
+ }.collect { clientAccounts ->
+ var currentState = (_homeUiState.value as? HomeUiState.Success)?.homeState ?: HomeState()
+ currentState = currentState.copy(
+ loanAmount = getLoanAccountDetails(clientAccounts.loanAccounts),
+ savingsAmount = getSavingAccountDetails(clientAccounts.savingsAccounts)
+ )
+ _homeUiState.value = HomeUiState.Success(currentState)
+ }
+ }
+ }
+
+ fun getUserDetails() {
+ viewModelScope.launch {
+ homeRepositoryImp.currentClient().catch {
+ _homeUiState.value = HomeUiState.Error(R.string.error_fetching_client)
+ }.collect { client ->
+ preferencesHelper.officeName = client.officeName
+ var currentState = (_homeUiState.value as? HomeUiState.Success)?.homeState ?: HomeState()
+ currentState = currentState.copy(username = client.displayName)
+ _homeUiState.value = HomeUiState.Success(currentState)
+ }
+ }
+ }
+
+ fun getUserImage() {
+ viewModelScope.launch {
+ setUserProfile(preferencesHelper.userProfileImage)
+ homeRepositoryImp.clientImage().catch {
+ Log.e("Client Image Exception", it.message ?: "")
+ }.collect {
+ val encodedString = it.string()
+ val pureBase64Encoded =
+ encodedString.substring(encodedString.indexOf(',') + 1)
+ preferencesHelper.userProfileImage = pureBase64Encoded
+ setUserProfile(pureBase64Encoded)
+ }
+ }
+ }
+
+
+ private fun setUserProfile(image: String?) {
+ if (image == null) {
+ return
+ }
+ val decodedBytes = Base64.decode(image, Base64.DEFAULT)
+ val decodedBitmap = ImageUtil.instance?.compressImage(decodedBytes)
+ var currentState = (_homeUiState.value as? HomeUiState.Success)?.homeState ?: HomeState()
+ currentState = currentState.copy(image = decodedBitmap)
+ _homeUiState.value = HomeUiState.Success(currentState)
+ }
+
+ val unreadNotificationsCount: Unit
+ get() {
+ viewModelScope.launch {
+ homeRepositoryImp.unreadNotificationsCount().catch {
+ _notificationsCount.value = 0
+ }.collect { integer ->
+ _notificationsCount.value = integer
+ }
+ }
+ }
+
+
+ /**
+ * Returns total Loan balance
+ *
+ * @param loanAccountList [List] of [LoanAccount] associated with the client
+ * @return Returns `totalAmount` which is calculated by adding all [LoanAccount]
+ * balance.
+ */
+ private fun getLoanAccountDetails(loanAccountList: List): Double {
+ var totalAmount = 0.0
+ for ((_, _, _, _, _, _, _, _, _, _, _, _, _, _, loanBalance) in loanAccountList) {
+ totalAmount += loanBalance
+ }
+ return totalAmount
+ }
+
+ /**
+ * Returns total Savings balance
+ *
+ * @param savingAccountList [List] of [SavingAccount] associated with the client
+ * @return Returns `totalAmount` which is calculated by adding all [SavingAccount]
+ * balance.
+ */
+ private fun getSavingAccountDetails(savingAccountList: List?): Double {
+ var totalAmount = 0.0
+ for ((_, _, _, _, _, accountBalance) in savingAccountList!!) {
+ totalAmount += accountBalance
+ }
+ return totalAmount
+ }
+
+ fun getHomeCardItems(): List {
+ return listOf(
+ HomeCardItem.AccountCard,
+ HomeCardItem.TransferCard,
+ HomeCardItem.ChargesCard,
+ HomeCardItem.LoanCard,
+ HomeCardItem.BeneficiariesCard,
+ HomeCardItem.SurveyCard
+ )
+ }
+}
+
+sealed class HomeCardItem(
+ val titleId: Int,
+ val drawableResId: Int
+) {
+ data object AccountCard :
+ HomeCardItem(R.string.accounts, R.drawable.ic_account_balance_black_24dp)
+
+ data object TransferCard :
+ HomeCardItem(R.string.transfer, R.drawable.ic_compare_arrows_black_24dp)
+
+ data object ChargesCard :
+ HomeCardItem(R.string.charges, R.drawable.ic_account_balance_wallet_black_24dp)
+
+ data object LoanCard : HomeCardItem(R.string.apply_for_loan, R.drawable.ic_loan)
+ data object BeneficiariesCard :
+ HomeCardItem(R.string.beneficiaries, R.drawable.ic_beneficiaries_48px)
+
+ data object SurveyCard : HomeCardItem(R.string.survey, R.drawable.ic_surveys_48px)
+}
+
+sealed class HomeUiState {
+ data object Loading : HomeUiState()
+ data class Error(val errorMessage: Int) : HomeUiState()
+ data class Success(val homeState: HomeState) : HomeUiState()
+}
+
+data class HomeState(
+ val username: String? = "",
+ val image: Bitmap? = null,
+ val loanAmount: Double = 0.0,
+ val savingsAmount: Double = 0.0
+)
\ No newline at end of file
diff --git a/app/src/main/java/org/mifos/mobile/ui/login/LoginScreen.kt b/app/src/main/java/org/mifos/mobile/ui/login/LoginScreen.kt
index c81d04aec..af1eaa45d 100644
--- a/app/src/main/java/org/mifos/mobile/ui/login/LoginScreen.kt
+++ b/app/src/main/java/org/mifos/mobile/ui/login/LoginScreen.kt
@@ -31,6 +31,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
@@ -46,6 +47,7 @@ import org.mifos.mobile.R
import org.mifos.mobile.core.ui.component.MifosMobileIcon
import org.mifos.mobile.core.ui.component.MifosOutlinedTextField
+@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun LoginScreen(
login: (username: String, password: String) -> Unit,
diff --git a/app/src/main/java/org/mifos/mobile/ui/registration/RegistrationScreen.kt b/app/src/main/java/org/mifos/mobile/ui/registration/RegistrationScreen.kt
index 55f6b0f1e..da1acd358 100644
--- a/app/src/main/java/org/mifos/mobile/ui/registration/RegistrationScreen.kt
+++ b/app/src/main/java/org/mifos/mobile/ui/registration/RegistrationScreen.kt
@@ -29,6 +29,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
@@ -49,6 +50,7 @@ import org.mifos.mobile.core.ui.component.MifosOutlinedTextField
* @since 28/12/2023
*/
+@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun RegistrationScreen(
register: (accountNumber: String, username: String, firstName: String, lastName: String, phoneNumber: String, email: String, password: String, authMode: String, countryCode: String) -> Unit,
diff --git a/app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileScreen.kt b/app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileScreen.kt
index 2c804913b..56f9cd909 100644
--- a/app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileScreen.kt
+++ b/app/src/main/java/org/mifos/mobile/ui/user_profile/UserProfileScreen.kt
@@ -9,9 +9,10 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.Divider
+import androidx.compose.material3.Divider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -58,7 +59,10 @@ fun UserProfileScreen(
.padding(top = 100.dp, bottom = 20.dp),
horizontalArrangement = Arrangement.Center
) {
- MifosUserImage(isDarkTheme = isSystemInDarkTheme(), bitmap = bitmap)
+ MifosUserImage(
+ bitmap = bitmap,
+ modifier = Modifier.size(100.dp)
+ )
}
Divider(color = Color(0xFF8E9099))
userDetails.userName?.let { UserProfileField(label = R.string.username, value = it) }
diff --git a/app/src/main/java/org/mifos/mobile/utils/FaqDiffUtil.kt b/app/src/main/java/org/mifos/mobile/utils/FaqDiffUtil.kt
deleted file mode 100644
index 78b23b7af..000000000
--- a/app/src/main/java/org/mifos/mobile/utils/FaqDiffUtil.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package org.mifos.mobile.utils
-
-import androidx.recyclerview.widget.DiffUtil
-import org.mifos.mobile.models.FAQ
-
-/**
- * Created by dilpreet on 12/8/17.
- */
-class FaqDiffUtil(private val oldFaq: ArrayList?, private val newFaq: ArrayList?) :
- DiffUtil.Callback() {
- override fun getOldListSize(): Int {
- return if (oldFaq?.size != null) {
- oldFaq.size
- } else {
- 0
- }
- }
-
- override fun getNewListSize(): Int {
- return if (newFaq?.size != null) {
- newFaq.size
- } else {
- 0
- }
- }
-
- override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
- return newFaq?.get(newItemPosition)?.answer?.let {
- oldFaq?.get(oldItemPosition)?.question?.compareTo(
- it,
- )
- } == 0
- }
-
- override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
- return oldFaq?.get(oldItemPosition) == newFaq?.get(newItemPosition)
- }
-}
diff --git a/app/src/main/java/org/mifos/mobile/utils/HelpUiState.kt b/app/src/main/java/org/mifos/mobile/utils/HelpUiState.kt
index ff5316fc4..c89e4dd93 100644
--- a/app/src/main/java/org/mifos/mobile/utils/HelpUiState.kt
+++ b/app/src/main/java/org/mifos/mobile/utils/HelpUiState.kt
@@ -4,5 +4,8 @@ import org.mifos.mobile.models.FAQ
sealed class HelpUiState {
object Initial : HelpUiState()
- data class ShowFaq(val faqArrayList: ArrayList) : HelpUiState()
+ data class ShowFaq(
+ val faqArrayList: ArrayList,
+ val selectedFaqPosition: Int = -1
+ ) : HelpUiState()
}
\ No newline at end of file
diff --git a/app/src/main/java/org/mifos/mobile/utils/HomeUiState.kt b/app/src/main/java/org/mifos/mobile/utils/HomeUiState.kt
deleted file mode 100644
index 80f6c4687..000000000
--- a/app/src/main/java/org/mifos/mobile/utils/HomeUiState.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.mifos.mobile.utils
-
-import android.graphics.Bitmap
-import org.mifos.mobile.models.client.Client
-
-sealed class HomeUiState {
- object Loading : HomeUiState()
- data class Error(val errorMessage: Int) : HomeUiState()
- data class ClientAccountDetails(val loanAccounts: Double, val savingsAccounts: Double) : HomeUiState()
- data class UserDetails(val client: Client) : HomeUiState()
- data class UserImage(val image: Bitmap?) : HomeUiState()
- data class UnreadNotificationsCount(val count: Int) : HomeUiState()
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/mifos/mobile/utils/LanguageHelper.kt b/app/src/main/java/org/mifos/mobile/utils/LanguageHelper.kt
index d10bd1522..df2493ebf 100644
--- a/app/src/main/java/org/mifos/mobile/utils/LanguageHelper.kt
+++ b/app/src/main/java/org/mifos/mobile/utils/LanguageHelper.kt
@@ -13,8 +13,17 @@ import java.util.*
object LanguageHelper {
// https://gunhansancar.com/change-language-programmatically-in-android/
fun onAttach(context: Context): Context? {
- val lang = getPersistedData(context, Locale.getDefault().language)
- return lang?.let { setLocale(context, it) }
+ val preferences = PreferenceManager.getDefaultSharedPreferences(context)
+ return if (preferences.getBoolean(context.getString(R.string.default_system_language), true)) {
+ if(!context.resources.getStringArray(R.array.languages_value).contains(Locale.getDefault().language)){
+ setLocale(context, "en")
+ }else{
+ setLocale(context, Locale.getDefault().language)
+ }
+ } else {
+ val lang = getPersistedData(context, Locale.getDefault().language)
+ lang?.let { setLocale(context, it) }
+ }
}
@JvmStatic
@@ -52,9 +61,7 @@ object LanguageHelper {
val resources = context?.resources
val configuration = resources?.configuration
configuration?.locale = locale
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- configuration?.setLayoutDirection(locale)
- }
+ configuration?.setLayoutDirection(locale)
resources?.updateConfiguration(configuration, resources.displayMetrics)
return context
}
diff --git a/app/src/main/java/org/mifos/mobile/utils/fcm/RegistrationIntentService.kt b/app/src/main/java/org/mifos/mobile/utils/fcm/RegistrationIntentService.kt
index 63f7ece41..31baa5157 100644
--- a/app/src/main/java/org/mifos/mobile/utils/fcm/RegistrationIntentService.kt
+++ b/app/src/main/java/org/mifos/mobile/utils/fcm/RegistrationIntentService.kt
@@ -20,12 +20,12 @@ import android.content.Intent
import android.util.Log
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.android.gms.tasks.OnCompleteListener
-import com.google.firebase.iid.FirebaseInstanceId
+import com.google.firebase.messaging.FirebaseMessaging
import org.mifos.mobile.utils.Constants
class RegistrationIntentService : IntentService(TAG) {
override fun onHandleIntent(intent: Intent?) {
- FirebaseInstanceId.getInstance().instanceId
+ FirebaseMessaging.getInstance().token
.addOnCompleteListener(
OnCompleteListener { task ->
if (!task.isSuccessful) {
@@ -34,7 +34,7 @@ class RegistrationIntentService : IntentService(TAG) {
}
// Get new Instance ID token
- val token = task.result?.token
+ val token = task.result
sendRegistrationToServer(token)
},
)
diff --git a/app/src/main/java/org/mifos/mobile/viewModels/HelpViewModel.kt b/app/src/main/java/org/mifos/mobile/viewModels/HelpViewModel.kt
deleted file mode 100644
index 352c52460..000000000
--- a/app/src/main/java/org/mifos/mobile/viewModels/HelpViewModel.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.mifos.mobile.viewModels
-
-import androidx.lifecycle.ViewModel
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import org.mifos.mobile.models.FAQ
-import org.mifos.mobile.utils.HelpUiState
-import java.util.Locale
-import javax.inject.Inject
-
-@HiltViewModel
-class HelpViewModel @Inject constructor() : ViewModel() {
-
- private val _helpUiState = MutableStateFlow(HelpUiState.Initial)
- val helpUiState: StateFlow get() = _helpUiState
-
- fun loadFaq(qs: Array?, ans: Array?) {
- val faqArrayList = ArrayList()
- if (qs != null) {
- for (i in qs.indices) {
- faqArrayList.add(FAQ(qs[i], ans?.get(i)))
- }
- }
- _helpUiState.value = HelpUiState.ShowFaq(faqArrayList)
- }
-
- fun filterList(faqArrayList: ArrayList?, query: String): ArrayList {
- val filteredList = ArrayList()
- if (faqArrayList != null) {
- for (faq in faqArrayList) {
- if (faq?.question?.lowercase(Locale.ROOT)
- ?.contains(query.lowercase(Locale.ROOT)) == true
- ) {
- filteredList.add(faq)
- }
- }
- }
- return filteredList
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/mifos/mobile/viewModels/HomeViewModel.kt b/app/src/main/java/org/mifos/mobile/viewModels/HomeViewModel.kt
deleted file mode 100644
index 07125e4be..000000000
--- a/app/src/main/java/org/mifos/mobile/viewModels/HomeViewModel.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-package org.mifos.mobile.viewModels
-
-import android.util.Base64
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.catch
-import kotlinx.coroutines.launch
-import org.mifos.mobile.R
-import org.mifos.mobile.api.local.PreferencesHelper
-import org.mifos.mobile.models.accounts.loan.LoanAccount
-import org.mifos.mobile.models.accounts.savings.SavingAccount
-import org.mifos.mobile.repositories.HomeRepository
-import org.mifos.mobile.utils.HomeUiState
-import org.mifos.mobile.utils.ImageUtil
-import javax.inject.Inject
-
-@HiltViewModel
-class HomeViewModel @Inject constructor(private val homeRepositoryImp: HomeRepository) :
- ViewModel() {
-
- @Inject
- lateinit var preferencesHelper: PreferencesHelper
-
- private val _homeUiState = MutableStateFlow(HomeUiState.Loading)
- val homeUiState: StateFlow = _homeUiState
-
- fun loadClientAccountDetails() {
- viewModelScope.launch {
- _homeUiState.value = HomeUiState.Loading
- homeRepositoryImp.clientAccounts().catch {
- _homeUiState.value = HomeUiState.Error(R.string.no_internet_connection)
- }.collect { clientAccounts ->
- _homeUiState.value = HomeUiState.ClientAccountDetails(
- getLoanAccountDetails(clientAccounts.loanAccounts),
- getSavingAccountDetails(clientAccounts.savingsAccounts)
- )
-
- }
- }
- }
-
-
- val userDetails: Unit
- get() {
- viewModelScope.launch {
- homeRepositoryImp.currentClient().catch {
- _homeUiState.value = HomeUiState.Error(R.string.error_fetching_client)
- }.collect { client ->
- preferencesHelper.officeName = client.officeName
- _homeUiState.value = HomeUiState.UserDetails(client)
- }
- }
- }
-
- val userImage: Unit
- get() {
- viewModelScope.launch {
- setUserProfile(preferencesHelper.userProfileImage)
- homeRepositoryImp.clientImage().catch {
- _homeUiState.value = HomeUiState.UserImage(null)
- }.collect {
- val encodedString = it.string()
- val pureBase64Encoded =
- encodedString.substring(encodedString.indexOf(',') + 1)
- preferencesHelper.userProfileImage = pureBase64Encoded
- setUserProfile(pureBase64Encoded)
- }
- }
- }
-
- fun setUserProfile(image: String?) {
- if (image == null) {
- return
- }
- val decodedBytes = Base64.decode(image, Base64.DEFAULT)
- val decodedBitmap = ImageUtil.instance?.compressImage(decodedBytes)
- _homeUiState.value = HomeUiState.UserImage(decodedBitmap)
- }
-
- val unreadNotificationsCount: Unit
- get() {
- viewModelScope.launch {
- homeRepositoryImp.unreadNotificationsCount().catch {
- _homeUiState.value = HomeUiState.UnreadNotificationsCount(0)
- }.collect { integer ->
- _homeUiState.value = HomeUiState.UnreadNotificationsCount(integer)
- }
- }
- }
-
-
- /**
- * Returns total Loan balance
- *
- * @param loanAccountList [List] of [LoanAccount] associated with the client
- * @return Returns `totalAmount` which is calculated by adding all [LoanAccount]
- * balance.
- */
- private fun getLoanAccountDetails(loanAccountList: List): Double {
- var totalAmount = 0.0
- for ((_, _, _, _, _, _, _, _, _, _, _, _, _, _, loanBalance) in loanAccountList) {
- totalAmount += loanBalance
- }
- return totalAmount
- }
-
- /**
- * Returns total Savings balance
- *
- * @param savingAccountList [List] of [SavingAccount] associated with the client
- * @return Returns `totalAmount` which is calculated by adding all [SavingAccount]
- * balance.
- */
- private fun getSavingAccountDetails(savingAccountList: List?): Double {
- var totalAmount = 0.0
- for ((_, _, _, _, _, accountBalance) in savingAccountList!!) {
- totalAmount += accountBalance
- }
- return totalAmount
- }
-}
\ No newline at end of file
diff --git a/app/src/main/res/layout/row_faq.xml b/app/src/main/res/layout/row_faq.xml
deleted file mode 100644
index 1487ca35f..000000000
--- a/app/src/main/res/layout/row_faq.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/row_saving_account_transaction.xml b/app/src/main/res/layout/row_saving_account_transaction.xml
index 8a7d59efc..ad9370fad 100644
--- a/app/src/main/res/layout/row_saving_account_transaction.xml
+++ b/app/src/main/res/layout/row_saving_account_transaction.xml
@@ -11,7 +11,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
- android:src="@drawable/triangular_green_view" />
+ />
language_type
+ default_system_language
theme_type
Total Savings Balance
Total Loan Balance
@@ -600,6 +601,7 @@
Change the application theme
+ - System Language
- English
- हिंदी
- عربى
@@ -620,6 +622,7 @@
+ - System_Language
- en
- hi
- ar
@@ -648,4 +651,5 @@
Login Failed, Please Try Again Later.
We were unable to update password.
We were unable to register the user.
+ No Questions Found
diff --git a/app/src/main/res/xml/settings_preference.xml b/app/src/main/res/xml/settings_preference.xml
index 483335fe9..554063636 100644
--- a/app/src/main/res/xml/settings_preference.xml
+++ b/app/src/main/res/xml/settings_preference.xml
@@ -19,7 +19,7 @@
().configureEach {
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_17.toString()
+ }
+}
+
+dependencies {
+ compileOnly(libs.android.gradlePlugin)
+ compileOnly(libs.android.tools.common)
+ compileOnly(libs.firebase.crashlytics.gradlePlugin)
+ compileOnly(libs.firebase.performance.gradlePlugin)
+ compileOnly(libs.kotlin.gradlePlugin)
+ compileOnly(libs.ksp.gradlePlugin)
+ compileOnly(libs.room.gradlePlugin)
+ implementation(libs.truth)
+}
+
+tasks {
+ validatePlugins {
+ enableStricterValidation = true
+ failOnWarning = true
+ }
+}
+
+gradlePlugin {
+ plugins {
+ register("androidApplicationCompose") {
+ id = "mifos.android.application.compose"
+ implementationClass = "AndroidApplicationComposeConventionPlugin"
+ }
+ register("androidApplication") {
+ id = "mifos.android.application"
+ implementationClass = "AndroidApplicationConventionPlugin"
+ }
+ register("androidHilt") {
+ id = "mifos.android.hilt"
+ implementationClass = "AndroidHiltConventionPlugin"
+ }
+ register("androidLibraryCompose") {
+ id = "mifos.android.library.compose"
+ implementationClass = "AndroidLibraryComposeConventionPlugin"
+ }
+ register("androidLibrary") {
+ id = "mifos.android.library"
+ implementationClass = "AndroidLibraryConventionPlugin"
+ }
+ register("androidFeature") {
+ id = "mifos.android.feature"
+ implementationClass = "AndroidFeatureConventionPlugin"
+ }
+ register("androidTest") {
+ id = "mifos.android.test"
+ implementationClass = "AndroidTestConventionPlugin"
+ }
+ register("androidRoom") {
+ id = "mifos.android.room"
+ implementationClass = "AndroidRoomConventionPlugin"
+ }
+ register("androidFirebase") {
+ id = "mifos.android.application.firebase"
+ implementationClass = "AndroidApplicationFirebaseConventionPlugin"
+ }
+ register("androidLint") {
+ id = "mifos.android.lint"
+ implementationClass = "AndroidLintConventionPlugin"
+ }
+ register("jvmLibrary") {
+ id = "mifos.jvm.library"
+ implementationClass = "JvmLibraryConventionPlugin"
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt
new file mode 100644
index 000000000..c4a4f0bb2
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt
@@ -0,0 +1,16 @@
+import com.android.build.api.dsl.ApplicationExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.getByType
+import org.mifos.mobile.configureAndroidCompose
+
+class AndroidApplicationComposeConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ pluginManager.apply("com.android.application")
+
+ val extension = extensions.getByType()
+ configureAndroidCompose(extension)
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
new file mode 100644
index 000000000..14897278b
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
@@ -0,0 +1,32 @@
+import com.android.build.api.dsl.ApplicationExtension
+import com.android.build.api.variant.ApplicationAndroidComponentsExtension
+import com.android.build.gradle.BaseExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.getByType
+import org.mifos.mobile.configureBadgingTasks
+import org.mifos.mobile.configureKotlinAndroid
+import org.mifos.mobile.configurePrintApksTask
+
+class AndroidApplicationConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ with(pluginManager) {
+ apply("com.android.application")
+ apply("org.jetbrains.kotlin.android")
+ apply("mifos.android.lint")
+ apply("com.dropbox.dependency-guard")
+ }
+
+ extensions.configure {
+ configureKotlinAndroid(this)
+ defaultConfig.targetSdk = 34
+ }
+ extensions.configure {
+ configurePrintApksTask(this)
+ configureBadgingTasks(extensions.getByType(), this)
+ }
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
new file mode 100644
index 000000000..3820afca3
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidApplicationFirebaseConventionPlugin.kt
@@ -0,0 +1,39 @@
+import com.android.build.api.dsl.ApplicationExtension
+import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+import org.mifos.mobile.libs
+
+class AndroidApplicationFirebaseConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ with(pluginManager) {
+ apply("com.google.gms.google-services")
+ apply("com.google.firebase.firebase-perf")
+ apply("com.google.firebase.crashlytics")
+ }
+
+ dependencies {
+ val bom = libs.findLibrary("firebase-bom").get()
+ add("implementation", platform(bom))
+ "implementation"(libs.findLibrary("firebase.analytics").get())
+ "implementation"(libs.findLibrary("firebase.performance").get())
+ "implementation"(libs.findLibrary("firebase.crashlytics").get())
+ "implementation"(libs.findLibrary("firebase.cloud.messaging").get())
+ }
+
+ extensions.configure {
+ buildTypes.configureEach {
+ // Disable the Crashlytics mapping file upload. This feature should only be
+ // enabled if a Firebase backend is available and configured in
+ // google-services.json.
+ configure {
+ mappingFileUploadEnabled = false
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt
new file mode 100644
index 000000000..3b445cf1f
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt
@@ -0,0 +1,32 @@
+import com.android.build.gradle.LibraryExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+import org.mifos.mobile.libs
+
+class AndroidFeatureConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ pluginManager.apply {
+ apply("mifos.android.library")
+ apply("mifos.android.hilt")
+ }
+ extensions.configure {
+ defaultConfig {
+ // set custom test runner
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+ }
+
+ dependencies {
+ add("implementation", project(":ui"))
+ //add("implementation", project(":core:designsystem"))
+
+ add("implementation", libs.findLibrary("androidx.hilt.navigation.compose").get())
+ add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get())
+ add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get())
+ }
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt
new file mode 100644
index 000000000..a0d1fb1d3
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidHiltConventionPlugin.kt
@@ -0,0 +1,25 @@
+
+import org.mifos.mobile.libs
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.dependencies
+
+class AndroidHiltConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ with(pluginManager) {
+ apply("dagger.hilt.android.plugin")
+ // KAPT must go last to avoid build warnings.
+ // See: https://stackoverflow.com/questions/70550883/warning-the-following-options-were-not-recognized-by-any-processor-dagger-f
+ apply("org.jetbrains.kotlin.kapt")
+ }
+
+ dependencies {
+ "implementation"(libs.findLibrary("hilt.android").get())
+ "kapt"(libs.findLibrary("hilt.compiler").get())
+ "kaptAndroidTest"(libs.findLibrary("hilt.compiler").get())
+ "kaptTest"(libs.findLibrary("hilt.compiler").get())
+ }
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt
new file mode 100644
index 000000000..8230f220a
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt
@@ -0,0 +1,17 @@
+import com.android.build.gradle.LibraryExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.getByType
+import org.mifos.mobile.configureAndroidCompose
+
+class AndroidLibraryComposeConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ pluginManager.apply("com.android.library")
+
+ val extension = extensions.getByType()
+ configureAndroidCompose(extension)
+ }
+ }
+
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
new file mode 100644
index 000000000..61fa7ed2d
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
@@ -0,0 +1,37 @@
+import com.android.build.api.variant.LibraryAndroidComponentsExtension
+import com.android.build.gradle.LibraryExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.kotlin
+import org.mifos.mobile.configureKotlinAndroid
+import org.mifos.mobile.configurePrintApksTask
+import org.mifos.mobile.disableUnnecessaryAndroidTests
+
+class AndroidLibraryConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ with(pluginManager) {
+ apply("com.android.library")
+ apply("org.jetbrains.kotlin.android")
+ apply("mifos.android.lint")
+ }
+
+ extensions.configure {
+ configureKotlinAndroid(this)
+ defaultConfig.targetSdk = 34
+ // The resource prefix is derived from the module name,
+ // so resources inside ":core:module1" must be prefixed with "core_module1_"
+ resourcePrefix = path.split("""\W""".toRegex()).drop(1).distinct().joinToString(separator = "_").lowercase() + "_"
+ }
+ extensions.configure {
+ configurePrintApksTask(this)
+ disableUnnecessaryAndroidTests(target)
+ }
+ dependencies {
+ add("testImplementation", kotlin("test"))
+ }
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt
new file mode 100644
index 000000000..54246d61e
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt
@@ -0,0 +1,30 @@
+import com.android.build.api.dsl.ApplicationExtension
+import com.android.build.api.dsl.LibraryExtension
+import com.android.build.api.dsl.Lint
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+
+class AndroidLintConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ when {
+ pluginManager.hasPlugin("com.android.application") ->
+ configure { lint(Lint::configure) }
+
+ pluginManager.hasPlugin("com.android.library") ->
+ configure { lint(Lint::configure) }
+
+ else -> {
+ pluginManager.apply("com.android.lint")
+ configure(Lint::configure)
+ }
+ }
+ }
+ }
+}
+
+private fun Lint.configure() {
+ xmlReport = true
+ checkDependencies = true
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt
new file mode 100644
index 000000000..332cd7c20
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidRoomConventionPlugin.kt
@@ -0,0 +1,29 @@
+import androidx.room.gradle.RoomExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+import org.mifos.mobile.libs
+
+class AndroidRoomConventionPlugin : Plugin {
+
+ override fun apply(target: Project) {
+ with(target) {
+ pluginManager.apply("androidx.room")
+ pluginManager.apply("com.google.devtools.ksp")
+
+ extensions.configure {
+ // The schemas directory contains a schema file for each version of the Room database.
+ // This is required to enable Room auto migrations.
+ // See https://developer.android.com/reference/kotlin/androidx/room/AutoMigration.
+ schemaDirectory("$projectDir/schemas")
+ }
+
+ dependencies {
+ add("implementation", libs.findLibrary("room.runtime").get())
+ add("implementation", libs.findLibrary("room.ktx").get())
+ add("ksp", libs.findLibrary("room.compiler").get())
+ }
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt
new file mode 100644
index 000000000..4f01e5dbd
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt
@@ -0,0 +1,21 @@
+import com.android.build.gradle.TestExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+import org.mifos.mobile.configureKotlinAndroid
+
+class AndroidTestConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ with(pluginManager) {
+ apply("com.android.test")
+ apply("org.jetbrains.kotlin.android")
+ }
+
+ extensions.configure {
+ configureKotlinAndroid(this)
+ defaultConfig.targetSdk = 34
+ }
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt
new file mode 100644
index 000000000..d5045d8ee
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/JvmLibraryConventionPlugin.kt
@@ -0,0 +1,15 @@
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.mifos.mobile.configureKotlinJvm
+
+class JvmLibraryConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ with(pluginManager) {
+ apply("org.jetbrains.kotlin.jvm")
+ apply("mifos.android.lint")
+ }
+ configureKotlinJvm()
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/org/mifos/mobile/AndroidCompose.kt b/build-logic/convention/src/main/kotlin/org/mifos/mobile/AndroidCompose.kt
new file mode 100644
index 000000000..1836ba3a4
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/mifos/mobile/AndroidCompose.kt
@@ -0,0 +1,69 @@
+package org.mifos.mobile
+
+import com.android.build.api.dsl.CommonExtension
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.withType
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+/**
+ * Configure Compose-specific options
+ */
+internal fun Project.configureAndroidCompose(
+ commonExtension: CommonExtension<*, *, *, *, *>,
+) {
+ commonExtension.apply {
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = libs.findVersion("androidxComposeCompiler").get().toString()
+ }
+
+ dependencies {
+ val bom = libs.findLibrary("androidx-compose-bom").get()
+ add("implementation", platform(bom))
+ add("androidTestImplementation", platform(bom))
+ }
+
+ testOptions {
+ unitTests {
+ // For Robolectric
+ isIncludeAndroidResources = true
+ }
+ }
+ }
+
+ tasks.withType().configureEach {
+ kotlinOptions {
+ freeCompilerArgs = freeCompilerArgs + buildComposeMetricsParameters()
+ }
+ }
+}
+
+private fun Project.buildComposeMetricsParameters(): List {
+ val metricParameters = mutableListOf()
+ val enableMetricsProvider = project.providers.gradleProperty("enableComposeCompilerMetrics")
+ val relativePath = projectDir.relativeTo(rootDir)
+ val buildDir = layout.buildDirectory.get().asFile
+ val enableMetrics = (enableMetricsProvider.orNull == "true")
+ if (enableMetrics) {
+ val metricsFolder = buildDir.resolve("compose-metrics").resolve(relativePath)
+ metricParameters.add("-P")
+ metricParameters.add(
+ "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + metricsFolder.absolutePath
+ )
+ }
+
+ val enableReportsProvider = project.providers.gradleProperty("enableComposeCompilerReports")
+ val enableReports = (enableReportsProvider.orNull == "true")
+ if (enableReports) {
+ val reportsFolder = buildDir.resolve("compose-reports").resolve(relativePath)
+ metricParameters.add("-P")
+ metricParameters.add(
+ "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + reportsFolder.absolutePath
+ )
+ }
+ return metricParameters.toList()
+}
diff --git a/build-logic/convention/src/main/kotlin/org/mifos/mobile/AndroidInstrumentedTests.kt b/build-logic/convention/src/main/kotlin/org/mifos/mobile/AndroidInstrumentedTests.kt
new file mode 100644
index 000000000..1e93dec84
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/mifos/mobile/AndroidInstrumentedTests.kt
@@ -0,0 +1,19 @@
+package org.mifos.mobile
+
+import com.android.build.api.variant.LibraryAndroidComponentsExtension
+import org.gradle.api.Project
+
+/**
+ * Disable unnecessary Android instrumented tests for the [project] if there is no `androidTest` folder.
+ * Otherwise, these projects would be compiled, packaged, installed and ran only to end-up with the following message:
+ *
+ * > Starting 0 tests on AVD
+ *
+ * Note: this could be improved by checking other potential sourceSets based on buildTypes and flavors.
+ */
+internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests(
+ project: Project,
+) = beforeVariants {
+ it.enableAndroidTest = it.enableAndroidTest
+ && project.projectDir.resolve("src/androidTest").exists()
+}
diff --git a/build-logic/convention/src/main/kotlin/org/mifos/mobile/Badging.kt b/build-logic/convention/src/main/kotlin/org/mifos/mobile/Badging.kt
new file mode 100644
index 000000000..3d52f8123
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/mifos/mobile/Badging.kt
@@ -0,0 +1,144 @@
+package org.mifos.mobile
+
+import com.android.build.api.artifact.SingleArtifact
+import com.android.build.api.variant.ApplicationAndroidComponentsExtension
+import com.android.build.gradle.BaseExtension
+import com.android.SdkConstants
+import com.google.common.truth.Truth.assertWithMessage
+import org.gradle.api.DefaultTask
+import org.gradle.api.Project
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Copy
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.configurationcache.extensions.capitalized
+import org.gradle.kotlin.dsl.register
+import org.gradle.language.base.plugins.LifecycleBasePlugin
+import org.gradle.process.ExecOperations
+import java.io.File
+import javax.inject.Inject
+
+@CacheableTask
+abstract class GenerateBadgingTask : DefaultTask() {
+
+ @get:OutputFile
+ abstract val badging: RegularFileProperty
+
+ @get:PathSensitive(PathSensitivity.NONE)
+ @get:InputFile
+ abstract val apk: RegularFileProperty
+
+ @get:PathSensitive(PathSensitivity.NONE)
+ @get:InputFile
+ abstract val aapt2Executable: RegularFileProperty
+
+ @get:Inject
+ abstract val execOperations: ExecOperations
+
+ @TaskAction
+ fun taskAction() {
+ execOperations.exec {
+ commandLine(
+ aapt2Executable.get().asFile.absolutePath,
+ "dump",
+ "badging",
+ apk.get().asFile.absolutePath,
+ )
+ standardOutput = badging.asFile.get().outputStream()
+ }
+ }
+}
+
+@CacheableTask
+abstract class CheckBadgingTask : DefaultTask() {
+
+ // In order for the task to be up-to-date when the inputs have not changed,
+ // the task must declare an output, even if it's not used. Tasks with no
+ // output are always run regardless of whether the inputs changed
+ @get:OutputDirectory
+ abstract val output: DirectoryProperty
+
+ @get:PathSensitive(PathSensitivity.NONE)
+ @get:InputFile
+ abstract val goldenBadging: RegularFileProperty
+
+ @get:PathSensitive(PathSensitivity.NONE)
+ @get:InputFile
+ abstract val generatedBadging: RegularFileProperty
+
+ @get:Input
+ abstract val updateBadgingTaskName: Property
+
+ override fun getGroup(): String = LifecycleBasePlugin.VERIFICATION_GROUP
+
+ @TaskAction
+ fun taskAction() {
+ assertWithMessage(
+ "Generated badging is different from golden badging! " +
+ "If this change is intended, run ./gradlew ${updateBadgingTaskName.get()}",
+ )
+ .that(generatedBadging.get().asFile.readText())
+ .isEqualTo(goldenBadging.get().asFile.readText())
+ }
+}
+
+fun Project.configureBadgingTasks(
+ baseExtension: BaseExtension,
+ componentsExtension: ApplicationAndroidComponentsExtension,
+) {
+ // Registers a callback to be called, when a new variant is configured
+ componentsExtension.onVariants { variant ->
+ // Registers a new task to verify the app bundle.
+ val capitalizedVariantName = variant.name.capitalized()
+ val generateBadgingTaskName = "generate${capitalizedVariantName}Badging"
+ val generateBadging =
+ tasks.register(generateBadgingTaskName) {
+ apk.set(
+ variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE),
+ )
+ aapt2Executable.set(
+ File(
+ baseExtension.sdkDirectory,
+ "${SdkConstants.FD_BUILD_TOOLS}/" +
+ "${baseExtension.buildToolsVersion}/" +
+ SdkConstants.FN_AAPT2,
+ ),
+ )
+
+ badging.set(
+ project.layout.buildDirectory.file(
+ "outputs/apk_from_bundle/${variant.name}/${variant.name}-badging.txt",
+ ),
+ )
+ }
+
+ val updateBadgingTaskName = "update${capitalizedVariantName}Badging"
+ tasks.register(updateBadgingTaskName) {
+ from(generateBadging.get().badging)
+ into(project.layout.projectDirectory)
+ }
+
+ val checkBadgingTaskName = "check${capitalizedVariantName}Badging"
+ tasks.register(checkBadgingTaskName) {
+ goldenBadging.set(
+ project.layout.projectDirectory.file("${variant.name}-badging.txt"),
+ )
+ generatedBadging.set(
+ generateBadging.get().badging,
+ )
+ this.updateBadgingTaskName.set(updateBadgingTaskName)
+
+ output.set(
+ project.layout.buildDirectory.dir("intermediates/$checkBadgingTaskName"),
+ )
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/org/mifos/mobile/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/org/mifos/mobile/KotlinAndroid.kt
new file mode 100644
index 000000000..35720b5ce
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/mifos/mobile/KotlinAndroid.kt
@@ -0,0 +1,75 @@
+package org.mifos.mobile
+
+import com.android.build.api.dsl.CommonExtension
+import org.gradle.api.JavaVersion
+import org.gradle.api.Project
+import org.gradle.api.plugins.JavaPluginExtension
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.provideDelegate
+import org.gradle.kotlin.dsl.withType
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+/**
+ * Configure base Kotlin with Android options
+ */
+internal fun Project.configureKotlinAndroid(
+ commonExtension: CommonExtension<*, *, *, *, *>,
+) {
+ commonExtension.apply {
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 24
+ }
+
+ compileOptions {
+ // Up to Java 11 APIs are available through desugaring
+ // https://developer.android.com/studio/write/java11-minimal-support-table
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ isCoreLibraryDesugaringEnabled = true
+ }
+ }
+
+ configureKotlin()
+
+ dependencies {
+ add("coreLibraryDesugaring", libs.findLibrary("android.desugarJdkLibs").get())
+ }
+}
+
+/**
+ * Configure base Kotlin options for JVM (non-Android)
+ */
+internal fun Project.configureKotlinJvm() {
+ extensions.configure {
+ // Up to Java 11 APIs are available through desugaring
+ // https://developer.android.com/studio/write/java11-minimal-support-table
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ configureKotlin()
+}
+
+/**
+ * Configure base Kotlin options
+ */
+private fun Project.configureKotlin() {
+ // Use withType to workaround https://youtrack.jetbrains.com/issue/KT-55947
+ tasks.withType().configureEach {
+ kotlinOptions {
+ // Set JVM target to 11
+ jvmTarget = JavaVersion.VERSION_17.toString()
+ // Treat all Kotlin warnings as errors (disabled by default)
+ // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties
+ val warningsAsErrors: String? by project
+ allWarningsAsErrors = warningsAsErrors.toBoolean()
+ freeCompilerArgs = freeCompilerArgs + listOf(
+ // Enable experimental coroutines APIs, including Flow
+ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
+ )
+ }
+ }
+}
diff --git a/build-logic/convention/src/main/kotlin/org/mifos/mobile/PrintTestApks.kt b/build-logic/convention/src/main/kotlin/org/mifos/mobile/PrintTestApks.kt
new file mode 100644
index 000000000..3e0ce77e0
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/mifos/mobile/PrintTestApks.kt
@@ -0,0 +1,87 @@
+package org.mifos.mobile
+
+import com.android.build.api.artifact.SingleArtifact
+import com.android.build.api.variant.AndroidComponentsExtension
+import com.android.build.api.variant.BuiltArtifactsLoader
+import com.android.build.api.variant.HasAndroidTest
+import org.gradle.api.DefaultTask
+import org.gradle.api.Project
+import org.gradle.api.file.Directory
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.work.DisableCachingByDefault
+import java.io.File
+
+internal fun Project.configurePrintApksTask(extension: AndroidComponentsExtension<*, *, *>) {
+ extension.onVariants { variant ->
+ if (variant is HasAndroidTest) {
+ val loader = variant.artifacts.getBuiltArtifactsLoader()
+ val artifact = variant.androidTest?.artifacts?.get(SingleArtifact.APK)
+ val javaSources = variant.androidTest?.sources?.java?.all
+ val kotlinSources = variant.androidTest?.sources?.kotlin?.all
+
+ val testSources = if (javaSources != null && kotlinSources != null) {
+ javaSources.zip(kotlinSources) { javaDirs, kotlinDirs ->
+ javaDirs + kotlinDirs
+ }
+ } else javaSources ?: kotlinSources
+
+ if (artifact != null && testSources != null) {
+ tasks.register(
+ "${variant.name}PrintTestApk",
+ PrintApkLocationTask::class.java
+ ) {
+ apkFolder.set(artifact)
+ builtArtifactsLoader.set(loader)
+ variantName.set(variant.name)
+ sources.set(testSources)
+ }
+ }
+ }
+ }
+}
+
+@DisableCachingByDefault(because = "Prints output")
+internal abstract class PrintApkLocationTask : DefaultTask() {
+
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ @get:InputDirectory
+ abstract val apkFolder: DirectoryProperty
+
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ @get:InputFiles
+ abstract val sources: ListProperty
+
+ @get:Internal
+ abstract val builtArtifactsLoader: Property
+
+ @get:Input
+ abstract val variantName: Property
+
+ @TaskAction
+ fun taskAction() {
+ val hasFiles = sources.orNull?.any { directory ->
+ directory.asFileTree.files.any {
+ it.isFile && "build${File.separator}generated" !in it.parentFile.path
+ }
+ } ?: throw RuntimeException("Cannot check androidTest sources")
+
+ // Don't print APK location if there are no androidTest source files
+ if (!hasFiles) return
+
+ val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get())
+ ?: throw RuntimeException("Cannot load APKs")
+ if (builtArtifacts.elements.size != 1)
+ throw RuntimeException("Expected one APK !")
+ val apk = File(builtArtifacts.elements.single().outputFile).toPath()
+ println(apk)
+ }
+}
\ No newline at end of file
diff --git a/build-logic/convention/src/main/kotlin/org/mifos/mobile/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/org/mifos/mobile/ProjectExtensions.kt
new file mode 100644
index 000000000..797c2a5be
--- /dev/null
+++ b/build-logic/convention/src/main/kotlin/org/mifos/mobile/ProjectExtensions.kt
@@ -0,0 +1,9 @@
+package org.mifos.mobile
+
+import org.gradle.api.Project
+import org.gradle.api.artifacts.VersionCatalog
+import org.gradle.api.artifacts.VersionCatalogsExtension
+import org.gradle.kotlin.dsl.getByType
+
+val Project.libs
+ get(): VersionCatalog = extensions.getByType().named("libs")
diff --git a/build-logic/gradle.properties b/build-logic/gradle.properties
new file mode 100644
index 000000000..1c9073eb9
--- /dev/null
+++ b/build-logic/gradle.properties
@@ -0,0 +1,4 @@
+# Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534
+org.gradle.parallel=true
+org.gradle.caching=true
+org.gradle.configureondemand=true
diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts
new file mode 100644
index 000000000..de9224e22
--- /dev/null
+++ b/build-logic/settings.gradle.kts
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+dependencyResolutionManagement {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ versionCatalogs {
+ create("libs") {
+ from(files("../gradle/libs.versions.toml"))
+ }
+ }
+}
+
+rootProject.name = "build-logic"
+include(":convention")
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 7645375df..000000000
--- a/build.gradle
+++ /dev/null
@@ -1,98 +0,0 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-
-buildscript {
- repositories {
- google()
- mavenCentral()
- maven {
- url "https://plugins.gradle.org/m2/"
- }
- jcenter()
- }
-
- ext {
- gradleVersion = '7.2.0'
- crashlyticsGradleVersion = '2.9.9'
- ossLicensesVersion = '0.10.5'
- googleServicesVersion = '4.3.15'
- kotlinGradlePluginversion = '1.6.21'
- }
-
- dependencies {
- classpath "com.android.tools.build:gradle:$gradleVersion"
- classpath "com.google.firebase:firebase-crashlytics-gradle:$crashlyticsGradleVersion"
- classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6'
- classpath "com.google.gms:google-services:$googleServicesVersion"
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinGradlePluginversion"
- classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:5.1.2"
- // NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
- }
-}
-plugins {
- id("com.google.dagger.hilt.android") version "2.48" apply false
- id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
-}
-allprojects {
- repositories {
- google()
- jcenter()
- mavenCentral()
- maven { url "https://www.jitpack.io" }
- }
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
-
-ext {
- // Sdk and tools
- minSdkVersion = 24
- targetSdkVersion = 34
- compileSdkVersion = 34
-
- // App dependencies
- countryCodePicker = '2.7.2'
- supportLibraryVersion = '1.4.2'
- designLibraryVersion = '1.9.0'
- recyclerViewVersion = '1.2.1'
- vectorDrawablesVersion = '1.1.0'
- lifecycleVersion = '2.4.1'
- lifecycleExtensionsVersion = '2.2.0'
- retrofitVersion = '2.9.0'
- coroutinesTest = '1.7.3'
- okHttp3Version = '3.14.9'
- butterKnifeVersion = '8.0.1'
- dbflowVersion = '4.2.4'
- playServicesVersion = '17.0.1'
- firebaseMessagingVersion = '21.1.0'
- oss_licenses = '17.0.0'
- kotlinVersion = '1.6.10'
- tableViewVersion = '0.8.9.4'
- biometric = '1.1.0'
- archCoreVersion = '2.2.0'
- coroutines = '1.6.4'
- jUnitVersion = '4.13.2'
- mockitoVersion = '5.4.0'
- runnerVersion = '1.6.0-alpha04'
- rulesVersion = '1.6.0-alpha01'
- espressoVersion = '3.5.1'
- zxingcoreVersion = '3.5.2'
- zxingbarcodescannerVersion = '1.9.13'
- rxjavaVersion = '2.2.21'
- rxandroidVersion = '2.1.1'
- sweeterrorVersion = '1.0.0'
- mifosPasscodeVersion = '1.0.0'
- cropviewVersion = '1.1.8'
- multiDexVersion = '2.0.1'
- annotationLibraryVersion ='1.1.0'
- firebaseCrashlyticsVersion = '18.4.1'
- activity_version = '1.5.0'
- fragment_version = '1.5.0'
- composeVersion = '1.6.0-alpha05'
- composeCompiler = '1.3.2'
- composeActivity = '1.7.2'
- materialVersion = '1.1.0'
- lifecycleVersion = '2.6.1'
-}
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 000000000..86cf1f78e
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,32 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath(libs.google.oss.licenses.plugin) {
+ exclude(group = "com.google.protobuf")
+ }
+ }
+}
+
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.android.test) apply false
+ alias(libs.plugins.kotlin.jvm) apply false
+ alias(libs.plugins.kotlin.serialization) apply false
+ alias(libs.plugins.kotlin.parcelize) apply false
+ alias(libs.plugins.dependencyGuard) apply false
+ alias(libs.plugins.firebase.crashlytics) apply false
+ alias(libs.plugins.firebase.perf) apply false
+ alias(libs.plugins.gms) apply false
+ alias(libs.plugins.hilt) apply false
+ alias(libs.plugins.ksp) apply false
+ alias(libs.plugins.roborazzi) apply false
+ alias(libs.plugins.secrets) apply false
+ alias(libs.plugins.room) apply false
+ alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.spotbugs) apply false
+}
\ No newline at end of file
diff --git a/config/quality/quality.gradle b/config/quality/quality.gradle
index 943851a71..23bdf8423 100755
--- a/config/quality/quality.gradle
+++ b/config/quality/quality.gradle
@@ -32,7 +32,7 @@ task checkstyle(type: Checkstyle, group: 'Verification', description: 'Runs code
include '**/*.java'
reports {
- xml.enabled = true
+ //xml.enabled = true
xml {
destination file("$reportsDir/checkstyle/checkstyle.xml")
}
@@ -41,37 +41,36 @@ task checkstyle(type: Checkstyle, group: 'Verification', description: 'Runs code
classpath = files( )
}
-
-
-tasks.matching {task -> task.name.startsWith('spotbugs')}.forEach {
- check.dependsOn it
- it.dependsOn(['compileDebugSources','compileReleaseSources'])
- it.group('Verification')
- it.description('Inspect java bytecode for bugs')
-
- it.ignoreFailures = false
- it.effort = "max"
- it.reportLevel = "high"
- it.excludeFilter = new File("$qualityConfigDir/findbugs/android-exclude-filter.xml")
- it.classes = files("$project.rootDir/app/build/intermediates/javac")
-
- it.source 'src'
- it.include '**/*.java'
- it.exclude '**/gen/**'
-
- it.reports {
- xml.enabled = false
- html.enabled = true
- xml {
- destination file("$reportsDir/findbugs/findbugs.xml")
- }
- html {
- destination file("$reportsDir/findbugs/findbugs.html")
- }
- }
-
- it.classpath = files()
-}
+// TODO un comment after fixing the plugin
+//tasks.matching {task -> task.name.startsWith('spotbugs')}.forEach {
+// check.dependsOn it
+// it.dependsOn(['compileDebugSources','compileReleaseSources'])
+// it.group('Verification')
+// it.description('Inspect java bytecode for bugs')
+//
+// it.ignoreFailures = false
+// it.effort = "max"
+// it.reportLevel = "high"
+// it.excludeFilter = new File("$qualityConfigDir/findbugs/android-exclude-filter.xml")
+// it.classes = files("$project.rootDir/app/build/intermediates/javac")
+//
+// it.source 'src'
+// it.include '**/*.java'
+// it.exclude '**/gen/**'
+//
+// it.reports {
+// // xml.enabled = false
+// // html.enabled = true
+// xml {
+// destination file("$reportsDir/findbugs/findbugs.xml")
+// }
+// html {
+// destination file("$reportsDir/findbugs/findbugs.html")
+// }
+// }
+//
+// it.classpath = files()
+//}
task pmd(type: Pmd, group: 'Verification', description: 'Inspect sourcecode for bugs') {
@@ -84,8 +83,8 @@ task pmd(type: Pmd, group: 'Verification', description: 'Inspect sourcecode for
exclude '**/gen/**'
reports {
- xml.enabled = true
- html.enabled = true
+ // xml.enabled = true
+ // html.enabled = true
xml {
destination file("$reportsDir/pmd/pmd.xml")
}
diff --git a/core/build.gradle b/core/build.gradle
deleted file mode 100644
index f318656db..000000000
--- a/core/build.gradle
+++ /dev/null
@@ -1,59 +0,0 @@
-plugins {
- id 'com.android.library'
- id 'org.jetbrains.kotlin.android'
-}
-
-android {
- namespace 'org.mifos.mobile.core'
- compileSdk 32
-
- defaultConfig {
- minSdk 21
- targetSdk 32
-
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles "consumer-rules.pro"
- }
-
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- }
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = '1.8'
- }
-
- buildFeatures {
- compose true
- }
-
- composeOptions {
- kotlinCompilerExtensionVersion "1.4.4"
- }
-
-}
-
-dependencies {
-
- implementation 'androidx.core:core-ktx:1.12.0'
- implementation 'androidx.appcompat:appcompat:1.6.1'
- implementation 'com.google.android.material:material:1.9.0'
- testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.5'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
-
- // Jetpack Compose
- implementation "androidx.compose.material:material:$rootProject.composeVersion"
- implementation "androidx.compose.compiler:compiler:$rootProject.composeCompiler"
- implementation "androidx.compose.ui:ui-tooling-preview:$rootProject.composeVersion"
- implementation "androidx.activity:activity-compose:$rootProject.composeActivity"
- debugImplementation "androidx.compose.ui:ui-tooling:$rootProject.composeVersion"
- implementation "androidx.compose.material3:material3:$rootProject.materialVersion"
- implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$rootProject.lifecycleVersion"
-}
\ No newline at end of file
diff --git a/core/src/main/java/org/mifos/mobile/core/ui/component/MifosUserImage.kt b/core/src/main/java/org/mifos/mobile/core/ui/component/MifosUserImage.kt
deleted file mode 100644
index e249affaa..000000000
--- a/core/src/main/java/org/mifos/mobile/core/ui/component/MifosUserImage.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.mifos.mobile.core.ui.component
-
-import android.graphics.Bitmap
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.unit.dp
-
-/**
- * @author pratyush
- * @since 20/12/2023
- */
-
-@Composable
-fun MifosUserImage(
- isDarkTheme: Boolean,
- bitmap: Bitmap
-) {
- val backgroundColor = if (isDarkTheme) {
- Color(0xFF9bb1e3)
- } else {
- Color(0xFF325ca8)
- }
- Image(
- modifier = Modifier
- .size(100.dp)
- .clip(CircleShape)
- .background(backgroundColor),
- bitmap = bitmap.asImageBitmap(),
- contentDescription = "Profile Image",
- contentScale = ContentScale.Crop,
- )
-
-}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 70564ff8b..14537dca4 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -9,20 +9,40 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-# Default value: -Xmx10248m -XX:MaxPermSize=256m
-# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
-org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
-
-# When configured, Gradle will run in incubating parallel mode.
-# This option should only be used with decoupled projects. More details, visit
org.gradle.parallel=true
-# When set to true the Gradle daemon is used to run the build. For local developer builds this is our favorite property.
-# The developer environment is optimized for speed and feedback so we nearly always run Gradle jobs with the daemon.
-org.gradle.daemon=true
+
+# Not encouraged by Gradle and can produce weird results. Wait for isolated projects instead.
+org.gradle.configureondemand=false
+
+# Enable caching between builds.
+org.gradle.caching=true
+
+# Enable configuration caching between builds.
+org.gradle.configuration-cache=true
+# This option is set because of https://github.com/google/play-services-plugins/issues/246
+# to generate the Configuration Cache regardless of incompatible tasks.
+# See https://github.com/android/nowinandroid/issues/1022 before using it.
+org.gradle.configuration-cache.problems=warn
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
-android.enableJetifier=true
\ No newline at end of file
+
+# Remove after jetpack compose implementation
+android.enableJetifier=true
+android.nonTransitiveRClass=false
+android.nonFinalResIds=false
+
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+
+# Disable build features that are enabled by default,
+# https://developer.android.com/build/releases/gradle-plugin#default-changes
+android.defaults.buildfeatures.resvalues=false
+android.defaults.buildfeatures.shaders=false
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 000000000..403619a67
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,148 @@
+[versions]
+appcompatVersion = "1.6.1"
+compileSdk = "34"
+constraintlayoutVersion = "2.1.4"
+coreKtxVersion = "1.12.0"
+minSdk = "24"
+targetSdk = "34"
+androidGradlePlugin = "8.2.1"
+androidTools = "31.2.1"
+androidxActivity = "1.7.2"
+androidxComposeBom = "2023.10.01"
+androidxComposeCompiler = "1.5.8"
+androidxCore = "1.10.1"
+androidxHilt = "1.1.0-alpha01"
+androidxLifecycle = "2.6.1"
+hilt = "2.50"
+junit = "4.13.2"
+kotlin = "1.9.22"
+ksp = "1.9.21-1.0.16"
+firebaseCrashlyticsPlugin = "2.9.2"
+gmsPlugin = "4.3.14"
+butterknifePlugin = "10.2.3"
+secrets = "2.0.1"
+lifecycleVersion = "2.6.2"
+lifecycleExtensionsVersion = "2.2.0"
+activityVersion = "1.5.0"
+fragmentVersion = "1.5.0"
+retrofitVersion = "2.9.0"
+okHttp3Version = "3.14.9"
+butterKnifeVersion = "10.2.3"
+rxandroidVersion = "2.1.1"
+rxjavaVersion = "2.2.21"
+junitVersion = "4.12"
+firebasePerfPlugin = "1.4.2"
+truth = "1.1.5"
+dependencyGuard = "0.4.3"
+room = "2.6.1"
+roborazzi = "1.6.0"
+googleOss = "17.0.1"
+googleOssPlugin = "0.10.6"
+firebaseBom = "32.7.1"
+androidDesugarJdkLibs = "2.0.4"
+androidx-test-ext-junit = "1.1.5"
+espresso-core = "3.5.1"
+material = "1.11.0"
+dbflowVersion = "4.2.4"
+preference = "1.0.0"
+playServicesVersion = "17.0.1"
+spotbugs = "4.8.0"
+
+[libraries]
+androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityVersion" }
+androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompatVersion" }
+androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayoutVersion" }
+androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtxVersion" }
+androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentVersion" }
+android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" }
+androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivity" }
+androidx-compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "androidxComposeCompiler" }
+androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
+androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" }
+androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3"}
+androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" }
+androidx-compose-ui = { group = "androidx.compose.ui", name = "ui"}
+androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4"}
+androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest"}
+androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling"}
+androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" }
+hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
+hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
+androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview"}
+androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util"}
+androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "androidxHilt" }
+androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
+androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
+androidx-lifecycle-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleVersion" }
+androidx-lifecycle-extensions = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "lifecycleExtensionsVersion" }
+jakewharton-butterknife = { module = "com.jakewharton:butterknife", version.ref = "butterKnifeVersion" }
+jakewharton-compiler = { module = "com.jakewharton:butterknife-compiler", version.ref = "butterKnifeVersion" }
+squareup-retrofit2 = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofitVersion" }
+squareup-retrofit-adapter-rxjava = { module = "com.squareup.retrofit2:adapter-rxjava2", version.ref = "retrofitVersion" }
+squareup-retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofitVersion" }
+squareup-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okHttp3Version" }
+squareup-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okHttp3Version" }
+reactivex-rxjava2-android = { module = "io.reactivex.rxjava2:rxandroid", version.ref = "rxandroidVersion" }
+reactivex-rxjava2 = { module = "io.reactivex.rxjava2:rxjava", version.ref = "rxjavaVersion" }
+truth = { group = "com.google.truth", name = "truth", version.ref = "truth" }
+junit = { module = "junit:junit", version.ref = "junitVersion"}
+google-oss-licenses = { group = "com.google.android.gms", name = "play-services-oss-licenses", version.ref = "googleOss" }
+google-oss-licenses-plugin = { group = "com.google.android.gms", name = "oss-licenses-plugin", version.ref = "googleOssPlugin" }
+jetbrains-kotlin-jdk7 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlin" }
+firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
+firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" }
+firebase-cloud-messaging = { group = "com.google.firebase", name = "firebase-messaging-ktx" }
+firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" }
+firebase-performance = { group = "com.google.firebase", name = "firebase-perf-ktx" }
+dbflow-processor = { group = "com.github.Raizlabs.DBFlow", name = "dbflow-processor", version.ref = "dbflowVersion" }
+dbflow-core = { group = "com.github.Raizlabs.DBFlow", name = "dbflow-core", version.ref = "dbflowVersion" }
+dbflow = { group = "com.github.Raizlabs.DBFlow", name = "dbflow", version.ref = "dbflowVersion" }
+androidx-preference = { group = "androidx.preference", name = "preference", version.ref = "preference" }
+play-services-maps = { group = "com.google.android.gms", name = "play-services-maps", version.ref = "playServicesVersion" }
+
+# Dependencies of the included build-logic
+android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
+android-tools-common = { group = "com.android.tools", name = "common", version.ref = "androidTools" }
+firebase-crashlytics-gradlePlugin = { group = "com.google.firebase", name = "firebase-crashlytics-gradle", version.ref = "firebaseCrashlyticsPlugin" }
+firebase-performance-gradlePlugin = { group = "com.google.firebase", name = "perf-plugin", version.ref = "firebasePerfPlugin" }
+kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
+ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
+room-gradlePlugin = { group = "androidx.room", name = "room-gradle-plugin", version.ref = "room" }
+work-testing = { group = "androidx.work", name = "work-testing", version = "2.8.1" }
+androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
+espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
+android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
+gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin" }
+hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
+firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" }
+firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin" }
+dependencyGuard = { id = "com.dropbox.dependency-guard", version.ref = "dependencyGuard" }
+secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
+android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
+kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
+roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"}
+room = { id = "androidx.room", version.ref = "room" }
+spotbugs = { id = "com.github.spotbugs", version.ref = "spotbugs" }
+
+# Plugins defined by this project
+mifos-android-application = { id = "mifos.android.application", version = "unspecified" }
+mifos-android-application-compose = { id = "mifos.android.application.compose", version = "unspecified" }
+mifos-android-application-firebase = { id = "mifos.android.application.firebase", version = "unspecified" }
+mifos-android-feature = { id = "mifos.android.feature", version = "unspecified" }
+mifos-android-hilt = { id = "mifos.android.hilt", version = "unspecified" }
+mifos-android-library = { id = "mifos.android.library", version = "unspecified" }
+mifos-android-library-compose = { id = "mifos.android.library.compose", version = "unspecified" }
+mifos-android-lint = { id = "mifos.android.lint", version = "unspecified" }
+mifos-android-room = { id = "mifos.android.room", version = "unspecified" }
+mifos-android-test = { id = "mifos.android.test", version = "unspecified" }
+mifos-jvm-library = { id = "mifos.jvm.library", version = "unspecified" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 5fabd8ddc..60b08099b 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
diff --git a/settings.gradle b/settings.gradle
deleted file mode 100644
index f8f9b77af..000000000
--- a/settings.gradle
+++ /dev/null
@@ -1,2 +0,0 @@
-include ':app'
-include ':core'
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 000000000..90c45123e
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,24 @@
+pluginManagement {
+ includeBuild("build-logic")
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ maven("https://www.jitpack.io")
+ maven("https://plugins.gradle.org/m2/")
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ maven("https://www.jitpack.io")
+ maven("https://plugins.gradle.org/m2/")
+ }
+}
+rootProject.name = "mifos-mobile"
+
+enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
+include(":app")
+include(":ui")
diff --git a/core/.gitignore b/ui/.gitignore
similarity index 100%
rename from core/.gitignore
rename to ui/.gitignore
diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts
new file mode 100644
index 000000000..1ab8acab0
--- /dev/null
+++ b/ui/build.gradle.kts
@@ -0,0 +1,30 @@
+plugins {
+ alias(libs.plugins.mifos.android.library)
+ alias(libs.plugins.mifos.android.library.compose)
+}
+
+android {
+ defaultConfig {
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+ namespace = "org.mifos.mobile.core"
+}
+
+dependencies {
+ api(libs.androidx.compose.foundation)
+ api(libs.androidx.compose.foundation.layout)
+ api(libs.androidx.compose.material.iconsExtended)
+ api(libs.androidx.compose.material3)
+ api(libs.androidx.compose.runtime)
+ api(libs.androidx.compose.ui.tooling.preview)
+ api(libs.androidx.compose.ui.util)
+
+ debugApi(libs.androidx.compose.ui.tooling)
+
+ implementation("androidx.core:core-ktx:1.12.0")
+ implementation("androidx.appcompat:appcompat:1.6.1")
+ implementation("com.google.android.material:material:1.9.0")
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.5")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+}
diff --git a/core/consumer-rules.pro b/ui/consumer-rules.pro
similarity index 100%
rename from core/consumer-rules.pro
rename to ui/consumer-rules.pro
diff --git a/core/proguard-rules.pro b/ui/proguard-rules.pro
similarity index 94%
rename from core/proguard-rules.pro
rename to ui/proguard-rules.pro
index 481bb4348..ff59496d8 100644
--- a/core/proguard-rules.pro
+++ b/ui/proguard-rules.pro
@@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
+# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
diff --git a/core/src/androidTest/java/org/mifos/mifos/core/ExampleInstrumentedTest.kt b/ui/src/androidTest/java/org/mifos/mifos/core/ExampleInstrumentedTest.kt
similarity index 100%
rename from core/src/androidTest/java/org/mifos/mifos/core/ExampleInstrumentedTest.kt
rename to ui/src/androidTest/java/org/mifos/mifos/core/ExampleInstrumentedTest.kt
diff --git a/core/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml
similarity index 100%
rename from core/src/main/AndroidManifest.xml
rename to ui/src/main/AndroidManifest.xml
diff --git a/core/src/main/java/org/mifos/mobile/core/ui/component/AboutUsItemCard.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/AboutUsItemCard.kt
similarity index 100%
rename from core/src/main/java/org/mifos/mobile/core/ui/component/AboutUsItemCard.kt
rename to ui/src/main/java/org/mifos/mobile/core/ui/component/AboutUsItemCard.kt
diff --git a/ui/src/main/java/org/mifos/mobile/core/ui/component/EmptyDataView.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/EmptyDataView.kt
new file mode 100644
index 000000000..9c89c407b
--- /dev/null
+++ b/ui/src/main/java/org/mifos/mobile/core/ui/component/EmptyDataView.kt
@@ -0,0 +1,47 @@
+package org.mifos.mobile.core.ui.component
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun EmptyDataView(
+ modifier: Modifier = Modifier,
+ icon: Int,
+ error: Int
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(
+ modifier = Modifier
+ .size(100.dp)
+ .padding(bottom = 12.dp),
+ painter = painterResource(id = icon),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSecondary
+ )
+
+ Text(
+ text = stringResource(id = error),
+ style = TextStyle(fontSize = 20.sp),
+ color = MaterialTheme.colorScheme.onSecondary
+ )
+ }
+}
\ No newline at end of file
diff --git a/ui/src/main/java/org/mifos/mobile/core/ui/component/FaqItemHolder.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/FaqItemHolder.kt
new file mode 100644
index 000000000..7a849068d
--- /dev/null
+++ b/ui/src/main/java/org/mifos/mobile/core/ui/component/FaqItemHolder.kt
@@ -0,0 +1,86 @@
+package org.mifos.mobile.core.ui.component
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material3.Divider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun FaqItemHolder(
+ question: String?,
+ answer: String?,
+ isSelected: Boolean,
+ onItemSelected: (Int) -> Unit,
+ index: Int
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ ) {
+ Row(
+ modifier = Modifier
+ .clickable {
+ onItemSelected.invoke(index)
+ }
+ .padding(vertical = 8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = question.orEmpty(),
+ style = MaterialTheme.typography.bodyMedium,
+ color = if (isSystemInDarkTheme()) Color.White else Color.Black,
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ )
+
+ Icon(
+ imageVector = Icons.Default.ArrowDropDown,
+ contentDescription = "drop down",
+ tint = if (isSystemInDarkTheme()) Color.White else Color.Gray,
+ modifier = Modifier
+ .scale(1f, if (isSelected) -1f else 1f)
+ )
+ }
+
+ AnimatedVisibility(
+ visible = isSelected,
+ enter = fadeIn() + expandVertically(
+ animationSpec = spring(
+ stiffness = Spring.StiffnessMedium
+ )
+ )
+ ) {
+ Text(
+ text = answer.orEmpty(),
+ style = MaterialTheme.typography.bodyMedium,
+ color = if (isSystemInDarkTheme()) Color.White else Color.Black,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 8.dp)
+ )
+ }
+
+ Divider()
+ }
+}
diff --git a/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosHiddenTextRow.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosHiddenTextRow.kt
new file mode 100644
index 000000000..54c7523fa
--- /dev/null
+++ b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosHiddenTextRow.kt
@@ -0,0 +1,65 @@
+package org.mifos.mobile.core.ui.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun MifosHiddenTextRow(
+ modifier: Modifier = Modifier,
+ title: String,
+ hiddenText: String,
+ hiddenColor: Color,
+ hidingText: String,
+ visibilityIconId: Int,
+ visibilityOffIconId: Int,
+ onClick: () -> Unit
+) {
+ var isHidden by remember { mutableStateOf(true) }
+ Row(modifier.clickable { onClick.invoke() }, verticalAlignment = Alignment.CenterVertically) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.labelMedium,
+ modifier = Modifier
+ .alpha(0.7f)
+ .weight(1f)
+ )
+ Text(
+ text = if (isHidden) hidingText
+ else hiddenText,
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Bold,
+ color = hiddenColor
+ )
+ IconButton(
+ onClick = { isHidden = !isHidden },
+ modifier = Modifier
+ .padding(start = 6.dp)
+ .size(24.dp)
+ ) {
+ Icon(
+ painter = if (isHidden) painterResource(id = visibilityIconId)
+ else painterResource(id = visibilityOffIconId),
+ contentDescription = "Show or hide total amount",
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/mifos/mobile/core/ui/component/MifosItemCard.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosItemCard.kt
similarity index 100%
rename from core/src/main/java/org/mifos/mobile/core/ui/component/MifosItemCard.kt
rename to ui/src/main/java/org/mifos/mobile/core/ui/component/MifosItemCard.kt
diff --git a/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosLinkText.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosLinkText.kt
new file mode 100644
index 000000000..1a9ef763f
--- /dev/null
+++ b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosLinkText.kt
@@ -0,0 +1,33 @@
+package org.mifos.mobile.core.ui.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.dp
+import org.mifos.mobile.core.ui.theme.Blue700
+
+@Composable
+fun MifosLinkText(
+ modifier: Modifier = Modifier,
+ text: String,
+ onClick: (String) -> Unit,
+ isUnderlined: Boolean = true
+) {
+ Text(
+ text = text,
+ style = MaterialTheme.typography.bodyMedium.copy(
+ color = MaterialTheme.colorScheme.primary,
+ textDecoration = if(isUnderlined) TextDecoration.Underline else null
+ ),
+ modifier = modifier
+ .padding(vertical = 2.dp)
+ .clickable {
+ onClick(text)
+ },
+ )
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/mifos/mobile/core/ui/component/MifosMobileIcon.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosMobileIcon.kt
similarity index 100%
rename from core/src/main/java/org/mifos/mobile/core/ui/component/MifosMobileIcon.kt
rename to ui/src/main/java/org/mifos/mobile/core/ui/component/MifosMobileIcon.kt
diff --git a/core/src/main/java/org/mifos/mobile/core/ui/component/MifosOutlinedTextField.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosOutlinedTextField.kt
similarity index 100%
rename from core/src/main/java/org/mifos/mobile/core/ui/component/MifosOutlinedTextField.kt
rename to ui/src/main/java/org/mifos/mobile/core/ui/component/MifosOutlinedTextField.kt
diff --git a/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosProgressIndicator.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosProgressIndicator.kt
new file mode 100644
index 000000000..447d40dca
--- /dev/null
+++ b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosProgressIndicator.kt
@@ -0,0 +1,23 @@
+package org.mifos.mobile.core.ui.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+
+@Preview(showSystemUi = true)
+@Composable
+fun MifosProgressIndicator(
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ CircularProgressIndicator()
+ }
+}
\ No newline at end of file
diff --git a/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosSearchTextField.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosSearchTextField.kt
new file mode 100644
index 000000000..c88fdb09e
--- /dev/null
+++ b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosSearchTextField.kt
@@ -0,0 +1,71 @@
+package org.mifos.mobile.core.ui.component
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.TextFieldValue
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MifosSearchTextField(
+ modifier: Modifier = Modifier,
+ value: TextFieldValue,
+ onValueChange: (TextFieldValue) -> Unit,
+ onSearchDismiss: () -> Unit
+) {
+ val focusManager = LocalFocusManager.current
+
+ TextField(
+ modifier = modifier,
+ value = value,
+ placeholder = {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = null,
+ tint = Color.DarkGray
+ )
+ },
+ onValueChange = { onValueChange(it) },
+ textStyle = MaterialTheme.typography.bodyLarge,
+ trailingIcon = {
+ IconButton(onClick = { onSearchDismiss.invoke() }) {
+ Icon(
+ imageVector = Icons.Default.Close,
+ contentDescription = "Close Icon",
+ tint = Color.DarkGray,
+ )
+ }
+ },
+ colors = TextFieldDefaults.textFieldColors(
+ containerColor = Color.Transparent,
+ focusedIndicatorColor = Color.LightGray,
+ unfocusedIndicatorColor = Color.LightGray,
+ focusedTextColor = if (isSystemInDarkTheme()) Color.White else Color.Black,
+ unfocusedTextColor = if (isSystemInDarkTheme()) Color.White else Color.Black
+ ),
+ keyboardOptions = KeyboardOptions(
+ imeAction = ImeAction.Search
+ ),
+ keyboardActions = KeyboardActions(
+ onSearch = {
+ focusManager.clearFocus()
+ }
+ ),
+ singleLine = true
+ )
+}
\ No newline at end of file
diff --git a/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTextButtonWithTopDrawable.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTextButtonWithTopDrawable.kt
new file mode 100644
index 000000000..5af9ad42e
--- /dev/null
+++ b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTextButtonWithTopDrawable.kt
@@ -0,0 +1,60 @@
+package org.mifos.mobile.core.ui.component
+
+import android.graphics.drawable.Drawable
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Phone
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+
+
+@Composable
+fun MifosTextButtonWithTopDrawable(
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit,
+ textResourceId: Int,
+ icon: ImageVector,
+ contentDescription: String?
+) {
+ TextButton(
+ onClick = { onClick.invoke() },
+ modifier = modifier,
+ colors = ButtonDefaults.textButtonColors(
+ contentColor = if (isSystemInDarkTheme()) Color(
+ 0xFF9bb1e3
+ ) else Color(0xFF325ca8)
+ ),
+ content = {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = contentDescription,
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = stringResource(id = textResourceId),
+ textAlign = TextAlign.Center
+ )
+ }
+ }
+ )
+}
diff --git a/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTextUserImage.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTextUserImage.kt
new file mode 100644
index 000000000..92ecb5017
--- /dev/null
+++ b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTextUserImage.kt
@@ -0,0 +1,44 @@
+package org.mifos.mobile.core.ui.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
+import kotlin.math.min
+
+@Composable
+fun MifosTextUserImage(modifier: Modifier = Modifier, text: String) {
+ var boxSize by remember { mutableStateOf(Size.Zero) }
+ Box(
+ modifier = modifier
+ .clip(CircleShape)
+ .background(color = MaterialTheme.colorScheme.primary)
+ .onGloballyPositioned { coordinates ->
+ boxSize = Size(
+ coordinates.size.width.toFloat(),
+ coordinates.size.height.toFloat()
+ )
+ },
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = text,
+ color = MaterialTheme.colorScheme.onPrimary,
+ fontSize = with(LocalDensity.current) {
+ (min(boxSize.width, boxSize.height) / 2).toSp()
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTitleSearchCard.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTitleSearchCard.kt
new file mode 100644
index 000000000..9ba942569
--- /dev/null
+++ b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTitleSearchCard.kt
@@ -0,0 +1,81 @@
+package org.mifos.mobile.core.ui.component
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun MifosTitleSearchCard(
+ searchQuery: (String) -> Unit,
+ titleResourceId: Int,
+ onSearchDismiss: () -> Unit
+) {
+ var isSearching by rememberSaveable { mutableStateOf(false) }
+ var searchedQuery by rememberSaveable(stateSaver = TextFieldValue.Saver) {
+ mutableStateOf(
+ TextFieldValue("")
+ )
+ }
+
+ if (!isSearching) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = stringResource(id = titleResourceId),
+ color = MaterialTheme.colorScheme.onSurface,
+ style = TextStyle(fontSize = 24.sp),
+ modifier = Modifier.weight(1f),
+ maxLines = 1
+ )
+
+ IconButton(onClick = { isSearching = true }) {
+ Icon(
+ imageVector = Icons.Default.Search,
+ contentDescription = "Search Icon",
+ tint = MaterialTheme.colorScheme.onSurface,
+ )
+ }
+ }
+ } else {
+ Row(
+ modifier = Modifier
+ .padding(horizontal = 4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ MifosSearchTextField(
+ modifier = Modifier.fillMaxWidth(),
+ value = searchedQuery,
+ onValueChange = {
+ searchedQuery = it
+ searchQuery(it.text)
+ },
+ onSearchDismiss = {
+ searchedQuery = TextFieldValue("")
+ isSearching = false
+ onSearchDismiss.invoke()
+ },
+ )
+ }
+ }
+}
diff --git a/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTopBar.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTopBar.kt
new file mode 100644
index 000000000..e4a7033f6
--- /dev/null
+++ b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosTopBar.kt
@@ -0,0 +1,44 @@
+package org.mifos.mobile.core.ui.component
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MifosTopBar(
+ modifier: Modifier = Modifier,
+ navigateBack: () -> Unit,
+ title: @Composable () -> Unit
+) {
+ TopAppBar(
+ modifier = Modifier,
+ title = title,
+ navigationIcon = {
+ IconButton(
+ onClick = { navigateBack.invoke() }
+ ) {
+ Icon(
+ imageVector = Icons.Filled.ArrowBack,
+ contentDescription = "Back Arrow",
+ tint = if (isSystemInDarkTheme()) Color.White else Color.Black,
+ )
+ }
+ },
+ colors = TopAppBarDefaults.topAppBarColors(
+ containerColor = if (isSystemInDarkTheme())
+ Color(0xFF1B1B1F)
+ else
+ Color(0xFFFEFBFF)
+ ),
+ )
+}
diff --git a/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosUserImage.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosUserImage.kt
new file mode 100644
index 000000000..72bca7019
--- /dev/null
+++ b/ui/src/main/java/org/mifos/mobile/core/ui/component/MifosUserImage.kt
@@ -0,0 +1,40 @@
+package org.mifos.mobile.core.ui.component
+
+import android.graphics.Bitmap
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.layout.ContentScale
+
+/**
+ * @author pratyush
+ * @since 20/12/2023
+ */
+
+@Composable
+fun MifosUserImage(
+ bitmap: Bitmap?,
+ modifier: Modifier = Modifier,
+ username: String? = null
+) {
+ if (bitmap == null) {
+ MifosTextUserImage(
+ modifier = modifier,
+ text = username?.firstOrNull()?.toString() ?: "M"
+ )
+ } else {
+ Image(
+ modifier = modifier
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.primary),
+ bitmap = bitmap.asImageBitmap(),
+ contentDescription = "Profile Image",
+ contentScale = ContentScale.Crop,
+ )
+ }
+}
diff --git a/core/src/main/java/org/mifos/mobile/core/ui/component/NoInternet.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/NoInternet.kt
similarity index 95%
rename from core/src/main/java/org/mifos/mobile/core/ui/component/NoInternet.kt
rename to ui/src/main/java/org/mifos/mobile/core/ui/component/NoInternet.kt
index ffa83bf9d..0e048fd9a 100644
--- a/core/src/main/java/org/mifos/mobile/core/ui/component/NoInternet.kt
+++ b/ui/src/main/java/org/mifos/mobile/core/ui/component/NoInternet.kt
@@ -6,8 +6,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.material.Icon
-import androidx.compose.material.Text
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
diff --git a/core/src/main/java/org/mifos/mobile/core/ui/component/UserProfileField.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/UserProfileField.kt
similarity index 96%
rename from core/src/main/java/org/mifos/mobile/core/ui/component/UserProfileField.kt
rename to ui/src/main/java/org/mifos/mobile/core/ui/component/UserProfileField.kt
index 6720fd44c..feb51ecea 100644
--- a/core/src/main/java/org/mifos/mobile/core/ui/component/UserProfileField.kt
+++ b/ui/src/main/java/org/mifos/mobile/core/ui/component/UserProfileField.kt
@@ -6,8 +6,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.material.Divider
-import androidx.compose.material.Icon
+import androidx.compose.material3.Divider
+import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
diff --git a/core/src/main/java/org/mifos/mobile/core/ui/component/UserProfileTopBar.kt b/ui/src/main/java/org/mifos/mobile/core/ui/component/UserProfileTopBar.kt
similarity index 100%
rename from core/src/main/java/org/mifos/mobile/core/ui/component/UserProfileTopBar.kt
rename to ui/src/main/java/org/mifos/mobile/core/ui/component/UserProfileTopBar.kt
diff --git a/core/src/main/java/org/mifos/mobile/core/ui/theme/Color.kt b/ui/src/main/java/org/mifos/mobile/core/ui/theme/Color.kt
similarity index 72%
rename from core/src/main/java/org/mifos/mobile/core/ui/theme/Color.kt
rename to ui/src/main/java/org/mifos/mobile/core/ui/theme/Color.kt
index 59520f9a3..c56cff0fa 100644
--- a/core/src/main/java/org/mifos/mobile/core/ui/theme/Color.kt
+++ b/ui/src/main/java/org/mifos/mobile/core/ui/theme/Color.kt
@@ -10,4 +10,7 @@ val Black2 = Color(0xFF000000)
val BackgroundLight = Color(0xFFFEFBFF)
val BackgroundDark = Color(0xFF1B1B1F)
-val RedErrorDark = Color(0xFFB00020)
\ No newline at end of file
+val RedErrorDark = Color(0xFFB00020)
+
+val LightPrimary = Color(0xFF325ca8)
+val DarkPrimary = Color(0xFF9bb1e3)
\ No newline at end of file
diff --git a/core/src/main/java/org/mifos/mobile/core/ui/theme/Theme.kt b/ui/src/main/java/org/mifos/mobile/core/ui/theme/Theme.kt
similarity index 61%
rename from core/src/main/java/org/mifos/mobile/core/ui/theme/Theme.kt
rename to ui/src/main/java/org/mifos/mobile/core/ui/theme/Theme.kt
index 924beda10..ac12da2a4 100644
--- a/core/src/main/java/org/mifos/mobile/core/ui/theme/Theme.kt
+++ b/ui/src/main/java/org/mifos/mobile/core/ui/theme/Theme.kt
@@ -4,22 +4,30 @@ import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
private val LightThemeColors = lightColorScheme(
- primary = Blue600,
- onPrimary = Black2,
+ primary = LightPrimary,
+ onPrimary = Color.White,
error = RedErrorDark,
background = BackgroundLight,
onSurface = Black2,
+ onSecondary = Color.Gray,
+ outlineVariant = Color.Gray,
)
private val DarkThemeColors = darkColorScheme(
- primary = Blue700,
+ primary = DarkPrimary,
+ onPrimary = Color.White,
secondary = Black1,
error = RedErrorDark,
background = BackgroundDark,
surface = Black1,
+ onSurface = Color.White,
+ onSecondary = Color.White,
+ outlineVariant = Color.White
)
@Composable
@@ -29,10 +37,10 @@ fun MifosMobileTheme(
) {
val context = LocalContext.current
val colors = when {
- (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> {
- if (useDarkTheme) dynamicDarkColorScheme(context)
- else dynamicLightColorScheme(context)
- }
+// (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> {
+// if (useDarkTheme) dynamicDarkColorScheme(context)
+// else dynamicLightColorScheme(context)
+// }
useDarkTheme -> DarkThemeColors
else -> LightThemeColors
}
diff --git a/core/src/test/java/org/mifos/mobile/core/ExampleUnitTest.kt b/ui/src/test/java/org/mifos/mobile/core/ExampleUnitTest.kt
similarity index 100%
rename from core/src/test/java/org/mifos/mobile/core/ExampleUnitTest.kt
rename to ui/src/test/java/org/mifos/mobile/core/ExampleUnitTest.kt