diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml new file mode 100644 index 00000000000..8a635f16dc2 --- /dev/null +++ b/.github/workflows/android-release.yml @@ -0,0 +1,96 @@ +# GitHub Actions Workflow for Kotlin Android Application Deployment +# +# OVERVIEW: +# This workflow supports building and publishing applications across multiple platforms: +# - Android (APK/AAB) +# +# PREREQUISITES: +# Ensure your project is configured with: +# - Gradle build system +# - Kotlin Multiplatform Project with Android, iOS, Desktop, and Web modules +# - Fastlane for deployment automation +# - Separate modules/package names for each platform +# +# REQUIRED SECRETS: +# Configure the following secrets in GitHub repository settings: +# - ORIGINAL_KEYSTORE_FILE: Base64 encoded Android release keystore +# - ORIGINAL_KEYSTORE_FILE_PASSWORD: Keystore password +# - ORIGINAL_KEYSTORE_ALIAS: Keystore alias +# - ORIGINAL_KEYSTORE_ALIAS_PASSWORD: Keystore alias password + +# - UPLOAD_KEYSTORE_FILE: Base64 encoded Android release keystore +# - UPLOAD_KEYSTORE_FILE_PASSWORD: Keystore password +# - UPLOAD_KEYSTORE_ALIAS: Keystore alias +# - UPLOAD_KEYSTORE_ALIAS_PASSWORD: Keystore alias password + +# - GOOGLESERVICES: Google Services configuration JSON +# - PLAYSTORECREDS: Play Store service account credentials +# - FIREBASECREDS: Firebase distribution credentials + +# WORKFLOW INPUTS: +# - release_type: 'internal' (default) or 'beta' +# - target_branch: Branch to use for release (default: 'dev') +# - android_package_name: Name of Android module + +# USAGE: +# 1. Ensure all required secrets are configured +# 2. Customize package names in workflow inputs +# 3. Toggle platform-specific publishing flags +# 4. Trigger workflow manually or via GitHub Actions UI + +# https://github.com/openMF/mifos-mobile-github-actions/blob/main/.github/workflows/android-build-and-publish.yaml + +# ############################################################################## +# DON'T EDIT THIS FILE UNLESS NECESSARY # +# ############################################################################## +name: Android Build and Publish + +on: + workflow_dispatch: + inputs: + release_type: + type: choice + options: + - internal + - beta + default: internal + description: Release Type + + target_branch: + type: string + default: 'development' + description: 'Target branch for release' + +permissions: + contents: write + id-token: write + pages: write + +concurrency: + group: "reusable" + cancel-in-progress: false + +jobs: + android_build_and_publish: + name: Android Build and Publish + uses: openMF/mifos-mobile-github-actions/.github/workflows/android-build-and-publish.yaml@main + with: + release_type: ${{ inputs.release_type }} + target_branch: ${{ inputs.target_branch }} + android_package_name: 'mifosng-android' # <-- Change this to your android package name + tester_groups: 'mifos-mobile-testers' # <-- Change this to your Firebase tester group + secrets: + original_keystore_file: ${{ secrets.ORIGINAL_KEYSTORE_FILE }} + original_keystore_file_password: ${{ secrets.ORIGINAL_KEYSTORE_FILE_PASSWORD }} + original_keystore_alias: ${{ secrets.ORIGINAL_KEYSTORE_ALIAS }} + original_keystore_alias_password: ${{ secrets.ORIGINAL_KEYSTORE_ALIAS_PASSWORD }} + + upload_keystore_file: ${{ secrets.UPLOAD_KEYSTORE_FILE }} + upload_keystore_file_password: ${{ secrets.UPLOAD_KEYSTORE_FILE_PASSWORD }} + upload_keystore_alias: ${{ secrets.UPLOAD_KEYSTORE_ALIAS }} + upload_keystore_alias_password: ${{ secrets.UPLOAD_KEYSTORE_ALIAS_PASSWORD }} + + google_services: ${{ secrets.GOOGLESERVICES }} + firebase_creds: ${{ secrets.FIREBASECREDS }} + playstore_creds: ${{ secrets.PLAYSTORECREDS }} + token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/monthly-release.yaml b/.github/workflows/monthly-release.yaml new file mode 100644 index 00000000000..35abad5de17 --- /dev/null +++ b/.github/workflows/monthly-release.yaml @@ -0,0 +1,66 @@ +# Automated Monthly Release Versioning Workflow +# ============================================ + +# Purpose: +# - Automatically create consistent monthly version tags +# - Implement a calendar-based versioning strategy +# - Facilitate easy tracking of monthly releases + +# Versioning Strategy: +# - Tag format: YYYY.MM.0 (e.g., 2024.01.0 for January 2024) +# - First digit: Full year +# - Second digit: Month (01-12) +# - Third digit: Patch version (starts at 0, allows for potential updates) + +# Key Features: +# - Runs automatically on the first day of each month at 3:30 AM UTC +# - Can be manually triggered via workflow_dispatch +# - Uses GitHub Actions to generate tags programmatically +# - Provides a predictable and systematic versioning approach + +# Prerequisites: +# - Repository configured with GitHub Actions +# - Permissions to create tags +# - Access to actions/checkout and tag creation actions + +# Workflow Triggers: +# - Scheduled monthly run +# - Manual workflow dispatch +# - Callable from other workflows + +# Actions Used: +# 1. actions/checkout@v4 - Checks out repository code +# 2. josStorer/get-current-time - Retrieves current timestamp +# 3. rickstaa/action-create-tag - Creates Git tags + +# Example Generated Tags: +# - 2024.01.0 (January 2024 initial release) +# - 2024.02.0 (February 2024 initial release) +# - 2024.02.1 (Potential patch for February 2024) + +# https://github.com/openMF/mifos-mobile-github-actions/blob/main/.github/workflows/monthly-version-tag.yaml + +# ############################################################################## +# DON'T EDIT THIS FILE UNLESS NECESSARY # +# ############################################################################## + +name: Tag Monthly Release + +on: + # Allow manual triggering of the workflow + workflow_dispatch: + # Schedule the workflow to run monthly + schedule: + # Runs at 03:30 UTC on the first day of every month + # Cron syntax: minute hour day-of-month month day-of-week + - cron: '30 3 1 * *' + +concurrency: + group: "monthly-release" + cancel-in-progress: false + +jobs: + monthly_release: + name: Tag Monthly Release + uses: openMF/mifos-mobile-github-actions/.github/workflows/monthly-version-tag.yaml@main + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/promote-to-production.yml b/.github/workflows/promote-to-production.yml new file mode 100644 index 00000000000..0ebbc40172b --- /dev/null +++ b/.github/workflows/promote-to-production.yml @@ -0,0 +1,75 @@ +# GitHub Actions Workflow for Play Store Release Promotion +# +# PURPOSE: +# This workflow automates the process of promoting a beta release +# to the production track on Google Play Store. +# +# PREREQUISITES: +# 1. Fastlane setup with Android deployment configurations +# 2. Configured Fastlane lanes: +# - `promote_to_production`: Handles beta to production promotion +# +# REQUIRED CONFIGURATION: +# - Secrets: +# PLAYSTORECREDS: Google Play Store service account JSON credentials +# +# INPUTS: +# - android_package_name: Name of the Android project module +# (REQUIRED, must match your project's module structure) +# +# WORKFLOW TRIGGERS: +# - Can be called manually or triggered by other workflows +# - Typically used after beta testing and validation +# +# DEPLOYMENT PROCESS: +# 1. Checks out repository code +# 2. Sets up Ruby and Fastlane environment +# 3. Inflates Play Store credentials +# 4. Runs Fastlane lane to promote beta to production +# +# IMPORTANT NOTES: +# - Requires proper Fastlane configuration in your project +# - Ensures consistent and automated Play Store deployments +# - Configurable retry mechanism for upload stability +# +# RECOMMENDED FASTLANE LANE IMPLEMENTATION: +# ```ruby +# lane :promote_to_production do +# upload_to_play_store( +# track: 'beta', +# track_promote_to: 'production', +# json_key: './playStorePublishServiceCredentialsFile.json' +# ) +# end +# ``` + +# https://github.com/openMF/mifos-mobile-github-actions/blob/main/.github/workflows/promote-to-production.yaml + +# ############################################################################## +# DON'T EDIT THIS FILE UNLESS NECESSARY # +# ############################################################################## + +name: Promote Release to Play Store + +# Workflow triggers: +# 1. Manual trigger with option to publish to Play Store +# 2. Automatic trigger when a GitHub release is published +on: + workflow_dispatch: + release: + types: [ released ] + +concurrency: + group: "production-deploy" + cancel-in-progress: false + +permissions: + contents: write + +jobs: + # Job to promote app from beta to production in Play Store + play_promote_production: + name: Promote Beta to Production Play Store + uses: openMF/mifos-mobile-github-actions/.github/workflows/promote-to-production.yaml@main + secrets: + playstore_creds: ${{ secrets.PLAYSTORECREDS }} \ No newline at end of file diff --git a/.github/workflows/upload-demo-app-on-firebase.yaml b/.github/workflows/upload-demo-app-on-firebase.yaml new file mode 100644 index 00000000000..f41ed452f64 --- /dev/null +++ b/.github/workflows/upload-demo-app-on-firebase.yaml @@ -0,0 +1,43 @@ +name: Upload Demo App on Firebase + +on: + workflow_dispatch: + inputs: + tester_groups: + description: 'Comma-separated list of tester groups' + required: true + default: 'mifos-mobile-testers' + type: string + + pull_request: + types: [ labeled ] + branches: + - 'development' + - 'master' + +concurrency: + group: firebase-${{ github.ref }} + cancel-in-progress: true + +jobs: + upload_demo_app_on_firebase: + name: Upload Demo App on Firebase + runs-on: macos-latest + if: github.event.label.name == 'firebase-test-on' || github.event_name == 'workflow_dispatch' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: ☁️ Deploy Android App on Firebase + uses: openMF/kmp-android-firebase-publish-action@v1.0.0 + with: + release_type: 'demo' + android_package_name: 'mifosng-android' + keystore_file: ${{ secrets.ORIGINAL_KEYSTORE_FILE }} + keystore_password: ${{ secrets.ORIGINAL_KEYSTORE_FILE_PASSWORD }} + keystore_alias: ${{ secrets.ORIGINAL_KEYSTORE_ALIAS }} + keystore_alias_password: ${{ secrets.ORIGINAL_KEYSTORE_ALIAS_PASSWORD }} + google_services: ${{ secrets.GOOGLESERVICES }} + firebase_creds: ${{ secrets.FIREBASECREDS }} + tester_groups: ${{ inputs.tester_groups }} \ No newline at end of file diff --git a/.github/workflows/weekly-release.yaml b/.github/workflows/weekly-release.yaml new file mode 100644 index 00000000000..712e97690b7 --- /dev/null +++ b/.github/workflows/weekly-release.yaml @@ -0,0 +1,39 @@ +name: Tag Weekly Release + +on: + workflow_dispatch: + schedule: + - cron: '0 4 * * 0' +jobs: + tag: + name: Tag Weekly Release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up JDK 17 + uses: actions/setup-java@v4.2.2 + with: + distribution: 'temurin' + java-version: '17' + + - name: Tag Weekly Release + env: + GITHUB_TOKEN: ${{ secrets.TAG_PUSH_TOKEN }} + run: ./gradlew :reckonTagPush -Preckon.stage=final + + - name: Trigger Workflow + uses: actions/github-script@v7 + with: + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'android-release.yml', + ref: 'dev', + inputs: { + "release_type": "beta", + }, + }) \ No newline at end of file diff --git a/.gitignore b/.gitignore index eb51c38bc36..a3f39954a5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,74 @@ +*.iml .gradle /local.properties /.idea/workspace.xml +/.idea/libraries .DS_Store -.idea/ +/build +/captures +.externalNativeBuild +.idea +/*.iml + +# files for the dex VM +*.dex + +# Java class files +*.class + +# generated files +bin/ +gen/ +out/ build/ -mifosng-android/src/main/res/values/com_crashlytics_export_strings.xml -mifosng-android/mifosng-android.apk -mifosng-android/src/main/assets/crashlytics-build.properties -android-client.iml -mifosng-android/mifosng-android.iml -*.hprof -android-client.iml -/secrets.properties \ No newline at end of file +.externalNativeBuild +.cxx +iosApp/Podfile.lock +iosApp/Pods/* +iosApp/iosApp.xcworkspace/* +iosApp/iosApp.xcodeproj/* +!iosApp/iosApp.xcodeproj/project.pbxproj +mifospay-shared/mifospay-shared.podspec + +# 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 + +# Kotlin +.kotlin + +# Android Studio captures folder +captures/ + +/app/app-release.apk +app/app.iml +app/manifest-merger-release-report.txt + +# Exclude Google services from prod flavour +androidApp/src/prod/google-services.json + +#*.keystore + +version.txt +fastlane/report.xml +firebaseAppDistributionServiceCredentialsFile.json +playStorePublishServiceCredentialsFile.json + +# Ruby stuff we don't care about +.bundle/ +vendor/ +secrets/ \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000000..cee1044d56a --- /dev/null +++ b/Gemfile @@ -0,0 +1,8 @@ +source "https://rubygems.org" + +ruby '3.3.5' + +gem "fastlane" + +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000000..3d88467ab87 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,235 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.3.0) + aws-partitions (1.1029.0) + aws-sdk-core (3.214.1) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.96.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.176.1) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.2.0) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.112.0) + faraday (1.10.4) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.1.0) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.3.1) + fastlane (2.226.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored (~> 1.2) + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (>= 0.1.1, < 1.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.0) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-plugin-firebase_app_distribution (0.10.0) + google-apis-firebaseappdistribution_v1 (~> 0.3.0) + google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) + fastlane-plugin-increment_build_number (0.0.4) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-firebaseappdistribution_v1 (0.3.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-firebaseappdistribution_v1alpha (0.2.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.7.1) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.6.2) + json (2.9.1) + jwt (2.10.1) + base64 + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.15.0) + multipart-post (2.4.1) + nanaimo (0.4.0) + naturally (2.2.1) + nkf (0.2.0) + optparse (0.6.0) + os (1.1.4) + plist (3.7.2) + public_suffix (6.0.1) + rake (13.2.1) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.4.0) + rouge (3.28.0) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.5) + signet (0.19.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + sysrandom (1.0.5) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.0) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + x64-mingw-ucrt + +DEPENDENCIES + fastlane + fastlane-plugin-firebase_app_distribution + fastlane-plugin-increment_build_number + +RUBY VERSION + ruby 3.3.5p100 + +BUNDLED WITH + 2.5.23 diff --git a/build-logic/convention/src/main/kotlin/org/mifos/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/org/mifos/ProjectExtensions.kt index 81d6152df6e..6a3628294b6 100644 --- a/build-logic/convention/src/main/kotlin/org/mifos/ProjectExtensions.kt +++ b/build-logic/convention/src/main/kotlin/org/mifos/ProjectExtensions.kt @@ -19,4 +19,7 @@ inline fun Project.detektGradle(crossinline configure: DetektExtension.() -> Uni inline fun Project.spotlessGradle(crossinline configure: SpotlessExtension.() -> Unit) = extensions.configure { configure() - } \ No newline at end of file + } + +val Project.dynamicVersion: String + get() = this.version.toString().split('+')[0] \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 6bea7a6f59a..6fe7a87d6b3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,4 +14,18 @@ plugins { alias(libs.plugins.detekt) apply false alias(libs.plugins.spotless) apply false alias(libs.plugins.ktlint) apply false + alias(libs.plugins.gms) apply false +} + +object DynamicVersion { + fun setDynamicVersion(file: File, version: String) { + val cleanedVersion = version.split('+')[0] + file.writeText(cleanedVersion) + } +} + +tasks.register("versionFile") { + val file = File(projectDir, "version.txt") + + DynamicVersion.setDynamicVersion(file, project.version.toString()) } \ No newline at end of file diff --git a/core/common/src/main/java/com/mifos/core/common/model/user/User.kt b/core/common/src/main/java/com/mifos/core/common/model/user/User.kt deleted file mode 100644 index 1af685c7ea7..00000000000 --- a/core/common/src/main/java/com/mifos/core/common/model/user/User.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.core.common.model.user - -class User { - var username: String? = null - - var userId = 0 - - var base64EncodedAuthenticationKey: String? = null - - var isAuthenticated = false - - var officeId = 0 - - var officeName: String? = null - - var roles: List = ArrayList() - - var permissions: List = ArrayList() -} diff --git a/core/common/src/main/java/com/mifos/core/common/utils/Utils.kt b/core/common/src/main/java/com/mifos/core/common/utils/Utils.kt index ece73b26978..fce032964b6 100644 --- a/core/common/src/main/java/com/mifos/core/common/utils/Utils.kt +++ b/core/common/src/main/java/com/mifos/core/common/utils/Utils.kt @@ -14,18 +14,28 @@ import java.util.Calendar import java.util.TimeZone object Utils { - fun getStringOfDate(dateObj: List): String { - val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) - dateObj.getOrNull(0)?.let { year -> - calendar.set(Calendar.YEAR, year) - } - dateObj.getOrNull(1)?.let { month -> - calendar.set(Calendar.MONTH, month - 1) + // Create a single DateFormat instance to reuse + private val dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM) + + fun getStringOfDate(dateObj: List?): String { + if (dateObj == null) { + return "" } - dateObj.getOrNull(2)?.let { day -> - calendar.set(Calendar.DAY_OF_MONTH, day) + + return synchronized(dateFormat) { + val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")) + + if (dateObj.getOrNull(0) != null) { + calendar.set(Calendar.YEAR, dateObj[0]!!) + } + if (dateObj.getOrNull(1) != null) { + calendar.set(Calendar.MONTH, dateObj[1]!! - 1) + } + if (dateObj.getOrNull(2) != null) { + calendar.set(Calendar.DAY_OF_MONTH, dateObj[2]!!) + } + + dateFormat.format(calendar.time) } - val dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM) - return dateFormat.format(calendar.time) } } diff --git a/core/data/src/main/java/com/mifos/core/data/di/RepositoryModule.kt b/core/data/src/main/java/com/mifos/core/data/di/RepositoryModule.kt index 7cf6251ce58..906429e30bc 100644 --- a/core/data/src/main/java/com/mifos/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/java/com/mifos/core/data/di/RepositoryModule.kt @@ -9,22 +9,73 @@ */ package com.mifos.core.data.di +import android.content.Context import com.mifos.core.data.repository.ClientDetailsRepository import com.mifos.core.data.repository.ClientListRepository +import com.mifos.core.data.repository.CreateNewClientRepository +import com.mifos.core.data.repository.DocumentDialogRepository import com.mifos.core.data.repository.LoginRepository +import com.mifos.core.data.repository.NoteRepository +import com.mifos.core.data.repository.OfflineDashboardRepository +import com.mifos.core.data.repository.PreferenceRepository +import com.mifos.core.data.repository.SavingsAccountActivateRepository +import com.mifos.core.data.repository.SavingsAccountApprovalRepository +import com.mifos.core.data.repository.SavingsAccountRepository +import com.mifos.core.data.repository.SavingsAccountSummaryRepository +import com.mifos.core.data.repository.SavingsAccountTransactionRepository +import com.mifos.core.data.repository.SyncCenterPayloadsRepository +import com.mifos.core.data.repository.SyncCentersDialogRepository +import com.mifos.core.data.repository.SyncClientPayloadsRepository +import com.mifos.core.data.repository.SyncClientsDialogRepository +import com.mifos.core.data.repository.SyncGroupPayloadsRepository +import com.mifos.core.data.repository.SyncGroupsDialogRepository +import com.mifos.core.data.repository.SyncLoanRepaymentTransactionRepository +import com.mifos.core.data.repository.SyncSavingsAccountTransactionRepository +import com.mifos.core.data.repository.SyncSurveysDialogRepository import com.mifos.core.data.repositoryImp.ClientDetailsRepositoryImp import com.mifos.core.data.repositoryImp.ClientListRepositoryImp +import com.mifos.core.data.repositoryImp.CreateNewClientRepositoryImp +import com.mifos.core.data.repositoryImp.DocumentDialogRepositoryImp import com.mifos.core.data.repositoryImp.LoginRepositoryImp +import com.mifos.core.data.repositoryImp.NoteRepositoryImp +import com.mifos.core.data.repositoryImp.OfflineDashboardRepositoryImp +import com.mifos.core.data.repositoryImp.PreferenceRepositoryImpl +import com.mifos.core.data.repositoryImp.SavingsAccountActivateRepositoryImp +import com.mifos.core.data.repositoryImp.SavingsAccountApprovalRepositoryImp +import com.mifos.core.data.repositoryImp.SavingsAccountRepositoryImp +import com.mifos.core.data.repositoryImp.SavingsAccountSummaryRepositoryImp +import com.mifos.core.data.repositoryImp.SavingsAccountTransactionRepositoryImp +import com.mifos.core.data.repositoryImp.SyncCenterPayloadsRepositoryImp +import com.mifos.core.data.repositoryImp.SyncCentersDialogRepositoryImp +import com.mifos.core.data.repositoryImp.SyncClientPayloadsRepositoryImp +import com.mifos.core.data.repositoryImp.SyncClientsDialogRepositoryImp +import com.mifos.core.data.repositoryImp.SyncGroupPayloadsRepositoryImp +import com.mifos.core.data.repositoryImp.SyncGroupsDialogRepositoryImp +import com.mifos.core.data.repositoryImp.SyncLoanRepaymentTransactionRepositoryImp +import com.mifos.core.data.repositoryImp.SyncSavingsAccountTransactionRepositoryImp +import com.mifos.core.data.repositoryImp.SyncSurveysDialogRepositoryImp +import com.mifos.core.datastore.PrefManager +import com.mifos.core.network.datamanager.DataManagerCenter import com.mifos.core.network.datamanager.DataManagerClient +import com.mifos.core.network.datamanager.DataManagerDocument +import com.mifos.core.network.datamanager.DataManagerGroups +import com.mifos.core.network.datamanager.DataManagerLoan +import com.mifos.core.network.datamanager.DataManagerNote +import com.mifos.core.network.datamanager.DataManagerOffices +import com.mifos.core.network.datamanager.DataManagerSavings +import com.mifos.core.network.datamanager.DataManagerStaff +import com.mifos.core.network.datamanager.DataManagerSurveys import com.mifos.core.network.datamanger.DataManagerAuth import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) +@Suppress("TooManyFunctions") object RepositoryModule { @Provides @@ -39,4 +90,154 @@ object RepositoryModule { @Provides fun providesLoginRepository(dataManagerAuth: DataManagerAuth): LoginRepository = LoginRepositoryImp(dataManagerAuth) + + @Provides + fun providesSavingsAccountSummaryRepository(dataManagerSavings: DataManagerSavings): SavingsAccountSummaryRepository { + return SavingsAccountSummaryRepositoryImp(dataManagerSavings) + } + + @Provides + fun providesNoteRepository(dataManagerNote: DataManagerNote): NoteRepository { + return NoteRepositoryImp(dataManagerNote) + } + + @Provides + fun providesSavingAccountRepository(dataManagerSavings: DataManagerSavings): SavingsAccountRepository { + return SavingsAccountRepositoryImp(dataManagerSavings) + } + + @Provides + fun providesCreateNewClientRepository( + dataManagerClient: DataManagerClient, + dataManagerOffices: DataManagerOffices, + dataManagerStaff: DataManagerStaff, + ): CreateNewClientRepository { + return CreateNewClientRepositoryImp(dataManagerClient, dataManagerOffices, dataManagerStaff) + } + + @Provides + fun providesSavingsAccountTransactionRepository(dataManagerSavings: DataManagerSavings): SavingsAccountTransactionRepository { + return SavingsAccountTransactionRepositoryImp(dataManagerSavings) + } + + @Provides + fun providesSavingsAccountActivateRepository(dataManagerSavings: DataManagerSavings): SavingsAccountActivateRepository { + return SavingsAccountActivateRepositoryImp(dataManagerSavings) + } + + @Provides + fun providesSavingsAccountApprovalRepository(dataManagerSavings: DataManagerSavings): SavingsAccountApprovalRepository { + return SavingsAccountApprovalRepositoryImp(dataManagerSavings) + } + + @Provides + fun providesDocumentDialogRepository(dataManagerDocument: DataManagerDocument): DocumentDialogRepository { + return DocumentDialogRepositoryImp(dataManagerDocument) + } + + @Provides + fun providesSyncSurveysDialogRepository(dataManagerSurvey: DataManagerSurveys): SyncSurveysDialogRepository { + return SyncSurveysDialogRepositoryImp(dataManagerSurvey) + } + + @Provides + fun providesSyncGroupsDialogRepository( + dataManagerGroups: DataManagerGroups, + dataManagerLoan: DataManagerLoan, + dataManagerSavings: DataManagerSavings, + dataManagerClient: DataManagerClient, + ): SyncGroupsDialogRepository { + return SyncGroupsDialogRepositoryImp( + dataManagerGroups, + dataManagerLoan, + dataManagerSavings, + dataManagerClient, + ) + } + + @Provides + fun providesSyncClientsDialogRepository( + dataManagerClient: DataManagerClient, + dataManagerLoan: DataManagerLoan, + dataManagerSavings: DataManagerSavings, + ): SyncClientsDialogRepository { + return SyncClientsDialogRepositoryImp( + dataManagerClient, + dataManagerLoan, + dataManagerSavings, + ) + } + + @Provides + fun providesSyncCentersDialogRepository( + dataManagerCenter: DataManagerCenter, + dataManagerLoan: DataManagerLoan, + dataManagerSavings: DataManagerSavings, + dataManagerGroups: DataManagerGroups, + dataManagerClient: DataManagerClient, + ): SyncCentersDialogRepository { + return SyncCentersDialogRepositoryImp( + dataManagerCenter, + dataManagerLoan, + dataManagerSavings, + dataManagerGroups, + dataManagerClient, + ) + } + + @Provides + fun providesOfflineDashboardRepository( + dataManagerClient: DataManagerClient, + dataManagerGroups: DataManagerGroups, + dataManagerCenter: DataManagerCenter, + dataManagerLoan: DataManagerLoan, + dataManagerSavings: DataManagerSavings, + ): OfflineDashboardRepository { + return OfflineDashboardRepositoryImp( + dataManagerClient, + dataManagerGroups, + dataManagerCenter, + dataManagerLoan, + dataManagerSavings, + ) + } + + @Provides + fun providesSyncCenterPayloadsRepository(dataManagerCenter: DataManagerCenter): SyncCenterPayloadsRepository { + return SyncCenterPayloadsRepositoryImp(dataManagerCenter) + } + + @Provides + fun providesSyncSavingsAccountTransactionRepository( + dataManagerSavings: DataManagerSavings, + dataManagerLoan: DataManagerLoan, + ): SyncSavingsAccountTransactionRepository { + return SyncSavingsAccountTransactionRepositoryImp(dataManagerSavings, dataManagerLoan) + } + + @Provides + fun providesSyncLoanRepaymentTransactionRepository(dataManagerLoan: DataManagerLoan): SyncLoanRepaymentTransactionRepository { + return SyncLoanRepaymentTransactionRepositoryImp(dataManagerLoan) + } + + @Provides + fun providesSyncGroupPayloadsRepository(dataManagerGroups: DataManagerGroups): SyncGroupPayloadsRepository { + return SyncGroupPayloadsRepositoryImp(dataManagerGroups) + } + + @Provides + fun providesSyncClientPayloadsRepository(dataManagerClient: DataManagerClient): SyncClientPayloadsRepository { + return SyncClientPayloadsRepositoryImp(dataManagerClient) + } + + @Provides + @Singleton + fun providePrefManager(@ApplicationContext context: Context): PrefManager { + return PrefManager(context) + } + + @Provides + fun providesPreferenceRepository(prefManager: PrefManager): PreferenceRepository { + return PreferenceRepositoryImpl(prefManager) + } } diff --git a/core/data/src/main/java/com/mifos/core/data/repository/PreferenceRepository.kt b/core/data/src/main/java/com/mifos/core/data/repository/PreferenceRepository.kt new file mode 100644 index 00000000000..d9096887b44 --- /dev/null +++ b/core/data/src/main/java/com/mifos/core/data/repository/PreferenceRepository.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.data.repository + +import com.mifos.core.model.ServerConfig +import com.mifos.core.model.UserData +import kotlinx.coroutines.flow.Flow +import org.openapitools.client.models.PostAuthenticationResponse + +interface PreferenceRepository { + val isAuthenticated: Flow + val userDetails: Flow + val serverConfig: Flow + val userData: Flow + + fun updateServerConfig(config: ServerConfig) + fun saveUserDetails(userDetails: PostAuthenticationResponse) + fun logOut(): Unit +} diff --git a/feature/settings/src/main/java/com/mifos/feature/settings/syncSurvey/SyncSurveysDialogRepository.kt b/core/data/src/main/java/com/mifos/core/data/repository/SyncSurveysDialogRepository.kt similarity index 95% rename from feature/settings/src/main/java/com/mifos/feature/settings/syncSurvey/SyncSurveysDialogRepository.kt rename to core/data/src/main/java/com/mifos/core/data/repository/SyncSurveysDialogRepository.kt index 2ea363d83ff..d288cd61b43 100644 --- a/feature/settings/src/main/java/com/mifos/feature/settings/syncSurvey/SyncSurveysDialogRepository.kt +++ b/core/data/src/main/java/com/mifos/core/data/repository/SyncSurveysDialogRepository.kt @@ -7,7 +7,7 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ -package com.mifos.feature.settings.syncSurvey +package com.mifos.core.data.repository import com.mifos.core.objects.survey.QuestionDatas import com.mifos.core.objects.survey.ResponseDatas diff --git a/core/data/src/main/java/com/mifos/core/data/repositoryImp/GroupDetailsRepositoryImp.kt b/core/data/src/main/java/com/mifos/core/data/repositoryImp/GroupDetailsRepositoryImp.kt index ce8b0441b33..a03c181b6b7 100644 --- a/core/data/src/main/java/com/mifos/core/data/repositoryImp/GroupDetailsRepositoryImp.kt +++ b/core/data/src/main/java/com/mifos/core/data/repositoryImp/GroupDetailsRepositoryImp.kt @@ -20,8 +20,9 @@ import javax.inject.Inject /** * Created by Aditya Gupta on 06/08/23. */ -class GroupDetailsRepositoryImp @Inject constructor(private val dataManagerGroups: DataManagerGroups) : - GroupDetailsRepository { +class GroupDetailsRepositoryImp @Inject constructor( + private val dataManagerGroups: DataManagerGroups, +) : GroupDetailsRepository { override fun getGroup(groupId: Int): Observable { return dataManagerGroups.getGroup(groupId) diff --git a/core/data/src/main/java/com/mifos/core/data/repositoryImp/PreferenceRepositoryImpl.kt b/core/data/src/main/java/com/mifos/core/data/repositoryImp/PreferenceRepositoryImpl.kt new file mode 100644 index 00000000000..4c2cad9072c --- /dev/null +++ b/core/data/src/main/java/com/mifos/core/data/repositoryImp/PreferenceRepositoryImpl.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.data.repositoryImp + +import com.mifos.core.data.repository.PreferenceRepository +import com.mifos.core.datastore.PrefManager +import com.mifos.core.model.ServerConfig +import com.mifos.core.model.UserData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.openapitools.client.models.PostAuthenticationResponse +import javax.inject.Inject + +class PreferenceRepositoryImpl @Inject constructor( + private val prefManager: PrefManager, +) : PreferenceRepository { + override val isAuthenticated: Flow + get() = prefManager.userDetails.map { it != null && it.authenticated == true } + + override val userDetails: Flow + get() = prefManager.userDetails + + override val serverConfig: Flow + get() = prefManager.serverConfigFlow + + override val userData: Flow + get() = prefManager.userData + + override fun updateServerConfig(config: ServerConfig) { + prefManager.updateServerConfig(config) + } + + override fun saveUserDetails(userDetails: PostAuthenticationResponse) { + prefManager.saveUserDetails(userDetails) + } + + override fun logOut() { + prefManager.clearUserDetails() + } +} diff --git a/feature/settings/src/main/java/com/mifos/feature/settings/syncSurvey/SyncSurveysDialogRepositoryImp.kt b/core/data/src/main/java/com/mifos/core/data/repositoryImp/SyncSurveysDialogRepositoryImp.kt similarity index 90% rename from feature/settings/src/main/java/com/mifos/feature/settings/syncSurvey/SyncSurveysDialogRepositoryImp.kt rename to core/data/src/main/java/com/mifos/core/data/repositoryImp/SyncSurveysDialogRepositoryImp.kt index 73789b3960a..c07759243e5 100644 --- a/feature/settings/src/main/java/com/mifos/feature/settings/syncSurvey/SyncSurveysDialogRepositoryImp.kt +++ b/core/data/src/main/java/com/mifos/core/data/repositoryImp/SyncSurveysDialogRepositoryImp.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Mifos Initiative + * Copyright 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -7,8 +7,9 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ -package com.mifos.feature.settings.syncSurvey +package com.mifos.core.data.repositoryImp +import com.mifos.core.data.repository.SyncSurveysDialogRepository import com.mifos.core.network.datamanager.DataManagerSurveys import com.mifos.core.objects.survey.QuestionDatas import com.mifos.core.objects.survey.ResponseDatas diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index 61f31786f13..b0b4aeb75dd 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -12,6 +12,7 @@ plugins { alias(libs.plugins.mifos.android.hilt) alias(libs.plugins.mifos.android.library.jacoco) id(libs.plugins.kotlin.parcelize.get().pluginId) + alias(libs.plugins.kotlin.serialization) } android { @@ -42,11 +43,7 @@ dependencies { implementation(libs.hilt.android) kapt(libs.hilt.compiler) - // fineract sdk dependencies - implementation(libs.mifos.android.sdk.arch) - - // sdk client - implementation(libs.fineract.client) + implementation(libs.kotlinx.serialization.json) androidTestImplementation(projects.core.testing) } \ No newline at end of file diff --git a/core/database/consumer-rules.pro b/core/database/consumer-rules.pro index e69de29bb2d..15462fd5d68 100644 --- a/core/database/consumer-rules.pro +++ b/core/database/consumer-rules.pro @@ -0,0 +1,8 @@ +# Keep generic signature of RxJava2 (R8 full mode strips signatures from non-kept items). +-keep,allowobfuscation,allowshrinking class io.reactivex.Flowable +-keep,allowobfuscation,allowshrinking class io.reactivex.Maybe +-keep,allowobfuscation,allowshrinking class io.reactivex.Observable +-keep,allowobfuscation,allowshrinking class io.reactivex.Single + +-keep class com.mifos.core.** { *; } +-dontshrink \ No newline at end of file diff --git a/core/database/src/main/java/com/mifos/core/objects/user/User.kt b/core/database/src/main/java/com/mifos/core/objects/user/User.kt deleted file mode 100644 index b8a3d50056a..00000000000 --- a/core/database/src/main/java/com/mifos/core/objects/user/User.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.core.objects.user - -import com.mifos.core.objects.client.Role - -class User { - // {"username":"User1","userId":1,"base64EncodedAuthenticationKey":"VXNlcjE6dGVjaDRtZg\u003d - // \u003d", - // "authenticated":true,"officeId":1,"officeName":"Office1", - // "roles":[{"id":1,"name":"Admin","description":"Admin"}], - // "permissions":["ALL_FUNCTIONS"],"shouldRenewPassword":false} - var username: String? = null - var userId = 0 - var base64EncodedAuthenticationKey: String? = null - var isAuthenticated = false - var officeId = 0 - var officeName: String? = null - var roles: List = ArrayList() - var permissions: List = ArrayList() - override fun toString(): String { - return "User{" + - "username='" + username + '\'' + - ", userId=" + userId + - ", base64EncodedAuthenticationKey='" + base64EncodedAuthenticationKey + '\'' + - ", authenticated=" + isAuthenticated + - ", officeId=" + officeId + - ", officeName='" + officeName + '\'' + - ", roles=" + roles + - ", permissions=" + permissions + - '}' - } - - companion object { - const val AUTHENTICATION_KEY = "authenticationKey" - } -} diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts index f7bd49852b0..a00d5b30611 100644 --- a/core/datastore/build.gradle.kts +++ b/core/datastore/build.gradle.kts @@ -17,7 +17,7 @@ android { namespace = "com.mifos.core.datastore" defaultConfig { - consumerProguardFiles("consumer-proguard-rules.pro") + consumerProguardFiles("consumer-rules.pro") } testOptions { unitTests { @@ -27,8 +27,9 @@ android { } dependencies { - api(projects.core.model) - api(projects.core.common) + implementation(projects.core.model) + implementation(projects.core.common) + implementation(libs.androidx.preference.ktx) api(libs.converter.gson) diff --git a/feature/splash/consumer-rules.pro b/core/datastore/consumer-rules.pro similarity index 100% rename from feature/splash/consumer-rules.pro rename to core/datastore/consumer-rules.pro diff --git a/core/datastore/src/main/java/com/mifos/core/datastore/PrefManager.kt b/core/datastore/src/main/java/com/mifos/core/datastore/PrefManager.kt index 1a8e8b1d746..c041808b586 100644 --- a/core/datastore/src/main/java/com/mifos/core/datastore/PrefManager.kt +++ b/core/datastore/src/main/java/com/mifos/core/datastore/PrefManager.kt @@ -11,87 +11,170 @@ package com.mifos.core.datastore import android.content.Context import android.content.SharedPreferences -import android.preference.PreferenceManager -import com.mifos.core.common.BuildConfig -import com.mifos.core.common.model.user.User -import com.mifos.core.common.utils.Constants -import com.mifos.core.common.utils.asServerConfig +import androidx.annotation.WorkerThread +import com.google.gson.Gson import com.mifos.core.model.ServerConfig +import com.mifos.core.model.UserData import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import org.mifos.core.sharedpreference.Key -import org.mifos.core.sharedpreference.UserPreferences +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.map import org.openapitools.client.models.PostAuthenticationResponse import javax.inject.Inject +import javax.inject.Singleton -/** - * Created by Aditya Gupta on 19/08/23. - */ -const val USER_DETAILS = "user_details" -const val AUTH_USERNAME = "auth_username" -const val AUTH_PASSWORD = "auth_password" - +@Singleton class PrefManager @Inject constructor( @ApplicationContext context: Context, -) : UserPreferences() { +) { + companion object { + private const val PREF_NAME = "mifos_pref" + private const val AUTH_USER = "user_details" + private const val SERVER_CONFIG_KEY = "server_config" + private const val USER_DATA = "user_data" + private const val USER_STATUS = "user_status" + private const val AUTH_USERNAME = "auth_username" + private const val AUTH_PASSWORD = "auth_password" + } - private val serverConfigKey = Key.Custom("SERVER_CONFIG_KEY") + private val preference: SharedPreferences = context.getSharedPreferences( + PREF_NAME, + Context.MODE_PRIVATE, + ) - override val preference: SharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context) + private val gson: Gson = Gson() - override fun getUser(): User { - return gson.fromJson(preference.getString(USER_DETAILS, ""), User::class.java) - } + val serverConfigFlow = preference.getStringFlowForKey(SERVER_CONFIG_KEY) + .map { gson.fromJson(it, ServerConfig::class.java) ?: ServerConfig.DEFAULT } - override fun saveUser(user: User) { - preference.edit().putString(USER_DETAILS, gson.toJson(user)).apply() - } + val serverConfig: ServerConfig + get() = preference.getString(SERVER_CONFIG_KEY, null) + ?.let { gson.fromJson(it, ServerConfig::class.java) } + ?: ServerConfig.DEFAULT + + // Usage example in PrefManager + val userDetails: Flow = preference.getFlow( + key = AUTH_USER, + deserializer = { gson.fromJson(it, PostAuthenticationResponse::class.java) }, + defaultValue = null, + ) + + val userData: Flow = preference.getFlow( + key = USER_DATA, + deserializer = { gson.fromJson(it, UserData::class.java) }, + defaultValue = UserData.DEFAULT, + ) + + val user: PostAuthenticationResponse? + get() = preference.getString(AUTH_USER, null) + ?.let { gson.fromJson(it, PostAuthenticationResponse::class.java) } - // Created this to store userDetails - fun savePostAuthenticationResponse(user: PostAuthenticationResponse) { - preference.edit().putString(USER_DETAILS, gson.toJson(user)).apply() + val isAuthenticated: Boolean + get() = user?.authenticated == true + + val token: String + get() = user?.base64EncodedAuthenticationKey?.let { "Basic $it" } ?: "" + + val userStatus: Boolean + get() = preference.getBoolean(USER_STATUS, false) + + fun updateUserStatus(status: Boolean) { + preference.edit { putBoolean(USER_STATUS, status) } } - fun setPermissionDeniedStatus(permissionDeniedStatus: String, status: Boolean) { - preference.edit().putBoolean(permissionDeniedStatus, status).apply() + fun updateServerConfig(config: ServerConfig?) { + config?.let { + val jsonConfig = gson.toJson(it, ServerConfig::class.java) + + jsonConfig?.let { + preference.edit { + putString(SERVER_CONFIG_KEY, jsonConfig) + } + } + } } - fun getPermissionDeniedStatus(permissionDeniedStatus: String): Boolean { - return preference.getBoolean(permissionDeniedStatus, true) + fun saveUserDetails(userDetails: PostAuthenticationResponse) { + val jsonUserDetails = gson.toJson(userDetails, PostAuthenticationResponse::class.java) + + jsonUserDetails?.let { + preference.edit { putString(AUTH_USER, jsonUserDetails) } + } } - var userStatus: Boolean - get() = preference.getBoolean(Constants.SERVICE_STATUS, false) - set(status) { - preference.edit().putBoolean(Constants.SERVICE_STATUS, status).apply() + fun saveUserData(userData: UserData) { + val jsonUserData = gson.toJson(userData, UserData::class.java) + + jsonUserData?.let { + preference.edit { putString(USER_DATA, jsonUserData) } } + } + + @WorkerThread + fun clearUserDetails() { + preference.edit { remove(AUTH_USER) } + } var usernamePassword: Pair get() = Pair( - preference.getString(AUTH_USERNAME, "")!!, - preference.getString(AUTH_PASSWORD, "")!!, + preference.getString(AUTH_USERNAME, "") ?: "", + preference.getString(AUTH_PASSWORD, "") ?: "", ) set(value) { - preference.edit().putString(AUTH_USERNAME, value.first).apply() - preference.edit().putString(AUTH_PASSWORD, value.second).apply() + preference.edit { + putString(AUTH_USERNAME, value.first) + putString(AUTH_PASSWORD, value.second) + } } - val getServerConfig: ServerConfig = - preference.getString(serverConfigKey.value, null)?.let { - gson.fromJson(it, ServerConfig::class.java) - } ?: BuildConfig.DEMO_SERVER_CONFIG.asServerConfig() - - fun updateServerConfig(config: ServerConfig?) { - this.put(serverConfigKey, config) + // Extension function for simplified SharedPreferences editing + private inline fun SharedPreferences.edit(action: SharedPreferences.Editor.() -> Unit) { + edit().apply(action).apply() } +} - fun getStringValue(key: String): Flow = flow { - emit(preference.getString(key, "")) +// Extension function for creating a Flow from SharedPreferences +private fun SharedPreferences.getStringFlowForKey(keyForString: String): Flow = + callbackFlow { + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> + if (keyForString == key) { + trySend(getString(key, "") ?: "") + } + } + registerOnSharedPreferenceChangeListener(listener) + + // Initial value emission + if (contains(keyForString)) { + trySend(getString(keyForString, "") ?: "") + } + + awaitClose { + unregisterOnSharedPreferenceChangeListener(listener) + } } - fun setStringValue(key: String, value: String) { - preference.edit().putString(key, value).apply() +private fun SharedPreferences.getFlow( + key: String, + deserializer: (String) -> T, + defaultValue: T, +): Flow { + val stateFlow = MutableStateFlow( + getString(key, null)?.let { deserializer(it) } ?: defaultValue, + ) + + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, changedKey -> + if (changedKey == key) { + val newValue = getString(key, null)?.let { deserializer(it) } ?: defaultValue + stateFlow.value = newValue + } } + + registerOnSharedPreferenceChangeListener(listener) + + return stateFlow + .asStateFlow() + .map { it ?: defaultValue } } diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt index 989190c5805..7374b460d44 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt @@ -12,7 +12,6 @@ package com.mifos.core.designsystem.component import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions @@ -26,7 +25,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -46,10 +44,6 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.mifos.core.designsystem.theme.BluePrimary -import com.mifos.core.designsystem.theme.BluePrimaryDark -import com.mifos.core.designsystem.theme.DarkGray -import com.mifos.core.designsystem.theme.White /** * Created by Aditya Gupta on 21/02/24. @@ -80,7 +74,6 @@ fun MifosOutlinedTextField( Icon( imageVector = icon, contentDescription = null, - tint = if (isSystemInDarkTheme()) White else DarkGray, ) } } else { @@ -89,9 +82,6 @@ fun MifosOutlinedTextField( trailingIcon = trailingIcon, maxLines = maxLines, singleLine = singleLine, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - ), textStyle = LocalDensity.current.run { TextStyle(fontSize = 18.sp) }, @@ -221,7 +211,9 @@ fun MifosOutlinedTextField( onValueChange: (String) -> Unit, label: String, error: Int?, - modifier: Modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp), + modifier: Modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp), maxLines: Int = 1, readOnly: Boolean = false, singleLine: Boolean = true, @@ -243,7 +235,6 @@ fun MifosOutlinedTextField( Icon( imageVector = icon, contentDescription = null, - tint = if (isSystemInDarkTheme()) White else DarkGray, ) } } else { @@ -252,11 +243,6 @@ fun MifosOutlinedTextField( trailingIcon = trailingIcon, maxLines = maxLines, singleLine = singleLine, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - focusedLabelColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - cursorColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - ), textStyle = LocalDensity.current.run { TextStyle(fontSize = 18.sp) }, @@ -330,7 +316,7 @@ private fun ClearIconButton( @Composable fun MifosDatePickerTextField( value: String, - modifier: Modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp), + modifier: Modifier = Modifier, label: Int? = null, labelString: String? = null, openDatePicker: () -> Unit, @@ -340,11 +326,10 @@ fun MifosDatePickerTextField( onValueChange = { }, label = { Text(text = labelString ?: label?.let { stringResource(id = label) } ?: "") }, readOnly = true, - modifier = modifier, + modifier = modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp), maxLines = 1, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - ), textStyle = LocalDensity.current.run { TextStyle(fontSize = 18.sp) }, diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEmptyContent.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEmptyContent.kt index f5f4d81b1dc..a41300abcb0 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEmptyContent.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEmptyContent.kt @@ -18,7 +18,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Error import androidx.compose.material3.Button 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 @@ -50,7 +49,6 @@ fun MifosErrorContent( Text( text = message, modifier = Modifier.padding(vertical = 16.dp), - color = MaterialTheme.colorScheme.onBackground, ) Button(onClick = onRefresh) { Text(text = refreshButtonText) diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosMenuDropdown.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosMenuDropdown.kt index 231ed54faf3..9161d7a4e9a 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosMenuDropdown.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosMenuDropdown.kt @@ -14,9 +14,7 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp @Composable fun MifosMenuDropDownItem( @@ -29,7 +27,6 @@ fun MifosMenuDropDownItem( Text( modifier = Modifier.padding(6.dp), text = option, - style = TextStyle(fontSize = 17.sp), ) }, onClick = { onClick() }, diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosPermissionBox.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosPermissionBox.kt index 0d3cd1a0a97..c4780423ed1 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosPermissionBox.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosPermissionBox.kt @@ -66,10 +66,10 @@ fun PermissionBox( } val decideCurrentPermissionStatus: (Boolean, Boolean) -> String = - { permissionGranted, shouldShowPermissionRationale -> - if (permissionGranted) { + { granted, rationale -> + if (granted) { "Granted" - } else if (shouldShowPermissionRationale) { + } else if (rationale) { "Rejected" } else { "Denied" @@ -89,7 +89,7 @@ fun PermissionBox( contract = ActivityResultContracts.RequestMultiplePermissions(), onResult = { permissionResults -> val isGranted = - requiredPermissions.all { permissionResults[it] ?: false } + requiredPermissions.all { permissionResults[it] == true } permissionGranted = isGranted @@ -112,24 +112,27 @@ fun PermissionBox( }, ) - DisposableEffect(key1 = lifecycleOwner, effect = { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_START && - !permissionGranted && - !shouldShowPermissionRationale - ) { - multiplePermissionLauncher.launch(requiredPermissions.toTypedArray()) + DisposableEffect( + key1 = lifecycleOwner, + effect = { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START && + !permissionGranted && + !shouldShowPermissionRationale + ) { + multiplePermissionLauncher.launch(requiredPermissions.toTypedArray()) + } } - } - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) - } - }) + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + }, + ) if (shouldShowPermissionRationale) { MifosDialogBox( - showDialogState = shouldShowPermissionRationale, + showDialogState = true, onDismiss = { shouldShowPermissionRationale = false }, title = title, message = description, diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosProgressIndicator.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosProgressIndicator.kt index 9b0d7a796c1..c2ad8b2517f 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosProgressIndicator.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosProgressIndicator.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.mifos.core.designsystem.theme.DarkGray /** * Created by Aditya Gupta on 21/02/24. @@ -74,7 +73,6 @@ fun MifosCircularProgress( .height(60.dp) .padding(8.dp), strokeWidth = 4.dp, - color = DarkGray, ) text?.let { Text(text = text) diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosScaffold.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosScaffold.kt index 62bbb00f94c..c22d528cdd2 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosScaffold.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosScaffold.kt @@ -13,6 +13,7 @@ package com.mifos.core.designsystem.component import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FabPosition import androidx.compose.material3.Icon @@ -22,20 +23,11 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.sp -import com.mifos.core.designsystem.theme.Black -import com.mifos.core.designsystem.theme.White @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -45,7 +37,6 @@ fun MifosScaffold( isAppBarPresent: Boolean = true, icon: ImageVector? = null, title: String? = null, - fontsizeInSp: Int = 24, onBackPressed: () -> Unit = {}, actions: @Composable () -> Unit = {}, bottomBar: @Composable () -> Unit = {}, @@ -57,7 +48,6 @@ fun MifosScaffold( topBar = { if (isAppBarPresent) { TopAppBar( - colors = TopAppBarDefaults.mediumTopAppBarColors(containerColor = White), navigationIcon = { if (icon != null) { IconButton( @@ -66,7 +56,6 @@ fun MifosScaffold( Icon( imageVector = icon, contentDescription = null, - tint = Black, ) } } @@ -75,13 +64,7 @@ fun MifosScaffold( title?.let { Text( text = it, - style = TextStyle( - fontSize = fontsizeInSp.sp, - fontWeight = FontWeight.Medium, - fontStyle = FontStyle.Normal, - ), - color = Black, - textAlign = TextAlign.Start, + fontWeight = FontWeight.SemiBold, ) } }, @@ -90,7 +73,6 @@ fun MifosScaffold( } }, snackbarHost = { snackbarHostState?.let { SnackbarHost(it) } }, - containerColor = White, bottomBar = bottomBar, floatingActionButton = floatingActionButton, ) { padding -> @@ -106,19 +88,15 @@ fun MifosScaffold( floatingActionButton: @Composable () -> Unit = {}, snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, floatingActionButtonPosition: FabPosition = FabPosition.End, - containerColor: Color = Color.White, - contentColor: Color = contentColorFor(containerColor), content: @Composable (PaddingValues) -> Unit, ) { Scaffold( - modifier = modifier, + modifier = modifier.fillMaxSize(), topBar = topBar, floatingActionButton = floatingActionButton, floatingActionButtonPosition = floatingActionButtonPosition, snackbarHost = { SnackbarHost(snackbarHostState) }, bottomBar = bottomBar, - containerColor = containerColor, - contentColor = contentColor, contentWindowInsets = WindowInsets(0, 0, 0, 0), ) { padding -> content(padding) diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosSweetError.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosSweetError.kt index c4566bc160b..ade2016edd7 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosSweetError.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosSweetError.kt @@ -9,7 +9,6 @@ */ package com.mifos.core.designsystem.component -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -22,14 +21,12 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults 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 import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics @@ -40,9 +37,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import com.mifos.core.designsystem.R -import com.mifos.core.designsystem.theme.BluePrimary -import com.mifos.core.designsystem.theme.BluePrimaryDark -import com.mifos.core.designsystem.theme.DarkGray @Composable fun MifosSweetError( @@ -64,7 +58,6 @@ fun MifosSweetError( modifier = Modifier.size(70.dp), model = R.drawable.core_designsystem_ic_error_black_24dp, contentDescription = null, - colorFilter = ColorFilter.tint(Color.Gray), ) Spacer(modifier = Modifier.height(20.dp)) Text( @@ -73,7 +66,6 @@ fun MifosSweetError( fontSize = 14.sp, fontWeight = FontWeight.Normal, fontStyle = FontStyle.Normal, - color = DarkGray, ), ) Text( @@ -82,7 +74,6 @@ fun MifosSweetError( fontSize = 14.sp, fontWeight = FontWeight.Normal, fontStyle = FontStyle.Normal, - color = DarkGray, ), ) if (isRetryEnabled) { @@ -90,9 +81,6 @@ fun MifosSweetError( Button( onClick = { onclick() }, contentPadding = PaddingValues(), - colors = ButtonDefaults.buttonColors( - containerColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - ), ) { Text( modifier = Modifier.padding(start = 20.dp, end = 20.dp), @@ -123,19 +111,10 @@ fun MifosPaginationSweetError( ) Text( text = stringResource(id = R.string.core_designsystem_unable_to_load), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = DarkGray, - ), ) Button( onClick = { onclick() }, contentPadding = PaddingValues(), - colors = ButtonDefaults.buttonColors( - containerColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - ), ) { Text( modifier = Modifier diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt index 3865a01cb65..58de294ad3f 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt @@ -11,16 +11,17 @@ package com.mifos.core.designsystem.component -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -28,14 +29,17 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.mifos.core.designsystem.theme.BluePrimary -import com.mifos.core.designsystem.theme.BluePrimaryDark +import androidx.compose.ui.unit.toSize +import androidx.compose.ui.window.PopupProperties @Composable fun MifosTextFieldDropdown( @@ -43,29 +47,35 @@ fun MifosTextFieldDropdown( onValueChanged: (String) -> Unit, onOptionSelected: (Int, String) -> Unit, options: List, - modifier: Modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp), + modifier: Modifier = Modifier, label: Int? = null, labelString: String? = null, readOnly: Boolean = false, ) { var isExpended by remember { mutableStateOf(false) } + val height = (LocalConfiguration.current.screenHeightDp / 2).dp + var textFieldSize by remember { mutableStateOf(Size.Zero) } + val width = with(LocalDensity.current) { textFieldSize.width.toDp() } ExposedDropdownMenuBox( expanded = isExpended, onExpandedChange = { isExpended = it }, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), ) { OutlinedTextField( value = value, onValueChange = { onValueChanged(it) }, label = { Text(text = labelString ?: label?.let { stringResource(id = label) } ?: "") }, - modifier = modifier.menuAnchor(), + modifier = Modifier + .fillMaxWidth() + .onGloballyPositioned { coordinates -> + // This is used to assign to the DropDown the same width + textFieldSize = coordinates.size.toSize() + } + .menuAnchor(), maxLines = 1, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - focusedLabelColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - ), textStyle = LocalDensity.current.run { TextStyle(fontSize = 18.sp) }, @@ -76,13 +86,24 @@ fun MifosTextFieldDropdown( readOnly = readOnly, ) - ExposedDropdownMenu( + DropdownMenu( expanded = isExpended, onDismissRequest = { isExpended = false }, + properties = PopupProperties( + focusable = false, + dismissOnBackPress = true, + dismissOnClickOutside = true, + excludeFromSystemGesture = true, + clippingEnabled = true, + ), + modifier = Modifier + .width(width = width) + .heightIn(max = height), ) { options.forEachIndexed { index, value -> DropdownMenuItem( text = { Text(text = value) }, + modifier = Modifier.fillMaxWidth(), onClick = { isExpended = false onOptionSelected(index, value) diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/icon/MifosIcons.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/icon/MifosIcons.kt index 78bd2e26c10..a2f8760eaa5 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/icon/MifosIcons.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/icon/MifosIcons.kt @@ -14,6 +14,7 @@ import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.AssignmentTurnedIn import androidx.compose.material.icons.filled.CloudDownload import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.WifiOff import androidx.compose.material.icons.outlined.DateRange import androidx.compose.material.icons.outlined.EventRepeat @@ -40,6 +41,7 @@ import androidx.compose.material.icons.rounded.Sync import androidx.compose.material.icons.rounded.Translate object MifosIcons { + val Link = Icons.Default.Link val Add = Icons.Rounded.Add val person = Icons.Rounded.PersonOutline val group = Icons.Outlined.Group diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Color.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Color.kt index 6df456f2c0c..31b1db95b5f 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Color.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Color.kt @@ -11,6 +11,222 @@ package com.mifos.core.designsystem.theme import androidx.compose.ui.graphics.Color +val primaryLight = Color(0xFF06438F) +val onPrimaryLight = Color(0xFFFFFFFF) +val primaryContainerLight = Color(0xFF2D5BA8) +val onPrimaryContainerLight = Color(0xFFC5D6FF) +val secondaryLight = Color(0xFF505E7E) +val onSecondaryLight = Color(0xFFFFFFFF) +val secondaryContainerLight = Color(0xFFCBDAFF) +val onSecondaryContainerLight = Color(0xFF515F7F) +val tertiaryLight = Color(0xFF682C74) +val onTertiaryLight = Color(0xFFFFFFFF) +val tertiaryContainerLight = Color(0xFF82448E) +val onTertiaryContainerLight = Color(0xFFFAC4FF) +val errorLight = Color(0xFFBA1A1A) +val onErrorLight = Color(0xFFFFFFFF) +val errorContainerLight = Color(0xFFFFDAD6) +val onErrorContainerLight = Color(0xFF93000A) +val backgroundLight = Color(0xFFF9F9FF) +val onBackgroundLight = Color(0xFF1A1B21) +val surfaceLight = Color(0xFFF9F9FF) +val onSurfaceLight = Color(0xFF1A1B21) +val surfaceVariantLight = Color(0xFFDFE2EF) +val onSurfaceVariantLight = Color(0xFF434751) +val outlineLight = Color(0xFF737782) +val outlineVariantLight = Color(0xFFC3C6D3) +val scrimLight = Color(0xFF000000) +val inverseSurfaceLight = Color(0xFF2F3036) +val inverseOnSurfaceLight = Color(0xFFF0F0F7) +val inversePrimaryLight = Color(0xFFADC6FF) +val surfaceDimLight = Color(0xFFD9D9E0) +val surfaceBrightLight = Color(0xFFF9F9FF) +val surfaceContainerLowestLight = Color(0xFFFFFFFF) +val surfaceContainerLowLight = Color(0xFFF3F3FA) +val surfaceContainerLight = Color(0xFFEDEDF4) +val surfaceContainerHighLight = Color(0xFFE8E7EF) +val surfaceContainerHighestLight = Color(0xFFE2E2E9) + +val primaryLightMediumContrast = Color(0xFF003474) +val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) +val primaryContainerLightMediumContrast = Color(0xFF2D5BA8) +val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val secondaryLightMediumContrast = Color(0xFF273653) +val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) +val secondaryContainerLightMediumContrast = Color(0xFF5E6D8D) +val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryLightMediumContrast = Color(0xFF571B64) +val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightMediumContrast = Color(0xFF82448E) +val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val errorLightMediumContrast = Color(0xFF740006) +val onErrorLightMediumContrast = Color(0xFFFFFFFF) +val errorContainerLightMediumContrast = Color(0xFFCF2C27) +val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) +val backgroundLightMediumContrast = Color(0xFFF9F9FF) +val onBackgroundLightMediumContrast = Color(0xFF1A1B21) +val surfaceLightMediumContrast = Color(0xFFF9F9FF) +val onSurfaceLightMediumContrast = Color(0xFF0F1116) +val surfaceVariantLightMediumContrast = Color(0xFFDFE2EF) +val onSurfaceVariantLightMediumContrast = Color(0xFF323640) +val outlineLightMediumContrast = Color(0xFF4E525D) +val outlineVariantLightMediumContrast = Color(0xFF696D78) +val scrimLightMediumContrast = Color(0xFF000000) +val inverseSurfaceLightMediumContrast = Color(0xFF2F3036) +val inverseOnSurfaceLightMediumContrast = Color(0xFFF0F0F7) +val inversePrimaryLightMediumContrast = Color(0xFFADC6FF) +val surfaceDimLightMediumContrast = Color(0xFFC6C6CD) +val surfaceBrightLightMediumContrast = Color(0xFFF9F9FF) +val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightMediumContrast = Color(0xFFF3F3FA) +val surfaceContainerLightMediumContrast = Color(0xFFE8E7EF) +val surfaceContainerHighLightMediumContrast = Color(0xFFDCDCE3) +val surfaceContainerHighestLightMediumContrast = Color(0xFFD1D1D8) + +val primaryLightHighContrast = Color(0xFF002A61) +val onPrimaryLightHighContrast = Color(0xFFFFFFFF) +val primaryContainerLightHighContrast = Color(0xFF104793) +val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) +val secondaryLightHighContrast = Color(0xFF1D2C49) +val onSecondaryLightHighContrast = Color(0xFFFFFFFF) +val secondaryContainerLightHighContrast = Color(0xFF3B4967) +val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) +val tertiaryLightHighContrast = Color(0xFF4B0E59) +val onTertiaryLightHighContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightHighContrast = Color(0xFF6C3079) +val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) +val errorLightHighContrast = Color(0xFF600004) +val onErrorLightHighContrast = Color(0xFFFFFFFF) +val errorContainerLightHighContrast = Color(0xFF98000A) +val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) +val backgroundLightHighContrast = Color(0xFFF9F9FF) +val onBackgroundLightHighContrast = Color(0xFF1A1B21) +val surfaceLightHighContrast = Color(0xFFF9F9FF) +val onSurfaceLightHighContrast = Color(0xFF000000) +val surfaceVariantLightHighContrast = Color(0xFFDFE2EF) +val onSurfaceVariantLightHighContrast = Color(0xFF000000) +val outlineLightHighContrast = Color(0xFF282C36) +val outlineVariantLightHighContrast = Color(0xFF454954) +val scrimLightHighContrast = Color(0xFF000000) +val inverseSurfaceLightHighContrast = Color(0xFF2F3036) +val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) +val inversePrimaryLightHighContrast = Color(0xFFADC6FF) +val surfaceDimLightHighContrast = Color(0xFFB8B8BF) +val surfaceBrightLightHighContrast = Color(0xFFF9F9FF) +val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightHighContrast = Color(0xFFF0F0F7) +val surfaceContainerLightHighContrast = Color(0xFFE2E2E9) +val surfaceContainerHighLightHighContrast = Color(0xFFD4D4DB) +val surfaceContainerHighestLightHighContrast = Color(0xFFC6C6CD) + +val primaryDark = Color(0xFFADC6FF) +val onPrimaryDark = Color(0xFF002E69) +val primaryContainerDark = Color(0xFF2D5BA8) +val onPrimaryContainerDark = Color(0xFFC5D6FF) +val secondaryDark = Color(0xFFB8C6EB) +val onSecondaryDark = Color(0xFF21304D) +val secondaryContainerDark = Color(0xFF3A4967) +val onSecondaryContainerDark = Color(0xFFAAB8DC) +val tertiaryDark = Color(0xFFF5ADFF) +val onTertiaryDark = Color(0xFF50145E) +val tertiaryContainerDark = Color(0xFF82448E) +val onTertiaryContainerDark = Color(0xFFFAC4FF) +val errorDark = Color(0xFFFFB4AB) +val onErrorDark = Color(0xFF690005) +val errorContainerDark = Color(0xFF93000A) +val onErrorContainerDark = Color(0xFFFFDAD6) +val backgroundDark = Color(0xFF111318) +val onBackgroundDark = Color(0xFFE2E2E9) +val surfaceDark = Color(0xFF111318) +val onSurfaceDark = Color(0xFFE2E2E9) +val surfaceVariantDark = Color(0xFF434751) +val onSurfaceVariantDark = Color(0xFFC3C6D3) +val outlineDark = Color(0xFF8D909C) +val outlineVariantDark = Color(0xFF434751) +val scrimDark = Color(0xFF000000) +val inverseSurfaceDark = Color(0xFFE2E2E9) +val inverseOnSurfaceDark = Color(0xFF2F3036) +val inversePrimaryDark = Color(0xFF2F5DAA) +val surfaceDimDark = Color(0xFF111318) +val surfaceBrightDark = Color(0xFF37393F) +val surfaceContainerLowestDark = Color(0xFF0C0E13) +val surfaceContainerLowDark = Color(0xFF1A1B21) +val surfaceContainerDark = Color(0xFF1E1F25) +val surfaceContainerHighDark = Color(0xFF282A2F) +val surfaceContainerHighestDark = Color(0xFF33353A) + +val primaryDarkMediumContrast = Color(0xFFCFDCFF) +val onPrimaryDarkMediumContrast = Color(0xFF002454) +val primaryContainerDarkMediumContrast = Color(0xFF6690E0) +val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) +val secondaryDarkMediumContrast = Color(0xFFCFDCFF) +val onSecondaryDarkMediumContrast = Color(0xFF162542) +val secondaryContainerDarkMediumContrast = Color(0xFF8291B3) +val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) +val tertiaryDarkMediumContrast = Color(0xFFFCCDFF) +val onTertiaryDarkMediumContrast = Color(0xFF430452) +val tertiaryContainerDarkMediumContrast = Color(0xFFBB78C6) +val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) +val errorDarkMediumContrast = Color(0xFFFFD2CC) +val onErrorDarkMediumContrast = Color(0xFF540003) +val errorContainerDarkMediumContrast = Color(0xFFFF5449) +val onErrorContainerDarkMediumContrast = Color(0xFF000000) +val backgroundDarkMediumContrast = Color(0xFF111318) +val onBackgroundDarkMediumContrast = Color(0xFFE2E2E9) +val surfaceDarkMediumContrast = Color(0xFF111318) +val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkMediumContrast = Color(0xFF434751) +val onSurfaceVariantDarkMediumContrast = Color(0xFFD9DCE9) +val outlineDarkMediumContrast = Color(0xFFAEB1BE) +val outlineVariantDarkMediumContrast = Color(0xFF8D909C) +val scrimDarkMediumContrast = Color(0xFF000000) +val inverseSurfaceDarkMediumContrast = Color(0xFFE2E2E9) +val inverseOnSurfaceDarkMediumContrast = Color(0xFF282A2F) +val inversePrimaryDarkMediumContrast = Color(0xFF0D4592) +val surfaceDimDarkMediumContrast = Color(0xFF111318) +val surfaceBrightDarkMediumContrast = Color(0xFF43444A) +val surfaceContainerLowestDarkMediumContrast = Color(0xFF06070C) +val surfaceContainerLowDarkMediumContrast = Color(0xFF1C1D23) +val surfaceContainerDarkMediumContrast = Color(0xFF26282D) +val surfaceContainerHighDarkMediumContrast = Color(0xFF313238) +val surfaceContainerHighestDarkMediumContrast = Color(0xFF3C3D43) + +val primaryDarkHighContrast = Color(0xFFECEFFF) +val onPrimaryDarkHighContrast = Color(0xFF000000) +val primaryContainerDarkHighContrast = Color(0xFFA7C2FF) +val onPrimaryContainerDarkHighContrast = Color(0xFF000A22) +val secondaryDarkHighContrast = Color(0xFFECEFFF) +val onSecondaryDarkHighContrast = Color(0xFF000000) +val secondaryContainerDarkHighContrast = Color(0xFFB4C2E7) +val onSecondaryContainerDarkHighContrast = Color(0xFF000A22) +val tertiaryDarkHighContrast = Color(0xFFFFEAFD) +val onTertiaryDarkHighContrast = Color(0xFF000000) +val tertiaryContainerDarkHighContrast = Color(0xFFF2A9FC) +val onTertiaryContainerDarkHighContrast = Color(0xFF1A0022) +val errorDarkHighContrast = Color(0xFFFFECE9) +val onErrorDarkHighContrast = Color(0xFF000000) +val errorContainerDarkHighContrast = Color(0xFFFFAEA4) +val onErrorContainerDarkHighContrast = Color(0xFF220001) +val backgroundDarkHighContrast = Color(0xFF111318) +val onBackgroundDarkHighContrast = Color(0xFFE2E2E9) +val surfaceDarkHighContrast = Color(0xFF111318) +val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkHighContrast = Color(0xFF434751) +val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF) +val outlineDarkHighContrast = Color(0xFFEDEFFD) +val outlineVariantDarkHighContrast = Color(0xFFBFC2CF) +val scrimDarkHighContrast = Color(0xFF000000) +val inverseSurfaceDarkHighContrast = Color(0xFFE2E2E9) +val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) +val inversePrimaryDarkHighContrast = Color(0xFF0D4592) +val surfaceDimDarkHighContrast = Color(0xFF111318) +val surfaceBrightDarkHighContrast = Color(0xFF4E5056) +val surfaceContainerLowestDarkHighContrast = Color(0xFF000000) +val surfaceContainerLowDarkHighContrast = Color(0xFF1E1F25) +val surfaceContainerDarkHighContrast = Color(0xFF2F3036) +val surfaceContainerHighDarkHighContrast = Color(0xFF3A3B41) +val surfaceContainerHighestDarkHighContrast = Color(0xFF45474C) + val White = Color(0xFFFFFFFF) val Black = Color(0xFF000000) val DarkGray = Color(0xFF696969) diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Theme.kt index 6a1f86c35bf..6b672b91a45 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Theme.kt +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Theme.kt @@ -9,49 +9,118 @@ */ package com.mifos.core.designsystem.theme +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext -private val LightThemeColors = lightColorScheme( - primary = BluePrimary, - onPrimary = Color.White, - error = Color.Red, - background = Color.White, - onSurface = BlueSecondary, - onSecondary = Color.Gray, - outlineVariant = Color.Gray, - surfaceTint = BlueSecondary, +private val lightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, ) -private val DarkThemeColors = darkColorScheme( - primary = BluePrimaryDark, - onPrimary = Color.White, - secondary = Black, - error = Color.Red, - background = Color.Black, - surface = BlueSecondary, - onSurface = Color.White, - onSecondary = Color.White, - outlineVariant = Color.White, - surfaceTint = BlueSecondary, +private val darkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, ) @Composable fun MifosTheme( - useDarkTheme: Boolean = isSystemInDarkTheme(), + darkTheme: Boolean = isSystemInDarkTheme(), + androidTheme: Boolean = false, + disableDynamicTheming: Boolean = true, content: @Composable () -> Unit, ) { - val colors = when { - useDarkTheme -> DarkThemeColors - else -> LightThemeColors + // Color scheme + val colorScheme = when { + // TODO:: Add support for Android theme + androidTheme -> if (darkTheme) darkScheme else lightScheme + !disableDynamicTheming && supportsDynamicTheming() -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + else -> if (darkTheme) darkScheme else lightScheme } MaterialTheme( - colorScheme = colors, + colorScheme = colorScheme, content = content, + typography = Typography, ) } + +@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S) +fun supportsDynamicTheming() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Type.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Type.kt new file mode 100644 index 00000000000..e2f8284498d --- /dev/null +++ b/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Type.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + displayLarge = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = (-0.25).sp, + ), + displayMedium = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 45.sp, + lineHeight = 52.sp, + letterSpacing = 0.sp, + ), + displaySmall = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 36.sp, + lineHeight = 44.sp, + letterSpacing = 0.sp, + ), + headlineLarge = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp, + ), + headlineMedium = TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.1.sp, + ), + headlineSmall = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.1.sp, + ), + titleLarge = TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.9.sp, + ), + titleMedium = TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + lineHeight = 24.sp, + letterSpacing = 0.1.sp, + ), + titleSmall = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + bodyLarge = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ), + bodyMedium = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + ), + bodySmall = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + letterSpacing = 0.25.sp, + ), + labelLarge = TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + letterSpacing = 0.25.sp, + fontFeatureSettings = "titl", + ), + labelMedium = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + letterSpacing = 0.25.sp, + ), + labelSmall = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), +) diff --git a/core/designsystem/src/main/res/font/core_designsystem_lato_black.ttf b/core/designsystem/src/main/res/font/core_designsystem_lato_black.ttf new file mode 100644 index 00000000000..4340502d93c Binary files /dev/null and b/core/designsystem/src/main/res/font/core_designsystem_lato_black.ttf differ diff --git a/core/designsystem/src/main/res/font/core_designsystem_lato_bold.ttf b/core/designsystem/src/main/res/font/core_designsystem_lato_bold.ttf new file mode 100644 index 00000000000..016068b486e Binary files /dev/null and b/core/designsystem/src/main/res/font/core_designsystem_lato_bold.ttf differ diff --git a/core/designsystem/src/main/res/font/core_designsystem_lato_regular.ttf b/core/designsystem/src/main/res/font/core_designsystem_lato_regular.ttf new file mode 100644 index 00000000000..bb2e8875a99 Binary files /dev/null and b/core/designsystem/src/main/res/font/core_designsystem_lato_regular.ttf differ diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index e499398859a..2788e27098c 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -25,8 +25,10 @@ dependencies { implementation(libs.dbflow) + implementation(libs.mifos.android.sdk.arch) + // sdk client -// implementation(libs.fineract.client) + implementation(libs.fineract.client) implementation(libs.rxandroid) implementation(libs.rxjava) diff --git a/core/domain/src/main/java/com/mifos/core/domain/useCases/GetGroupDetailsUseCase.kt b/core/domain/src/main/java/com/mifos/core/domain/useCases/GetGroupDetailsUseCase.kt index 0c2fdc48ce6..3ee910eb983 100644 --- a/core/domain/src/main/java/com/mifos/core/domain/useCases/GetGroupDetailsUseCase.kt +++ b/core/domain/src/main/java/com/mifos/core/domain/useCases/GetGroupDetailsUseCase.kt @@ -14,40 +14,35 @@ import com.mifos.core.data.repository.GroupDetailsRepository import com.mifos.core.objects.zipmodels.GroupAndGroupAccounts import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.channelFlow import rx.Observable -import rx.Subscriber import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import javax.inject.Inject class GetGroupDetailsUseCase @Inject constructor(private val repository: GroupDetailsRepository) { + operator fun invoke(groupId: Int): Flow> = + channelFlow { + val disposable = Observable.combineLatest( + repository.getGroup(groupId), + repository.getGroupAccounts(groupId), + ) { group, groupAccounts -> + GroupAndGroupAccounts(group, groupAccounts) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { groupAndGroupAccounts -> + trySend(Resource.Success(groupAndGroupAccounts)) + }, + { error -> + trySend(Resource.Error(error.message ?: "Unknown error occurred")) + }, + ) - suspend operator fun invoke(groupId: Int): Flow> = - callbackFlow { - try { - trySend(Resource.Loading()) - Observable.combineLatest( - repository.getGroup(groupId), - repository.getGroupAccounts(groupId), - ) { group, groupAccounts -> GroupAndGroupAccounts(group, groupAccounts) } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeOn(Schedulers.io()) - .subscribe(object : Subscriber() { - override fun onCompleted() {} - - override fun onError(e: Throwable) { - trySend(Resource.Error(e.message.toString())) - } - - override fun onNext(groupAndGroupAccounts: GroupAndGroupAccounts) { - trySend(Resource.Success(groupAndGroupAccounts)) - } - }) - - awaitClose { channel.close() } - } catch (exception: Exception) { - trySend(Resource.Error(exception.message.toString())) + // Cleanup subscription when the flow is cancelled + awaitClose { + disposable.unsubscribe() } } } diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index cf6c1a6bc27..f9c09169492 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -9,11 +9,16 @@ */ plugins { alias(libs.plugins.mifos.android.library) + id("kotlin-parcelize") } android{ namespace = "com.mifos.core.model" + defaultConfig { + consumerProguardFiles("consumer-rules.pro") + } } + dependencies { implementation(libs.converter.gson) } diff --git a/core/model/consumer-rules.pro b/core/model/consumer-rules.pro index e69de29bb2d..700feab89a8 100644 --- a/core/model/consumer-rules.pro +++ b/core/model/consumer-rules.pro @@ -0,0 +1,2 @@ +# Keep your model classes +-keep class com.mifos.core.model.** { *; } \ No newline at end of file diff --git a/core/common/src/main/java/com/mifos/core/common/model/user/Role.kt b/core/model/src/main/kotlin/com/mifos/core/model/DarkThemeConfig.kt similarity index 55% rename from core/common/src/main/java/com/mifos/core/common/model/user/Role.kt rename to core/model/src/main/kotlin/com/mifos/core/model/DarkThemeConfig.kt index 904d8897a99..3adfb4d8b53 100644 --- a/core/common/src/main/java/com/mifos/core/common/model/user/Role.kt +++ b/core/model/src/main/kotlin/com/mifos/core/model/DarkThemeConfig.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Mifos Initiative + * Copyright 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -7,15 +7,10 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ -package com.mifos.core.common.model.user +package com.mifos.core.model -/** - * Created by ishankhanna on 09/02/14. - */ -data class Role( - var id: Int = 0, - - var name: String? = null, - - var description: String? = null, -) +enum class DarkThemeConfig { + FOLLOW_SYSTEM, + LIGHT, + DARK, +} diff --git a/core/common/src/main/java/com/mifos/core/common/enums/MifosAppLanguage.kt b/core/model/src/main/kotlin/com/mifos/core/model/MifosAppLanguage.kt similarity index 91% rename from core/common/src/main/java/com/mifos/core/common/enums/MifosAppLanguage.kt rename to core/model/src/main/kotlin/com/mifos/core/model/MifosAppLanguage.kt index 600c788746a..1003fef69a1 100644 --- a/core/common/src/main/java/com/mifos/core/common/enums/MifosAppLanguage.kt +++ b/core/model/src/main/kotlin/com/mifos/core/model/MifosAppLanguage.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Mifos Initiative + * Copyright 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -7,10 +7,11 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ -package com.mifos.core.common.enums +package com.mifos.core.model -enum class MifosAppLanguage(val code: String, val displayName: String) { +import com.mifos.core.model.MifosAppLanguage.entries +enum class MifosAppLanguage(val code: String, val displayName: String) { SYSTEM_LANGUAGE("System_Language", "System Language"), ENGLISH("en", "English"), HINDI("hi", "हिंदी"), diff --git a/core/model/src/main/kotlin/com/mifos/core/model/ServerConfig.kt b/core/model/src/main/kotlin/com/mifos/core/model/ServerConfig.kt index 03a55d62ccd..c7930a9dd5a 100644 --- a/core/model/src/main/kotlin/com/mifos/core/model/ServerConfig.kt +++ b/core/model/src/main/kotlin/com/mifos/core/model/ServerConfig.kt @@ -9,8 +9,11 @@ */ package com.mifos.core.model +import android.os.Parcelable import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize +@Parcelize data class ServerConfig( val protocol: String, @SerializedName("end_point") @@ -19,7 +22,17 @@ data class ServerConfig( val apiPath: String, val port: String, val tenant: String, -) +) : Parcelable { + companion object { + val DEFAULT = ServerConfig( + protocol = "https://", + endPoint = "dev.mifos.io", + apiPath = "/fineract-provider/api/v1/", + port = "80", + tenant = "default", + ) + } +} fun ServerConfig.getInstanceUrl(): String { return "$protocol$endPoint$apiPath" diff --git a/feature/splash/src/main/java/com/mifos/feature/splash/navigation/SplashScreens.kt b/core/model/src/main/kotlin/com/mifos/core/model/ThemeBrand.kt similarity index 52% rename from feature/splash/src/main/java/com/mifos/feature/splash/navigation/SplashScreens.kt rename to core/model/src/main/kotlin/com/mifos/core/model/ThemeBrand.kt index 2943581b753..c1168d29a57 100644 --- a/feature/splash/src/main/java/com/mifos/feature/splash/navigation/SplashScreens.kt +++ b/core/model/src/main/kotlin/com/mifos/core/model/ThemeBrand.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Mifos Initiative + * Copyright 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -7,11 +7,9 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ -package com.mifos.feature.splash.navigation +package com.mifos.core.model -sealed class SplashScreens(val route: String) { - - data object SplashScreenRoute : SplashScreens("splash_screen_route") - - data object SplashScreen : SplashScreens("splash_screen") +enum class ThemeBrand { + DEFAULT, + ANDROID, } diff --git a/core/model/src/main/kotlin/com/mifos/core/model/UserData.kt b/core/model/src/main/kotlin/com/mifos/core/model/UserData.kt new file mode 100644 index 00000000000..7ef7c878b2f --- /dev/null +++ b/core/model/src/main/kotlin/com/mifos/core/model/UserData.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class UserData( + val themeBrand: ThemeBrand, + val darkThemeConfig: DarkThemeConfig, + val useDynamicColor: Boolean, + val language: MifosAppLanguage, +) : Parcelable { + companion object { + val DEFAULT = UserData( + themeBrand = ThemeBrand.DEFAULT, + darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM, + useDynamicColor = false, + language = MifosAppLanguage.SYSTEM_LANGUAGE, + ) + } +} diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index 61ff64d0bd8..dd37c930fb4 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -12,12 +12,17 @@ plugins { alias(libs.plugins.mifos.android.library.jacoco) alias(libs.plugins.mifos.android.hilt) alias(libs.plugins.secrets) + alias(libs.plugins.kotlin.serialization) id(libs.plugins.kotlin.parcelize.get().pluginId) } android { namespace = "com.mifos.core.network" + defaultConfig { + consumerProguardFiles("consumer-rules.pro") + } + testOptions { unitTests { isIncludeAndroidResources = true @@ -50,10 +55,8 @@ dependencies { implementation(libs.dbflow) //Square dependencies - implementation("com.squareup.retrofit2:retrofit:2.9.0") { - // exclude Retrofit’s OkHttp peer-dependency module and define your own module import - exclude(module = "okhttp") - } + implementation(libs.retrofit.core) + implementation(libs.converter.json) implementation(libs.converter.gson) implementation(libs.converter.scalars) implementation(libs.adapter.rxjava) @@ -67,4 +70,6 @@ dependencies { implementation(libs.stetho.okhttp3) implementation(libs.coil.kt.compose) + + implementation(libs.kotlinx.serialization.json) } \ No newline at end of file diff --git a/core/network/consumer-rules.pro b/core/network/consumer-rules.pro index e69de29bb2d..93ed8fdbed0 100644 --- a/core/network/consumer-rules.pro +++ b/core/network/consumer-rules.pro @@ -0,0 +1,164 @@ +# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and +# EnclosingMethod is required to use InnerClasses. +-keepattributes Signature, InnerClasses, EnclosingMethod + +# Retrofit does reflection on method and parameter annotations. +-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations + +# Keep annotation default values (e.g., retrofit2.http.Field.encoded). +-keepattributes AnnotationDefault + +# Retain service method parameters when optimizing. +-if interface * +-keepclasseswithmembers,allowobfuscation interface <1> { + @retrofit2.http.* ; +} + +# Ignore annotation used for build tooling. +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement + +# Ignore JSR 305 annotations for embedding nullability information. +-dontwarn javax.annotation.** + +# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. +-dontwarn kotlin.Unit + +# Top-level functions that can only be used by Kotlin. +-dontwarn retrofit2.KotlinExtensions +-dontwarn retrofit2.KotlinExtensions$* + +# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy +# and replaces all potential values with null. Explicitly keeping the interfaces prevents this. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface <1> + +# Keep inherited services. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface * extends <1> + +# With R8 full mode generic signatures are stripped for classes that are not +# kept. Suspend functions are wrapped in continuations where the type argument +# is used. +-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation + +# R8 full mode strips generic signatures from return types if not kept. +-if interface * { @retrofit2.http.* public *** *(...); } +-keep,allowoptimization,allowshrinking,allowobfuscation class <3> + +# With R8 full mode generic signatures are stripped for classes that are not kept. +-keep,allowobfuscation,allowshrinking interface retrofit2.Call +-keep,allowobfuscation,allowshrinking class retrofit2.Response + +# Keep all Retrofit service interfaces and their implementations +-keep class * extends retrofit2.Converter +-keep class * extends retrofit2.CallAdapter +-keep class * extends retrofit2.Callback +-keepclasseswithmembers class * { + @retrofit2.http.* ; +} + +# JSR 305 annotations are for embedding nullability information. +-dontwarn javax.annotation.* + +# A resource is loaded with a relative path so the package of this class must be preserved. +-keeppackagenames okhttp3.internal.publicsuffix.* +-adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz + +# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. +-dontwarn org.codehaus.mojo.animal_sniffer.* + +# OkHttp platform used only on JVM and when Conscrypt and other security providers are available. +-dontwarn okhttp3.internal.platform.** +-dontwarn org.conscrypt.** +-dontwarn org.bouncycastle.** +-dontwarn org.openjsse.** + +-keepattributes Signature +-keepattributes *Annotation* +-dontwarn sun.misc.** +-keep class com.google.gson.examples.android.model.** { ; } +-keep class * extends com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +-keep class com.mifos.core.network.** { *; } +-keep interface com.mifos.core.network.services.** { *; } + +### Gson ProGuard and R8 rules which are relevant for all users +### This file is automatically recognized by ProGuard and R8, see https://developer.android.com/build/shrink-code#configuration-files +### +### IMPORTANT: +### - These rules are additive; don't include anything here which is not specific to Gson (such as completely +### disabling obfuscation for all classes); the user would be unable to disable that then +### - These rules are not complete; users will most likely have to add additional rules for their specific +### classes, for example to disable obfuscation for certain fields or to keep no-args constructors +### + +# Keep generic signatures; needed for correct type resolution +-keepattributes Signature + +# Keep Gson annotations +# Note: Cannot perform finer selection here to only cover Gson annotations, see also https://stackoverflow.com/q/47515093 +-keepattributes RuntimeVisibleAnnotations,AnnotationDefault + +### The following rules are needed for R8 in "full mode" which only adheres to `-keepattribtues` if +### the corresponding class or field is matches by a `-keep` rule as well, see +### https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#r8-full-mode + +# Keep class TypeToken (respectively its generic signature) if present +-if class com.google.gson.reflect.TypeToken +-keep,allowobfuscation class com.google.gson.reflect.TypeToken + +# Keep any (anonymous) classes extending TypeToken +-keep,allowobfuscation class * extends com.google.gson.reflect.TypeToken + +# Keep classes with @JsonAdapter annotation +-keep,allowobfuscation,allowoptimization @com.google.gson.annotations.JsonAdapter class * + +# Keep fields with any other Gson annotation +# Also allow obfuscation, assuming that users will additionally use @SerializedName or +# other means to preserve the field names +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.Expose ; + @com.google.gson.annotations.JsonAdapter ; + @com.google.gson.annotations.Since ; + @com.google.gson.annotations.Until ; +} + +# Keep no-args constructor of classes which can be used with @JsonAdapter +# By default their no-args constructor is invoked to create an adapter instance +-keepclassmembers class * extends com.google.gson.TypeAdapter { + (); +} +-keepclassmembers class * implements com.google.gson.TypeAdapterFactory { + (); +} +-keepclassmembers class * implements com.google.gson.JsonSerializer { + (); +} +-keepclassmembers class * implements com.google.gson.JsonDeserializer { + (); +} + +# Keep fields annotated with @SerializedName for classes which are referenced. +# If classes with fields annotated with @SerializedName have a no-args +# constructor keep that as well. Based on +# https://issuetracker.google.com/issues/150189783#comment11. +# See also https://github.com/google/gson/pull/2420#discussion_r1241813541 +# for a more detailed explanation. +-if class * +-keepclasseswithmembers,allowobfuscation class <1> { + @com.google.gson.annotations.SerializedName ; +} +-if class * { + @com.google.gson.annotations.SerializedName ; +} +-keepclassmembers,allowobfuscation,allowoptimization class <1> { + (); +} + +-keep class org.mifos.core.** { *; } \ No newline at end of file diff --git a/core/network/src/main/java/com/mifos/core/network/BaseApiManager.kt b/core/network/src/main/java/com/mifos/core/network/BaseApiManager.kt index c47310f2a21..95c3a790981 100644 --- a/core/network/src/main/java/com/mifos/core/network/BaseApiManager.kt +++ b/core/network/src/main/java/com/mifos/core/network/BaseApiManager.kt @@ -10,7 +10,9 @@ package com.mifos.core.network import com.google.gson.GsonBuilder +import com.mifos.core.datastore.PrefManager import com.mifos.core.model.getInstanceUrl +import com.mifos.core.network.adapter.FlowCallAdapterFactory import com.mifos.core.network.services.CenterService import com.mifos.core.network.services.ChargeService import com.mifos.core.network.services.CheckerInboxService @@ -28,10 +30,13 @@ import com.mifos.core.network.services.SavingsAccountService import com.mifos.core.network.services.SearchService import com.mifos.core.network.services.StaffService import com.mifos.core.network.services.SurveyService +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType import org.mifos.core.utils.JsonDateSerializer import retrofit2.Retrofit import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.kotlinx.serialization.asConverterFactory import retrofit2.converter.scalars.ScalarsConverterFactory import java.util.Date import javax.inject.Inject @@ -39,8 +44,9 @@ import javax.inject.Inject /** * @author fomenkoo */ -class BaseApiManager @Inject constructor(private val prefManager: com.mifos.core.datastore.PrefManager) { - +class BaseApiManager @Inject constructor( + prefManager: PrefManager, +) { init { createService(prefManager) } @@ -158,14 +164,18 @@ class BaseApiManager @Inject constructor(private val prefManager: com.mifos.core return mRetrofit!!.create(clazz) } - fun createService(prefManager: com.mifos.core.datastore.PrefManager) { + fun createService(prefManager: PrefManager) { val gson = GsonBuilder() .registerTypeAdapter(Date::class.java, JsonDateSerializer()).create() + val json = Json { ignoreUnknownKeys = true } + mRetrofit = Retrofit.Builder() - .baseUrl(prefManager.getServerConfig.getInstanceUrl()) + .baseUrl(prefManager.serverConfig.getInstanceUrl()) .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create(gson)) + .addCallAdapterFactory(FlowCallAdapterFactory()) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .client(MifosOkHttpClient(prefManager).okHttpClient) .build() init() diff --git a/core/network/src/main/java/com/mifos/core/network/MifosInterceptor.kt b/core/network/src/main/java/com/mifos/core/network/MifosInterceptor.kt index 173add934ea..0b92f944020 100644 --- a/core/network/src/main/java/com/mifos/core/network/MifosInterceptor.kt +++ b/core/network/src/main/java/com/mifos/core/network/MifosInterceptor.kt @@ -9,6 +9,7 @@ */ package com.mifos.core.network +import com.mifos.core.datastore.PrefManager import okhttp3.Interceptor import okhttp3.Response import java.io.IOException @@ -16,14 +17,13 @@ import java.io.IOException /** * @author fomenkoo */ -class MifosInterceptor(private val prefManager: com.mifos.core.datastore.PrefManager) : Interceptor { - +class MifosInterceptor(private val prefManager: PrefManager) : Interceptor { @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { val chianrequest = chain.request() val builder = chianrequest.newBuilder() - .header(HEADER_TENANT, prefManager.getServerConfig.tenant) - if (prefManager.isAuthenticated()) builder.header(HEADER_AUTH, prefManager.getToken()) + .header(HEADER_TENANT, prefManager.serverConfig.tenant) + if (prefManager.isAuthenticated) builder.header(HEADER_AUTH, prefManager.token) val request = builder.build() return chain.proceed(request) } diff --git a/core/network/src/main/java/com/mifos/core/network/MifosOkHttpClient.kt b/core/network/src/main/java/com/mifos/core/network/MifosOkHttpClient.kt index 24655bdfebb..1e0d4019281 100644 --- a/core/network/src/main/java/com/mifos/core/network/MifosOkHttpClient.kt +++ b/core/network/src/main/java/com/mifos/core/network/MifosOkHttpClient.kt @@ -10,79 +10,18 @@ package com.mifos.core.network import com.facebook.stetho.okhttp3.StethoInterceptor +import com.mifos.core.datastore.PrefManager import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor -import java.security.SecureRandom -import java.security.cert.CertificateException -import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.X509TrustManager /** * Created by Rajan Maurya on 16/06/16. */ -class MifosOkHttpClient(private val prefManager: com.mifos.core.datastore.PrefManager) { - // Create a trust manager that does not validate certificate chains +class MifosOkHttpClient(private val prefManager: PrefManager) { val okHttpClient: OkHttpClient - - // Install the all-trusting trust manager - // Create an ssl socket factory with our all-trusting manager - - // Enable Full Body Logging - - // Set SSL certificate to OkHttpClient Builder - - // Enable Full Body Logging - - // Setting Timeout 30 Seconds - - // Interceptor :> Full Body Logger and ApiRequest Header get() { val builder = OkHttpClient.Builder() - try { - // Create a trust manager that does not validate certificate chains - val trustAllCerts = arrayOf( - object : X509TrustManager { - @Throws(CertificateException::class) - override fun checkClientTrusted( - chain: Array, - authType: String, - ) { - } - - @Throws(CertificateException::class) - override fun checkServerTrusted( - chain: Array, - authType: String, - ) { - } - - override fun getAcceptedIssuers(): Array { - return emptyArray() - } - }, - ) - - // Install the all-trusting trust manager - val sslContext = SSLContext.getInstance("SSL") - sslContext.init(null, trustAllCerts, SecureRandom()) - // Create an ssl socket factory with our all-trusting manager - val sslSocketFactory = sslContext.socketFactory - - // Enable Full Body Logging - val logger = HttpLoggingInterceptor() - logger.level = HttpLoggingInterceptor.Level.BODY - - // Set SSL certificate to OkHttpClient Builder -// builder.sslSocketFactory(sslSocketFactory) - builder.sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager) - builder.hostnameVerifier { hostname, session -> true } - } catch (e: Exception) { - throw RuntimeException(e) - } - // Enable Full Body Logging val logger = HttpLoggingInterceptor() logger.level = HttpLoggingInterceptor.Level.BODY diff --git a/core/network/src/main/java/com/mifos/core/network/adapter/FlowCallAdapterFactory.kt b/core/network/src/main/java/com/mifos/core/network/adapter/FlowCallAdapterFactory.kt new file mode 100644 index 00000000000..d36d47c00e3 --- /dev/null +++ b/core/network/src/main/java/com/mifos/core/network/adapter/FlowCallAdapterFactory.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.network.adapter + +import com.mifos.core.network.adapter.internal.BodyCallAdapter +import com.mifos.core.network.adapter.internal.ResponseCallAdapter +import kotlinx.coroutines.flow.Flow +import retrofit2.CallAdapter +import retrofit2.Response +import retrofit2.Retrofit +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +class FlowCallAdapterFactory : CallAdapter.Factory() { + + override fun get( + returnType: Type, + annotations: Array, + retrofit: Retrofit, + ): CallAdapter<*, *>? { + if (Flow::class.java != getRawType(returnType)) { + return null + } + + if (returnType !is ParameterizedType) { + error( + "Flow return type must be parameterized as Flow", + ) + } + + val responseType = getParameterUpperBound(0, returnType) + val rawDeferredType = getRawType(responseType) + + return if (rawDeferredType == Response::class.java) { + if (responseType !is ParameterizedType) { + error( + "Response must be parameterized as Response or Response", + ) + } + ResponseCallAdapter(getParameterUpperBound(0, responseType)) + } else { + BodyCallAdapter(responseType) + } + } +} diff --git a/core/network/src/main/java/com/mifos/core/network/adapter/internal/BodyCallAdapter.kt b/core/network/src/main/java/com/mifos/core/network/adapter/internal/BodyCallAdapter.kt new file mode 100644 index 00000000000..1555aa3d45e --- /dev/null +++ b/core/network/src/main/java/com/mifos/core/network/adapter/internal/BodyCallAdapter.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.network.adapter.internal + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.suspendCancellableCoroutine +import retrofit2.Call +import retrofit2.CallAdapter +import retrofit2.HttpException +import java.lang.reflect.Type + +internal class BodyCallAdapter( + private val responseType: Type, +) : CallAdapter> { + + override fun responseType(): Type = + responseType + + override fun adapt(call: Call): Flow = flow { + emit( + suspendCancellableCoroutine { continuation -> + call.registerCallback(continuation) { response -> + continuation.resumeWith( + kotlin.runCatching { + if (response.isSuccessful) { + response.body() + ?: throw NullPointerException("Response body is null: $response") + } else { + throw HttpException(response) + } + }, + ) + } + + call.registerOnCancellation(continuation) + }, + ) + } +} diff --git a/core/network/src/main/java/com/mifos/core/network/adapter/internal/InternalUtil.kt b/core/network/src/main/java/com/mifos/core/network/adapter/internal/InternalUtil.kt new file mode 100644 index 00000000000..18dfa822a7d --- /dev/null +++ b/core/network/src/main/java/com/mifos/core/network/adapter/internal/InternalUtil.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.network.adapter.internal + +import kotlinx.coroutines.CancellableContinuation +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import kotlin.coroutines.resumeWithException + +internal fun Call<*>.registerOnCancellation( + continuation: CancellableContinuation<*>, +) { + continuation.invokeOnCancellation { + try { + cancel() + } catch (e: Exception) { + // Ignore cancel exception + } + } +} + +internal fun Call.registerCallback( + continuation: CancellableContinuation<*>, + success: (response: Response) -> Unit, +) { + enqueue( + object : Callback { + override fun onResponse(call: Call, response: Response) { + success(response) + } + + override fun onFailure(call: Call, t: Throwable) { + continuation.resumeWithException(t) + } + }, + ) +} diff --git a/core/network/src/main/java/com/mifos/core/network/adapter/internal/ResponseCallAdapter.kt b/core/network/src/main/java/com/mifos/core/network/adapter/internal/ResponseCallAdapter.kt new file mode 100644 index 00000000000..a08890d47f2 --- /dev/null +++ b/core/network/src/main/java/com/mifos/core/network/adapter/internal/ResponseCallAdapter.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.network.adapter.internal + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.suspendCancellableCoroutine +import retrofit2.Call +import retrofit2.CallAdapter +import retrofit2.HttpException +import retrofit2.Response +import java.lang.reflect.Type + +internal class ResponseCallAdapter( + private val responseType: Type, +) : CallAdapter>> { + + override fun responseType(): Type = + responseType + + override fun adapt(call: Call): Flow> = flow { + emit( + suspendCancellableCoroutine { continuation -> + call.registerCallback(continuation) { response -> + continuation.resumeWith( + kotlin.runCatching { + if (response.isSuccessful) { + response + } else { + throw HttpException(response) + } + }, + ) + } + + call.registerOnCancellation(continuation) + }, + ) + } +} diff --git a/core/network/src/main/java/com/mifos/core/network/datamanager/DataManagerOffices.kt b/core/network/src/main/java/com/mifos/core/network/datamanager/DataManagerOffices.kt index 2f11a5929fc..ecfc9329cdd 100644 --- a/core/network/src/main/java/com/mifos/core/network/datamanager/DataManagerOffices.kt +++ b/core/network/src/main/java/com/mifos/core/network/datamanager/DataManagerOffices.kt @@ -10,7 +10,6 @@ package com.mifos.core.network.datamanager import com.mifos.core.network.BaseApiManager -import com.mifos.core.network.mappers.offices.GetOfficeResponseMapper import com.mifos.core.objects.organisation.Office import javax.inject.Inject import javax.inject.Singleton @@ -24,31 +23,12 @@ import javax.inject.Singleton */ @Singleton class DataManagerOffices @Inject constructor( - val mBaseApiManager: BaseApiManager, -// private val mDatabaseHelperOffices: DatabaseHelperOffices, - private val baseApiManager: org.mifos.core.apimanager.BaseApiManager, -// private val prefManager: com.mifos.core.datastore.PrefManager, + private val baseApiManager: BaseApiManager, ) { /** * return all List of Offices from DatabaseHelperOffices */ suspend fun offices(): List { - return baseApiManager.getOfficeApi().retrieveOffices(null, null, null).map( - GetOfficeResponseMapper::mapFromEntity, - ) + return baseApiManager.officeApi.allOffices() } -// val offices: Observable> -// get() = when (prefManager.userStatus) { -// false -> baseApiManager.getOfficeApi().retrieveOffices(null, null, null) -// .map(GetOfficeResponseMapper::mapFromEntityList) -// -// true -> -// /** -// * return all List of Offices from DatabaseHelperOffices -// */ -// /** -// * return all List of Offices from DatabaseHelperOffices -// */ -// mDatabaseHelperOffices.readAllOffices() -// } } diff --git a/core/network/src/main/java/com/mifos/core/network/di/NetworkModule.kt b/core/network/src/main/java/com/mifos/core/network/di/NetworkModule.kt index 2b9db1561d3..bdb75e596b0 100644 --- a/core/network/src/main/java/com/mifos/core/network/di/NetworkModule.kt +++ b/core/network/src/main/java/com/mifos/core/network/di/NetworkModule.kt @@ -13,6 +13,7 @@ import android.content.Context import androidx.core.os.trace import coil.ImageLoader import coil.util.DebugLogger +import com.mifos.core.datastore.PrefManager import com.mifos.core.model.getInstanceUrl import dagger.Module import dagger.Provides @@ -30,20 +31,20 @@ object NetworkModule { @Provides @Singleton - fun provideBaseApiManager(prefManager: com.mifos.core.datastore.PrefManager): com.mifos.core.network.BaseApiManager { + fun provideBaseApiManager(prefManager: PrefManager): com.mifos.core.network.BaseApiManager { return com.mifos.core.network.BaseApiManager(prefManager) } @Provides @Singleton - fun provideSdkBaseApiManager(prefManager: com.mifos.core.datastore.PrefManager): BaseApiManager { + fun provideSdkBaseApiManager(prefManager: PrefManager): BaseApiManager { val usernamePassword: Pair = prefManager.usernamePassword val baseManager = BaseApiManager.getInstance() baseManager.createService( usernamePassword.first, usernamePassword.second, - prefManager.getServerConfig.getInstanceUrl().dropLast(3), - prefManager.getServerConfig.tenant, + prefManager.serverConfig.getInstanceUrl().dropLast(3), + prefManager.serverConfig.tenant, false, ) return baseManager diff --git a/core/network/src/main/java/com/mifos/core/network/utils/ImageLoaderUtils.kt b/core/network/src/main/java/com/mifos/core/network/utils/ImageLoaderUtils.kt index a64bf806833..a0c5446ef14 100644 --- a/core/network/src/main/java/com/mifos/core/network/utils/ImageLoaderUtils.kt +++ b/core/network/src/main/java/com/mifos/core/network/utils/ImageLoaderUtils.kt @@ -13,19 +13,21 @@ import android.content.Context import coil.ImageLoader import coil.request.ImageRequest import coil.request.ImageResult +import com.mifos.core.datastore.PrefManager +import com.mifos.core.model.getInstanceUrl import com.mifos.core.network.MifosInterceptor import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject class ImageLoaderUtils @Inject constructor( - private val prefManager: com.mifos.core.datastore.PrefManager, + private val prefManager: PrefManager, private val imageLoader: ImageLoader, @ApplicationContext private val context: Context, ) { private fun buildImageUrl(clientId: Int): String { return ( - prefManager.getInstanceUrl() + + prefManager.serverConfig.getInstanceUrl() + "clients/" + clientId + "/images?maxHeight=120&maxWidth=120" @@ -35,8 +37,8 @@ class ImageLoaderUtils @Inject constructor( suspend fun loadImage(clientId: Int): ImageResult { val request = ImageRequest.Builder(context) .data(buildImageUrl(clientId)) - .addHeader(MifosInterceptor.HEADER_TENANT, prefManager.getTenant()) - .addHeader(MifosInterceptor.HEADER_AUTH, prefManager.getToken()) + .addHeader(MifosInterceptor.HEADER_TENANT, prefManager.serverConfig.tenant) + .addHeader(MifosInterceptor.HEADER_AUTH, prefManager.token) .addHeader("Accept", "application/octet-stream") .build() return imageLoader.execute(request) diff --git a/core/ui/src/main/java/com/mifos/core/ui/components/MifosFAB.kt b/core/ui/src/main/java/com/mifos/core/ui/components/MifosFAB.kt index 5c29b3bcba6..e84038cf74e 100644 --- a/core/ui/src/main/java/com/mifos/core/ui/components/MifosFAB.kt +++ b/core/ui/src/main/java/com/mifos/core/ui/components/MifosFAB.kt @@ -11,20 +11,20 @@ package com.mifos.core.ui.components import androidx.compose.foundation.layout.Box import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon 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 com.mifos.core.designsystem.theme.BlueSecondary @Composable fun MifosFAB( icon: ImageVector, onClick: () -> Unit, modifier: Modifier = Modifier, - containerColor: Color = BlueSecondary, + containerColor: Color = FloatingActionButtonDefaults.containerColor, ) { Box( modifier = modifier, diff --git a/fastlane-config/android_config.rb b/fastlane-config/android_config.rb new file mode 100644 index 00000000000..7000cc1538f --- /dev/null +++ b/fastlane-config/android_config.rb @@ -0,0 +1,23 @@ +module FastlaneConfig + module AndroidConfig + STORE_CONFIG = { + default_store_file: "release_keystore.keystore", + default_store_password: "mifos1234", + default_key_alias: "mifos", + default_key_password: "mifos1234" + } + + FIREBASE_CONFIG = { + firebase_prod_app_id: "1:728434912738:android:ecdb5b96f0e735661a1dbb", + firebase_demo_app_id: "1:728434912738:android:53d0930e402622611a1dbb", + firebase_service_creds_file: "secrets/firebaseAppDistributionServiceCredentialsFile.json", + firebase_groups: "mifos-mobile-testers" + } + + BUILD_PATHS = { + prod_apk_path: "mifosng-android/build/outputs/apk/prod/release/mifosng-android-prod-release.apk", + demo_apk_path: "mifosng-android/build/outputs/apk/demo/release/mifosng-android-demo-release.apk", + prod_aab_path: "mifosng-android/build/outputs/bundle/prodRelease/mifosng-android-prod-release.aab" + } + end +end \ No newline at end of file diff --git a/fastlane-config/ios_config.rb b/fastlane-config/ios_config.rb new file mode 100644 index 00000000000..29c4bc36c52 --- /dev/null +++ b/fastlane-config/ios_config.rb @@ -0,0 +1,15 @@ +module FastlaneConfig + module IosConfig + FIREBASE_CONFIG = { + firebase_app_id: "1:728434912738:ios:shjhsa78392shja", + firebase_service_creds_file: "secrets/firebaseAppDistributionServiceCredentialsFile.json", + firebase_groups: "kmp-project-template-testers" + } + + BUILD_CONFIG = { + project_path: "cmp-ios/iosApp.xcodeproj", + scheme: "iosApp", + output_directory: "cmp-ios/build" + } + end +end \ No newline at end of file diff --git a/fastlane/AppFile b/fastlane/AppFile new file mode 100644 index 00000000000..51ef651ded4 --- /dev/null +++ b/fastlane/AppFile @@ -0,0 +1,2 @@ +json_key_file("secrets/playStorePublishServiceCredentialsFile.json") +package_name("com.mifos.mifosxdroid") # e.g. cmp.android.app \ No newline at end of file diff --git a/fastlane/FastFile b/fastlane/FastFile new file mode 100644 index 00000000000..938a8bbc068 --- /dev/null +++ b/fastlane/FastFile @@ -0,0 +1,382 @@ +project_dir = File.expand_path('..', Dir.pwd) + +require_relative File.join(project_dir, 'fastlane-config', 'android_config') +require_relative File.join(project_dir, 'fastlane-config', 'ios_config') +require_relative './config/config_helpers' + +default_platform(:android) + +platform :android do + desc "Assemble debug APKs." + lane :assembleDebugApks do |options| + gradle( + tasks: ["assembleDebug"], + ) + end + + desc "Assemble Release APK" + lane :assembleReleaseApks do |options| + signing_config = FastlaneConfig.get_android_signing_config(options) + + # Generate version + generateVersion = generateVersion() + + buildAndSignApp( + taskName: "assemble", + buildType: "Release", + **signing_config + ) + end + + desc "Bundle Release APK" + lane :bundleReleaseApks do |options| + signing_config = FastlaneConfig.get_android_signing_config(options) + + # Generate version + generateVersion = generateVersion() + + buildAndSignApp( + taskName: "bundle", + buildType: "Release", + **signing_config + ) + end + + desc "Publish Release Artifacts to Firebase App Distribution" + lane :deployReleaseApkOnFirebase do |options| + signing_config = FastlaneConfig.get_android_signing_config(options) + firebase_config = FastlaneConfig.get_firebase_config(:android, :prod) + build_paths = FastlaneConfig::AndroidConfig::BUILD_PATHS + + # Generate version + generateVersion = generateVersion( + platform: "firebase", + **firebase_config + ) + + # Generate Release Note + releaseNotes = generateReleaseNote() + + buildAndSignApp( + taskName: "assembleProd", + buildType: "Release", + **signing_config + ) + + firebase_app_distribution( + app: firebase_config[:appId], + android_artifact_type: "APK", + android_artifact_path: build_paths[:prod_apk_path], + service_credentials_file: firebase_config[:serviceCredsFile], + groups: firebase_config[:groups], + release_notes: releaseNotes + ) + end + + desc "Publish Demo Artifacts to Firebase App Distribution" + lane :deployDemoApkOnFirebase do |options| + signing_config = FastlaneConfig.get_android_signing_config(options) + firebase_config = FastlaneConfig.get_firebase_config(:android, :demo) + build_paths = FastlaneConfig::AndroidConfig::BUILD_PATHS + + # Generate version + generateVersion = generateVersion( + platform: "firebase", + **firebase_config + ) + + # Generate Release Note + releaseNotes = generateReleaseNote() + + buildAndSignApp( + taskName: "assembleDemo", + buildType: "Release", + **signing_config + ) + + firebase_app_distribution( + app: firebase_config[:appId], + android_artifact_type: "APK", + android_artifact_path: build_paths[:demo_apk_path], + service_credentials_file: firebase_config[:serviceCredsFile], + groups: firebase_config[:groups], + release_notes: releaseNotes + ) + end + + desc "Deploy internal tracks to Google Play" + lane :deployInternal do |options| + signing_config = FastlaneConfig.get_android_signing_config(options) + build_paths = FastlaneConfig::AndroidConfig::BUILD_PATHS + + # Generate version + generateVersion = generateVersion(platform: "playstore") + + # Generate Release Note + releaseNotes = generateReleaseNote() + + # Write the generated release notes to default.txt + buildConfigPath = "metadata/android/en-US/changelogs/default.txt" + FileUtils.mkdir_p(File.dirname(buildConfigPath)) + File.write(buildConfigPath, releaseNotes) + + buildAndSignApp( + taskName: "bundleProd", + buildType: "Release", + **signing_config + ) + + upload_to_play_store( + track: 'internal', + aab: build_paths[:prod_aab_path], + skip_upload_metadata: true, + skip_upload_images: true, + skip_upload_screenshots: true, + ) + end + + desc "Promote internal tracks to beta on Google Play" + lane :promoteToBeta do + upload_to_play_store( + track: 'internal', + track_promote_to: 'beta', + skip_upload_changelogs: true, + skip_upload_metadata: true, + skip_upload_images: true, + skip_upload_screenshots: true, + ) + end + + desc "Promote beta tracks to production on Google Play" + lane :promote_to_production do + upload_to_play_store( + track: 'beta', + track_promote_to: 'production', + skip_upload_changelogs: true, + skip_upload_metadata: true, + skip_upload_images: true, + skip_upload_screenshots: true, + ) + end + + desc "Generate artifacts for the given [build] signed with the provided [keystore] and credentials." + private_lane :buildAndSignApp do |options| + # Get the project root directory + project_dir = File.expand_path('..', Dir.pwd) + + # Construct the absolute path to the keystore + keystore_path = File.join(project_dir, 'keystores', options[:storeFile]) + + # Check if keystore exists + unless File.exist?(keystore_path) + UI.error "Keystore file not found at: #{keystore_path}" + UI.error "Please ensure the keystore file exists at the correct location" + exit 1 # Exit with error code 1 + end + + gradle( + task: options[:taskName], + build_type: options[:buildType], + properties: { + "android.injected.signing.store.file" => keystore_path, + "android.injected.signing.store.password" => options[:storePassword], + "android.injected.signing.key.alias" => options[:keyAlias], + "android.injected.signing.key.password" => options[:keyPassword], + }, + print_command: false, + ) + end + + desc "Generate Version for different platforms" + lane :generateVersion do |options| + platform = (options[:platform] || 'git').downcase + + # Generate version file for all platforms + gradle(tasks: ["versionFile"]) + + # Set version from file with fallback + version = File.read("../version.txt").strip rescue "1.0.0" + ENV['VERSION'] = version + + case platform + when 'playstore' + prod_codes = google_play_track_version_codes(track: 'production') + beta_codes = google_play_track_version_codes(track: 'beta') + latest_code = (prod_codes + beta_codes).max || 1 + ENV['VERSION_CODE'] = (latest_code + 1).to_s + + when 'firebase' + begin + latest_release = firebase_app_distribution_get_latest_release( + app: options[:appId], + service_credentials_file: options[:serviceCredsFile] + ) + latest_build_version = latest_release ? latest_release[:buildVersion].to_i : 0 + ENV['VERSION_CODE'] = (latest_build_version + 1).to_s + rescue => e + UI.error("Error generating Firebase version: #{e.message}") + raise e + end + + when 'git' + # Calculate version code from git history + commit_count = `git rev-list --count HEAD`.to_i + ENV['VERSION_CODE'] = (commit_count << 1).to_s + else + UI.user_error!("Unsupported platform: #{platform}. Supported platforms are: playstore, firebase, git") + end + + # Output the results + UI.success("Generated version for #{platform}") + UI.success("Set VERSION=#{ENV['VERSION']} VERSION_CODE=#{ENV['VERSION_CODE']}") + version + end + + desc "Generate release notes" + lane :generateReleaseNote do |options| + releaseNotes = changelog_from_git_commits( + commits_count: 1, + ) + releaseNotes + end + + desc "Generate full release notes from specified tag or latest release tag" + lane :generateFullReleaseNote do |options| + def get_latest_tag + latest = `git describe --tags --abbrev=0`.strip + return latest unless latest.empty? + + latest = `git tag --sort=-creatordate`.split("\n").first + return latest unless latest.nil? || latest.empty? + + nil + end + + from_tag = options[:fromTag] || get_latest_tag + UI.message "Using tag: #{from_tag || 'No tags found. Getting all commits...'}" + + commits = if from_tag && !from_tag.empty? + `git log #{from_tag}..HEAD --pretty=format:"%B"`.split("\n") + else + `git log --pretty=format:"%B"`.split("\n") + end + + categories = process_commits(commits) + format_release_notes(categories) + end + + private_lane :process_commits do |commits| + notes = { + "breaking" => [], "feat" => [], "fix" => [], + "perf" => [], "refactor" => [], "style" => [], + "docs" => [], "test" => [], "build" => [], + "ci" => [], "chore" => [], "other" => [] + } + + commits.each do |commit| + next if commit.empty? || commit.start_with?("Co-authored-by:", "Merge") + + if commit.include?("BREAKING CHANGE:") || commit.include?("!") + notes["breaking"] << commit.sub(/^[^:]+:\s*/, "") + elsif commit =~ /^(feat|fix|perf|refactor|style|docs|test|build|ci|chore)(\(.+?\))?:/ + notes[$1] << commit.sub(/^[^:]+:\s*/, "") + else + notes["other"] << commit + end + end + notes + end + + private_lane :format_release_notes do |categories| + sections = { + "breaking" => "💥 Breaking Changes", + "feat" => "🚀 New Features", + "fix" => "🐛 Bug Fixes", + "perf" => "⚡ Performance Improvements", + "refactor" => "♻️ Refactoring", + "style" => "💅 Style Changes", + "docs" => "📚 Documentation", + "test" => "🧪 Tests", + "build" => "📦 Build System", + "ci" => "👷 CI Changes", + "chore" => "🔧 Maintenance", + "other" => "📝 Other Changes" + } + + notes = ["# Release Notes", "\nRelease date: #{Time.now.strftime('%d-%m-%Y')}"] + + sections.each do |type, title| + next if categories[type].empty? + notes << "\n## #{title}" + categories[type].each { |commit| notes << "\n- #{commit}" } + end + + UI.message "Generated Release Notes:" + UI.message notes.join("\n") + notes.join("\n") + end + +end + +platform :ios do + desc "Build iOS application" + lane :build_ios do |options| + # Set default configuration if not provided + options[:configuration] ||= "Debug" + ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG + + update_code_signing_settings( + use_automatic_signing: true, + path: ios_config[:project_path] + ) + + build_ios_app( + project: ios_config[:project_path], + scheme: ios_config[:scheme], + configuration: options[:configuration], + skip_codesigning: "true", + output_directory: ios_config[:output_directory], + skip_archive: "true" + ) + end + + lane :increment_version do |options| + firebase_config = FastlaneConfig.get_firebase_config(:ios) + ios_config = FastlaneConfig::IosConfig::BUILD_CONFIG + + latest_release = firebase_app_distribution_get_latest_release( + app: firebase_config[:appId], + service_credentials_file: options[:serviceCredsFile] || firebase_config[:serviceCredsFile] + ) + + increment_build_number( + xcodeproj: ios_config[:project_path], + build_number: latest_release[:buildVersion].to_i + 1 + ) + end + + desc "Upload iOS application to Firebase App Distribution" + lane :deploy_on_firebase do |options| + firebase_config = FastlaneConfig.get_firebase_config(:ios) + + increment_version(serviceCredsFile: firebase_config[:serviceCredsFile]) + build_ios() + releaseNotes = generateReleaseNote() + + firebase_app_distribution( + app: firebase_config[:appId], + service_credentials_file: firebase_config[:serviceCredsFile], + release_notes: releaseNotes, + groups: firebase_config[:groups] + ) + end + + desc "Generate release notes" + lane :generateReleaseNote do + branchName = `git rev-parse --abbrev-ref HEAD`.chomp() + releaseNotes = changelog_from_git_commits( + commits_count: 1, + ) + releaseNotes + end +end \ No newline at end of file diff --git a/fastlane/PluginFile b/fastlane/PluginFile new file mode 100644 index 00000000000..c3f69f2b0d2 --- /dev/null +++ b/fastlane/PluginFile @@ -0,0 +1,5 @@ +# Autogenerated by fastlane +# +# Ensure this file is checked in to source control! +gem 'fastlane-plugin-firebase_app_distribution' +gem 'fastlane-plugin-increment_build_number' diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 00000000000..c8de7f00214 --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,149 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## Android + +### android assembleDebugApks + +```sh +[bundle exec] fastlane android assembleDebugApks +``` + +Assemble debug APKs. + +### android assembleReleaseApks + +```sh +[bundle exec] fastlane android assembleReleaseApks +``` + +Assemble Release APK + +### android bundleReleaseApks + +```sh +[bundle exec] fastlane android bundleReleaseApks +``` + +Bundle Release APK + +### android deployReleaseApkOnFirebase + +```sh +[bundle exec] fastlane android deployReleaseApkOnFirebase +``` + +Publish Release Artifacts to Firebase App Distribution + +### android deployDemoApkOnFirebase + +```sh +[bundle exec] fastlane android deployDemoApkOnFirebase +``` + +Publish Demo Artifacts to Firebase App Distribution + +### android deployInternal + +```sh +[bundle exec] fastlane android deployInternal +``` + +Deploy internal tracks to Google Play + +### android promoteToBeta + +```sh +[bundle exec] fastlane android promoteToBeta +``` + +Promote internal tracks to beta on Google Play + +### android promote_to_production + +```sh +[bundle exec] fastlane android promote_to_production +``` + +Promote beta tracks to production on Google Play + +### android generateVersion + +```sh +[bundle exec] fastlane android generateVersion +``` + +Generate Version for different platforms + +### android generateReleaseNote + +```sh +[bundle exec] fastlane android generateReleaseNote +``` + +Generate release notes + +### android generateFullReleaseNote + +```sh +[bundle exec] fastlane android generateFullReleaseNote +``` + +Generate full release notes from specified tag or latest release tag + +---- + + +## iOS + +### ios build_ios + +```sh +[bundle exec] fastlane ios build_ios +``` + +Build iOS application + +### ios increment_version + +```sh +[bundle exec] fastlane ios increment_version +``` + + + +### ios deploy_on_firebase + +```sh +[bundle exec] fastlane ios deploy_on_firebase +``` + +Upload iOS application to Firebase App Distribution + +### ios generateReleaseNote + +```sh +[bundle exec] fastlane ios generateReleaseNote +``` + +Generate release notes + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/fastlane/config/config_helpers.rb b/fastlane/config/config_helpers.rb new file mode 100644 index 00000000000..ffd101524e9 --- /dev/null +++ b/fastlane/config/config_helpers.rb @@ -0,0 +1,32 @@ +require_relative '../../fastlane-config/android_config' +require_relative '../../fastlane-config/ios_config' + +module FastlaneConfig + # Move methods directly into FastlaneConfig module instead of nested Helpers module + def self.get_android_signing_config(options = {}) + { + storeFile: options[:store_file] || ENV['ANDROID_STORE_FILE'] || AndroidConfig::STORE_CONFIG[:default_store_file], + storePassword: options[:store_password] || ENV['ANDROID_STORE_PASSWORD'] || AndroidConfig::STORE_CONFIG[:default_store_password], + keyAlias: options[:key_alias] || ENV['ANDROID_KEY_ALIAS'] || AndroidConfig::STORE_CONFIG[:default_key_alias], + keyPassword: options[:key_password] || ENV['ANDROID_KEY_PASSWORD'] || AndroidConfig::STORE_CONFIG[:default_key_password] + } + end + + def self.get_firebase_config(platform, type = :prod) + case platform + when :android + app_id = type == :prod ? AndroidConfig::FIREBASE_CONFIG[:firebase_prod_app_id] : AndroidConfig::FIREBASE_CONFIG[:firebase_demo_app_id] + { + appId: ENV['FIREBASE_ANDROID_APP_ID'] || app_id, + serviceCredsFile: ENV['FIREBASE_SERVICE_CREDS_FILE'] || AndroidConfig::FIREBASE_CONFIG[:firebase_service_creds_file], + groups: ENV['FIREBASE_GROUPS'] || AndroidConfig::FIREBASE_CONFIG[:firebase_groups] + } + when :ios + { + appId: ENV['FIREBASE_IOS_APP_ID'] || IosConfig::FIREBASE_CONFIG[:firebase_app_id], + serviceCredsFile: ENV['FIREBASE_SERVICE_CREDS_FILE'] || IosConfig::FIREBASE_CONFIG[:firebase_service_creds_file], + groups: ENV['FIREBASE_GROUPS'] || IosConfig::FIREBASE_CONFIG[:firebase_groups] + } + end + end +end \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/6.txt b/fastlane/metadata/android/en-US/changelogs/6.txt new file mode 100644 index 00000000000..22b99611ff1 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/6.txt @@ -0,0 +1,11 @@ +- Create center new groups & centers +- Create clients & groups from parent entity +- Improved navigation & advanced search +- Open, approve, disburse new loan & savings accounts +- Attach documents to loan & savings accounts. +- Offline Data Collection & Synchronization +- Synchronize clients and groups for offline +- Enter repayments, deposits, and withdrawals while offline +- Create new clients, loans & savings accounts while offline +- Pinpoint client GPS location +- Track route of field officer \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/default.txt b/fastlane/metadata/android/en-US/changelogs/default.txt new file mode 100644 index 00000000000..aa949b47b7b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/default.txt @@ -0,0 +1 @@ +Initial Release \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt new file mode 100644 index 00000000000..b24261aa3db --- /dev/null +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -0,0 +1,35 @@ +Financial Inclusion is at your fingertips directly in the field. This app gives financial institutions the full power of Mifos X wherever they are by allowing field-based staff to handle all their daily operations from onboarding new clients and accounts to collecting repayments and deposits, and surveying clients in the field. Supervisors can now ensure greater efficiency and transparency with field operations. Offline data synchronization for clients and groups is now available to support opening new clients and doing field collections while in remote regions without connectivity. + +The following field operations are supported: + +Office Management +- Create center new groups and centers +- Create clients from within parent group +- Create new groups from with parent center. + +Client Management +- Create new clients individually and within a group +- View client details. +- Add identifiers and documents to clients. +- Take client photo via webcam. +- Pinpoint client GPS location + +Account Management +- Open, approve, and disburse new loan accounts +- Open, approve, and activate new savings accounts +- Attach documents to loan and savings accounts. +- Support for data tables & adding documents +- Input repayments for loans +- Input deposits and withdrawals for savings accounts. +- Attach charges to accounts. +- View full details and transaction history for loan and savings accounts + +Offline Data Collection & Synchronization +- Synchronize clients and groups for offline data entry +- Enter repayments, deposits, and withdrawals while offline +- Create new clients while offline +- Create new loan and savings accounts while offline. + +GIS & Location-Based Features +- Pinpoint GPS location of a client residence. +- Track route of field officer. \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png new file mode 100644 index 00000000000..e85d66bd170 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 00000000000..d0d044386f9 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png new file mode 100644 index 00000000000..da39b8e55a6 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png new file mode 100644 index 00000000000..aa3c66d5a6b Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png new file mode 100644 index 00000000000..9d94b735f62 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png new file mode 100644 index 00000000000..a6d3951f4c6 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png new file mode 100644 index 00000000000..84306987a7c Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.png new file mode 100644 index 00000000000..db146fae984 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.png new file mode 100644 index 00000000000..d24d50875ba Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/8_en-US.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/8_en-US.png new file mode 100644 index 00000000000..82b93db78f4 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/8_en-US.png differ diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt new file mode 100644 index 00000000000..80c5a472d56 --- /dev/null +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -0,0 +1 @@ +Mobile field operations app for financial inclusion built on Apache Fineract. \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt new file mode 100644 index 00000000000..df5a78c6fce --- /dev/null +++ b/fastlane/metadata/android/en-US/title.txt @@ -0,0 +1 @@ +MifosX Android Client \ No newline at end of file diff --git a/core/datastore/consumer-proguard-rules.pro b/fastlane/metadata/android/en-US/video.txt similarity index 100% rename from core/datastore/consumer-proguard-rules.pro rename to fastlane/metadata/android/en-US/video.txt diff --git a/feature/auth/src/main/java/com/mifos/feature/auth/login/LoginScreen.kt b/feature/auth/src/main/java/com/mifos/feature/auth/login/LoginScreen.kt index 118122aa53a..19d6be48f82 100644 --- a/feature/auth/src/main/java/com/mifos/feature/auth/login/LoginScreen.kt +++ b/feature/auth/src/main/java/com/mifos/feature/auth/login/LoginScreen.kt @@ -9,7 +9,6 @@ */ package com.mifos.feature.auth.login -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -30,14 +29,11 @@ import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.NavigationBar import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -69,8 +65,7 @@ import androidx.compose.ui.window.DialogProperties import androidx.hilt.navigation.compose.hiltViewModel import com.mifos.core.designsystem.component.MifosAndroidClientIcon import com.mifos.core.designsystem.component.MifosOutlinedTextField -import com.mifos.core.designsystem.theme.BluePrimary -import com.mifos.core.designsystem.theme.BluePrimaryDark +import com.mifos.core.designsystem.component.MifosScaffold import com.mifos.core.designsystem.theme.DarkGray import com.mifos.core.designsystem.theme.White import com.mifos.feature.auth.R @@ -81,8 +76,7 @@ import com.mifos.feature.auth.R @Composable internal fun LoginScreen( - homeIntent: () -> Unit, - passcodeIntent: () -> Unit, + navigatePasscode: () -> Unit, onClickToUpdateServerConfig: () -> Unit, modifier: Modifier = Modifier, loginViewModel: LoginViewModel = hiltViewModel(), @@ -128,45 +122,40 @@ internal fun LoginScreen( passwordError.value = state.passwordError } - LoginUiState.HomeActivityIntent -> { + is LoginUiState.Success -> { + navigatePasscode() showDialog.value = false - homeIntent() - } - - LoginUiState.PassCodeActivityIntent -> { - showDialog.value = false - passcodeIntent() } } - Scaffold( + MifosScaffold( modifier = modifier - .fillMaxSize() - .padding(16.dp), - containerColor = Color.White, - snackbarHost = { SnackbarHost(snackbarHostState) }, + .fillMaxSize(), + snackbarHostState = snackbarHostState, bottomBar = { - Box( + NavigationBar( modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center, + containerColor = Color.Transparent, ) { - FilledTonalButton( - onClick = onClickToUpdateServerConfig, + Box( modifier = Modifier - .align(Alignment.Center), - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - contentColor = MaterialTheme.colorScheme.tertiary, - ), + .fillMaxWidth() + .padding(12.dp), + contentAlignment = Alignment.Center, ) { - Text(text = "Update Server Configuration") + FilledTonalButton( + onClick = onClickToUpdateServerConfig, + modifier = Modifier.heightIn(44.dp), + ) { + Text(text = "Update Server Configuration") - Spacer(modifier = Modifier.width(4.dp)) + Spacer(modifier = Modifier.width(4.dp)) - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowForward, - contentDescription = "ArrowForward", - ) + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = "ArrowForward", + ) + } } } }, @@ -175,9 +164,9 @@ internal fun LoginScreen( modifier = Modifier .fillMaxWidth() .padding(it) + .padding(12.dp) .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, - ) { Spacer(modifier = Modifier.height(80.dp)) @@ -221,7 +210,11 @@ internal fun LoginScreen( onValueChange = { value -> password = value }, - visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(), + visualTransformation = if (passwordVisibility) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, icon = Icons.Filled.Lock, label = R.string.feature_auth_password, error = passwordError.value, @@ -244,19 +237,22 @@ internal fun LoginScreen( Spacer(modifier = Modifier.height(8.dp)) Button( - onClick = { loginViewModel.validateUserInputs(userName.text, password.text) }, + onClick = { + loginViewModel.validateUserInputs( + username = userName.text, + password = password.text, + ) + }, modifier = Modifier .fillMaxWidth() .heightIn(44.dp) .padding(start = 16.dp, end = 16.dp), contentPadding = PaddingValues(), - colors = ButtonDefaults.buttonColors( - containerColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - ), ) { Text(text = "Login", fontSize = 16.sp) } } + if (showDialog.value) { Dialog( onDismissRequest = { showDialog.value }, @@ -271,8 +267,11 @@ internal fun LoginScreen( } } -@Preview(showSystemUi = true, device = "id:pixel_7") +@Preview @Composable private fun LoginScreenPreview() { - LoginScreen({}, {}, {}) + LoginScreen( + navigatePasscode = {}, + onClickToUpdateServerConfig = {}, + ) } diff --git a/feature/auth/src/main/java/com/mifos/feature/auth/login/LoginUiState.kt b/feature/auth/src/main/java/com/mifos/feature/auth/login/LoginUiState.kt index 7cf388cdd0a..4c9b7d7944c 100644 --- a/feature/auth/src/main/java/com/mifos/feature/auth/login/LoginUiState.kt +++ b/feature/auth/src/main/java/com/mifos/feature/auth/login/LoginUiState.kt @@ -21,10 +21,10 @@ sealed class LoginUiState { data class ShowError(val message: Int) : LoginUiState() - data class ShowValidationError(val usernameError: Int? = null, val passwordError: Int? = null) : - LoginUiState() + data class ShowValidationError( + val usernameError: Int? = null, + val passwordError: Int? = null, + ) : LoginUiState() - data object HomeActivityIntent : LoginUiState() - - data object PassCodeActivityIntent : LoginUiState() + data object Success : LoginUiState() } diff --git a/feature/auth/src/main/java/com/mifos/feature/auth/login/LoginViewModel.kt b/feature/auth/src/main/java/com/mifos/feature/auth/login/LoginViewModel.kt index 2c2f1e3232b..a63627efd2d 100644 --- a/feature/auth/src/main/java/com/mifos/feature/auth/login/LoginViewModel.kt +++ b/feature/auth/src/main/java/com/mifos/feature/auth/login/LoginViewModel.kt @@ -21,6 +21,7 @@ import com.mifos.core.domain.useCases.PasswordValidationUseCase import com.mifos.core.domain.useCases.UsernameValidationUseCase import com.mifos.core.model.getInstanceUrl import com.mifos.feature.auth.R +import com.mifos.feature.auth.login.LoginUiState.ShowError import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -28,7 +29,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.mifos.core.apimanager.BaseApiManager -import org.openapitools.client.models.PostAuthenticationResponse import javax.inject.Inject /** @@ -43,8 +43,7 @@ class LoginViewModel @Inject constructor( private val passwordValidationUseCase: PasswordValidationUseCase, private val baseApiManager: BaseApiManager, private val loginUseCase: LoginUseCase, -) : - ViewModel() { +) : ViewModel() { private val _loginUiState = MutableStateFlow(LoginUiState.Empty) val loginUiState = _loginUiState.asStateFlow() @@ -69,19 +68,10 @@ class LoginViewModel @Inject constructor( } private fun setupPrefManger(username: String, password: String) { - // Updating Services - baseApiManager.createService( - username, - password, - prefManager.getServerConfig.getInstanceUrl().dropLast(3), - prefManager.getServerConfig.tenant, - true, - ) if (Network.isOnline(context)) { login(username, password) } else { - _loginUiState.value = - LoginUiState.ShowError(R.string.feature_auth_error_not_connected_internet) + _loginUiState.value = ShowError(R.string.feature_auth_error_not_connected_internet) } } @@ -90,9 +80,7 @@ class LoginViewModel @Inject constructor( loginUseCase(username, password).collect { result -> when (result) { is Resource.Error -> { - _loginUiState.value = - LoginUiState.ShowError(R.string.feature_auth_error_login_failed) - Log.e("@@@", "login: ${result.message}") + _loginUiState.value = ShowError(R.string.feature_auth_error_login_failed) } is Resource.Loading -> { @@ -100,36 +88,24 @@ class LoginViewModel @Inject constructor( } is Resource.Success -> { - if (result.data?.authenticated == true && result.data != null) { - onLoginSuccessful(result.data!!, username, password) - } else { - _loginUiState.value = - LoginUiState.ShowError(R.string.feature_auth_error_login_failed) + result.data?.let { + Log.d("Android", "$it") + prefManager.saveUserDetails(it) + // Saving username password + prefManager.usernamePassword = Pair(username, password) + // Updating Services + baseApiManager.createService( + username, + password, + baseUrl = prefManager.serverConfig.getInstanceUrl().dropLast(3), + tenant = prefManager.serverConfig.tenant, + secured = true, + ) + _loginUiState.value = LoginUiState.Success } } } } } } - - private fun onLoginSuccessful( - user: PostAuthenticationResponse, - username: String, - password: String, - ) { - // Saving username password - prefManager.usernamePassword = Pair(username, password) - // Saving userID - prefManager.setUserId(user.userId!!.toInt()) - // Saving user's token - prefManager.saveToken("Basic " + user.base64EncodedAuthenticationKey) - // Saving user - prefManager.savePostAuthenticationResponse(user) - - if (prefManager.getPassCodeStatus()) { - _loginUiState.value = LoginUiState.HomeActivityIntent - } else { - _loginUiState.value = LoginUiState.PassCodeActivityIntent - } - } } diff --git a/feature/auth/src/main/java/com/mifos/feature/auth/navigation/AuthNavigation.kt b/feature/auth/src/main/java/com/mifos/feature/auth/navigation/AuthNavigation.kt index 78aefd27114..87e57b79693 100644 --- a/feature/auth/src/main/java/com/mifos/feature/auth/navigation/AuthNavigation.kt +++ b/feature/auth/src/main/java/com/mifos/feature/auth/navigation/AuthNavigation.kt @@ -16,24 +16,22 @@ import androidx.navigation.navigation import com.mifos.feature.auth.login.LoginScreen fun NavGraphBuilder.authNavGraph( - navigateHome: () -> Unit, + route: String, navigatePasscode: () -> Unit, updateServerConfig: () -> Unit, ) { navigation( startDestination = AuthScreens.LoginScreen.route, - route = AuthScreens.LoginScreenRoute.route, + route = route, ) { loginRoute( navigatePasscode = navigatePasscode, - navigateHome = navigateHome, updateServerConfig = updateServerConfig, ) } } private fun NavGraphBuilder.loginRoute( - navigateHome: () -> Unit, navigatePasscode: () -> Unit, updateServerConfig: () -> Unit, ) { @@ -41,8 +39,7 @@ private fun NavGraphBuilder.loginRoute( route = AuthScreens.LoginScreen.route, ) { LoginScreen( - homeIntent = navigateHome, - passcodeIntent = navigatePasscode, + navigatePasscode = navigatePasscode, onClickToUpdateServerConfig = updateServerConfig, ) } diff --git a/feature/center/src/main/java/com/mifos/feature/center/centerDetails/CenterDetailsScreen.kt b/feature/center/src/main/java/com/mifos/feature/center/centerDetails/CenterDetailsScreen.kt index 919e4d0fa91..1bb9ccc7cdf 100644 --- a/feature/center/src/main/java/com/mifos/feature/center/centerDetails/CenterDetailsScreen.kt +++ b/feature/center/src/main/java/com/mifos/feature/center/centerDetails/CenterDetailsScreen.kt @@ -59,7 +59,6 @@ import com.mifos.core.designsystem.icon.MifosIcons import com.mifos.core.designsystem.theme.Black import com.mifos.core.designsystem.theme.BluePrimary import com.mifos.core.designsystem.theme.BluePrimaryDark -import com.mifos.core.designsystem.theme.DarkGray import com.mifos.core.designsystem.theme.White import com.mifos.core.objects.group.CenterInfo import com.mifos.core.objects.group.CenterWithAssociations @@ -297,7 +296,11 @@ private fun CenterDetailsContent( } @Composable -private fun MifosCenterDetailsText(icon: ImageVector, field: String, value: String) { +private fun MifosCenterDetailsText( + icon: ImageVector, + field: String, + value: String, +) { Row( modifier = Modifier .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp) @@ -308,7 +311,6 @@ private fun MifosCenterDetailsText(icon: ImageVector, field: String, value: Stri modifier = Modifier.size(18.dp), imageVector = icon, contentDescription = null, - tint = DarkGray, ) Text( modifier = Modifier @@ -320,20 +322,19 @@ private fun MifosCenterDetailsText(icon: ImageVector, field: String, value: Stri fontWeight = FontWeight.Normal, fontStyle = FontStyle.Normal, ), - color = Black, - textAlign = TextAlign.Start, - ) - Text( - - text = value, - style = TextStyle( - fontSize = 18.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - ), - color = DarkGray, textAlign = TextAlign.Start, ) + if (value.isNotEmpty()) { + Text( + text = value, + style = TextStyle( + fontSize = 18.sp, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + ), + textAlign = TextAlign.Start, + ) + } } } diff --git a/feature/center/src/main/java/com/mifos/feature/center/centerList/ui/CenterListScreen.kt b/feature/center/src/main/java/com/mifos/feature/center/centerList/ui/CenterListScreen.kt index d6fad7813c3..c6658d1f649 100644 --- a/feature/center/src/main/java/com/mifos/feature/center/centerList/ui/CenterListScreen.kt +++ b/feature/center/src/main/java/com/mifos/feature/center/centerList/ui/CenterListScreen.kt @@ -20,14 +20,17 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues 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.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowRight import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState @@ -35,28 +38,24 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf 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.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -72,13 +71,10 @@ import androidx.paging.compose.collectAsLazyPagingItems import coil.compose.AsyncImage import com.mifos.core.designsystem.component.MifosCircularProgress import com.mifos.core.designsystem.component.MifosPagingAppendProgress +import com.mifos.core.designsystem.component.MifosScaffold import com.mifos.core.designsystem.component.MifosSweetError import com.mifos.core.designsystem.icon.MifosIcons -import com.mifos.core.designsystem.theme.Black -import com.mifos.core.designsystem.theme.BlueSecondary import com.mifos.core.designsystem.theme.DarkGray -import com.mifos.core.designsystem.theme.LightGray -import com.mifos.core.designsystem.theme.White import com.mifos.core.objects.group.Center import com.mifos.core.ui.components.SelectionModeTopAppBar import com.mifos.feature.center.R @@ -95,10 +91,6 @@ internal fun CenterListScreen( val refreshState by viewModel.isRefreshing.collectAsStateWithLifecycle() val state by viewModel.centerListUiState.collectAsStateWithLifecycle() - LaunchedEffect(key1 = true) { - viewModel.getCenterList() - } - CenterListScreen( paddingValues = paddingValues, state = state, @@ -122,17 +114,13 @@ internal fun CenterListScreen( onCenterSelect: (Int) -> Unit, ) { val snackbarHostState = remember { SnackbarHostState() } - var isInSelectionMode by rememberSaveable { mutableStateOf(false) } val selectedItems = remember { SelectedItemsState() } - val resetSelectionMode = { - isInSelectionMode = false - selectedItems.clear() - } val sync = rememberSaveable { mutableStateOf(false) } - BackHandler(enabled = isInSelectionMode) { - resetSelectionMode() + + BackHandler(enabled = selectedItems.size() > 0) { + selectedItems.clear() } val pullRefreshState = rememberPullRefreshState( @@ -140,22 +128,13 @@ internal fun CenterListScreen( onRefresh = onRefresh, ) - LaunchedEffect( - key1 = isInSelectionMode, - key2 = selectedItems.size(), - ) { - if (isInSelectionMode && selectedItems.isEmpty()) { - isInSelectionMode = false - } - } - - Scaffold( + MifosScaffold( modifier = Modifier.padding(paddingValues), topBar = { - if (isInSelectionMode) { + if (selectedItems.size() > 0) { SelectionModeTopAppBar( itemCount = selectedItems.size(), - resetSelectionMode = resetSelectionMode, + resetSelectionMode = selectedItems::clear, actions = { FilledTonalButton( onClick = { @@ -172,11 +151,10 @@ internal fun CenterListScreen( ) } }, - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + snackbarHostState = snackbarHostState, floatingActionButton = { FloatingActionButton( - onClick = { createNewCenter() }, - containerColor = BlueSecondary, + onClick = createNewCenter, ) { Icon( imageVector = MifosIcons.Add, @@ -187,6 +165,7 @@ internal fun CenterListScreen( ) { paddingValue -> Column( modifier = Modifier + .fillMaxSize() .padding(paddingValue), verticalArrangement = Arrangement.Center, ) { @@ -204,29 +183,25 @@ internal fun CenterListScreen( is CenterListUiState.CenterList -> { CenterListContent( + modifier = Modifier, centerPagingList = state.centers.collectAsLazyPagingItems(), - isInSelectionMode = isInSelectionMode, selectedItems = selectedItems, - onRefresh = { - onRefresh() - }, - onCenterSelect = { + onRefresh = onRefresh, + onCenterClick = { onCenterSelect(it) }, - selectedMode = { - isInSelectionMode = true - }, ) } - is CenterListUiState.CenterListDb -> CenterListDbContent(centerList = state.centers) + is CenterListUiState.CenterListDb -> { + CenterListDbContent(centerList = state.centers) + } } if (sync.value) { SyncCenterDialogScreen( dismiss = { sync.value = false selectedItems.clear() - resetSelectionMode() }, hide = { sync.value = false }, centers = selectedItems.toList(), @@ -243,49 +218,50 @@ internal fun CenterListScreen( } private class SelectedItemsState(initialSelectedItems: List
= emptyList()) { - private val _selectedItems = mutableStateListOf
().also { it.addAll(initialSelectedItems) } - val selectedItems: State> = derivedStateOf { _selectedItems } + private val selectedItems = mutableStateListOf
().also { + it.addAll(initialSelectedItems) + } fun add(item: Center) { - _selectedItems.add(item) + if (item in selectedItems) { + selectedItems.remove(item) + } else { + selectedItems.add(item) + } } - fun remove(item: Center) { - _selectedItems.remove(item) - } fun toList(): List
{ - return _selectedItems.toList() + return selectedItems.toList() } + fun contains(item: Center): Boolean { - return _selectedItems.contains(item) + return selectedItems.contains(item) } fun clear() { - _selectedItems.clear() + selectedItems.clear() } fun size(): Int { - return _selectedItems.size - } - fun isEmpty(): Boolean { - return _selectedItems.isEmpty() + return selectedItems.size } } @Composable private fun CenterListContent( centerPagingList: LazyPagingItems
, - isInSelectionMode: Boolean, selectedItems: SelectedItemsState, onRefresh: () -> Unit, - onCenterSelect: (Int) -> Unit, - selectedMode: () -> Unit, + onCenterClick: (Int) -> Unit, + modifier: Modifier = Modifier, + lazyListState: LazyListState = rememberLazyListState(), ) { when (centerPagingList.loadState.refresh) { is LoadState.Error -> { - MifosSweetError(message = stringResource(id = R.string.feature_center_error_loading_centers)) { - onRefresh() - } + MifosSweetError( + message = stringResource(id = R.string.feature_center_error_loading_centers), + onclick = onRefresh, + ) } is LoadState.Loading -> MifosCircularProgress() @@ -293,150 +269,31 @@ private fun CenterListContent( is LoadState.NotLoading -> Unit } - LazyColumn { - items(centerPagingList.itemCount) { index -> - - val isSelected = selectedItems.contains(centerPagingList[index]!!) - var cardColor by remember { mutableStateOf(White) } - - OutlinedCard( - modifier = Modifier - .padding(6.dp) - .combinedClickable( - onClick = { - if (isInSelectionMode) { - cardColor = if (isSelected) { - centerPagingList[index]?.let { selectedItems.remove(it) } - White - } else { - centerPagingList[index]?.let { selectedItems.add(it) } - LightGray - } - } else { - centerPagingList[index]?.id?.let { onCenterSelect(it) } - } - }, - onLongClick = { - if (isInSelectionMode) { - cardColor = if (isSelected) { - centerPagingList[index]?.let { selectedItems.remove(it) } - White - } else { - centerPagingList[index]?.let { selectedItems.add(it) } - LightGray - } - } else { - selectedMode() - centerPagingList[index]?.let { selectedItems.add(it) } - cardColor = LightGray - } - }, - ), - colors = CardDefaults.cardColors( - containerColor = if (selectedItems.isEmpty()) { - cardColor = White - White - } else { - cardColor - }, - ), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding( - start = 16.dp, - end = 16.dp, - top = 24.dp, - bottom = 24.dp, - ), - verticalAlignment = Alignment.CenterVertically, - ) { - Canvas( - modifier = Modifier.size(16.dp), - onDraw = { - drawCircle( - color = if (centerPagingList[index]?.active == true) Color.Green else Color.Red, - ) - }, - ) - Column( - modifier = Modifier - .weight(1f) - .padding(start = 16.dp), - ) { - centerPagingList[index]?.name?.let { - Text( - text = it, - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = Black, - ), - ) - } - Text( - text = centerPagingList[index]?.accountNo.toString(), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = DarkGray, - ), - ) - Row { - Text( - text = centerPagingList[index]?.officeName.toString(), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = DarkGray, - ), - ) - Spacer(modifier = Modifier.width(26.dp)) - Text( - text = centerPagingList[index]?.officeId.toString(), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = DarkGray, - ), - ) - } - Row { - Text( - text = centerPagingList[index]?.staffName.toString(), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = DarkGray, - ), - ) - Spacer(modifier = Modifier.width(26.dp)) - Text( - text = centerPagingList[index]?.staffId.toString(), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = DarkGray, - ), - ) - } - } - if (centerPagingList[index]?.sync == true) { - AsyncImage( - modifier = Modifier.size(20.dp), - model = R.drawable.feature_center_ic_done_all_black_24dp, - contentDescription = null, - ) - } - } - } + LazyColumn( + modifier = modifier.fillMaxSize(), + state = lazyListState, + contentPadding = PaddingValues(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items( + count = centerPagingList.itemCount, + key = { + centerPagingList[it]?.id ?: it + }, + ) { index -> + val center: Center = centerPagingList[index]!! + + CenterCard( + center = center, + selected = selectedItems.contains(center), + isInSelectionMode = selectedItems.size() > 0, + onSelect = { + selectedItems.add(it) + }, + onClick = { + onCenterClick(it.id ?: 0) + }, + ) } when (centerPagingList.loadState.append) { @@ -475,118 +332,109 @@ private fun CenterListContent( @Composable private fun CenterListDbContent( centerList: List
, + modifier: Modifier = Modifier, + lazyListState: LazyListState = rememberLazyListState(), ) { - LazyColumn { - items(centerList) { center -> - - OutlinedCard( - modifier = Modifier - .padding(6.dp), - colors = CardDefaults.cardColors( - containerColor = White, - ), - ) { + LazyColumn( + modifier = modifier, + state = lazyListState, + ) { + items( + items = centerList, + key = { + it.id ?: it.hashCode() + }, + ) { center -> + CenterCard( + center = center, + selected = false, + isInSelectionMode = false, + onSelect = {}, + onClick = {}, + ) + } + } +} + +@Composable +private fun CenterCard( + center: Center, + selected: Boolean, + isInSelectionMode: Boolean, + onSelect: (Center) -> Unit, + modifier: Modifier = Modifier, + selectedColor: Color = MaterialTheme.colorScheme.secondaryContainer, + unselectedColor: Color = MaterialTheme.colorScheme.surface, + onClick: (Center) -> Unit, +) { + val containerColor = if (selected) selectedColor else unselectedColor + + OutlinedCard( + modifier = modifier + .clip(CardDefaults.outlinedShape) + .combinedClickable( + onClick = { + if (isInSelectionMode) { + onSelect(center) + } else { + onClick(center) + } + }, + onLongClick = { + onSelect(center) + }, + ), + colors = CardDefaults.cardColors( + containerColor = containerColor, + ), + ) { + ListItem( + leadingContent = { + Canvas( + modifier = Modifier.size(16.dp), + onDraw = { + drawCircle( + color = if (center.active == true) Color.Green else Color.Red, + ) + }, + ) + }, + headlineContent = { + Text(text = center.name.toString()) + }, + supportingContent = center.accountNo?.let { + { Text(text = it) } + }, + overlineContent = center.officeName?.let { + { Text(text = it) } + }, + trailingContent = { Row( - modifier = Modifier - .fillMaxWidth() - .padding( - start = 16.dp, - end = 16.dp, - top = 24.dp, - bottom = 24.dp, - ), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - Canvas( - modifier = Modifier.size(16.dp), - onDraw = { - drawCircle( - color = if (center.active == true) Color.Green else Color.Red, - ) - }, - ) - Column( - modifier = Modifier - .weight(1f) - .padding(start = 16.dp), - ) { - center.name?.let { - Text( - text = it, - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = Black, - ), - ) - } - Text( - text = center.accountNo.toString(), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = DarkGray, - ), + if (center.sync == true) { + AsyncImage( + modifier = Modifier.size(20.dp), + model = R.drawable.feature_center_ic_done_all_black_24dp, + contentDescription = null, ) - Row { - Text( - text = center.officeName.toString(), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = DarkGray, - ), - ) - Spacer(modifier = Modifier.width(26.dp)) - Text( - text = center.officeId.toString(), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = DarkGray, - ), - ) - } - Row { - Text( - text = center.staffName.toString(), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = DarkGray, - ), - ) - Spacer(modifier = Modifier.width(26.dp)) - Text( - text = center.staffId.toString(), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = DarkGray, - ), - ) - } } - AsyncImage( - modifier = Modifier.size(20.dp), - model = R.drawable.feature_center_ic_done_all_black_24dp, + + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowRight, contentDescription = null, ) } - } - } + }, + colors = ListItemDefaults.colors( + containerColor = Color.Unspecified, + ), + ) } } -class CenterListUiStateProvider : - PreviewParameterProvider { - +class CenterListUiStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( CenterListUiState.Loading, @@ -601,11 +449,9 @@ class CenterListUiStateProvider : private fun CenterListContentPreview() { CenterListContent( centerPagingList = sampleCenterList.collectAsLazyPagingItems(), - isInSelectionMode = false, selectedItems = SelectedItemsState(), onRefresh = {}, - onCenterSelect = {}, - selectedMode = {}, + onCenterClick = {}, ) } @@ -618,7 +464,8 @@ private fun CenterListDbContentPreview() { @Preview(showBackground = true) @Composable private fun CenterListScreenPreview( - @PreviewParameter(CenterListUiStateProvider::class) centerListUiState: CenterListUiState, + @PreviewParameter(CenterListUiStateProvider::class) + centerListUiState: CenterListUiState, ) { CenterListScreen( paddingValues = PaddingValues(), diff --git a/feature/center/src/main/java/com/mifos/feature/center/centerList/ui/CenterListViewModel.kt b/feature/center/src/main/java/com/mifos/feature/center/centerList/ui/CenterListViewModel.kt index 3af23f94def..ee0d43813ad 100644 --- a/feature/center/src/main/java/com/mifos/feature/center/centerList/ui/CenterListViewModel.kt +++ b/feature/center/src/main/java/com/mifos/feature/center/centerList/ui/CenterListViewModel.kt @@ -43,6 +43,10 @@ class CenterListViewModel @Inject constructor( private val _centerListUiState = MutableStateFlow(CenterListUiState.Loading) val centerListUiState = _centerListUiState.asStateFlow() + init { + getCenterList() + } + fun getCenterList() { if (prefManager.userStatus) { loadCentersFromDb() diff --git a/feature/center/src/main/java/com/mifos/feature/center/createCenter/CreateNewCenterScreen.kt b/feature/center/src/main/java/com/mifos/feature/center/createCenter/CreateNewCenterScreen.kt index ed19c3bdf56..5002dabd734 100644 --- a/feature/center/src/main/java/com/mifos/feature/center/createCenter/CreateNewCenterScreen.kt +++ b/feature/center/src/main/java/com/mifos/feature/center/createCenter/CreateNewCenterScreen.kt @@ -60,6 +60,7 @@ import com.mifos.core.designsystem.component.MifosOutlinedTextField import com.mifos.core.designsystem.component.MifosScaffold import com.mifos.core.designsystem.component.MifosSweetError import com.mifos.core.designsystem.component.MifosTextFieldDropdown +import com.mifos.core.designsystem.icon.MifosIcons import com.mifos.core.designsystem.theme.BluePrimary import com.mifos.core.designsystem.theme.BluePrimaryDark import com.mifos.core.objects.organisation.Office @@ -70,6 +71,7 @@ import java.util.Locale @Composable internal fun CreateNewCenterScreen( onCreateSuccess: () -> Unit, + onNavigateBack: () -> Unit, viewModel: CreateNewCenterViewModel = hiltViewModel(), ) { val state by viewModel.createNewCenterUiState.collectAsStateWithLifecycle() @@ -87,6 +89,7 @@ internal fun CreateNewCenterScreen( viewModel.createNewCenter(it) }, onCreateSuccess = onCreateSuccess, + onNavigateBack = onNavigateBack, ) } @@ -96,12 +99,15 @@ internal fun CreateNewCenterScreen( onRetry: () -> Unit, createCenter: (CenterPayload) -> Unit, onCreateSuccess: () -> Unit, + onNavigateBack: () -> Unit, ) { val snackbarHostState = remember { SnackbarHostState() } MifosScaffold( title = stringResource(id = R.string.feature_center_create_new_center), snackbarHostState = snackbarHostState, + icon = MifosIcons.arrowBack, + onBackPressed = onNavigateBack, ) { paddingValues -> Column(modifier = Modifier.padding(paddingValues)) { when (state) { @@ -324,6 +330,7 @@ private fun CreateNewCenterPreview( onRetry = {}, createCenter = {}, onCreateSuccess = {}, + onNavigateBack = {}, ) } diff --git a/feature/center/src/main/java/com/mifos/feature/center/navigation/CenterNavigation.kt b/feature/center/src/main/java/com/mifos/feature/center/navigation/CenterNavigation.kt index e5a2efc1947..cdeeca47b52 100644 --- a/feature/center/src/main/java/com/mifos/feature/center/navigation/CenterNavigation.kt +++ b/feature/center/src/main/java/com/mifos/feature/center/navigation/CenterNavigation.kt @@ -50,6 +50,7 @@ fun NavGraphBuilder.centerNavGraph( ) createCenterScreenRoute( onCreateSuccess = navController::popBackStack, + onNavigateBack = navController::popBackStack, ) } } @@ -106,12 +107,14 @@ fun NavGraphBuilder.centerGroupListScreenRoute( fun NavGraphBuilder.createCenterScreenRoute( onCreateSuccess: () -> Unit, + onNavigateBack: () -> Unit, ) { composable( route = CenterScreens.CreateCenterScreen.route, ) { CreateNewCenterScreen( onCreateSuccess = onCreateSuccess, + onNavigateBack = onNavigateBack, ) } } diff --git a/feature/center/src/main/res/values/strings.xml b/feature/center/src/main/res/values/strings.xml index 5d1f5401c03..739449238d3 100644 --- a/feature/center/src/main/res/values/strings.xml +++ b/feature/center/src/main/res/values/strings.xml @@ -15,7 +15,7 @@ Add Savings Account Group List - Center + Center Details Activation Date Next Meeting On diff --git a/feature/checker-inbox-task/src/main/java/com/mifos/feature/checkerInboxTask/checkerInbox/CheckerInboxScreen.kt b/feature/checker-inbox-task/src/main/java/com/mifos/feature/checkerInboxTask/checkerInbox/CheckerInboxScreen.kt index 7a43ee5550d..d83c919c547 100644 --- a/feature/checker-inbox-task/src/main/java/com/mifos/feature/checkerInboxTask/checkerInbox/CheckerInboxScreen.kt +++ b/feature/checker-inbox-task/src/main/java/com/mifos/feature/checkerInboxTask/checkerInbox/CheckerInboxScreen.kt @@ -18,10 +18,10 @@ package com.mifos.feature.checkerInboxTask.checkerInbox import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -36,14 +36,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State @@ -61,7 +59,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -73,11 +70,10 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.mifos.core.designsystem.component.MifosCircularProgress import com.mifos.core.designsystem.component.MifosDialogBox +import com.mifos.core.designsystem.component.MifosScaffold import com.mifos.core.designsystem.component.MifosSweetError import com.mifos.core.designsystem.icon.MifosIcons import com.mifos.core.designsystem.theme.Black -import com.mifos.core.designsystem.theme.LightGray -import com.mifos.core.designsystem.theme.White import com.mifos.core.objects.checkerinboxandtasks.CheckerTask import com.mifos.core.ui.components.SelectionModeTopAppBar import com.mifos.feature.checkerInboxTask.checkerInboxDialog.CheckerInboxTasksFilterDialog @@ -276,40 +272,43 @@ private fun CheckerInboxScreen( dismissButtonText = R.string.feature_checker_inbox_task_no, ) - Scaffold( + MifosScaffold( topBar = { if (isInSelectionMode) { SelectionModeTopAppBar( itemCount = selectedItemsState.size(), resetSelectionMode = resetSelectionMode, actions = { - IconButton(onClick = { - onApproveList(selectedItemsState.selectedItems.value) - resetSelectionMode() - }) { + IconButton( + onClick = { + onApproveList(selectedItemsState.selectedItems.value) + resetSelectionMode() + }, + ) { Icon( imageVector = MifosIcons.check, - tint = Color.Green, contentDescription = null, ) } - IconButton(onClick = { - onRejectList(selectedItemsState.selectedItems.value) - resetSelectionMode() - }) { + IconButton( + onClick = { + onRejectList(selectedItemsState.selectedItems.value) + resetSelectionMode() + }, + ) { Icon( imageVector = MifosIcons.close, - tint = Color.Yellow, contentDescription = null, ) } - IconButton(onClick = { - onDeleteList(selectedItemsState.selectedItems.value) - resetSelectionMode() - }) { + IconButton( + onClick = { + onDeleteList(selectedItemsState.selectedItems.value) + resetSelectionMode() + }, + ) { Icon( imageVector = MifosIcons.delete, - tint = Color.Red, contentDescription = null, ) } @@ -317,7 +316,6 @@ private fun CheckerInboxScreen( ) } else { TopAppBar( - colors = TopAppBarDefaults.mediumTopAppBarColors(containerColor = White), navigationIcon = { IconButton( onClick = { onBackPressed() }, @@ -325,41 +323,29 @@ private fun CheckerInboxScreen( Icon( imageVector = MifosIcons.arrowBack, contentDescription = null, - tint = Black, ) } }, title = { - Text( - text = stringResource(id = R.string.feature_checker_inbox_task_checker_inbox), - style = TextStyle( - fontSize = 24.sp, - fontWeight = FontWeight.Medium, - fontStyle = FontStyle.Normal, - ), - color = Black, - textAlign = TextAlign.Start, - ) + Text(text = stringResource(id = R.string.feature_checker_inbox_task_checker_inbox)) }, actions = { }, ) } }, - snackbarHost = { SnackbarHost(snackbarHostState) }, - contentColor = Color.White, + snackbarHostState = snackbarHostState, ) { paddingValues -> Column(modifier = Modifier.padding(paddingValues)) { ElevatedCard( modifier = Modifier.padding(8.dp), elevation = CardDefaults.elevatedCardElevation(4.dp), - colors = CardDefaults.elevatedCardColors(White), ) { Row( modifier = Modifier.padding(4.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - Image( + Icon( modifier = Modifier.weight(1f), imageVector = MifosIcons.search, contentDescription = null, @@ -374,11 +360,9 @@ private fun CheckerInboxScreen( search.invoke(it) }, placeholder = { Text(stringResource(id = R.string.feature_checker_inbox_task_search_by_user)) }, - colors = TextFieldDefaults.colors( - focusedContainerColor = White, - unfocusedContainerColor = White, - focusedIndicatorColor = Color.White, - unfocusedIndicatorColor = Color.White, + colors = TextFieldDefaults.colors().copy( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, ), ) IconButton( @@ -398,6 +382,8 @@ private fun CheckerInboxScreen( fetchedList = state.checkerTasks setList.invoke(fetchedList) CheckerInboxContent( + isInSelectionMode = isInSelectionMode, + selectedItemsState = selectedItemsState, checkerTaskList = if (isFiltered || isSearching) filteredList else fetchedList, onApprove = { approveId = it @@ -411,12 +397,9 @@ private fun CheckerInboxScreen( deleteId = it showDeleteDialog = true }, - isInSelectionMode = isInSelectionMode, - selectedItemsState = selectedItemsState, - selectedMode = { - isInSelectionMode = true - }, - ) + ) { + isInSelectionMode = true + } } is CheckerInboxUiState.Error -> { @@ -444,16 +427,24 @@ private fun CheckerInboxScreen( @Composable private fun CheckerInboxContent( + isInSelectionMode: Boolean, + selectedItemsState: SelectedItemsState, checkerTaskList: List, onApprove: (Int) -> Unit, onReject: (Int) -> Unit, onDelete: (Int) -> Unit, - isInSelectionMode: Boolean, - selectedItemsState: SelectedItemsState, + modifier: Modifier = Modifier, selectedMode: () -> Unit, ) { - LazyColumn { - items(checkerTaskList.size) { index -> + LazyColumn( + modifier = modifier, + contentPadding = PaddingValues(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items( + count = checkerTaskList.size, + key = { index -> checkerTaskList[index].id }, + ) { index -> CheckerInboxItem( checkerTask = checkerTaskList[index], onApprove = onApprove, @@ -502,10 +493,13 @@ private fun CheckerInboxItem( selectedItemsState: SelectedItemsState, selectedMode: () -> Unit, ) { + val selectedColor = MaterialTheme.colorScheme.primaryContainer + val unselectedColor = MaterialTheme.colorScheme.surface + val selectedItems by selectedItemsState.selectedItems val isSelected = selectedItemsState.contains(checkerTask.id) - var cardColor by remember { mutableStateOf(White) } + var cardColor by remember { mutableStateOf(unselectedColor) } var expendCheckerTask by remember { mutableStateOf(false) } @@ -517,10 +511,10 @@ private fun CheckerInboxItem( if (isInSelectionMode) { cardColor = if (isSelected) { selectedItemsState.remove(checkerTask.id) - White + unselectedColor } else { selectedItemsState.add(checkerTask.id) - LightGray + selectedColor } } else { expendCheckerTask = expendCheckerTask.not() @@ -530,22 +524,22 @@ private fun CheckerInboxItem( if (isInSelectionMode) { cardColor = if (isSelected) { selectedItemsState.remove(checkerTask.id) - White + unselectedColor } else { selectedItemsState.add(checkerTask.id) - LightGray + selectedColor } } else { selectedMode() selectedItemsState.add(checkerTask.id) - cardColor = LightGray + cardColor = selectedColor } }, ), colors = CardDefaults.cardColors( containerColor = if (selectedItems.isEmpty()) { - cardColor = White - White + cardColor = unselectedColor + unselectedColor } else { cardColor }, @@ -555,13 +549,6 @@ private fun CheckerInboxItem( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, ) { - Card( - modifier = Modifier - .width(8.dp) - .height(60.dp), - colors = CardDefaults.cardColors(Color.Yellow), - ) { - } Column( modifier = Modifier.padding(16.dp), ) { @@ -570,7 +557,6 @@ private fun CheckerInboxItem( style = TextStyle( fontSize = 18.sp, fontWeight = FontWeight.Medium, - color = Black, ), ) Spacer(modifier = Modifier.height(16.dp)) @@ -732,14 +718,13 @@ private fun CheckerInboxItemPreview() { @Composable private fun CheckerInboxContentPreview() { CheckerInboxContent( + isInSelectionMode = false, + selectedItemsState = SelectedItemsState(), checkerTaskList = sampleCheckerTaskList, onApprove = {}, onReject = {}, onDelete = {}, - isInSelectionMode = false, - selectedItemsState = SelectedItemsState(), - selectedMode = {}, - ) + ) {} } @Preview(showBackground = true) diff --git a/feature/checker-inbox-task/src/main/java/com/mifos/feature/checkerInboxTask/checkerInboxTasks/CheckerInboxTasksScreen.kt b/feature/checker-inbox-task/src/main/java/com/mifos/feature/checkerInboxTask/checkerInboxTasks/CheckerInboxTasksScreen.kt index 42c820656e0..e6acce4b839 100644 --- a/feature/checker-inbox-task/src/main/java/com/mifos/feature/checkerInboxTask/checkerInboxTasks/CheckerInboxTasksScreen.kt +++ b/feature/checker-inbox-task/src/main/java/com/mifos/feature/checkerInboxTask/checkerInboxTasks/CheckerInboxTasksScreen.kt @@ -9,6 +9,7 @@ */ package com.mifos.feature.checkerInboxTask.checkerInboxTasks +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -17,13 +18,13 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue 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.font.FontWeight @@ -84,7 +85,11 @@ internal fun CheckerInboxTasksScreen( } is CheckerInboxTasksUiState.Success -> { - Column(modifier = Modifier.padding(padding)) { + Column( + modifier = Modifier + .padding(padding), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { TaskOptions( leadingIcon = R.drawable.feature_checker_inbox_task_ic_mail_outline_24dp, option = stringResource(id = R.string.feature_checker_inbox_task_checker_Inbox), @@ -124,10 +129,14 @@ internal fun CheckerInboxTasksScreen( } @Composable -private fun TaskOptions(leadingIcon: Int, option: String, badge: String, onClick: () -> Unit) { +private fun TaskOptions( + leadingIcon: Int, + option: String, + badge: String, + onClick: () -> Unit, +) { Card( modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(White), onClick = { onClick() }, @@ -155,7 +164,9 @@ private fun TaskOptions(leadingIcon: Int, option: String, badge: String, onClick ), ) Card( - colors = CardDefaults.cardColors(Color.Red), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondary, + ), shape = RoundedCornerShape(10.dp), ) { Text( diff --git a/feature/client/src/main/AndroidManifest.xml b/feature/client/src/main/AndroidManifest.xml index f99f6837506..1fde686120b 100644 --- a/feature/client/src/main/AndroidManifest.xml +++ b/feature/client/src/main/AndroidManifest.xml @@ -11,16 +11,11 @@ - - - - 0) { + selectedItems.clear() } - Scaffold( + MifosScaffold( modifier = Modifier .padding(paddingValues), topBar = { - if (isInSelectionMode) { + if (selectedItems.size() > 0) { SelectionModeTopAppBar( currentSelectedItems = selectedItems.selectedItems.value, syncClicked = { sync.value = true }, - resetSelectionMode = resetSelectionMode, + resetSelectionMode = selectedItems::clear, ) } }, floatingActionButton = { FloatingActionButton( onClick = { createNewClient() }, - containerColor = BlueSecondary, ) { Icon( imageVector = Icons.Filled.Add, @@ -151,10 +144,8 @@ internal fun ClientListScreen( ) } }, - containerColor = White, - snackbarHost = { SnackbarHost(snackbarHostState) }, - ) { padding -> - + snackbarHostState = snackbarHostState, + ) { SwipeRefresh( state = swipeRefreshState, onRefresh = { @@ -162,23 +153,18 @@ internal fun ClientListScreen( }, ) { Column( - modifier = Modifier - .padding(padding), + modifier = Modifier, verticalArrangement = Arrangement.Center, ) { when (state) { is ClientListUiState.ClientListApi -> { LazyColumnForClientListApi( clientPagingList = state.list.collectAsLazyPagingItems(), - isInSelectionMode = isInSelectionMode, selectedItems = selectedItems, - onClientSelect = { + onClientClick = { onClientSelect(it) }, failedRefresh = { viewModel.refreshClientList() }, - selectedMode = { - isInSelectionMode = true - }, ) } @@ -199,7 +185,6 @@ internal fun ClientListScreen( if (sync.value) { SyncClientsDialogScreen( dismiss = { - resetSelectionMode.invoke() selectedItems.clear() sync.value = false }, @@ -260,11 +245,16 @@ private fun SelectionModeTopAppBar( } class ClientSelectionState(initialSelectedItems: List = emptyList()) { - private val _selectedItems = mutableStateListOf().also { it.addAll(initialSelectedItems) } + private val _selectedItems = + mutableStateListOf().also { it.addAll(initialSelectedItems) } var selectedItems: State> = derivedStateOf { _selectedItems } fun add(client: Client) { - _selectedItems.add(client) + if (_selectedItems.contains(client)) { + _selectedItems.remove(client) + } else { + _selectedItems.add(client) + } } fun remove(client: Client) { @@ -274,9 +264,6 @@ class ClientSelectionState(initialSelectedItems: List = emptyList()) { fun contains(client: Client): Boolean { return _selectedItems.contains(client) } - fun isEmpty(): Boolean { - return _selectedItems.isEmpty() - } fun clear() { _selectedItems.clear() @@ -285,19 +272,16 @@ class ClientSelectionState(initialSelectedItems: List = emptyList()) { fun size(): Int { return _selectedItems.size } - fun toList(): List { - return _selectedItems.toList() - } } @Composable private fun LazyColumnForClientListApi( clientPagingList: LazyPagingItems, - isInSelectionMode: Boolean, selectedItems: ClientSelectionState, failedRefresh: () -> Unit, - onClientSelect: (Int) -> Unit, - selectedMode: () -> Unit, + onClientClick: (Int) -> Unit, + modifier: Modifier = Modifier, + lazyListState: LazyListState = rememberLazyListState(), ) { when (clientPagingList.loadState.refresh) { is LoadState.Error -> { @@ -311,113 +295,35 @@ private fun LazyColumnForClientListApi( is LoadState.NotLoading -> Unit } - LazyColumn { - items(clientPagingList.itemCount) { index -> - - val isSelected = clientPagingList[index]?.let { selectedItems.contains(it) } - var cardColor by remember { mutableStateOf(White) } - - OutlinedCard( - modifier = Modifier - .padding(6.dp) - .combinedClickable( - onClick = { - if (isInSelectionMode) { - cardColor = if (isSelected == true) { - clientPagingList[index]?.let { selectedItems.remove(it) } - White - } else { - clientPagingList[index]?.let { selectedItems.add(it) } - LightGray - } - } else { - clientPagingList[index]?.id?.let { onClientSelect(it) } - } - }, - onLongClick = { - if (isInSelectionMode) { - cardColor = if (isSelected == true) { - clientPagingList[index]?.let { selectedItems.remove(it) } - White - } else { - clientPagingList[index]?.let { selectedItems.add(it) } - LightGray - } - } else { - selectedMode() - clientPagingList[index]?.let { selectedItems.add(it) } - cardColor = LightGray - } - }, - ), - colors = CardDefaults.cardColors( - containerColor = if (selectedItems.isEmpty()) { - cardColor = White - White - } else { - cardColor - }, - ), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding( - start = 16.dp, - end = 16.dp, - top = 24.dp, - bottom = 24.dp, - ), - verticalAlignment = Alignment.CenterVertically, - ) { - AsyncImage( - modifier = Modifier - .size(40.dp) - .clip(CircleShape) - .border(width = 1.dp, LightGray, shape = CircleShape), - model = R.drawable.feature_client_ic_dp_placeholder, - contentDescription = null, - ) - Column( - modifier = Modifier - .weight(1f) - .padding(start = 16.dp), - ) { - clientPagingList[index]?.displayName?.let { - Text( - text = it, - style = TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = Black, - ), - ) - } - Text( - text = clientPagingList[index]?.accountNo.toString(), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = DarkGray, - ), - ) - } - if (clientPagingList[index]?.sync == true) { - AsyncImage( - modifier = Modifier.size(20.dp), - model = R.drawable.feature_client_ic_done_all_black_24dp, - contentDescription = null, - ) - } - } - } + LazyColumn( + modifier = modifier, + state = lazyListState, + contentPadding = PaddingValues(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items( + count = clientPagingList.itemCount, + key = { + clientPagingList[it]?.id ?: it + }, + ) { index -> + val client = clientPagingList[index]!! + + ClientItem( + client = client, + selected = selectedItems.contains(client), + inSelectionMode = selectedItems.size() > 0, + onClientClick = { + onClientClick(client.id) + }, + onSelectItem = { + selectedItems.add(client) + }, + ) } when (clientPagingList.loadState.append) { - is LoadState.Error -> { - } + is LoadState.Error -> {} is LoadState.Loading -> { item { @@ -449,6 +355,86 @@ private fun LazyColumnForClientListApi( } } +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun ClientItem( + client: Client, + selected: Boolean, + inSelectionMode: Boolean, + onClientClick: () -> Unit, + modifier: Modifier = Modifier, + onSelectItem: () -> Unit, +) { + val borderStroke = if (selected) { + BorderStroke(1.dp, BluePrimary) + } else { + CardDefaults.outlinedCardBorder() + } + val containerColor = if (selected) { + MaterialTheme.colorScheme.secondaryContainer + } else { + Color.Unspecified + } + + OutlinedCard( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .combinedClickable( + onClick = { + if (inSelectionMode) { + onSelectItem() + } else { + onClientClick() + } + }, + onLongClick = onSelectItem, + ), + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.outlinedCardColors( + containerColor = containerColor, + ), + border = borderStroke, + ) { + ListItem( + leadingContent = { + AsyncImage( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .border(width = 1.dp, LightGray, shape = CircleShape), + model = R.drawable.feature_client_ic_dp_placeholder, + contentDescription = null, + ) + }, + headlineContent = { + Text(text = client.displayName.toString()) + }, + supportingContent = client.accountNo?.let { + { Text(text = it) } + }, + trailingContent = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + if (client.sync == true) { + Icon(imageVector = Icons.Default.DoneAll, contentDescription = "Sync") + } + + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowRight, + contentDescription = null, + ) + } + }, + colors = ListItemDefaults.colors( + containerColor = Color.Unspecified, + ), + ) + } +} + @Composable private fun LazyColumnForClientListDb(clientList: List) { LazyColumn { diff --git a/feature/client/src/main/java/com/mifos/feature/client/clientSurveySubmit/SurveySubmitViewModel.kt b/feature/client/src/main/java/com/mifos/feature/client/clientSurveySubmit/SurveySubmitViewModel.kt index 567a64e5f28..97e781db71b 100644 --- a/feature/client/src/main/java/com/mifos/feature/client/clientSurveySubmit/SurveySubmitViewModel.kt +++ b/feature/client/src/main/java/com/mifos/feature/client/clientSurveySubmit/SurveySubmitViewModel.kt @@ -11,13 +11,17 @@ package com.mifos.feature.client.clientSurveySubmit import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.mifos.core.common.utils.Constants import com.mifos.core.data.repository.SurveySubmitRepository import com.mifos.core.datastore.PrefManager import com.mifos.core.objects.survey.Scorecard import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import rx.Subscriber import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers @@ -35,29 +39,38 @@ class SurveySubmitViewModel @Inject constructor( val clientId = savedStateHandle.getStateFlow(key = Constants.CLIENT_ID, initialValue = -1) + val userId = prefManager.userDetails.map { + it?.userId?.toInt() ?: 0 + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = 0, + ) + private val _surveySubmitUiState = MutableStateFlow(SurveySubmitUiState.Initial) val surveySubmitUiState: StateFlow get() = _surveySubmitUiState - val userId = MutableStateFlow(prefManager.getUserId()) - fun submitSurvey(survey: Int, scorecardPayload: Scorecard?) { _surveySubmitUiState.value = SurveySubmitUiState.ShowProgressbar repository.submitScore(survey, scorecardPayload) .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) - .subscribe(object : Subscriber() { - override fun onCompleted() {} - override fun onError(e: Throwable) { - _surveySubmitUiState.value = SurveySubmitUiState.ShowError(e.message.toString()) - } - - override fun onNext(scorecard: Scorecard) { - _surveySubmitUiState.value = - SurveySubmitUiState.ShowSurveySubmittedSuccessfully(scorecard) - } - }) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + _surveySubmitUiState.value = + SurveySubmitUiState.ShowError(e.message.toString()) + } + + override fun onNext(scorecard: Scorecard) { + _surveySubmitUiState.value = + SurveySubmitUiState.ShowSurveySubmittedSuccessfully(scorecard) + } + }, + ) } } diff --git a/feature/client/src/main/java/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt b/feature/client/src/main/java/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt index 8ad9346ae0f..f43b1c979a3 100644 --- a/feature/client/src/main/java/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt +++ b/feature/client/src/main/java/com/mifos/feature/client/createNewClient/CreateNewClientScreen.kt @@ -99,6 +99,7 @@ import com.mifos.core.designsystem.component.MifosScaffold import com.mifos.core.designsystem.component.MifosSweetError import com.mifos.core.designsystem.component.MifosTextFieldDropdown import com.mifos.core.designsystem.component.PermissionBox +import com.mifos.core.designsystem.icon.MifosIcons import com.mifos.core.designsystem.theme.BluePrimary import com.mifos.core.designsystem.theme.BluePrimaryDark import com.mifos.core.designsystem.theme.BlueSecondary @@ -169,6 +170,8 @@ internal fun CreateNewClientScreen( MifosScaffold( title = stringResource(id = R.string.feature_client_create_new_client), snackbarHostState = snackbarHostState, + icon = MifosIcons.arrowBack, + onBackPressed = navigateBack, ) { paddingValues -> Column(modifier = Modifier.padding(paddingValues)) { when (uiState) { @@ -331,7 +334,11 @@ private fun CreateNewClientContent( } LaunchedEffect(key1 = staffInOffices) { if (staffInOffices.isEmpty()) { - Toast.makeText(context, context.resources.getString(R.string.feature_client_no_staff_associated_with_office), Toast.LENGTH_SHORT).show() + Toast.makeText( + context, + context.resources.getString(R.string.feature_client_no_staff_associated_with_office), + Toast.LENGTH_SHORT, + ).show() staff = "" selectedStaffId = 0 } @@ -614,11 +621,13 @@ private fun CreateNewClientContent( } } } + data class Name( val firstName: String, val lastName: String, val middleName: String, ) + private fun handleSubmitClick( context: Context, clientNames: Name, @@ -640,7 +649,13 @@ private fun handleSubmitClick( mobileNumber: String, externalId: String, ) { - if (!isAllFieldsValid(context, clientNames.firstName, clientNames.middleName, clientNames.lastName)) { + if (!isAllFieldsValid( + context, + clientNames.firstName, + clientNames.middleName, + clientNames.lastName, + ) + ) { return } @@ -695,8 +710,10 @@ private fun createClientPayload( // Optional fields with default values clientPayload.active = isActive - clientPayload.activationDate = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format(activationDate) - clientPayload.dateOfBirth = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format(dateOfBirth) + clientPayload.activationDate = + SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format(activationDate) + clientPayload.dateOfBirth = + SimpleDateFormat("dd MMMM yyyy", Locale.getDefault()).format(dateOfBirth) // Optional fields if (middleName.isNotEmpty()) { @@ -1009,7 +1026,8 @@ private fun isMiddleNameValid(name: String, context: Context): Boolean { } } -private class CreateNewClientScreenPreviewProvider : PreviewParameterProvider { +private class CreateNewClientScreenPreviewProvider : + PreviewParameterProvider { override val values: Sequence get() = sequenceOf( CreateNewClientUiState.ShowClientTemplate( diff --git a/feature/client/src/main/res/values/strings.xml b/feature/client/src/main/res/values/strings.xml index ed4d5ad6613..15a4e37f8c3 100644 --- a/feature/client/src/main/res/values/strings.xml +++ b/feature/client/src/main/res/values/strings.xml @@ -10,7 +10,7 @@ --> Failed To Load Clients - Client + Client Details Failed to load Database Clients No more Clients Available diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/createNewGroup/CreateNewGroupScreen.kt b/feature/groups/src/main/java/com/mifos/feature/groups/createNewGroup/CreateNewGroupScreen.kt index 5a71c68ae20..eb79a2d6570 100644 --- a/feature/groups/src/main/java/com/mifos/feature/groups/createNewGroup/CreateNewGroupScreen.kt +++ b/feature/groups/src/main/java/com/mifos/feature/groups/createNewGroup/CreateNewGroupScreen.kt @@ -69,6 +69,7 @@ import com.mifos.core.designsystem.component.MifosOutlinedTextField import com.mifos.core.designsystem.component.MifosScaffold import com.mifos.core.designsystem.component.MifosSweetError import com.mifos.core.designsystem.component.MifosTextFieldDropdown +import com.mifos.core.designsystem.icon.MifosIcons import com.mifos.core.designsystem.theme.BluePrimary import com.mifos.core.designsystem.theme.BluePrimaryDark import com.mifos.core.objects.group.GroupPayload @@ -84,8 +85,10 @@ import java.util.Locale @Composable internal fun CreateNewGroupScreen( - viewModel: CreateNewGroupViewModel = hiltViewModel(), onGroupCreated: (group: SaveResponse?, userStatus: Boolean) -> Unit, + modifier: Modifier = Modifier, + viewModel: CreateNewGroupViewModel = hiltViewModel(), + navigateBack: () -> Unit, ) { val uiState by viewModel.createNewGroupUiState.collectAsStateWithLifecycle() @@ -94,6 +97,7 @@ internal fun CreateNewGroupScreen( } CreateNewGroupScreen( + modifier = modifier, uiState = uiState, onRetry = { viewModel.loadOffices() }, invokeGroupCreation = { groupPayload -> @@ -101,6 +105,7 @@ internal fun CreateNewGroupScreen( }, onGroupCreated = { onGroupCreated(it, viewModel.getUserStatus()) }, getResponse = { viewModel.getResponse() }, + navigateBack = navigateBack, ) } @@ -110,8 +115,9 @@ internal fun CreateNewGroupScreen( onRetry: () -> Unit, invokeGroupCreation: (GroupPayload) -> Unit, onGroupCreated: (group: SaveResponse?) -> Unit, - modifier: Modifier = Modifier, getResponse: () -> String, + modifier: Modifier = Modifier, + navigateBack: () -> Unit, ) { val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } @@ -120,6 +126,8 @@ internal fun CreateNewGroupScreen( modifier = modifier, title = stringResource(id = R.string.feature_groups_create_new_group), snackbarHostState = snackbarHostState, + icon = MifosIcons.arrowBack, + onBackPressed = navigateBack, ) { paddingValues -> Box( modifier = Modifier @@ -392,7 +400,11 @@ private fun CreateNewGroupContent( } } -private fun validateFields(groupName: String, officeName: String, context: Context): Boolean { +private fun validateFields( + groupName: String, + officeName: String, + context: Context, +): Boolean { return when { groupName.isEmpty() -> { Toast.makeText( @@ -434,7 +446,8 @@ private fun validateFields(groupName: String, officeName: String, context: Conte } } -private class CreateNewGroupScreenPreviewProvider : PreviewParameterProvider { +private class CreateNewGroupScreenPreviewProvider : + PreviewParameterProvider { override val values: Sequence get() = sequenceOf( CreateNewGroupUiState.ShowProgressbar, @@ -447,7 +460,8 @@ private class CreateNewGroupScreenPreviewProvider : PreviewParameterProvider }, getResponse = { "" }, + navigateBack = {}, ) } diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/createNewGroup/CreateNewGroupViewModel.kt b/feature/groups/src/main/java/com/mifos/feature/groups/createNewGroup/CreateNewGroupViewModel.kt index 3a25f0e319b..d88187e4772 100644 --- a/feature/groups/src/main/java/com/mifos/feature/groups/createNewGroup/CreateNewGroupViewModel.kt +++ b/feature/groups/src/main/java/com/mifos/feature/groups/createNewGroup/CreateNewGroupViewModel.kt @@ -16,9 +16,12 @@ import com.mifos.core.datastore.PrefManager import com.mifos.core.domain.useCases.CreateNewGroupUseCase import com.mifos.core.domain.useCases.GetGroupOfficesUseCase import com.mifos.core.objects.group.GroupPayload +import com.mifos.feature.groups.createNewGroup.CreateNewGroupUiState.ShowFetchingError +import com.mifos.feature.groups.createNewGroup.CreateNewGroupUiState.ShowGroupCreatedSuccessfully +import com.mifos.feature.groups.createNewGroup.CreateNewGroupUiState.ShowOffices import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -35,8 +38,7 @@ class CreateNewGroupViewModel @Inject constructor( private val _createNewGroupUiState = MutableStateFlow( CreateNewGroupUiState.ShowProgressbar, ) - val createNewGroupUiState: StateFlow - get() = _createNewGroupUiState + val createNewGroupUiState = _createNewGroupUiState.asStateFlow() fun getUserStatus() = prefManager.userStatus @@ -50,17 +52,17 @@ class CreateNewGroupViewModel @Inject constructor( fun loadOffices() = viewModelScope.launch { getGroupOfficesUseCase().collect { result -> when (result) { - is Resource.Loading -> - _createNewGroupUiState.value = - CreateNewGroupUiState.ShowProgressbar + is Resource.Loading -> { + _createNewGroupUiState.value = CreateNewGroupUiState.ShowProgressbar + } - is Resource.Error -> - _createNewGroupUiState.value = - CreateNewGroupUiState.ShowFetchingError(result.message.toString()) + is Resource.Error -> { + _createNewGroupUiState.value = ShowFetchingError(result.message.toString()) + } - is Resource.Success -> - _createNewGroupUiState.value = - CreateNewGroupUiState.ShowOffices(result.data ?: emptyList()) + is Resource.Success -> { + _createNewGroupUiState.value = ShowOffices(result.data ?: emptyList()) + } } } } @@ -68,17 +70,19 @@ class CreateNewGroupViewModel @Inject constructor( fun createGroup(groupPayload: GroupPayload) = viewModelScope.launch { createNewGroupUseCase(groupPayload).collect { result -> when (result) { - is Resource.Error -> - _createNewGroupUiState.value = - CreateNewGroupUiState.ShowFetchingError(result.message.toString()) + is Resource.Error -> { + _createNewGroupUiState.value = ShowFetchingError(result.message.toString()) + } - is Resource.Loading -> - _createNewGroupUiState.value = - CreateNewGroupUiState.ShowProgressbar + is Resource.Loading -> { + _createNewGroupUiState.value = CreateNewGroupUiState.ShowProgressbar + } - is Resource.Success -> - _createNewGroupUiState.value = - result.data?.let { CreateNewGroupUiState.ShowGroupCreatedSuccessfully(it) }!! + is Resource.Success -> { + _createNewGroupUiState.value = result.data?.let { + ShowGroupCreatedSuccessfully(it) + }!! + } } } } diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/groupList/GroupsListScreen.kt b/feature/groups/src/main/java/com/mifos/feature/groups/groupList/GroupsListScreen.kt index 13056900b70..40fa9ab9200 100644 --- a/feature/groups/src/main/java/com/mifos/feature/groups/groupList/GroupsListScreen.kt +++ b/feature/groups/src/main/java/com/mifos/feature/groups/groupList/GroupsListScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Canvas import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement @@ -22,23 +23,25 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row 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.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowRight import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.DoneAll import androidx.compose.material3.CardDefaults import androidx.compose.material3.FabPosition import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -72,10 +75,10 @@ import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.mifos.core.designsystem.component.MifosCircularProgress import com.mifos.core.designsystem.component.MifosPaginationSweetError import com.mifos.core.designsystem.component.MifosPagingAppendProgress +import com.mifos.core.designsystem.component.MifosScaffold import com.mifos.core.designsystem.component.MifosSweetError import com.mifos.core.designsystem.icon.MifosIcons import com.mifos.core.designsystem.theme.BluePrimary -import com.mifos.core.designsystem.theme.BlueSecondary import com.mifos.core.designsystem.theme.DarkGray import com.mifos.core.objects.group.Group import com.mifos.core.testing.repository.sampleGroups @@ -161,7 +164,7 @@ fun GroupsListScreen( ) } - Scaffold( + MifosScaffold( modifier = modifier, floatingActionButton = { MifosFAB(icon = Icons.Default.Add, onClick = onAddGroupClick) @@ -210,7 +213,13 @@ fun GroupsListScreen( .fillMaxSize() .padding(paddingValues), state = lazyListState, - verticalArrangement = if (data.itemCount < 1) Arrangement.Center else Arrangement.Top, + contentPadding = PaddingValues(12.dp), + verticalArrangement = if (data.itemCount < 1) { + Arrangement.Center + } else { + Arrangement.spacedBy(12.dp, Alignment.Top) + }, + horizontalAlignment = Alignment.CenterHorizontally, ) { refreshState(data) @@ -300,6 +309,7 @@ private fun LazyListScope.successState( ) { items( count = pagingItems.itemCount, + key = { index -> pagingItems[index]?.id ?: index }, ) { index -> pagingItems[index]?.let { group -> GroupItem( @@ -332,15 +342,17 @@ private fun GroupItem( } else { CardDefaults.outlinedCardBorder() } - val containerColor = if (doesSelected) BlueSecondary else Color.Unspecified + val containerColor = if (doesSelected) { + MaterialTheme.colorScheme.secondaryContainer + } else { + Color.Unspecified + } group.name?.let { OutlinedCard( modifier = modifier .testTag(it) .fillMaxWidth() - .padding(8.dp) - .height(70.dp) .clip(RoundedCornerShape(8.dp)) .combinedClickable( onClick = { @@ -358,22 +370,45 @@ private fun GroupItem( ), border = borderStroke, ) { - Row( - modifier = Modifier - .fillMaxSize() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = it, - style = MaterialTheme.typography.titleMedium, - ) + ListItem( + leadingContent = { + Canvas( + modifier = Modifier.size(16.dp), + onDraw = { + drawCircle( + color = if (group.active == true) Color.Green else Color.Red, + ) + }, + ) + }, + headlineContent = { + Text(text = it) + }, + supportingContent = group.accountNo?.let { + { Text(text = it) } + }, + overlineContent = group.officeName?.let { + { Text(text = it) } + }, + trailingContent = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + if (group.sync == true) { + Icon(imageVector = Icons.Default.DoneAll, contentDescription = "Sync") + } - if (group.sync) { - Icon(imageVector = Icons.Default.DoneAll, contentDescription = "Sync") - } - } + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowRight, + contentDescription = null, + ) + } + }, + colors = ListItemDefaults.colors( + containerColor = Color.Unspecified, + ), + ) } } } diff --git a/feature/groups/src/main/java/com/mifos/feature/groups/navigation/GroupNavGraph.kt b/feature/groups/src/main/java/com/mifos/feature/groups/navigation/GroupNavGraph.kt index 4b21e97ccb8..48e781e8105 100644 --- a/feature/groups/src/main/java/com/mifos/feature/groups/navigation/GroupNavGraph.kt +++ b/feature/groups/src/main/java/com/mifos/feature/groups/navigation/GroupNavGraph.kt @@ -71,6 +71,7 @@ fun NavGraphBuilder.groupNavGraph( groups?.groupId?.let { navController.navigateToGroupDetailsScreen(it) } } }, + navigateBack = navController::popBackStack, ) } } @@ -124,10 +125,12 @@ fun NavGraphBuilder.groupDetailsRoute( fun NavGraphBuilder.addNewGroupRoute( onGroupCreated: (group: SaveResponse?, userStatus: Boolean) -> Unit, + navigateBack: () -> Unit, ) { composable(route = GroupScreen.CreateNewGroupScreen.route) { CreateNewGroupScreen( onGroupCreated = onGroupCreated, + navigateBack = navigateBack, ) } } diff --git a/feature/groups/src/main/res/values/strings.xml b/feature/groups/src/main/res/values/strings.xml index 850534ca91b..db7f2401182 100644 --- a/feature/groups/src/main/res/values/strings.xml +++ b/feature/groups/src/main/res/values/strings.xml @@ -32,7 +32,7 @@ Failed to Load Client Activate Group Sync - Group + Group Details Name Not connected to Network Office* diff --git a/feature/path-tracking/src/main/java/com/mifos/feature/pathTracking/PathTrackingViewModel.kt b/feature/path-tracking/src/main/java/com/mifos/feature/pathTracking/PathTrackingViewModel.kt index 5462a978385..567bd21033b 100644 --- a/feature/path-tracking/src/main/java/com/mifos/feature/pathTracking/PathTrackingViewModel.kt +++ b/feature/path-tracking/src/main/java/com/mifos/feature/pathTracking/PathTrackingViewModel.kt @@ -16,7 +16,6 @@ import com.mifos.core.datastore.PrefManager import com.mifos.core.domain.useCases.GetUserPathTrackingUseCase import com.mifos.feature.path.tracking.R import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -28,6 +27,8 @@ class PathTrackingViewModel @Inject constructor( private val prefManager: PrefManager, ) : ViewModel() { + private val userId = prefManager.user?.userId?.toInt() ?: 0 + private val _pathTrackingUiState = MutableStateFlow(PathTrackingUiState.Loading) val pathTrackingUiState = _pathTrackingUiState.asStateFlow() @@ -44,8 +45,8 @@ class PathTrackingViewModel @Inject constructor( _isRefreshing.value = false } - fun loadPathTracking() = viewModelScope.launch(Dispatchers.IO) { - getUserPathTrackingUseCase(prefManager.getUserId()).collect { result -> + fun loadPathTracking() = viewModelScope.launch { + getUserPathTrackingUseCase(userId).collect { result -> when (result) { is Resource.Error -> _pathTrackingUiState.value = @@ -68,8 +69,8 @@ class PathTrackingViewModel @Inject constructor( } } - fun updateUserStatus(status: Boolean) = viewModelScope.launch(Dispatchers.IO) { - prefManager.userStatus = status + fun updateUserStatus(status: Boolean) = viewModelScope.launch { + prefManager.updateUserStatus(status) _userStatus.value = status } } diff --git a/feature/savings/src/main/java/com/mifos/feature/savings/savingsAccountSummary/SavingsAccountSummaryScreen.kt b/feature/savings/src/main/java/com/mifos/feature/savings/savingsAccountSummary/SavingsAccountSummaryScreen.kt index fbfc9168cec..4b1b1f9c913 100644 --- a/feature/savings/src/main/java/com/mifos/feature/savings/savingsAccountSummary/SavingsAccountSummaryScreen.kt +++ b/feature/savings/src/main/java/com/mifos/feature/savings/savingsAccountSummary/SavingsAccountSummaryScreen.kt @@ -151,7 +151,6 @@ internal fun SavingsAccountSummaryScreen( onBackPressed = navigateBack, title = stringResource(id = R.string.feature_savings_savingsAccountSummary), icon = MifosIcons.arrowBack, - fontsizeInSp = 22, actions = { IconButton(onClick = { showDropdown = !showDropdown }) { Icon(imageVector = MifosIcons.moreVert, contentDescription = "") diff --git a/feature/savings/src/main/java/com/mifos/feature/savings/savingsAccountTransaction/SavingsAccountTransactionViewModel.kt b/feature/savings/src/main/java/com/mifos/feature/savings/savingsAccountTransaction/SavingsAccountTransactionViewModel.kt index c3aa6746586..7ccb8118051 100644 --- a/feature/savings/src/main/java/com/mifos/feature/savings/savingsAccountTransaction/SavingsAccountTransactionViewModel.kt +++ b/feature/savings/src/main/java/com/mifos/feature/savings/savingsAccountTransaction/SavingsAccountTransactionViewModel.kt @@ -13,7 +13,6 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.gson.Gson -import com.mifos.core.common.utils.Constants import com.mifos.core.common.utils.Resource import com.mifos.core.datastore.PrefManager import com.mifos.core.domain.useCases.GetSavingsAccountTransactionTemplateUseCase @@ -56,7 +55,7 @@ class SavingsAccountTransactionViewModel @Inject constructor( val savingsAccountTransactionUiState: StateFlow get() = _savingsAccountTransactionUiState fun setUserOffline() { - prefManager.userStatus = Constants.USER_OFFLINE + prefManager.updateUserStatus(true) } fun loadSavingAccountTemplate() = diff --git a/feature/search/src/main/java/com/mifos/feature/search/SearchScreen.kt b/feature/search/src/main/java/com/mifos/feature/search/SearchScreen.kt index 560edecc227..9e403b7b00e 100644 --- a/feature/search/src/main/java/com/mifos/feature/search/SearchScreen.kt +++ b/feature/search/src/main/java/com/mifos/feature/search/SearchScreen.kt @@ -67,7 +67,9 @@ internal fun SearchScreenContent( modifier: Modifier = Modifier, ) { val snackbarHostState = remember { SnackbarHostState() } - var fabButtonState by remember { mutableStateOf(FabButtonState.Collapsed) } + var fabButtonState by remember { + mutableStateOf(FabButtonState.Collapsed) + } MifosScaffold( modifier = modifier, diff --git a/feature/settings/src/main/java/com/mifos/feature/settings/navigation/SettingsNavigation.kt b/feature/settings/src/main/java/com/mifos/feature/settings/navigation/SettingsNavigation.kt index 71421f6157f..7abcedbf00c 100644 --- a/feature/settings/src/main/java/com/mifos/feature/settings/navigation/SettingsNavigation.kt +++ b/feature/settings/src/main/java/com/mifos/feature/settings/navigation/SettingsNavigation.kt @@ -9,27 +9,45 @@ */ package com.mifos.feature.settings.navigation +import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import com.mifos.feature.settings.settings.SettingsScreen +import com.mifos.feature.settings.updateServer.UpdateServerConfigScreenRoute /** * Created by Pronay Sarker on 10/08/2024 (7:52 AM) */ fun NavGraphBuilder.settingsScreen( navigateBack: () -> Unit, - navigateToLoginScreen: () -> Unit, - changePasscode: (String) -> Unit, - languageChanged: () -> Unit, + changePasscode: () -> Unit, + onUpdateConfig: () -> Unit, + onClickUpdateConfig: () -> Unit, ) { composable( route = SettingsScreens.SettingsScreen.route, ) { SettingsScreen( onBackPressed = navigateBack, - navigateToLoginScreen = navigateToLoginScreen, - languageChanged = languageChanged, changePasscode = changePasscode, + onClickUpdateConfig = onClickUpdateConfig, ) } + + composable( + route = SettingsScreens.ChangeServerConfig.route, + ) { + UpdateServerConfigScreenRoute( + onBackClick = navigateBack, + onSuccessful = onUpdateConfig, + ) + } +} + +fun NavController.navigateToSettingsScreen() { + navigate(SettingsScreens.SettingsScreen.route) +} + +fun NavController.navigateToUpdateServerConfig() { + navigate(SettingsScreens.ChangeServerConfig.route) } diff --git a/feature/settings/src/main/java/com/mifos/feature/settings/navigation/SettingsScreens.kt b/feature/settings/src/main/java/com/mifos/feature/settings/navigation/SettingsScreens.kt index b94420fdd43..6a6ef1ddae5 100644 --- a/feature/settings/src/main/java/com/mifos/feature/settings/navigation/SettingsScreens.kt +++ b/feature/settings/src/main/java/com/mifos/feature/settings/navigation/SettingsScreens.kt @@ -14,4 +14,5 @@ package com.mifos.feature.settings.navigation */ sealed class SettingsScreens(val route: String) { data object SettingsScreen : SettingsScreens("settings_screen") + data object ChangeServerConfig : SettingsScreens("change_server_config") } diff --git a/feature/settings/src/main/java/com/mifos/feature/settings/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/mifos/feature/settings/settings/SettingsScreen.kt index a29377970fa..94aa97f3e00 100644 --- a/feature/settings/src/main/java/com/mifos/feature/settings/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/mifos/feature/settings/settings/SettingsScreen.kt @@ -9,29 +9,28 @@ */ package com.mifos.feature.settings.settings -import android.content.ComponentName import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.os.CountDownTimer -import android.widget.Toast +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.RadioButton import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -45,85 +44,67 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.mifos.core.common.enums.MifosAppLanguage import com.mifos.core.common.utils.LanguageHelper import com.mifos.core.designsystem.component.MifosRadioButtonDialog import com.mifos.core.designsystem.component.MifosScaffold -import com.mifos.core.designsystem.component.UpdateEndpointDialogScreen import com.mifos.core.designsystem.icon.MifosIcons +import com.mifos.core.model.DarkThemeConfig +import com.mifos.core.model.MifosAppLanguage +import com.mifos.core.model.ThemeBrand +import com.mifos.core.model.UserData import com.mifos.feature.settings.R import com.mifos.feature.settings.syncSurvey.SyncSurveysDialog -import com.mifos.feature.settings.updateServer.UpdateServerConfigScreenRoute import java.util.Locale @Composable internal fun SettingsScreen( onBackPressed: () -> Unit, - navigateToLoginScreen: () -> Unit, - changePasscode: (String) -> Unit, - languageChanged: () -> Unit, + changePasscode: () -> Unit, + onClickUpdateConfig: () -> Unit, viewModel: SettingsViewModel = hiltViewModel(), ) { - val baseURL by viewModel.baseUrl.collectAsStateWithLifecycle() - val tenant by viewModel.tenant.collectAsStateWithLifecycle() - val passcode by viewModel.passcode.collectAsStateWithLifecycle() - val theme by viewModel.theme.collectAsStateWithLifecycle() - val language by viewModel.language.collectAsStateWithLifecycle() val context = LocalContext.current + val userData by viewModel.userData.collectAsStateWithLifecycle() SettingsScreen( + userData = userData, onBackPressed = onBackPressed, - selectedLanguage = language ?: "System Language", - selectedTheme = theme ?: "System Theme", - baseURL = baseURL ?: "", - tenant = tenant ?: "", - changePasscode = { changePasscode(passcode ?: "") }, - handleEndpointUpdate = { baseURL, tenant -> - if (viewModel.tryUpdatingEndpoint(selectedBaseUrl = baseURL, selectedTenant = tenant)) { - navigateToLoginScreen() - } - }, - updateTheme = { - viewModel.updateTheme(it) - }, - updateLanguage = { - val isSystemLanguage = viewModel.updateLanguage(it.code) + updateTheme = viewModel::changeThemeBrand, + updateThemeConfig = viewModel::changeDarkThemeConfig, + updateLanguage = { language -> + viewModel.changeLanguage(language) updateLanguageLocale( context = context, - language = it.code, - isSystemLanguage = isSystemLanguage, + language = language.code, + isSystemLanguage = language == MifosAppLanguage.SYSTEM_LANGUAGE, ) - languageChanged() }, + changePasscode = changePasscode, + onClickUpdateConfig = onClickUpdateConfig, ) } @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun SettingsScreen( - onBackPressed: () -> Unit, - selectedLanguage: String, - selectedTheme: String, - baseURL: String, - tenant: String, - changePasscode: () -> Unit, - handleEndpointUpdate: (baseURL: String, tenant: String) -> Unit, - updateTheme: (theme: AppTheme) -> Unit, + userData: UserData, + updateTheme: (theme: ThemeBrand) -> Unit, + updateThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit, updateLanguage: (language: MifosAppLanguage) -> Unit, + changePasscode: () -> Unit, + onBackPressed: () -> Unit, + onClickUpdateConfig: () -> Unit, ) { val snackbarHostState = remember { SnackbarHostState() } var showLanguageUpdateDialog by rememberSaveable { mutableStateOf(false) } - var showEndpointUpdateDialog by rememberSaveable { mutableStateOf(false) } var showThemeUpdateDialog by rememberSaveable { mutableStateOf(false) } var showSyncSurveyDialog by rememberSaveable { mutableStateOf(false) } - var showServerConfig by rememberSaveable { mutableStateOf(false) } - - val sheetState = rememberModalBottomSheetState() - val context = LocalContext.current MifosScaffold( icon = MifosIcons.arrowBack, @@ -145,9 +126,7 @@ internal fun SettingsScreen( SettingsCardItem.PASSCODE -> changePasscode() - SettingsCardItem.ENDPOINT -> showEndpointUpdateDialog = true - - SettingsCardItem.SERVER_CONFIG -> showServerConfig = true + SettingsCardItem.SERVER_CONFIG -> onClickUpdateConfig() } }, ) @@ -162,47 +141,25 @@ internal fun SettingsScreen( ) } - if (showServerConfig) { - ModalBottomSheet( - onDismissRequest = { showServerConfig = false }, - sheetState = sheetState, - ) { - UpdateServerConfigScreenRoute( - onCloseClick = { showServerConfig = false }, - onSuccessful = { - showServerConfig = false - showRestartCountdownToast(context, 2) - }, - ) - } - } - if (showLanguageUpdateDialog) { MifosRadioButtonDialog( titleResId = R.string.feature_settings_choose_language, items = stringArrayResource(R.array.feature_settings_languages), selectItem = { _, index -> updateLanguage(MifosAppLanguage.entries[index]) }, onDismissRequest = { showLanguageUpdateDialog = false }, - selectedItem = MifosAppLanguage.fromCode(selectedLanguage).displayName, + selectedItem = userData.language.displayName, ) } if (showThemeUpdateDialog) { - MifosRadioButtonDialog( - titleResId = R.string.feature_settings_change_app_theme, - items = AppTheme.entries.map { it.themeName }.toTypedArray(), - selectItem = { _, index -> updateTheme(AppTheme.entries[index]) }, + ThemeDialog( + selectedTheme = userData.themeBrand, + darkThemeConfig = userData.darkThemeConfig, + onThemeSelected = { theme -> updateTheme(theme) }, + onDarkThemeConfigSelected = { darkThemeConfig -> + updateThemeConfig(darkThemeConfig) + }, onDismissRequest = { showThemeUpdateDialog = false }, - selectedItem = selectedTheme, - ) - } - - if (showEndpointUpdateDialog) { - UpdateEndpointDialogScreen( - initialBaseURL = baseURL, - initialTenant = tenant, - onDismissRequest = { showEndpointUpdateDialog = false }, - handleEndpointUpdate = handleEndpointUpdate, ) } } @@ -258,6 +215,7 @@ private fun SettingsCardItem( Text( text = stringResource(id = title), style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, ) Text( modifier = Modifier.padding(end = 16.dp), @@ -270,7 +228,11 @@ private fun SettingsCardItem( } } -private fun updateLanguageLocale(context: Context, language: String, isSystemLanguage: Boolean) { +private fun updateLanguageLocale( + context: Context, + language: String, + isSystemLanguage: Boolean, +) { if (isSystemLanguage) { LanguageHelper.setLocale(context, language) } else { @@ -283,45 +245,98 @@ private fun updateLanguageLocale(context: Context, language: String, isSystemLan } } -private fun showRestartCountdownToast(context: Context, seconds: Int) { - val countDownTimer = object : CountDownTimer((seconds * 1000).toLong(), 1000) { - override fun onTick(millisUntilFinished: Long) { - val secondsRemaining = millisUntilFinished / 1000 - Toast.makeText( - context, - "Restarting app in $secondsRemaining seconds", - Toast.LENGTH_SHORT, - ).show() - } +@Composable +private fun ThemeDialog( + selectedTheme: ThemeBrand, + darkThemeConfig: DarkThemeConfig, + onThemeSelected: (ThemeBrand) -> Unit, + onDarkThemeConfigSelected: (DarkThemeConfig) -> Unit, + modifier: Modifier = Modifier, + onDismissRequest: () -> Unit, +) { + Dialog( + onDismissRequest = onDismissRequest, + ) { + Card(modifier = modifier) { + Column(modifier = Modifier.padding(20.dp)) { + Text(text = stringResource(id = R.string.feature_settings_change_app_theme)) + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 500.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + itemsIndexed( + items = ThemeBrand.entries, + ) { index, item -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable { + onDismissRequest.invoke() + onThemeSelected(item) + } + .fillMaxWidth(), + ) { + RadioButton( + selected = (item == selectedTheme), + onClick = { + onDismissRequest.invoke() + onThemeSelected(item) + }, + ) + Text( + text = item.name, + modifier = Modifier.padding(start = 4.dp), + ) + } + } - override fun onFinish() { - context.restartApplication() + item { + HorizontalDivider() + } + + itemsIndexed( + items = DarkThemeConfig.entries, + ) { index, item -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable { + onDismissRequest.invoke() + onDarkThemeConfigSelected(item) + } + .fillMaxWidth(), + ) { + RadioButton( + selected = (item == darkThemeConfig), + onClick = { + onDismissRequest.invoke() + onDarkThemeConfigSelected(item) + }, + ) + Text( + text = item.name, + modifier = Modifier.padding(start = 4.dp), + ) + } + } + } + } } } - countDownTimer.start() -} - -private fun Context.restartApplication() { - val packageManager: PackageManager = this.packageManager - val intent: Intent = packageManager.getLaunchIntentForPackage(this.packageName)!! - val componentName: ComponentName = intent.component!! - val restartIntent: Intent = Intent.makeRestartActivityTask(componentName) - this.startActivity(restartIntent) - Runtime.getRuntime().exit(0) } @Composable @Preview(showSystemUi = true, showBackground = true) private fun PreviewSettingsScreen() { SettingsScreen( - onBackPressed = {}, - selectedLanguage = "", - selectedTheme = "", - baseURL = "", - tenant = "", - handleEndpointUpdate = { _, _ -> }, - updateLanguage = {}, + userData = UserData.DEFAULT, updateTheme = {}, + updateThemeConfig = {}, + updateLanguage = {}, changePasscode = {}, + onBackPressed = {}, + onClickUpdateConfig = {}, ) } diff --git a/feature/settings/src/main/java/com/mifos/feature/settings/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/mifos/feature/settings/settings/SettingsViewModel.kt index 4cbb02c6875..b76636b4fc9 100644 --- a/feature/settings/src/main/java/com/mifos/feature/settings/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/java/com/mifos/feature/settings/settings/SettingsViewModel.kt @@ -9,18 +9,18 @@ */ package com.mifos.feature.settings.settings -import androidx.appcompat.app.AppCompatDelegate import androidx.compose.ui.graphics.vector.ImageVector import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.mifos.core.common.enums.MifosAppLanguage -import com.mifos.core.common.utils.Constants import com.mifos.core.datastore.PrefManager import com.mifos.core.designsystem.icon.MifosIcons +import com.mifos.core.model.DarkThemeConfig +import com.mifos.core.model.MifosAppLanguage +import com.mifos.core.model.ThemeBrand +import com.mifos.core.model.UserData import com.mifos.feature.settings.R import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @@ -28,46 +28,25 @@ import javax.inject.Inject class SettingsViewModel @Inject constructor( private val prefManager: PrefManager, ) : ViewModel() { + val userData = prefManager.userData.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = UserData.DEFAULT, + ) - val tenant: StateFlow = prefManager.getStringValue(Constants.TENANT) - .stateIn(viewModelScope, SharingStarted.Eagerly, null) - - val baseUrl: StateFlow = prefManager.getStringValue(Constants.BASE_URL) - .stateIn(viewModelScope, SharingStarted.Eagerly, null) - - val passcode: StateFlow = prefManager.getStringValue(Constants.PASSCODE) - .stateIn(viewModelScope, SharingStarted.Eagerly, null) - - val theme: StateFlow = prefManager.getStringValue(Constants.THEME) - .stateIn(viewModelScope, SharingStarted.Eagerly, "System Theme") - - val language: StateFlow = prefManager.getStringValue(Constants.LANGUAGE) - .stateIn(viewModelScope, SharingStarted.Eagerly, "System Language") - - fun updateTheme(theme: AppTheme) { - prefManager.setStringValue(Constants.THEME, theme.themeName) - AppCompatDelegate.setDefaultNightMode( - when (theme) { - AppTheme.DARK -> AppCompatDelegate.MODE_NIGHT_YES - AppTheme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO - else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM - }, - ) + fun changeThemeBrand(themeBrand: ThemeBrand) { + val updatedUserData = userData.value.copy(themeBrand = themeBrand) + prefManager.saveUserData(updatedUserData) } - fun updateLanguage(language: String): Boolean { - prefManager.setStringValue(Constants.LANGUAGE, language) - val isSystemLanguage = (language == MifosAppLanguage.SYSTEM_LANGUAGE.code) - return isSystemLanguage + fun changeLanguage(language: MifosAppLanguage) { + val updatedUserData = userData.value.copy(language = language) + prefManager.saveUserData(updatedUserData) } - fun tryUpdatingEndpoint(selectedBaseUrl: String, selectedTenant: String): Boolean { - val isEndpointUpdated = !(baseUrl.value == selectedBaseUrl && tenant.value == selectedTenant) - if (isEndpointUpdated) { - prefManager.setStringValue(Constants.BASE_URL, selectedBaseUrl) - prefManager.setStringValue(Constants.TENANT, selectedTenant) - } - return !(baseUrl.equals(selectedBaseUrl) && tenant.equals(selectedTenant)) + fun changeDarkThemeConfig(darkThemeConfig: DarkThemeConfig) { + val updatedUserData = userData.value.copy(darkThemeConfig = darkThemeConfig) + prefManager.saveUserData(updatedUserData) } } @@ -79,7 +58,7 @@ enum class SettingsCardItem( SYNC_SURVEY( title = R.string.feature_settings_sync_survey, details = R.string.feature_settings_sync_survey_desc, - icon = null, + icon = MifosIcons.sync, ), LANGUAGE( title = R.string.feature_settings_language, @@ -96,22 +75,9 @@ enum class SettingsCardItem( details = R.string.feature_settings_change_passcode_desc, icon = MifosIcons.password, ), - ENDPOINT( - title = R.string.feature_settings_instance_url, - details = R.string.feature_settings_instance_url_desc, - icon = null, - ), SERVER_CONFIG( title = R.string.feature_settings_server_config, details = R.string.feature_settings_server_config_desc, - icon = null, + icon = MifosIcons.Link, ), } - -enum class AppTheme( - val themeName: String, -) { - SYSTEM(themeName = "System Theme"), - LIGHT(themeName = "Light Theme"), - DARK(themeName = "Dark Theme"), -} diff --git a/feature/settings/src/main/java/com/mifos/feature/settings/syncSurvey/SyncSurveysDialogViewModel.kt b/feature/settings/src/main/java/com/mifos/feature/settings/syncSurvey/SyncSurveysDialogViewModel.kt index d1fa6a35559..cb16ad6eaf3 100644 --- a/feature/settings/src/main/java/com/mifos/feature/settings/syncSurvey/SyncSurveysDialogViewModel.kt +++ b/feature/settings/src/main/java/com/mifos/feature/settings/syncSurvey/SyncSurveysDialogViewModel.kt @@ -11,6 +11,7 @@ package com.mifos.feature.settings.syncSurvey import androidx.lifecycle.ViewModel import com.mifos.core.common.utils.NetworkUtilsWrapper +import com.mifos.core.data.repository.SyncSurveysDialogRepository import com.mifos.core.objects.survey.QuestionDatas import com.mifos.core.objects.survey.ResponseDatas import com.mifos.core.objects.survey.Survey diff --git a/feature/settings/src/main/java/com/mifos/feature/settings/updateServer/UpdateServerConfigScreen.kt b/feature/settings/src/main/java/com/mifos/feature/settings/updateServer/UpdateServerConfigScreen.kt index 814215837c0..c079adbf480 100644 --- a/feature/settings/src/main/java/com/mifos/feature/settings/updateServer/UpdateServerConfigScreen.kt +++ b/feature/settings/src/main/java/com/mifos/feature/settings/updateServer/UpdateServerConfigScreen.kt @@ -10,25 +10,20 @@ package com.mifos.feature.settings.updateServer import androidx.annotation.VisibleForTesting -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues 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.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AddLink -import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.Save @@ -36,8 +31,6 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ElevatedButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedIconButton -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -46,21 +39,21 @@ 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.font.FontWeight import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.mifos.core.designsystem.component.MifosOutlinedTextField +import com.mifos.core.designsystem.component.MifosScaffold +import com.mifos.core.designsystem.icon.MifosIcons import com.mifos.core.designsystem.theme.BluePrimary import com.mifos.core.model.ServerConfig import com.mifos.core.ui.util.DevicePreviews import com.mifos.feature.settings.R @Composable -internal fun UpdateServerConfigScreenRoute( - onCloseClick: () -> Unit, +fun UpdateServerConfigScreenRoute( + onBackClick: () -> Unit, onSuccessful: () -> Unit, modifier: Modifier = Modifier, viewModel: UpdateServerConfigViewModel = hiltViewModel(), @@ -88,7 +81,7 @@ internal fun UpdateServerConfigScreenRoute( portError = portError, tenantError = tenantError, onEvent = viewModel::onEvent, - onCloseClick = onCloseClick, + onBackClick = onBackClick, ) } @@ -103,7 +96,7 @@ internal fun UpdateServerConfigScreenContent( endPointError: Int? = null, portError: Int? = null, tenantError: Int? = null, - onCloseClick: () -> Unit, + onBackClick: () -> Unit, ) { val lazyListState = rememberLazyListState() val hasAnyError = listOf( @@ -113,194 +106,156 @@ internal fun UpdateServerConfigScreenContent( portError, tenantError, ).any { it != null } - Surface( + + MifosScaffold( modifier = modifier, - shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + title = stringResource(R.string.feature_settings_title), + icon = MifosIcons.arrowBack, + onBackPressed = onBackClick, + snackbarHostState = null, ) { - Column( + LazyColumn( modifier = Modifier - .fillMaxWidth() - .padding(bottom = 24.dp) - .imePadding(), - verticalArrangement = Arrangement.spacedBy(8.dp), + .fillMaxSize() + .padding(it), + contentPadding = PaddingValues(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + state = lazyListState, ) { - Box( - modifier = Modifier - .fillMaxWidth() - .background( - MaterialTheme.colorScheme.secondaryContainer, - ) - .padding(12.dp), - ) { - Row( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(R.string.feature_settings_title), - style = MaterialTheme.typography.headlineSmall, - fontWeight = FontWeight.SemiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - - Spacer(modifier = Modifier.width(8.dp)) + item { + MifosOutlinedTextField( + value = serverConfig.protocol, + label = stringResource(R.string.feature_settings_label_protocol), + leadingIcon = Icons.Default.AddLink, + isError = protocolError != null, + errorText = protocolError?.let { stringResource(it) }, + placeholder = stringResource(R.string.feature_settings_protocol_placeholder), + keyboardType = KeyboardType.Uri, + showClearIcon = serverConfig.protocol.isNotEmpty(), + onClickClearIcon = { + onEvent(UpdateServerConfigEvent.UpdateProtocol("")) + }, + onValueChange = { + onEvent(UpdateServerConfigEvent.UpdateProtocol(it)) + }, + ) + } - OutlinedIconButton( - onClick = onCloseClick, - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(R.string.feature_settings_close_bottomsheet), - ) - } - } + item { + MifosOutlinedTextField( + value = serverConfig.endPoint, + label = stringResource(R.string.feature_settings_label_endpoint), + leadingIcon = Icons.Default.Link, + isError = endPointError != null, + errorText = endPointError?.let { stringResource(it) }, + placeholder = stringResource(R.string.feature_settings_endpoint_placeholder), + showClearIcon = serverConfig.endPoint.isNotEmpty(), + onClickClearIcon = { + onEvent(UpdateServerConfigEvent.UpdateEndPoint("")) + }, + onValueChange = { + onEvent(UpdateServerConfigEvent.UpdateEndPoint(it)) + }, + ) } - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp), - contentPadding = PaddingValues(4.dp), - state = lazyListState, - ) { - item { - MifosOutlinedTextField( - value = serverConfig.protocol, - label = stringResource(R.string.feature_settings_label_protocol), - leadingIcon = Icons.Default.AddLink, - isError = protocolError != null, - errorText = protocolError?.let { stringResource(it) }, - placeholder = stringResource(R.string.feature_settings_protocol_placeholder), - keyboardType = KeyboardType.Uri, - showClearIcon = serverConfig.protocol.isNotEmpty(), - onClickClearIcon = { - onEvent(UpdateServerConfigEvent.UpdateProtocol("")) - }, - onValueChange = { - onEvent(UpdateServerConfigEvent.UpdateProtocol(it)) - }, - ) - } + item { + MifosOutlinedTextField( + value = serverConfig.apiPath, + label = stringResource(R.string.feature_settings_label_api_path), + leadingIcon = Icons.Default.Link, + isError = apiPathError != null, + errorText = apiPathError?.let { stringResource(it) }, + placeholder = stringResource(R.string.feature_settings_api_path_placeholder), + showClearIcon = serverConfig.endPoint.isNotEmpty(), + onClickClearIcon = { + onEvent(UpdateServerConfigEvent.UpdateEndPoint("")) + }, + onValueChange = { + onEvent(UpdateServerConfigEvent.UpdateApiPath(it)) + }, + ) + } - item { - MifosOutlinedTextField( - value = serverConfig.endPoint, - label = stringResource(R.string.feature_settings_label_endpoint), - leadingIcon = Icons.Default.Link, - isError = endPointError != null, - errorText = endPointError?.let { stringResource(it) }, - placeholder = stringResource(R.string.feature_settings_endpoint_placeholder), - showClearIcon = serverConfig.endPoint.isNotEmpty(), - onClickClearIcon = { - onEvent(UpdateServerConfigEvent.UpdateEndPoint("")) - }, - onValueChange = { - onEvent(UpdateServerConfigEvent.UpdateEndPoint(it)) - }, - ) - } + item { + MifosOutlinedTextField( + value = serverConfig.port, + label = stringResource(R.string.feature_settings_label_port), + leadingIcon = Icons.Default.Link, + isError = portError != null, + errorText = portError?.let { stringResource(it) }, + placeholder = stringResource(R.string.feature_settings_port_placeholder), + keyboardType = KeyboardType.Number, + showClearIcon = serverConfig.port.isNotEmpty(), + onClickClearIcon = { + onEvent(UpdateServerConfigEvent.UpdatePort("")) + }, + onValueChange = { + onEvent(UpdateServerConfigEvent.UpdatePort(it)) + }, + ) + } - item { - MifosOutlinedTextField( - value = serverConfig.apiPath, - label = stringResource(R.string.feature_settings_label_api_path), - leadingIcon = Icons.Default.Link, - isError = apiPathError != null, - errorText = apiPathError?.let { stringResource(it) }, - placeholder = stringResource(R.string.feature_settings_api_path_placeholder), - showClearIcon = serverConfig.endPoint.isNotEmpty(), - onClickClearIcon = { - onEvent(UpdateServerConfigEvent.UpdateEndPoint("")) - }, - onValueChange = { - onEvent(UpdateServerConfigEvent.UpdateApiPath(it)) - }, - ) - } + item { + MifosOutlinedTextField( + value = serverConfig.tenant, + label = stringResource(R.string.feature_settings_label_tenant), + leadingIcon = Icons.Default.Link, + isError = tenantError != null, + errorText = tenantError?.let { stringResource(it) }, + placeholder = stringResource(R.string.feature_settings_tenant_placeholder), + showClearIcon = serverConfig.tenant.isNotEmpty(), + onClickClearIcon = { + onEvent(UpdateServerConfigEvent.UpdateTenant("")) + }, + onValueChange = { + onEvent(UpdateServerConfigEvent.UpdateTenant(it)) + }, + ) + } - item { - MifosOutlinedTextField( - value = serverConfig.port, - label = stringResource(R.string.feature_settings_label_port), - leadingIcon = Icons.Default.Link, - isError = portError != null, - errorText = portError?.let { stringResource(it) }, - placeholder = stringResource(R.string.feature_settings_port_placeholder), - keyboardType = KeyboardType.Number, - showClearIcon = serverConfig.port.isNotEmpty(), - onClickClearIcon = { - onEvent(UpdateServerConfigEvent.UpdatePort("")) - }, - onValueChange = { - onEvent(UpdateServerConfigEvent.UpdatePort(it)) - }, + item { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start), + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = "infoIcon", + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(20.dp), ) - } - item { - MifosOutlinedTextField( - value = serverConfig.tenant, - label = stringResource(R.string.feature_settings_label_tenant), - leadingIcon = Icons.Default.Link, - isError = tenantError != null, - errorText = tenantError?.let { stringResource(it) }, - placeholder = stringResource(R.string.feature_settings_tenant_placeholder), - showClearIcon = serverConfig.tenant.isNotEmpty(), - onClickClearIcon = { - onEvent(UpdateServerConfigEvent.UpdateTenant("")) - }, - onValueChange = { - onEvent(UpdateServerConfigEvent.UpdateTenant(it)) - }, + Text( + text = stringResource(R.string.feature_settings_note_text), + style = MaterialTheme.typography.labelSmall, ) } - item { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start), - ) { - Icon( - imageVector = Icons.Default.Info, - contentDescription = "infoIcon", - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(20.dp), - ) - - Text( - text = stringResource(R.string.feature_settings_note_text), - style = MaterialTheme.typography.labelSmall, - ) - } - - Spacer(Modifier.height(8.dp)) - } + Spacer(Modifier.height(8.dp)) + } - item { - ElevatedButton( - onClick = { - onEvent(UpdateServerConfigEvent.UpdateServerConfig) - }, - modifier = Modifier - .fillMaxWidth() - .height(48.dp), - enabled = !hasAnyError, - colors = ButtonDefaults.elevatedButtonColors( - containerColor = BluePrimary, - contentColor = Color.White, - ), - ) { - Icon( - imageVector = Icons.Default.Save, - contentDescription = "updateConfig", - ) - Spacer(modifier = Modifier.width(4.dp)) - Text(stringResource(R.string.feature_settings_update_config_btn_text).uppercase()) - } + item { + ElevatedButton( + onClick = { + onEvent(UpdateServerConfigEvent.UpdateServerConfig) + }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + enabled = !hasAnyError, + colors = ButtonDefaults.elevatedButtonColors( + containerColor = BluePrimary, + contentColor = Color.White, + ), + ) { + Icon( + imageVector = Icons.Default.Save, + contentDescription = "updateConfig", + ) + Spacer(modifier = Modifier.width(4.dp)) + Text(stringResource(R.string.feature_settings_update_config_btn_text).uppercase()) } } } @@ -320,7 +275,7 @@ private fun UpdateServerConfigScreenEmptyData() { tenant = "", ), onEvent = {}, - onCloseClick = {}, + onBackClick = {}, ) } } diff --git a/feature/settings/src/main/java/com/mifos/feature/settings/updateServer/UpdateServerConfigViewModel.kt b/feature/settings/src/main/java/com/mifos/feature/settings/updateServer/UpdateServerConfigViewModel.kt index d649bb8b150..ecde83132ce 100644 --- a/feature/settings/src/main/java/com/mifos/feature/settings/updateServer/UpdateServerConfigViewModel.kt +++ b/feature/settings/src/main/java/com/mifos/feature/settings/updateServer/UpdateServerConfigViewModel.kt @@ -32,9 +32,13 @@ class UpdateServerConfigViewModel @Inject constructor( private val validator: ServerConfigValidatorUseCase, ) : ViewModel() { - private val serverConfig = prefManager.getServerConfig + private val serverConfig = prefManager.serverConfigFlow.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = ServerConfig.DEFAULT, + ) - private val _state = mutableStateOf(serverConfig) + private val _state = mutableStateOf(serverConfig.value) val state: State get() = _state private val _result = MutableSharedFlow() diff --git a/feature/splash/.gitignore b/feature/splash/.gitignore deleted file mode 100644 index 42afabfd2ab..00000000000 --- a/feature/splash/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/feature/splash/build.gradle.kts b/feature/splash/build.gradle.kts deleted file mode 100644 index d4a05dd1786..00000000000 --- a/feature/splash/build.gradle.kts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -plugins { - alias(libs.plugins.mifos.android.feature) - alias(libs.plugins.mifos.android.library.compose) - alias(libs.plugins.mifos.android.library.jacoco) -} - -android { - namespace = "com.mifos.feature.splash" -} - -dependencies { - implementation(projects.core.domain) - - //DBFlow dependencies - kapt(libs.dbflow.processor) - implementation(libs.dbflow) - kapt(libs.github.dbflow.processor) - testImplementation(libs.hilt.android.testing) - testImplementation(projects.core.testing) - - androidTestImplementation(projects.core.testing) - -} \ No newline at end of file diff --git a/feature/splash/proguard-rules.pro b/feature/splash/proguard-rules.pro deleted file mode 100644 index 481bb434814..00000000000 --- a/feature/splash/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/splash/src/androidTest/java/com/mifos/feature/splash/ExampleInstrumentedTest.kt b/feature/splash/src/androidTest/java/com/mifos/feature/splash/ExampleInstrumentedTest.kt deleted file mode 100644 index 9d9ec54fbac..00000000000 --- a/feature/splash/src/androidTest/java/com/mifos/feature/splash/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.feature.splash - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import junit.framework.TestCase.assertEquals -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.mifos.feature.splash.test", appContext.packageName) - } -} diff --git a/feature/splash/src/main/java/com/mifos/feature/splash/navigation/SplashNavigation.kt b/feature/splash/src/main/java/com/mifos/feature/splash/navigation/SplashNavigation.kt deleted file mode 100644 index e4f73ac38cd..00000000000 --- a/feature/splash/src/main/java/com/mifos/feature/splash/navigation/SplashNavigation.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.feature.splash.navigation - -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import androidx.navigation.navigation -import com.mifos.feature.splash.splash.SplashScreen - -fun NavGraphBuilder.splashNavGraph( - navigatePasscode: () -> Unit, - navigateLogin: () -> Unit, -) { - navigation( - startDestination = SplashScreens.SplashScreen.route, - route = SplashScreens.SplashScreenRoute.route, - ) { - splashScreenRoute( - navigateLogin = navigateLogin, - navigatePasscode = navigatePasscode, - ) - } -} - -fun NavGraphBuilder.splashScreenRoute( - navigatePasscode: () -> Unit, - navigateLogin: () -> Unit, -) { - composable( - route = SplashScreens.SplashScreen.route, - ) { - SplashScreen( - navigatePasscode = navigatePasscode, - navigateLogin = navigateLogin, - ) - } -} diff --git a/feature/splash/src/main/java/com/mifos/feature/splash/splash/SplashScreen.kt b/feature/splash/src/main/java/com/mifos/feature/splash/splash/SplashScreen.kt deleted file mode 100644 index a9ce41f1c16..00000000000 --- a/feature/splash/src/main/java/com/mifos/feature/splash/splash/SplashScreen.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.feature.splash.splash - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -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.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.mifos.core.designsystem.component.MifosScaffold -import com.mifos.core.designsystem.theme.SummerSky -import com.mifos.feature.splash.R - -@Composable -internal fun SplashScreen( - navigatePasscode: () -> Unit, - viewmodel: SplashScreenViewmodel = hiltViewModel(), - navigateLogin: () -> Unit, -) { - val state by viewmodel.isAuthenticated.collectAsStateWithLifecycle() - - SplashScreen( - state = state, - navigatePasscode = navigatePasscode, - navigateLogin = navigateLogin, - ) -} - -@Composable -internal fun SplashScreen( - state: Boolean?, - navigatePasscode: () -> Unit, - navigateLogin: () -> Unit, - modifier: Modifier = Modifier, -) { - when (state) { - false -> navigateLogin() - true -> navigatePasscode() - else -> {} - } - - MifosScaffold( - modifier = modifier, - containerColor = SummerSky, - ) { paddingValues -> - Column( - modifier = Modifier - .padding(paddingValues) - .fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image( - modifier = Modifier.size(100.dp), - painter = painterResource(id = R.drawable.feature_splash_icon), - contentDescription = null, - ) - } - } -} - -@Preview -@Composable -private fun SplashScreenPreview() { - SplashScreen( - state = false, - navigatePasscode = {}, - navigateLogin = {}, - ) -} diff --git a/feature/splash/src/main/java/com/mifos/feature/splash/splash/SplashScreenViewmodel.kt b/feature/splash/src/main/java/com/mifos/feature/splash/splash/SplashScreenViewmodel.kt deleted file mode 100644 index a4bacd67ad0..00000000000 --- a/feature/splash/src/main/java/com/mifos/feature/splash/splash/SplashScreenViewmodel.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.feature.splash.splash - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.mifos.core.datastore.PrefManager -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class SplashScreenViewmodel @Inject constructor( - private val prefManager: PrefManager, -) : ViewModel() { - - private val _isAuthenticated = MutableStateFlow(null) - val isAuthenticated = _isAuthenticated.asStateFlow() - - init { - checkAuthenticationStatus() - } - - private fun checkAuthenticationStatus() = viewModelScope.launch(Dispatchers.IO) { - delay(2000) - _isAuthenticated.value = prefManager.isAuthenticated() - } -} diff --git a/feature/splash/src/test/java/com/mifos/feature/splash/ExampleUnitTest.kt b/feature/splash/src/test/java/com/mifos/feature/splash/ExampleUnitTest.kt deleted file mode 100644 index d313d85f15a..00000000000 --- a/feature/splash/src/test/java/com/mifos/feature/splash/ExampleUnitTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.feature.splash - -import junit.framework.TestCase.assertEquals -import org.junit.Test - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d4145fb90b7..012444d1dd8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,8 +7,8 @@ androidDesugarJdkLibs = "2.0.4" androidIconifyMaterial = "2.2.2" androidJob = "1.2.6" androidMapsUtils = "0.4.2" -androidGradlePlugin = "8.5.0" -androidTools = "31.6.0" +androidGradlePlugin = "8.7.0" +androidTools = "31.7.0" androidxActivity = "1.8.2" androidxAppCompat = "1.6.1" androidxArchCore = '2.2.0' @@ -57,7 +57,7 @@ firebaseCrashlyticsPlugin = "2.9.9" firebasePerfPlugin = "1.4.2" fliptables = "1.0.1" glide = "4.15.1" -gmsPlugin = "4.4.1" +gmsPlugin = "4.4.2" googleOss = "17.0.1" googleOssPlugin = "0.10.6" hilt = "2.51" @@ -105,11 +105,11 @@ protobufPlugin = "0.9.4" playIntegrity = '1.3.0' playUpdate = "2.1.0" playScanner = "16.1.0" -playService = "18.3.0" +playService = "18.5.0" realmVersion = "1.13.0" recyclerview = "1.3.2" room = "2.6.1" -retrofit = "2.9.0" +retrofit = "2.11.0" rxandroidVersion = "1.1.0" rxjava = "1.3.8" rxjava3 = "3.1.8" # 1.3.8 @@ -255,6 +255,7 @@ retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "ret adapter-rxjava = { module = "com.squareup.retrofit2:adapter-rxjava", version.ref = "adapterRxjava" } converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" } converter-scalars = { module = "com.squareup.retrofit2:converter-scalars", version.ref = "converterScalars" } +converter-json = { module = "com.squareup.retrofit2:converter-kotlinx-serialization", version.ref = "retrofit" } # Coil Compose coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil-compose" } @@ -414,7 +415,7 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-kapt = { id = "kotlin-kapt", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -kotlin-parcelize = { id = "kotlin-parcelize", version.ref = "kotlin" } +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } realm = { id = "io.realm.kotlin", version.ref = "realmVersion" } diff --git a/default_key_store.jks b/keystores/release_keystore.keystore similarity index 100% rename from default_key_store.jks rename to keystores/release_keystore.keystore diff --git a/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/PassCodeScreen.kt b/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/PassCodeScreen.kt index 099fddf2da3..4a08a2561bc 100644 --- a/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/PassCodeScreen.kt +++ b/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/PassCodeScreen.kt @@ -55,7 +55,6 @@ import org.mifos.library.passcode.component.PasscodeSkipButton import org.mifos.library.passcode.component.PasscodeToolbar import org.mifos.library.passcode.theme.blueTint import org.mifos.library.passcode.utility.Constants.PASSCODE_LENGTH -import org.mifos.library.passcode.utility.PreferenceManager import org.mifos.library.passcode.utility.ShakeAnimation.performShakeAnimation import org.mifos.library.passcode.utility.VibrationFeedback.vibrateFeedback import org.mifos.library.passcode.viewmodels.PasscodeViewModel @@ -70,7 +69,7 @@ internal fun PasscodeScreen( viewModel: PasscodeViewModel = hiltViewModel(), ) { val context = LocalContext.current - val preferenceManager = remember { PreferenceManager(context) } + val hasPasscode = viewModel.hasPasscode val activeStep by viewModel.activeStep.collectAsStateWithLifecycle() val filledDots by viewModel.filledDots.collectAsStateWithLifecycle() @@ -106,10 +105,13 @@ internal fun PasscodeScreen( .padding(paddingValues), horizontalAlignment = Alignment.CenterHorizontally, ) { - PasscodeToolbar(activeStep = activeStep, preferenceManager.hasPasscode) + PasscodeToolbar( + activeStep = activeStep, + hasPasscode, + ) PasscodeSkipButton( - hasPassCode = preferenceManager.hasPasscode, + hasPassCode = hasPasscode, onSkipButton = onSkipButton, ) @@ -123,7 +125,7 @@ internal fun PasscodeScreen( ) { PasscodeHeader( activeStep = activeStep, - isPasscodeAlreadySet = preferenceManager.hasPasscode, + isPasscodeAlreadySet = hasPasscode, ) PasscodeView( restart = { viewModel.restart() }, @@ -149,7 +151,7 @@ internal fun PasscodeScreen( Spacer(modifier = Modifier.height(8.dp)) PasscodeForgotButton( - hasPassCode = preferenceManager.hasPasscode, + hasPassCode = hasPasscode, onForgotButton = onForgotButton, ) } diff --git a/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/data/PasscodeManager.kt b/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/data/PasscodeManager.kt index 446f4ed56b7..c26c9c3459e 100644 --- a/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/data/PasscodeManager.kt +++ b/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/data/PasscodeManager.kt @@ -10,25 +10,34 @@ package org.mifos.library.passcode.data import android.content.Context +import androidx.core.content.edit +import com.mifos.library.passcode.R import dagger.hilt.android.qualifiers.ApplicationContext -import org.mifos.library.passcode.utility.PreferenceManager import javax.inject.Inject class PasscodeManager @Inject constructor( @ApplicationContext private val context: Context, ) { - private val passcodePreferencesHelper = PreferenceManager(context) - - val getPasscode = passcodePreferencesHelper.getSavedPasscode() - - val hasPasscode = passcodePreferencesHelper.hasPasscode + private val sharedPreference = context.getSharedPreferences( + R.string.lib_mifos_passcode_pref_name.toString(), + Context.MODE_PRIVATE, + ) fun savePasscode(passcode: String) { - passcodePreferencesHelper.savePasscode(passcode) + sharedPreference.edit { + putString(R.string.lib_mifos_passcode.toString(), passcode) + } + } + + fun getSavedPasscode(): String { + return sharedPreference.getString( + R.string.lib_mifos_passcode.toString(), + "", + ).toString() } fun clearPasscode() { - passcodePreferencesHelper.clearPasscode() + sharedPreference.edit { clear() } } } diff --git a/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/data/PasscodeRepositoryImpl.kt b/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/data/PasscodeRepositoryImpl.kt index 91d92809583..1bb0e4fbe63 100644 --- a/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/data/PasscodeRepositoryImpl.kt +++ b/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/data/PasscodeRepositoryImpl.kt @@ -9,15 +9,14 @@ */ package org.mifos.library.passcode.data -import org.mifos.library.passcode.utility.PreferenceManager import javax.inject.Inject class PasscodeRepositoryImpl @Inject constructor( - private val preferenceManager: PreferenceManager, + private val preferenceManager: PasscodeManager, ) : PasscodeRepository { override val hasPasscode: Boolean - get() = preferenceManager.hasPasscode + get() = preferenceManager.getSavedPasscode().isNotEmpty() override fun getSavedPasscode(): String { return preferenceManager.getSavedPasscode() diff --git a/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/di/ApplicationModule.kt b/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/di/ApplicationModule.kt index bec4e5edbed..7932dff67ee 100644 --- a/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/di/ApplicationModule.kt +++ b/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/di/ApplicationModule.kt @@ -15,9 +15,9 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import org.mifos.library.passcode.data.PasscodeManager import org.mifos.library.passcode.data.PasscodeRepository import org.mifos.library.passcode.data.PasscodeRepositoryImpl -import org.mifos.library.passcode.utility.PreferenceManager import javax.inject.Singleton @Module @@ -25,13 +25,13 @@ import javax.inject.Singleton object ApplicationModule { @Provides @Singleton - fun providePrefManager(@ApplicationContext context: Context): PreferenceManager { - return PreferenceManager(context) + fun providePasscodeManager(@ApplicationContext context: Context): PasscodeManager { + return PasscodeManager(context) } @Provides @Singleton - fun providesPasscodeRepository(preferenceManager: PreferenceManager): PasscodeRepository { + fun providesPasscodeRepository(preferenceManager: PasscodeManager): PasscodeRepository { return PasscodeRepositoryImpl(preferenceManager) } } diff --git a/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/utility/PreferenceManager.kt b/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/utility/PreferenceManager.kt deleted file mode 100644 index 3190b8d0fb2..00000000000 --- a/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/utility/PreferenceManager.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package org.mifos.library.passcode.utility - -import android.content.Context -import com.mifos.library.passcode.R - -class PreferenceManager(context: Context) { - private val sharedPreference = context.getSharedPreferences( - R.string.lib_mifos_passcode_pref_name.toString(), - Context.MODE_PRIVATE, - ) - - var hasPasscode: Boolean - get() = sharedPreference.getBoolean(R.string.lib_mifos_passcode_has_passcode.toString(), false) - set(value) = sharedPreference.edit().putBoolean(R.string.lib_mifos_passcode_has_passcode.toString(), value) - .apply() - - fun savePasscode(passcode: String) { - sharedPreference.edit().putString(R.string.lib_mifos_passcode.toString(), passcode).apply() - hasPasscode = true - } - - fun getSavedPasscode(): String { - return sharedPreference.getString(R.string.lib_mifos_passcode.toString(), "").toString() - } - - fun clearPasscode() { - sharedPreference.edit().clear().apply() - hasPasscode = false - } -} diff --git a/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt b/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt index 22bcb4f9df3..18ffe33fb5b 100644 --- a/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt +++ b/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt @@ -27,6 +27,7 @@ import javax.inject.Inject internal class PasscodeViewModel @Inject constructor( private val passcodeRepository: PasscodeRepository, ) : ViewModel() { + val hasPasscode: Boolean = passcodeRepository.hasPasscode private val mOnPasscodeConfirmed = MutableSharedFlow() val onPasscodeConfirmed = mOnPasscodeConfirmed.asSharedFlow() diff --git a/mifosng-android/build.gradle.kts b/mifosng-android/build.gradle.kts index 9344617c017..fac2a36dfb0 100644 --- a/mifosng-android/build.gradle.kts +++ b/mifosng-android/build.gradle.kts @@ -7,72 +7,60 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ -import org.gradle.api.tasks.testing.logging.TestLogEvent import org.mifos.MifosBuildType - -/* - * This project is licensed under the open source MPL V2. - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ +import org.mifos.dynamicVersion plugins { alias(libs.plugins.mifos.android.application) alias(libs.plugins.mifos.android.application.compose) alias(libs.plugins.mifos.android.application.flavors) -// alias(libs.plugins.mifos.android.application.jacoco) alias(libs.plugins.mifos.android.hilt) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.secrets) alias(libs.plugins.androidx.navigation) + alias(libs.plugins.gms) } android { namespace = "com.mifos.mifosxdroid" - compileSdk = 34 defaultConfig { applicationId = "com.mifos.mifosxdroid" - minSdk = 26 - targetSdk = 34 - versionCode = 6 - versionName = "1.0.1" - - multiDexEnabled = true - compileSdkPreview = "UpsideDownCake" - // A test runner provided by https://code.google.com/p/android-test-kit/ - testInstrumentationRunner = "com.mifos.core.testing.MifosTestRunner" + versionName = project.dynamicVersion + versionCode = System.getenv("VERSION_CODE")?.toIntOrNull() ?: 1 vectorDrawables.useSupportLibrary = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + multiDexEnabled = true } signingConfigs { create("release") { - storeFile = file("../default_key_store.jks") - storePassword = "mifos1234" - keyAlias = "mifos" - keyPassword = "mifos1234" + storeFile = + file(System.getenv("KEYSTORE_PATH") ?: "../keystores/release_keystore.keystore") + storePassword = System.getenv("KEYSTORE_PASSWORD") ?: "mifos1234" + keyAlias = System.getenv("KEYSTORE_ALIAS") ?: "mifos" + keyPassword = System.getenv("KEYSTORE_ALIAS_PASSWORD") ?: "mifos1234" + enableV1Signing = true + enableV2Signing = true } } buildTypes { - debug { isMinifyEnabled = false applicationIdSuffix = MifosBuildType.DEBUG.applicationIdSuffix - // TODO Uses new built-in shrinker, To Enable update buils tools to 2.2 - // TODO http://tools.android.com/tech-docs/new-build-system/built-in-shrinker - //useProguard false - //proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - //testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguardTest-rules.pro' } + // TODO:: Fix the proguard rules for release build release { isMinifyEnabled = false - isDebuggable = false + isDebuggable = true + isShrinkResources = false applicationIdSuffix = MifosBuildType.RELEASE.applicationIdSuffix proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") testProguardFiles( getDefaultProguardFile("proguard-android.txt"), - "proguardTest-rules.pro" + "proguardTest-rules.pro", ) } } @@ -86,38 +74,9 @@ android { resources.excludes.add("META-INF/dbflow-kotlinextensions-compileReleaseKotlin.kotlin_module") } - useLibrary("org.apache.http.legacy") - - // Always show the result of every unit test, even if it passes. - testOptions.unitTests.all { - it.apply { - testLogging.events = setOf( - TestLogEvent.PASSED, - TestLogEvent.SKIPPED, - TestLogEvent.FAILED, - TestLogEvent.STANDARD_OUT, - TestLogEvent.STANDARD_ERROR - ) - } - } - buildFeatures { - viewBinding = true - buildConfig = true compose = true - } - - composeOptions { - kotlinCompilerExtensionVersion = "1.5.8" - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = "17" + buildConfig = true } } @@ -144,7 +103,6 @@ dependencies { implementation(projects.feature.document) implementation(projects.feature.dataTable) implementation(projects.feature.search) - implementation(projects.feature.splash) implementation(projects.libs.mifosPasscode) @@ -160,65 +118,8 @@ dependencies { // Multidex dependency implementation(libs.androidx.multidex) - // Text drawable dependency - implementation(libs.textdrawable) - - // Kotlin standard library - implementation(libs.kotlin.stdlib) - - //DBFlow dependencies - kapt(libs.dbflow.processor) - implementation(libs.dbflow) - // App's Support dependencies, including test implementation(libs.androidx.appcompat) - implementation(libs.androidx.legacy.support.v4) - implementation(libs.androidx.recyclerview) - implementation(libs.material) - implementation(libs.play.services.places) - implementation(libs.play.services.location) - implementation(libs.play.services.maps) - implementation(libs.android.maps.utils) - implementation(libs.androidx.espresso.idling.resource) - - //LifeCycle - implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.lifecycle.extensions) - implementation(libs.androidx.lifecycle.reactivestreams.ktx) - implementation(libs.androidx.lifecycle.common.java8) - - //Square dependencies - implementation("com.squareup.retrofit2:retrofit:2.9.0") { - // exclude Retrofit’s OkHttp peer-dependency module and define your own module import - exclude(module = "okhttp") - } - implementation(libs.converter.gson) - implementation(libs.converter.scalars) - implementation(libs.adapter.rxjava) - implementation(libs.squareup.okhttp) - implementation(libs.logging.interceptor) - - implementation(libs.fliptables) - - //sweet error dependency - implementation(libs.sweet.error) - - //rxjava dependencies - implementation(libs.rxandroid) - implementation(libs.rxjava) - - //stetho dependencies - implementation(libs.stetho) - implementation(libs.stetho.okhttp3) - - //showcase View dependency - implementation(libs.materialshowcaseview) - - //Iconify dependency - implementation(libs.android.iconify.material) - - //glide dependency - implementation(libs.glide) // Mockito and jUnit dependencies testImplementation(libs.junit4) @@ -226,51 +127,29 @@ dependencies { testImplementation(libs.junit.jupiter) testImplementation(libs.androidx.core.testing) - //Android-Jobs - implementation(libs.android.job) - - // androidx annotations - implementation(libs.androidx.annotation) - - //preferences - implementation(libs.androidx.preference.ktx) - //Splash Screen implementation(libs.androidx.core.splashscreen) - // Navigation Components - implementation(libs.androidx.navigation.fragment.ktx) - implementation(libs.androidx.navigation.ui.ktx) - // Hilt dependency implementation(libs.hilt.android) kapt(libs.hilt.compiler) - // fineract sdk dependencies - implementation(libs.mifos.android.sdk.arch) - - // sdk client - implementation(libs.fineract.client) - // Jetpack Compose - implementation(libs.androidx.material) implementation(libs.androidx.compiler) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.activity.compose) debugImplementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.compose.material3) - implementation(libs.androidx.lifecycle.viewModelCompose) implementation(libs.androidx.compose.material.iconsExtended) + implementation(libs.androidx.navigation.compose) // ViewModel utilities for Compose - implementation(libs.androidx.lifecycle.viewModelCompose) implementation(libs.androidx.hilt.navigation.compose) + //LifeCycle + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewModelCompose) implementation(libs.androidx.lifecycle.runtimeCompose) - - //coil - implementation(libs.coil.kt.compose) - } dependencyGuard { diff --git a/mifosng-android/dependencies/demoDebugCompileClasspath.txt b/mifosng-android/dependencies/demoDebugCompileClasspath.txt index b110e25982f..f6d8cc1a311 100644 --- a/mifosng-android/dependencies/demoDebugCompileClasspath.txt +++ b/mifosng-android/dependencies/demoDebugCompileClasspath.txt @@ -2,16 +2,13 @@ androidx.activity:activity-compose:1.8.2 androidx.activity:activity-ktx:1.8.2 androidx.activity:activity:1.8.2 androidx.annotation:annotation-experimental:1.4.0 -androidx.annotation:annotation-jvm:1.7.1 -androidx.annotation:annotation:1.7.1 +androidx.annotation:annotation-jvm:1.7.0 +androidx.annotation:annotation:1.7.0 androidx.appcompat:appcompat-resources:1.7.0 androidx.appcompat:appcompat:1.7.0 androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-runtime:2.2.0 -androidx.asynclayoutinflater:asynclayoutinflater:1.0.0 -androidx.cardview:cardview:1.0.0 androidx.collection:collection-jvm:1.4.0 -androidx.collection:collection-ktx:1.4.0 androidx.collection:collection:1.4.0 androidx.compose.animation:animation-android:1.6.8 androidx.compose.animation:animation-core-android:1.6.8 @@ -24,14 +21,12 @@ androidx.compose.foundation:foundation-layout:1.6.8 androidx.compose.foundation:foundation:1.6.8 androidx.compose.material3:material3-android:1.2.1 androidx.compose.material3:material3:1.2.1 -androidx.compose.material:material-android:1.6.8 androidx.compose.material:material-icons-core-android:1.6.8 androidx.compose.material:material-icons-core:1.6.8 androidx.compose.material:material-icons-extended-android:1.6.8 androidx.compose.material:material-icons-extended:1.6.8 androidx.compose.material:material-ripple-android:1.6.8 androidx.compose.material:material-ripple:1.6.8 -androidx.compose.material:material:1.6.8 androidx.compose.runtime:runtime-android:1.6.8 androidx.compose.runtime:runtime-saveable-android:1.6.8 androidx.compose.runtime:runtime-saveable:1.6.8 @@ -61,108 +56,58 @@ androidx.compose.ui:ui-util:1.6.8 androidx.compose.ui:ui:1.6.8 androidx.compose:compose-bom:2024.02.01 androidx.concurrent:concurrent-futures:1.1.0 -androidx.constraintlayout:constraintlayout-solver:2.0.1 -androidx.constraintlayout:constraintlayout:2.0.1 -androidx.coordinatorlayout:coordinatorlayout:1.1.0 androidx.core:core-ktx:1.13.0 androidx.core:core-splashscreen:1.0.1 androidx.core:core:1.13.0 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview:1.1.0 -androidx.databinding:viewbinding:8.5.0 -androidx.documentfile:documentfile:1.0.0 -androidx.drawerlayout:drawerlayout:1.1.1 -androidx.dynamicanimation:dynamicanimation:1.0.0 -androidx.exifinterface:exifinterface:1.3.6 -androidx.fragment:fragment-ktx:1.6.2 -androidx.fragment:fragment:1.6.2 +androidx.drawerlayout:drawerlayout:1.0.0 +androidx.fragment:fragment:1.5.4 androidx.hilt:hilt-navigation-compose:1.2.0 androidx.hilt:hilt-navigation:1.2.0 androidx.interpolator:interpolator:1.0.0 -androidx.legacy:legacy-support-core-ui:1.0.0 -androidx.legacy:legacy-support-core-utils:1.0.0 -androidx.legacy:legacy-support-v4:1.0.0 androidx.lifecycle:lifecycle-common-java8:2.7.0 androidx.lifecycle:lifecycle-common:2.7.0 -androidx.lifecycle:lifecycle-extensions:2.2.0 androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0 androidx.lifecycle:lifecycle-livedata-core:2.7.0 androidx.lifecycle:lifecycle-livedata-ktx:2.7.0 androidx.lifecycle:lifecycle-livedata:2.7.0 -androidx.lifecycle:lifecycle-process:2.7.0 -androidx.lifecycle:lifecycle-reactivestreams-ktx:2.7.0 -androidx.lifecycle:lifecycle-reactivestreams:2.7.0 androidx.lifecycle:lifecycle-runtime-compose:2.7.0 androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 androidx.lifecycle:lifecycle-runtime:2.7.0 -androidx.lifecycle:lifecycle-service:2.7.0 androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0 androidx.lifecycle:lifecycle-viewmodel:2.7.0 androidx.loader:loader:1.0.0 -androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 -androidx.media:media:1.0.0 androidx.metrics:metrics-performance:1.0.0-beta01 androidx.multidex:multidex:2.0.1 androidx.navigation:navigation-common-ktx:2.7.7 androidx.navigation:navigation-common:2.7.7 androidx.navigation:navigation-compose:2.7.7 -androidx.navigation:navigation-fragment-ktx:2.7.7 -androidx.navigation:navigation-fragment:2.7.7 androidx.navigation:navigation-runtime-ktx:2.7.7 androidx.navigation:navigation-runtime:2.7.7 -androidx.navigation:navigation-ui-ktx:2.7.7 -androidx.navigation:navigation-ui:2.7.7 androidx.paging:paging-common-ktx:3.2.1 androidx.paging:paging-common:3.2.1 androidx.paging:paging-runtime-ktx:3.2.1 androidx.paging:paging-runtime:3.2.1 -androidx.preference:preference-ktx:1.2.1 -androidx.preference:preference:1.2.1 -androidx.print:print:1.0.0 -androidx.recyclerview:recyclerview:1.3.2 -androidx.resourceinspection:resourceinspection-annotation:1.0.1 +androidx.recyclerview:recyclerview:1.2.1 androidx.savedstate:savedstate-ktx:1.2.1 androidx.savedstate:savedstate:1.2.1 -androidx.slidingpanelayout:slidingpanelayout:1.2.0 -androidx.startup:startup-runtime:1.1.1 -androidx.swiperefreshlayout:swiperefreshlayout:1.0.0 -androidx.test.espresso:espresso-idling-resource:3.5.1 androidx.test.ext:junit:1.1.5 androidx.test.services:storage:1.4.2 androidx.test:annotation:1.0.1 androidx.test:core:1.5.0 androidx.test:monitor:1.6.1 androidx.tracing:tracing:1.2.0 -androidx.transition:transition:1.4.1 androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 -androidx.viewpager2:viewpager2:1.1.0-beta02 androidx.viewpager:viewpager:1.0.0 -com.evernote:android-job:1.2.6 -com.facebook.stetho:stetho-okhttp3:1.3.1 -com.facebook.stetho:stetho:1.3.1 -com.github.amulyakhare:TextDrawable:558677ea31 -com.github.bumptech.glide:annotations:4.15.1 -com.github.bumptech.glide:disklrucache:4.15.1 -com.github.bumptech.glide:gifdecoder:4.15.1 -com.github.bumptech.glide:glide:4.15.1 -com.github.deano2390:MaterialShowcaseView:1.3.7 com.github.openMF:fineract-client-cmp:0.0.1 com.github.openMF:fineract-client-sdk-cmp:0.0.1 com.github.raizlabs.dbflow.dbflow:dbflow-core:3.1.1 com.github.raizlabs.dbflow.dbflow:dbflow:3.1.1 -com.github.therajanmaurya:sweet-error:1.0.9 -com.google.android.gms:play-services-base:18.1.0 -com.google.android.gms:play-services-basement:18.1.0 -com.google.android.gms:play-services-location:21.1.0 -com.google.android.gms:play-services-maps:18.2.0 -com.google.android.gms:play-services-places-placereport:17.0.0 -com.google.android.gms:play-services-places:17.0.0 -com.google.android.gms:play-services-tasks:18.0.2 -com.google.android.material:material:1.11.0 com.google.code.findbugs:jsr305:3.0.2 com.google.code.gson:gson:2.10.1 com.google.dagger:dagger-lint-aar:2.51 @@ -170,23 +115,11 @@ com.google.dagger:dagger:2.51 com.google.dagger:hilt-android:2.51 com.google.dagger:hilt-core:2.51 com.google.guava:listenablefuture:1.0 -com.google.maps.android:android-maps-utils:0.4.2 -com.jakewharton.fliptables:fliptables:1.0.1 -com.joanzapata.iconify:android-iconify-material:2.2.2 -com.joanzapata.iconify:android-iconify:2.2.2 -com.squareup.okhttp3:logging-interceptor:4.12.0 com.squareup.okhttp3:okhttp:4.12.0 com.squareup.okio:okio-jvm:3.7.0 com.squareup.okio:okio:3.7.0 -com.squareup.retrofit2:adapter-rxjava:2.9.0 com.squareup.retrofit2:converter-gson:2.10.0 -com.squareup.retrofit2:converter-scalars:2.10.0 -com.squareup.retrofit2:retrofit:2.10.0 -commons-cli:commons-cli:1.2 -io.coil-kt:coil-base:2.5.0 -io.coil-kt:coil-compose-base:2.5.0 -io.coil-kt:coil-compose:2.5.0 -io.coil-kt:coil:2.5.0 +com.squareup.retrofit2:retrofit:2.11.0 io.reactivex:rxandroid:1.1.0 io.reactivex:rxjava:1.3.8 javax.inject:javax.inject:1 @@ -204,4 +137,3 @@ org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.8.0 org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0 org.jetbrains:annotations:23.0.0 -org.reactivestreams:reactive-streams:1.0.0 diff --git a/mifosng-android/dependencies/demoReleaseCompileClasspath.txt b/mifosng-android/dependencies/demoReleaseCompileClasspath.txt index 41e67601279..9c3d213a64b 100644 --- a/mifosng-android/dependencies/demoReleaseCompileClasspath.txt +++ b/mifosng-android/dependencies/demoReleaseCompileClasspath.txt @@ -2,16 +2,13 @@ androidx.activity:activity-compose:1.8.2 androidx.activity:activity-ktx:1.8.2 androidx.activity:activity:1.8.2 androidx.annotation:annotation-experimental:1.4.0 -androidx.annotation:annotation-jvm:1.7.1 -androidx.annotation:annotation:1.7.1 +androidx.annotation:annotation-jvm:1.7.0 +androidx.annotation:annotation:1.7.0 androidx.appcompat:appcompat-resources:1.7.0 androidx.appcompat:appcompat:1.7.0 androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-runtime:2.2.0 -androidx.asynclayoutinflater:asynclayoutinflater:1.0.0 -androidx.cardview:cardview:1.0.0 androidx.collection:collection-jvm:1.4.0 -androidx.collection:collection-ktx:1.4.0 androidx.collection:collection:1.4.0 androidx.compose.animation:animation-android:1.6.8 androidx.compose.animation:animation-core-android:1.6.8 @@ -24,14 +21,12 @@ androidx.compose.foundation:foundation-layout:1.6.8 androidx.compose.foundation:foundation:1.6.8 androidx.compose.material3:material3-android:1.2.1 androidx.compose.material3:material3:1.2.1 -androidx.compose.material:material-android:1.6.8 androidx.compose.material:material-icons-core-android:1.6.8 androidx.compose.material:material-icons-core:1.6.8 androidx.compose.material:material-icons-extended-android:1.6.8 androidx.compose.material:material-icons-extended:1.6.8 androidx.compose.material:material-ripple-android:1.6.8 androidx.compose.material:material-ripple:1.6.8 -androidx.compose.material:material:1.6.8 androidx.compose.runtime:runtime-android:1.6.8 androidx.compose.runtime:runtime-saveable-android:1.6.8 androidx.compose.runtime:runtime-saveable:1.6.8 @@ -56,108 +51,58 @@ androidx.compose.ui:ui-util:1.6.8 androidx.compose.ui:ui:1.6.8 androidx.compose:compose-bom:2024.02.01 androidx.concurrent:concurrent-futures:1.1.0 -androidx.constraintlayout:constraintlayout-solver:2.0.1 -androidx.constraintlayout:constraintlayout:2.0.1 -androidx.coordinatorlayout:coordinatorlayout:1.1.0 androidx.core:core-ktx:1.13.0 androidx.core:core-splashscreen:1.0.1 androidx.core:core:1.13.0 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview:1.1.0 -androidx.databinding:viewbinding:8.5.0 -androidx.documentfile:documentfile:1.0.0 -androidx.drawerlayout:drawerlayout:1.1.1 -androidx.dynamicanimation:dynamicanimation:1.0.0 -androidx.exifinterface:exifinterface:1.3.6 -androidx.fragment:fragment-ktx:1.6.2 -androidx.fragment:fragment:1.6.2 +androidx.drawerlayout:drawerlayout:1.0.0 +androidx.fragment:fragment:1.5.4 androidx.hilt:hilt-navigation-compose:1.2.0 androidx.hilt:hilt-navigation:1.2.0 androidx.interpolator:interpolator:1.0.0 -androidx.legacy:legacy-support-core-ui:1.0.0 -androidx.legacy:legacy-support-core-utils:1.0.0 -androidx.legacy:legacy-support-v4:1.0.0 androidx.lifecycle:lifecycle-common-java8:2.7.0 androidx.lifecycle:lifecycle-common:2.7.0 -androidx.lifecycle:lifecycle-extensions:2.2.0 androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0 androidx.lifecycle:lifecycle-livedata-core:2.7.0 androidx.lifecycle:lifecycle-livedata-ktx:2.7.0 androidx.lifecycle:lifecycle-livedata:2.7.0 -androidx.lifecycle:lifecycle-process:2.7.0 -androidx.lifecycle:lifecycle-reactivestreams-ktx:2.7.0 -androidx.lifecycle:lifecycle-reactivestreams:2.7.0 androidx.lifecycle:lifecycle-runtime-compose:2.7.0 androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 androidx.lifecycle:lifecycle-runtime:2.7.0 -androidx.lifecycle:lifecycle-service:2.7.0 androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0 androidx.lifecycle:lifecycle-viewmodel:2.7.0 androidx.loader:loader:1.0.0 -androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 -androidx.media:media:1.0.0 androidx.metrics:metrics-performance:1.0.0-beta01 androidx.multidex:multidex:2.0.1 androidx.navigation:navigation-common-ktx:2.7.7 androidx.navigation:navigation-common:2.7.7 androidx.navigation:navigation-compose:2.7.7 -androidx.navigation:navigation-fragment-ktx:2.7.7 -androidx.navigation:navigation-fragment:2.7.7 androidx.navigation:navigation-runtime-ktx:2.7.7 androidx.navigation:navigation-runtime:2.7.7 -androidx.navigation:navigation-ui-ktx:2.7.7 -androidx.navigation:navigation-ui:2.7.7 androidx.paging:paging-common-ktx:3.2.1 androidx.paging:paging-common:3.2.1 androidx.paging:paging-runtime-ktx:3.2.1 androidx.paging:paging-runtime:3.2.1 -androidx.preference:preference-ktx:1.2.1 -androidx.preference:preference:1.2.1 -androidx.print:print:1.0.0 -androidx.recyclerview:recyclerview:1.3.2 -androidx.resourceinspection:resourceinspection-annotation:1.0.1 +androidx.recyclerview:recyclerview:1.2.1 androidx.savedstate:savedstate-ktx:1.2.1 androidx.savedstate:savedstate:1.2.1 -androidx.slidingpanelayout:slidingpanelayout:1.2.0 -androidx.startup:startup-runtime:1.1.1 -androidx.swiperefreshlayout:swiperefreshlayout:1.0.0 -androidx.test.espresso:espresso-idling-resource:3.5.1 androidx.test.ext:junit:1.1.5 androidx.test.services:storage:1.4.2 androidx.test:annotation:1.0.1 androidx.test:core:1.5.0 androidx.test:monitor:1.6.1 androidx.tracing:tracing:1.2.0 -androidx.transition:transition:1.4.1 androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 -androidx.viewpager2:viewpager2:1.1.0-beta02 androidx.viewpager:viewpager:1.0.0 -com.evernote:android-job:1.2.6 -com.facebook.stetho:stetho-okhttp3:1.3.1 -com.facebook.stetho:stetho:1.3.1 -com.github.amulyakhare:TextDrawable:558677ea31 -com.github.bumptech.glide:annotations:4.15.1 -com.github.bumptech.glide:disklrucache:4.15.1 -com.github.bumptech.glide:gifdecoder:4.15.1 -com.github.bumptech.glide:glide:4.15.1 -com.github.deano2390:MaterialShowcaseView:1.3.7 com.github.openMF:fineract-client-cmp:0.0.1 com.github.openMF:fineract-client-sdk-cmp:0.0.1 com.github.raizlabs.dbflow.dbflow:dbflow-core:3.1.1 com.github.raizlabs.dbflow.dbflow:dbflow:3.1.1 -com.github.therajanmaurya:sweet-error:1.0.9 -com.google.android.gms:play-services-base:18.1.0 -com.google.android.gms:play-services-basement:18.1.0 -com.google.android.gms:play-services-location:21.1.0 -com.google.android.gms:play-services-maps:18.2.0 -com.google.android.gms:play-services-places-placereport:17.0.0 -com.google.android.gms:play-services-places:17.0.0 -com.google.android.gms:play-services-tasks:18.0.2 -com.google.android.material:material:1.11.0 com.google.code.findbugs:jsr305:3.0.2 com.google.code.gson:gson:2.10.1 com.google.dagger:dagger-lint-aar:2.51 @@ -165,23 +110,11 @@ com.google.dagger:dagger:2.51 com.google.dagger:hilt-android:2.51 com.google.dagger:hilt-core:2.51 com.google.guava:listenablefuture:1.0 -com.google.maps.android:android-maps-utils:0.4.2 -com.jakewharton.fliptables:fliptables:1.0.1 -com.joanzapata.iconify:android-iconify-material:2.2.2 -com.joanzapata.iconify:android-iconify:2.2.2 -com.squareup.okhttp3:logging-interceptor:4.12.0 com.squareup.okhttp3:okhttp:4.12.0 com.squareup.okio:okio-jvm:3.7.0 com.squareup.okio:okio:3.7.0 -com.squareup.retrofit2:adapter-rxjava:2.9.0 com.squareup.retrofit2:converter-gson:2.10.0 -com.squareup.retrofit2:converter-scalars:2.10.0 -com.squareup.retrofit2:retrofit:2.10.0 -commons-cli:commons-cli:1.2 -io.coil-kt:coil-base:2.5.0 -io.coil-kt:coil-compose-base:2.5.0 -io.coil-kt:coil-compose:2.5.0 -io.coil-kt:coil:2.5.0 +com.squareup.retrofit2:retrofit:2.11.0 io.reactivex:rxandroid:1.1.0 io.reactivex:rxjava:1.3.8 javax.inject:javax.inject:1 @@ -199,4 +132,3 @@ org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.8.0 org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0 org.jetbrains:annotations:23.0.0 -org.reactivestreams:reactive-streams:1.0.0 diff --git a/mifosng-android/dependencies/prodDebugCompileClasspath.txt b/mifosng-android/dependencies/prodDebugCompileClasspath.txt index b110e25982f..f6d8cc1a311 100644 --- a/mifosng-android/dependencies/prodDebugCompileClasspath.txt +++ b/mifosng-android/dependencies/prodDebugCompileClasspath.txt @@ -2,16 +2,13 @@ androidx.activity:activity-compose:1.8.2 androidx.activity:activity-ktx:1.8.2 androidx.activity:activity:1.8.2 androidx.annotation:annotation-experimental:1.4.0 -androidx.annotation:annotation-jvm:1.7.1 -androidx.annotation:annotation:1.7.1 +androidx.annotation:annotation-jvm:1.7.0 +androidx.annotation:annotation:1.7.0 androidx.appcompat:appcompat-resources:1.7.0 androidx.appcompat:appcompat:1.7.0 androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-runtime:2.2.0 -androidx.asynclayoutinflater:asynclayoutinflater:1.0.0 -androidx.cardview:cardview:1.0.0 androidx.collection:collection-jvm:1.4.0 -androidx.collection:collection-ktx:1.4.0 androidx.collection:collection:1.4.0 androidx.compose.animation:animation-android:1.6.8 androidx.compose.animation:animation-core-android:1.6.8 @@ -24,14 +21,12 @@ androidx.compose.foundation:foundation-layout:1.6.8 androidx.compose.foundation:foundation:1.6.8 androidx.compose.material3:material3-android:1.2.1 androidx.compose.material3:material3:1.2.1 -androidx.compose.material:material-android:1.6.8 androidx.compose.material:material-icons-core-android:1.6.8 androidx.compose.material:material-icons-core:1.6.8 androidx.compose.material:material-icons-extended-android:1.6.8 androidx.compose.material:material-icons-extended:1.6.8 androidx.compose.material:material-ripple-android:1.6.8 androidx.compose.material:material-ripple:1.6.8 -androidx.compose.material:material:1.6.8 androidx.compose.runtime:runtime-android:1.6.8 androidx.compose.runtime:runtime-saveable-android:1.6.8 androidx.compose.runtime:runtime-saveable:1.6.8 @@ -61,108 +56,58 @@ androidx.compose.ui:ui-util:1.6.8 androidx.compose.ui:ui:1.6.8 androidx.compose:compose-bom:2024.02.01 androidx.concurrent:concurrent-futures:1.1.0 -androidx.constraintlayout:constraintlayout-solver:2.0.1 -androidx.constraintlayout:constraintlayout:2.0.1 -androidx.coordinatorlayout:coordinatorlayout:1.1.0 androidx.core:core-ktx:1.13.0 androidx.core:core-splashscreen:1.0.1 androidx.core:core:1.13.0 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview:1.1.0 -androidx.databinding:viewbinding:8.5.0 -androidx.documentfile:documentfile:1.0.0 -androidx.drawerlayout:drawerlayout:1.1.1 -androidx.dynamicanimation:dynamicanimation:1.0.0 -androidx.exifinterface:exifinterface:1.3.6 -androidx.fragment:fragment-ktx:1.6.2 -androidx.fragment:fragment:1.6.2 +androidx.drawerlayout:drawerlayout:1.0.0 +androidx.fragment:fragment:1.5.4 androidx.hilt:hilt-navigation-compose:1.2.0 androidx.hilt:hilt-navigation:1.2.0 androidx.interpolator:interpolator:1.0.0 -androidx.legacy:legacy-support-core-ui:1.0.0 -androidx.legacy:legacy-support-core-utils:1.0.0 -androidx.legacy:legacy-support-v4:1.0.0 androidx.lifecycle:lifecycle-common-java8:2.7.0 androidx.lifecycle:lifecycle-common:2.7.0 -androidx.lifecycle:lifecycle-extensions:2.2.0 androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0 androidx.lifecycle:lifecycle-livedata-core:2.7.0 androidx.lifecycle:lifecycle-livedata-ktx:2.7.0 androidx.lifecycle:lifecycle-livedata:2.7.0 -androidx.lifecycle:lifecycle-process:2.7.0 -androidx.lifecycle:lifecycle-reactivestreams-ktx:2.7.0 -androidx.lifecycle:lifecycle-reactivestreams:2.7.0 androidx.lifecycle:lifecycle-runtime-compose:2.7.0 androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 androidx.lifecycle:lifecycle-runtime:2.7.0 -androidx.lifecycle:lifecycle-service:2.7.0 androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0 androidx.lifecycle:lifecycle-viewmodel:2.7.0 androidx.loader:loader:1.0.0 -androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 -androidx.media:media:1.0.0 androidx.metrics:metrics-performance:1.0.0-beta01 androidx.multidex:multidex:2.0.1 androidx.navigation:navigation-common-ktx:2.7.7 androidx.navigation:navigation-common:2.7.7 androidx.navigation:navigation-compose:2.7.7 -androidx.navigation:navigation-fragment-ktx:2.7.7 -androidx.navigation:navigation-fragment:2.7.7 androidx.navigation:navigation-runtime-ktx:2.7.7 androidx.navigation:navigation-runtime:2.7.7 -androidx.navigation:navigation-ui-ktx:2.7.7 -androidx.navigation:navigation-ui:2.7.7 androidx.paging:paging-common-ktx:3.2.1 androidx.paging:paging-common:3.2.1 androidx.paging:paging-runtime-ktx:3.2.1 androidx.paging:paging-runtime:3.2.1 -androidx.preference:preference-ktx:1.2.1 -androidx.preference:preference:1.2.1 -androidx.print:print:1.0.0 -androidx.recyclerview:recyclerview:1.3.2 -androidx.resourceinspection:resourceinspection-annotation:1.0.1 +androidx.recyclerview:recyclerview:1.2.1 androidx.savedstate:savedstate-ktx:1.2.1 androidx.savedstate:savedstate:1.2.1 -androidx.slidingpanelayout:slidingpanelayout:1.2.0 -androidx.startup:startup-runtime:1.1.1 -androidx.swiperefreshlayout:swiperefreshlayout:1.0.0 -androidx.test.espresso:espresso-idling-resource:3.5.1 androidx.test.ext:junit:1.1.5 androidx.test.services:storage:1.4.2 androidx.test:annotation:1.0.1 androidx.test:core:1.5.0 androidx.test:monitor:1.6.1 androidx.tracing:tracing:1.2.0 -androidx.transition:transition:1.4.1 androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 -androidx.viewpager2:viewpager2:1.1.0-beta02 androidx.viewpager:viewpager:1.0.0 -com.evernote:android-job:1.2.6 -com.facebook.stetho:stetho-okhttp3:1.3.1 -com.facebook.stetho:stetho:1.3.1 -com.github.amulyakhare:TextDrawable:558677ea31 -com.github.bumptech.glide:annotations:4.15.1 -com.github.bumptech.glide:disklrucache:4.15.1 -com.github.bumptech.glide:gifdecoder:4.15.1 -com.github.bumptech.glide:glide:4.15.1 -com.github.deano2390:MaterialShowcaseView:1.3.7 com.github.openMF:fineract-client-cmp:0.0.1 com.github.openMF:fineract-client-sdk-cmp:0.0.1 com.github.raizlabs.dbflow.dbflow:dbflow-core:3.1.1 com.github.raizlabs.dbflow.dbflow:dbflow:3.1.1 -com.github.therajanmaurya:sweet-error:1.0.9 -com.google.android.gms:play-services-base:18.1.0 -com.google.android.gms:play-services-basement:18.1.0 -com.google.android.gms:play-services-location:21.1.0 -com.google.android.gms:play-services-maps:18.2.0 -com.google.android.gms:play-services-places-placereport:17.0.0 -com.google.android.gms:play-services-places:17.0.0 -com.google.android.gms:play-services-tasks:18.0.2 -com.google.android.material:material:1.11.0 com.google.code.findbugs:jsr305:3.0.2 com.google.code.gson:gson:2.10.1 com.google.dagger:dagger-lint-aar:2.51 @@ -170,23 +115,11 @@ com.google.dagger:dagger:2.51 com.google.dagger:hilt-android:2.51 com.google.dagger:hilt-core:2.51 com.google.guava:listenablefuture:1.0 -com.google.maps.android:android-maps-utils:0.4.2 -com.jakewharton.fliptables:fliptables:1.0.1 -com.joanzapata.iconify:android-iconify-material:2.2.2 -com.joanzapata.iconify:android-iconify:2.2.2 -com.squareup.okhttp3:logging-interceptor:4.12.0 com.squareup.okhttp3:okhttp:4.12.0 com.squareup.okio:okio-jvm:3.7.0 com.squareup.okio:okio:3.7.0 -com.squareup.retrofit2:adapter-rxjava:2.9.0 com.squareup.retrofit2:converter-gson:2.10.0 -com.squareup.retrofit2:converter-scalars:2.10.0 -com.squareup.retrofit2:retrofit:2.10.0 -commons-cli:commons-cli:1.2 -io.coil-kt:coil-base:2.5.0 -io.coil-kt:coil-compose-base:2.5.0 -io.coil-kt:coil-compose:2.5.0 -io.coil-kt:coil:2.5.0 +com.squareup.retrofit2:retrofit:2.11.0 io.reactivex:rxandroid:1.1.0 io.reactivex:rxjava:1.3.8 javax.inject:javax.inject:1 @@ -204,4 +137,3 @@ org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.8.0 org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0 org.jetbrains:annotations:23.0.0 -org.reactivestreams:reactive-streams:1.0.0 diff --git a/mifosng-android/dependencies/prodReleaseCompileClasspath.txt b/mifosng-android/dependencies/prodReleaseCompileClasspath.txt index 41e67601279..9c3d213a64b 100644 --- a/mifosng-android/dependencies/prodReleaseCompileClasspath.txt +++ b/mifosng-android/dependencies/prodReleaseCompileClasspath.txt @@ -2,16 +2,13 @@ androidx.activity:activity-compose:1.8.2 androidx.activity:activity-ktx:1.8.2 androidx.activity:activity:1.8.2 androidx.annotation:annotation-experimental:1.4.0 -androidx.annotation:annotation-jvm:1.7.1 -androidx.annotation:annotation:1.7.1 +androidx.annotation:annotation-jvm:1.7.0 +androidx.annotation:annotation:1.7.0 androidx.appcompat:appcompat-resources:1.7.0 androidx.appcompat:appcompat:1.7.0 androidx.arch.core:core-common:2.2.0 androidx.arch.core:core-runtime:2.2.0 -androidx.asynclayoutinflater:asynclayoutinflater:1.0.0 -androidx.cardview:cardview:1.0.0 androidx.collection:collection-jvm:1.4.0 -androidx.collection:collection-ktx:1.4.0 androidx.collection:collection:1.4.0 androidx.compose.animation:animation-android:1.6.8 androidx.compose.animation:animation-core-android:1.6.8 @@ -24,14 +21,12 @@ androidx.compose.foundation:foundation-layout:1.6.8 androidx.compose.foundation:foundation:1.6.8 androidx.compose.material3:material3-android:1.2.1 androidx.compose.material3:material3:1.2.1 -androidx.compose.material:material-android:1.6.8 androidx.compose.material:material-icons-core-android:1.6.8 androidx.compose.material:material-icons-core:1.6.8 androidx.compose.material:material-icons-extended-android:1.6.8 androidx.compose.material:material-icons-extended:1.6.8 androidx.compose.material:material-ripple-android:1.6.8 androidx.compose.material:material-ripple:1.6.8 -androidx.compose.material:material:1.6.8 androidx.compose.runtime:runtime-android:1.6.8 androidx.compose.runtime:runtime-saveable-android:1.6.8 androidx.compose.runtime:runtime-saveable:1.6.8 @@ -56,108 +51,58 @@ androidx.compose.ui:ui-util:1.6.8 androidx.compose.ui:ui:1.6.8 androidx.compose:compose-bom:2024.02.01 androidx.concurrent:concurrent-futures:1.1.0 -androidx.constraintlayout:constraintlayout-solver:2.0.1 -androidx.constraintlayout:constraintlayout:2.0.1 -androidx.coordinatorlayout:coordinatorlayout:1.1.0 androidx.core:core-ktx:1.13.0 androidx.core:core-splashscreen:1.0.1 androidx.core:core:1.13.0 androidx.cursoradapter:cursoradapter:1.0.0 androidx.customview:customview:1.1.0 -androidx.databinding:viewbinding:8.5.0 -androidx.documentfile:documentfile:1.0.0 -androidx.drawerlayout:drawerlayout:1.1.1 -androidx.dynamicanimation:dynamicanimation:1.0.0 -androidx.exifinterface:exifinterface:1.3.6 -androidx.fragment:fragment-ktx:1.6.2 -androidx.fragment:fragment:1.6.2 +androidx.drawerlayout:drawerlayout:1.0.0 +androidx.fragment:fragment:1.5.4 androidx.hilt:hilt-navigation-compose:1.2.0 androidx.hilt:hilt-navigation:1.2.0 androidx.interpolator:interpolator:1.0.0 -androidx.legacy:legacy-support-core-ui:1.0.0 -androidx.legacy:legacy-support-core-utils:1.0.0 -androidx.legacy:legacy-support-v4:1.0.0 androidx.lifecycle:lifecycle-common-java8:2.7.0 androidx.lifecycle:lifecycle-common:2.7.0 -androidx.lifecycle:lifecycle-extensions:2.2.0 androidx.lifecycle:lifecycle-livedata-core-ktx:2.7.0 androidx.lifecycle:lifecycle-livedata-core:2.7.0 androidx.lifecycle:lifecycle-livedata-ktx:2.7.0 androidx.lifecycle:lifecycle-livedata:2.7.0 -androidx.lifecycle:lifecycle-process:2.7.0 -androidx.lifecycle:lifecycle-reactivestreams-ktx:2.7.0 -androidx.lifecycle:lifecycle-reactivestreams:2.7.0 androidx.lifecycle:lifecycle-runtime-compose:2.7.0 androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 androidx.lifecycle:lifecycle-runtime:2.7.0 -androidx.lifecycle:lifecycle-service:2.7.0 androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0 androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0 androidx.lifecycle:lifecycle-viewmodel:2.7.0 androidx.loader:loader:1.0.0 -androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 -androidx.media:media:1.0.0 androidx.metrics:metrics-performance:1.0.0-beta01 androidx.multidex:multidex:2.0.1 androidx.navigation:navigation-common-ktx:2.7.7 androidx.navigation:navigation-common:2.7.7 androidx.navigation:navigation-compose:2.7.7 -androidx.navigation:navigation-fragment-ktx:2.7.7 -androidx.navigation:navigation-fragment:2.7.7 androidx.navigation:navigation-runtime-ktx:2.7.7 androidx.navigation:navigation-runtime:2.7.7 -androidx.navigation:navigation-ui-ktx:2.7.7 -androidx.navigation:navigation-ui:2.7.7 androidx.paging:paging-common-ktx:3.2.1 androidx.paging:paging-common:3.2.1 androidx.paging:paging-runtime-ktx:3.2.1 androidx.paging:paging-runtime:3.2.1 -androidx.preference:preference-ktx:1.2.1 -androidx.preference:preference:1.2.1 -androidx.print:print:1.0.0 -androidx.recyclerview:recyclerview:1.3.2 -androidx.resourceinspection:resourceinspection-annotation:1.0.1 +androidx.recyclerview:recyclerview:1.2.1 androidx.savedstate:savedstate-ktx:1.2.1 androidx.savedstate:savedstate:1.2.1 -androidx.slidingpanelayout:slidingpanelayout:1.2.0 -androidx.startup:startup-runtime:1.1.1 -androidx.swiperefreshlayout:swiperefreshlayout:1.0.0 -androidx.test.espresso:espresso-idling-resource:3.5.1 androidx.test.ext:junit:1.1.5 androidx.test.services:storage:1.4.2 androidx.test:annotation:1.0.1 androidx.test:core:1.5.0 androidx.test:monitor:1.6.1 androidx.tracing:tracing:1.2.0 -androidx.transition:transition:1.4.1 androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 -androidx.viewpager2:viewpager2:1.1.0-beta02 androidx.viewpager:viewpager:1.0.0 -com.evernote:android-job:1.2.6 -com.facebook.stetho:stetho-okhttp3:1.3.1 -com.facebook.stetho:stetho:1.3.1 -com.github.amulyakhare:TextDrawable:558677ea31 -com.github.bumptech.glide:annotations:4.15.1 -com.github.bumptech.glide:disklrucache:4.15.1 -com.github.bumptech.glide:gifdecoder:4.15.1 -com.github.bumptech.glide:glide:4.15.1 -com.github.deano2390:MaterialShowcaseView:1.3.7 com.github.openMF:fineract-client-cmp:0.0.1 com.github.openMF:fineract-client-sdk-cmp:0.0.1 com.github.raizlabs.dbflow.dbflow:dbflow-core:3.1.1 com.github.raizlabs.dbflow.dbflow:dbflow:3.1.1 -com.github.therajanmaurya:sweet-error:1.0.9 -com.google.android.gms:play-services-base:18.1.0 -com.google.android.gms:play-services-basement:18.1.0 -com.google.android.gms:play-services-location:21.1.0 -com.google.android.gms:play-services-maps:18.2.0 -com.google.android.gms:play-services-places-placereport:17.0.0 -com.google.android.gms:play-services-places:17.0.0 -com.google.android.gms:play-services-tasks:18.0.2 -com.google.android.material:material:1.11.0 com.google.code.findbugs:jsr305:3.0.2 com.google.code.gson:gson:2.10.1 com.google.dagger:dagger-lint-aar:2.51 @@ -165,23 +110,11 @@ com.google.dagger:dagger:2.51 com.google.dagger:hilt-android:2.51 com.google.dagger:hilt-core:2.51 com.google.guava:listenablefuture:1.0 -com.google.maps.android:android-maps-utils:0.4.2 -com.jakewharton.fliptables:fliptables:1.0.1 -com.joanzapata.iconify:android-iconify-material:2.2.2 -com.joanzapata.iconify:android-iconify:2.2.2 -com.squareup.okhttp3:logging-interceptor:4.12.0 com.squareup.okhttp3:okhttp:4.12.0 com.squareup.okio:okio-jvm:3.7.0 com.squareup.okio:okio:3.7.0 -com.squareup.retrofit2:adapter-rxjava:2.9.0 com.squareup.retrofit2:converter-gson:2.10.0 -com.squareup.retrofit2:converter-scalars:2.10.0 -com.squareup.retrofit2:retrofit:2.10.0 -commons-cli:commons-cli:1.2 -io.coil-kt:coil-base:2.5.0 -io.coil-kt:coil-compose-base:2.5.0 -io.coil-kt:coil-compose:2.5.0 -io.coil-kt:coil:2.5.0 +com.squareup.retrofit2:retrofit:2.11.0 io.reactivex:rxandroid:1.1.0 io.reactivex:rxjava:1.3.8 javax.inject:javax.inject:1 @@ -199,4 +132,3 @@ org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.8.0 org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0 org.jetbrains:annotations:23.0.0 -org.reactivestreams:reactive-streams:1.0.0 diff --git a/mifosng-android/google-services.json b/mifosng-android/google-services.json new file mode 100644 index 00000000000..ef556049367 --- /dev/null +++ b/mifosng-android/google-services.json @@ -0,0 +1,287 @@ +{ + "project_info": { + "project_number": "728434912738", + "project_id": "mifos-mobile-apps", + "storage_bucket": "mifos-mobile-apps.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "kjdslkds8392kjlds", + "android_client_info": { + "package_name": "com.mifos.mifosxdroid" + } + }, + "oauth_client": [ + { + "client_id": "shjdkldhsdsld3209dkjs", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "djksdhkjdjskhkfjsh38sdkjs" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "ds783892jhkdsssssssssdkj982hdshdskjsd", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "kjdslkds8392kjlds", + "android_client_info": { + "package_name": "com.mifos.mifosxdroid.demo.debug" + } + }, + "oauth_client": [ + { + "client_id": "shjdkldhsdsld3209dkjs", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "djksdhkjdjskhkfjsh38sdkjs" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "ds783892jhkdsssssssssdkj982hdshdskjsd", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "12hjkh3khhhhhhhhhhhhhhk4j2h2k3jkjgl1k2jhkh1gjjf12", + "android_client_info": { + "package_name": "com.mifos.mifosxdroid.demo" + } + }, + "oauth_client": [ + { + "client_id": "cvjbkjnldsahgjkalds546789hjgaksddgaskjh", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "sgdhkajlghjkkdsuygkadshjl6798ghjkdsa" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "dsgjhkadslajkhkgasldjhgdshghasjdjahksh3127898132kkad1273ajdk", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:728434912738:android:d853a78f14af0c381a1dbb", + "android_client_info": { + "package_name": "org.mifos.mobile" + } + }, + "oauth_client": [ + { + "client_id": "dsgjhkadslajkhkgasldjhgdshghasjdjahksh3127898132kkad1273ajdk", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCUz3P8uUExMFcPHa1Ga3DBKhjK5zxNn70" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "dsgjhkadslajkhkgasldjhgdshghasjdjahksh3127898132kkad1273ajdk", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:728434912738:android:7845cce9777d9cf11a1dbb", + "android_client_info": { + "package_name": "org.mifos.mobile.demo" + } + }, + "oauth_client": [ + { + "client_id": "dsgjhkadslajkhkgasldjhgdshghasjdjahksh3127898132kkad1273ajdk", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCUz3P8uUExMFcPHa1Ga3DBKhjK5zxNn70" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "dsgjhkadslajkhkgasldjhgdshghasjdjahksh3127898132kkad1273ajdk", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:728434912738:android:49282a75468730891a1dbb", + "android_client_info": { + "package_name": "org.mifos.pisp.android" + } + }, + "oauth_client": [ + { + "client_id": "dsgjhkadslajkhkgasldjhgdshghasjdjahksh3127898132kkad1273ajdk", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCUz3P8uUExMFcPHa1Ga3DBKhjK5zxNn70" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "dsgjhkadslajkhkgasldjhgdshghasjdjahksh3127898132kkad1273ajdk", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:728434912738:android:ef7156e455c6a1a41a1dbb", + "android_client_info": { + "package_name": "org.mifos.pisp.android.debug" + } + }, + "oauth_client": [ + { + "client_id": "dsgjhkadslajkhkgasldjhgdshghasjdjahksh3127898132kkad1273ajdk", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCUz3P8uUExMFcPHa1Ga3DBKhjK5zxNn70" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "dsgjhkadslajkhkgasldjhgdshghasjdjahksh3127898132kkad1273ajdk", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:728434912738:android:0490c291986f0a691a1dbb", + "android_client_info": { + "package_name": "org.mifospay" + } + }, + "oauth_client": [ + { + "client_id": "728434912738-d4hshajpu39bq9m5e55s5d2u5hplh5ie.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "org.mifospay", + "certificate_hash": "d2fd346d362db97e693cfb13424b82bb94b92b56" + } + }, + { + "client_id": "dsgjhkadslajkhkgasldjhgdshghasjdjahksh3127898132kkad1273ajdk", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCUz3P8uUExMFcPHa1Ga3DBKhjK5zxNn70" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "dsgjhkadslajkhkgasldjhgdshghasjdjahksh3127898132kkad1273ajdk", + "client_type": 3 + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:728434912738:android:48ccd9153349f31e1a1dbb", + "android_client_info": { + "package_name": "org.mifospay.demo" + } + }, + "oauth_client": [ + { + "client_id": "dhskjl326980287dskhjlljhgjhk237khjdahsk", + "client_type": 1, + "android_info": { + "package_name": "org.mifospay.demo", + "certificate_hash": "d2fd346d362db97e693cfb13424b82bb94b92b56" + } + }, + { + "client_id": "dsgjhkadslajkhkgasldjhgdshghasjdjahksh3127898132kkad1273ajdk", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "dsjhklsjhgfj31729hgjkdsla1879" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "dsgjhkadslajkhkgasldjhgdshghasjdjahksh3127898132kkad1273ajdk", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/mifosng-android/proguard-rules.pro b/mifosng-android/proguard-rules.pro index f6aa4e23e13..fdc3ae1ec1d 100755 --- a/mifosng-android/proguard-rules.pro +++ b/mifosng-android/proguard-rules.pro @@ -49,21 +49,68 @@ # Retrofit 2.X ## https://square.github.io/retrofit/ ## --dontwarn retrofit2.** --keep class retrofit2.** { *; } --keepattributes Signature --keepattributes Exceptions +# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and +# EnclosingMethod is required to use InnerClasses. +-keepattributes Signature, InnerClasses, EnclosingMethod + +# Retrofit does reflection on method and parameter annotations. +-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations --keepclasseswithmembers class * { +# Keep annotation default values (e.g., retrofit2.http.Field.encoded). +-keepattributes AnnotationDefault + +# Retain service method parameters when optimizing. +-keepclassmembers,allowshrinking,allowobfuscation interface * { @retrofit2.http.* ; } +# Ignore annotation used for build tooling. +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement + +# Ignore JSR 305 annotations for embedding nullability information. +-dontwarn javax.annotation.** + +# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. +-dontwarn kotlin.Unit + +# Top-level functions that can only be used by Kotlin. +-dontwarn retrofit2.KotlinExtensions +-dontwarn retrofit2.KotlinExtensions$* + +# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy +# and replaces all potential values with null. Explicitly keeping the interfaces prevents this. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface <1> + +# Keep inherited services. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface * extends <1> + +# With R8 full mode generic signatures are stripped for classes that are not +# kept. Suspend functions are wrapped in continuations where the type argument +# is used. +-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation + +# R8 full mode strips generic signatures from return types if not kept. +-if interface * { @retrofit2.http.* public *** *(...); } +-keep,allowoptimization,allowshrinking,allowobfuscation class <3> + +# With R8 full mode generic signatures are stripped for classes that are not kept. +-keep,allowobfuscation,allowshrinking class retrofit2.Response + # OkHttp 3 --keepattributes Signature --keepattributes *Annotation* --keep class okhttp3.** { *; } --keep interface okhttp3.** { *; } --dontwarn okhttp3.** +# JSR 305 annotations are for embedding nullability information. +-dontwarn javax.annotation.** + +# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java. +-dontwarn org.codehaus.mojo.animal_sniffer.* + +# OkHttp platform used only on JVM and when Conscrypt and other security providers are available. +# May be used with robolectric or deliberate use of Bouncy Castle on Android +-dontwarn okhttp3.internal.platform.** +-dontwarn org.conscrypt.** +-dontwarn org.bouncycastle.** +-dontwarn org.openjsse.** # RxJava rules # RxAndroid will soon ship with rules so this may not be needed in the future @@ -115,4 +162,79 @@ #-keepresourcexmlelements manifest/application/meta-data@value=GlideModule # Iconify --keep class com.joanzapata.iconify.** { *; } \ No newline at end of file +-keep class com.joanzapata.iconify.** { *; } +-keep class org.slf4j.impl.StaticLoggerBinder { *; } +-dontwarn org.slf4j.impl.StaticLoggerBinder + +### Gson ProGuard and R8 rules which are relevant for all users +### This file is automatically recognized by ProGuard and R8, see https://developer.android.com/build/shrink-code#configuration-files +### +### IMPORTANT: +### - These rules are additive; don't include anything here which is not specific to Gson (such as completely +### disabling obfuscation for all classes); the user would be unable to disable that then +### - These rules are not complete; users will most likely have to add additional rules for their specific +### classes, for example to disable obfuscation for certain fields or to keep no-args constructors +### + +# Keep generic signatures; needed for correct type resolution +-keepattributes Signature + +# Keep Gson annotations +# Note: Cannot perform finer selection here to only cover Gson annotations, see also https://stackoverflow.com/q/47515093 +-keepattributes RuntimeVisibleAnnotations,AnnotationDefault + +### The following rules are needed for R8 in "full mode" which only adheres to `-keepattribtues` if +### the corresponding class or field is matches by a `-keep` rule as well, see +### https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#r8-full-mode + +# Keep class TypeToken (respectively its generic signature) if present +-if class com.google.gson.reflect.TypeToken +-keep,allowobfuscation class com.google.gson.reflect.TypeToken + +# Keep any (anonymous) classes extending TypeToken +-keep,allowobfuscation class * extends com.google.gson.reflect.TypeToken + +# Keep classes with @JsonAdapter annotation +-keep,allowobfuscation,allowoptimization @com.google.gson.annotations.JsonAdapter class * + +# Keep fields with any other Gson annotation +# Also allow obfuscation, assuming that users will additionally use @SerializedName or +# other means to preserve the field names +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.Expose ; + @com.google.gson.annotations.JsonAdapter ; + @com.google.gson.annotations.Since ; + @com.google.gson.annotations.Until ; +} + +# Keep no-args constructor of classes which can be used with @JsonAdapter +# By default their no-args constructor is invoked to create an adapter instance +-keepclassmembers class * extends com.google.gson.TypeAdapter { + (); +} +-keepclassmembers class * implements com.google.gson.TypeAdapterFactory { + (); +} +-keepclassmembers class * implements com.google.gson.JsonSerializer { + (); +} +-keepclassmembers class * implements com.google.gson.JsonDeserializer { + (); +} + +# Keep fields annotated with @SerializedName for classes which are referenced. +# If classes with fields annotated with @SerializedName have a no-args +# constructor keep that as well. Based on +# https://issuetracker.google.com/issues/150189783#comment11. +# See also https://github.com/google/gson/pull/2420#discussion_r1241813541 +# for a more detailed explanation. +-if class * +-keepclasseswithmembers,allowobfuscation class <1> { + @com.google.gson.annotations.SerializedName ; +} +-if class * { + @com.google.gson.annotations.SerializedName ; +} +-keepclassmembers,allowobfuscation,allowoptimization class <1> { + (); +} \ No newline at end of file diff --git a/mifosng-android/src/main/AndroidManifest.xml b/mifosng-android/src/main/AndroidManifest.xml index 6055b6ff933..ae537b48d9f 100755 --- a/mifosng-android/src/main/AndroidManifest.xml +++ b/mifosng-android/src/main/AndroidManifest.xml @@ -12,21 +12,21 @@ xmlns:tools="http://schemas.android.com/tools"> - - - + + tools:targetApi="33"> + android:configChanges="uiMode" + android:exported="true"> @@ -47,6 +47,28 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mifosng-android/src/main/assets/icudt46l.zip b/mifosng-android/src/main/assets/icudt46l.zip deleted file mode 100755 index 91dc7f71d30..00000000000 Binary files a/mifosng-android/src/main/assets/icudt46l.zip and /dev/null differ diff --git a/mifosng-android/src/main/ic_launcher-playstore.png b/mifosng-android/src/main/ic_launcher-playstore.png new file mode 100644 index 00000000000..b413a62c711 Binary files /dev/null and b/mifosng-android/src/main/ic_launcher-playstore.png differ diff --git a/mifosng-android/src/main/java/com/mifos/application/App.kt b/mifosng-android/src/main/java/com/mifos/application/App.kt deleted file mode 100644 index f874465d220..00000000000 --- a/mifosng-android/src/main/java/com/mifos/application/App.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.application - -import android.content.Context -import android.graphics.Typeface -import android.os.StrictMode -import android.os.StrictMode.VmPolicy -import androidx.multidex.MultiDexApplication -import com.facebook.stetho.Stetho -import com.mifos.core.common.utils.LanguageHelper.onAttach -import com.raizlabs.android.dbflow.config.FlowConfig -import com.raizlabs.android.dbflow.config.FlowManager -import dagger.hilt.android.HiltAndroidApp - -/** - * Created by ishankhanna on 13/03/15. - */ -@HiltAndroidApp -class App : MultiDexApplication() { - override fun onCreate() { - super.onCreate() - - instance = this -// JobManager.create(this).addJobCreator(OfflineJobCreator()) - // Initializing the DBFlow and SQL Cipher Encryption - FlowManager.init(FlowConfig.Builder(this).build()) - Stetho.initializeWithDefaults(this) - val policy = VmPolicy.Builder() - .detectFileUriExposure() - .build() - StrictMode.setVmPolicy(policy) - } - - override fun attachBaseContext(base: Context) { - super.attachBaseContext(onAttach(base)) - } - - companion object { - @JvmField - val typefaceManager: MutableMap = HashMap() - - @JvmStatic - var instance: App? = null - - @JvmStatic - val context: App? - get() = instance - - @JvmStatic - operator fun get(context: Context): App { - return context.applicationContext as App - } - } -} diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/AndroidClient.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/AndroidClient.kt index 560dd411b6a..ff2f0d7cc4c 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/AndroidClient.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/AndroidClient.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Mifos Initiative + * Copyright 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -9,41 +9,50 @@ */ package com.mifos.mifosxdroid -import androidx.compose.runtime.Composable -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.rememberNavController -import com.mifos.feature.auth.navigation.authNavGraph -import com.mifos.feature.auth.navigation.navigateToLogin -import com.mifos.feature.splash.navigation.SplashScreens -import com.mifos.feature.splash.navigation.splashNavGraph -import com.mifos.mifosxdroid.navigation.homeGraph -import com.mifos.mifosxdroid.navigation.navigateHome -import com.mifos.mifosxdroid.navigation.passcodeNavGraph -import org.mifos.library.passcode.navigateToPasscodeScreen - -@Composable -fun AndroidClient() { - val navController = rememberNavController() - - NavHost( - navController = navController, - startDestination = SplashScreens.SplashScreenRoute.route, - ) { - splashNavGraph( - navigatePasscode = navController::navigateHome, - navigateLogin = navController::navigateToLogin, - ) - - passcodeNavGraph( - navController = navController, - ) - - authNavGraph( - navigatePasscode = navController::navigateToPasscodeScreen, - navigateHome = navController::navigateHome, - updateServerConfig = {}, - ) - - homeGraph() +import android.content.Context +import android.graphics.Typeface +import android.os.StrictMode +import androidx.multidex.MultiDexApplication +import com.mifos.core.common.utils.LanguageHelper +import com.raizlabs.android.dbflow.config.FlowConfig +import com.raizlabs.android.dbflow.config.FlowManager +import dagger.hilt.android.HiltAndroidApp + +/** + * Created by ishankhanna on 13/03/15. + */ +@HiltAndroidApp +class AndroidClient : MultiDexApplication() { + override fun onCreate() { + super.onCreate() + + instance = this + // Initializing the DBFlow and SQL Cipher Encryption + FlowManager.init(FlowConfig.Builder(this).build()) + val policy = StrictMode.VmPolicy.Builder() + .detectFileUriExposure() + .build() + StrictMode.setVmPolicy(policy) + } + + override fun attachBaseContext(base: Context) { + super.attachBaseContext(LanguageHelper.onAttach(base)) + } + + companion object { + @JvmField + val typefaceManager: MutableMap = HashMap() + + @JvmStatic + var instance: AndroidClient? = null + + @JvmStatic + val context: AndroidClient? + get() = instance + + @JvmStatic + operator fun get(context: Context): AndroidClient { + return context.applicationContext as AndroidClient + } } } diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/AndroidClientActivity.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/AndroidClientActivity.kt index 37434930fe9..d3e56bd1b9d 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/AndroidClientActivity.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/AndroidClientActivity.kt @@ -11,16 +11,137 @@ package com.mifos.mifosxdroid import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.WindowCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.compose.rememberNavController +import com.mifos.core.designsystem.theme.MifosTheme +import com.mifos.mifosxdroid.MainState.Loading +import com.mifos.mifosxdroid.navigation.MifosNavGraph.AUTH_GRAPH +import com.mifos.mifosxdroid.navigation.MifosNavGraph.PASSCODE_GRAPH +import com.mifos.mifosxdroid.navigation.RootNavGraph +import com.mifos.mifosxdroid.utils.isSystemInDarkTheme import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch @AndroidEntryPoint class AndroidClientActivity : ComponentActivity() { + private val viewModel: AndroidClientViewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { + val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) + + // We keep this as a mutable state, so that we can track changes inside the composition. + // This allows us to react to dark/light mode changes. + var themeSettings by mutableStateOf( + ThemeSettings( + darkTheme = resources.configuration.isSystemInDarkTheme, + androidTheme = Loading.shouldUseAndroidTheme, + disableDynamicTheming = Loading.shouldDisableDynamicTheming, + ), + ) + + var authenticated: Boolean by mutableStateOf(false) + + WindowCompat.setDecorFitsSystemWindows(window, false) + + // Update the uiState + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + combine( + isSystemInDarkTheme(), + viewModel.state, + ) { systemDark, uiState -> + ThemeSettings( + darkTheme = uiState.shouldUseDarkTheme(systemDark), + androidTheme = uiState.shouldUseAndroidTheme, + disableDynamicTheming = uiState.shouldDisableDynamicTheming, + ) + }.onEach { themeSettings = it } + .map { it.darkTheme } + .distinctUntilChanged() + .collect { darkTheme -> + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.auto( + lightScrim = android.graphics.Color.TRANSPARENT, + darkScrim = android.graphics.Color.TRANSPARENT, + ) { darkTheme }, + navigationBarStyle = SystemBarStyle.auto( + lightScrim = lightScrim, + darkScrim = darkScrim, + ) { darkTheme }, + ) + } + } + } + + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.authenticated.onEach { authenticated = it }.collect() + } + } + + // Keep the splash screen on-screen until the UI state is loaded. This condition is + // evaluated each time the app needs to be redrawn so it should be fast to avoid blocking + // the UI. + splashScreen.setKeepOnScreenCondition { viewModel.state.value.shouldKeepSplashScreen() } + setContent { - AndroidClient() + val navController = rememberNavController() + val startDestination = if (authenticated) { + PASSCODE_GRAPH + } else { + AUTH_GRAPH + } + + MifosTheme( + darkTheme = themeSettings.darkTheme, + androidTheme = themeSettings.androidTheme, + disableDynamicTheming = themeSettings.disableDynamicTheming, + ) { + RootNavGraph( + navController = navController, + onClickLogout = { + viewModel.logOut() + navController.navigate(AUTH_GRAPH) { + popUpTo(navController.graph.id) { + inclusive = true + } + } + }, + onUpdateConfig = viewModel::updateConfig, + startDestination = startDestination, + ) + } } } } + +private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF) +private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b) + +/** + * Class for the system theme settings. + * This wrapping class allows us to combine all the changes and prevent unnecessary recompositions. + */ +data class ThemeSettings( + val darkTheme: Boolean, + val androidTheme: Boolean, + val disableDynamicTheming: Boolean, +) diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/AndroidClientViewModel.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/AndroidClientViewModel.kt new file mode 100644 index 00000000000..809af741321 --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/AndroidClientViewModel.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.mifosxdroid + +import android.app.Application +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.CountDownTimer +import android.widget.Toast +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifos.core.data.repository.PreferenceRepository +import com.mifos.core.model.DarkThemeConfig +import com.mifos.core.model.ThemeBrand +import com.mifos.core.model.UserData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.mifos.library.passcode.data.PasscodeManager +import javax.inject.Inject + +@HiltViewModel +class AndroidClientViewModel @Inject constructor( + private val application: Application, + private val preferenceRepository: PreferenceRepository, + private val passcodeManager: PasscodeManager, +) : ViewModel() { + + val state = preferenceRepository.userData.map { + MainState.Success(it) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = MainState.Loading, + ) + + val authenticated = preferenceRepository.isAuthenticated.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + + fun logOut() { + viewModelScope.launch { + preferenceRepository.logOut() + passcodeManager.clearPasscode() + } + } + + fun updateConfig() { + preferenceRepository.logOut() + passcodeManager.clearPasscode() + showRestartCountdownToast(application, 5) + } +} + +sealed interface MainState { + data object Loading : MainState + + data class Success(val userData: UserData) : MainState { + override val shouldDisableDynamicTheming = !userData.useDynamicColor + + override val shouldUseAndroidTheme: Boolean = when (userData.themeBrand) { + ThemeBrand.DEFAULT -> false + ThemeBrand.ANDROID -> true + } + + override fun shouldUseDarkTheme(isSystemDarkTheme: Boolean) = + when (userData.darkThemeConfig) { + DarkThemeConfig.FOLLOW_SYSTEM -> isSystemDarkTheme + DarkThemeConfig.LIGHT -> false + DarkThemeConfig.DARK -> true + } + } + + /** + * Returns `true` if the state wasn't loaded yet and it should keep showing the splash screen. + */ + fun shouldKeepSplashScreen() = this is Loading + + /** + * Returns `true` if the dynamic color is disabled. + */ + val shouldDisableDynamicTheming: Boolean get() = true + + /** + * Returns `true` if the Android theme should be used. + */ + val shouldUseAndroidTheme: Boolean get() = false + + /** + * Returns `true` if dark theme should be used. + */ + fun shouldUseDarkTheme(isSystemDarkTheme: Boolean) = isSystemDarkTheme +} + +private fun showRestartCountdownToast(context: Context, seconds: Int) { + val countDownTimer = object : CountDownTimer( + /* millisInFuture = */ + (seconds * 1000).toLong(), + /* countDownInterval = */ + 1000, + ) { + override fun onTick(millisUntilFinished: Long) { + val secondsRemaining = millisUntilFinished / 1000 + Toast.makeText( + context, + "Restarting app in $secondsRemaining seconds", + Toast.LENGTH_SHORT, + ).show() + } + + override fun onFinish() { + context.restartApplication() + } + } + countDownTimer.start() +} + +private fun Context.restartApplication() { + val packageManager: PackageManager = this.packageManager + val intent: Intent = packageManager.getLaunchIntentForPackage(this.packageName)!! + val componentName: ComponentName = intent.component!! + val restartIntent: Intent = Intent.makeRestartActivityTask(componentName) + this.startActivity(restartIntent) + Runtime.getRuntime().exit(0) +} diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/HomeDestinationsScreen.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/components/HomeDestinationsScreen.kt similarity index 98% rename from mifosng-android/src/main/java/com/mifos/mifosxdroid/HomeDestinationsScreen.kt rename to mifosng-android/src/main/java/com/mifos/mifosxdroid/components/HomeDestinationsScreen.kt index 0e786acd36d..52bd2f348d3 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/HomeDestinationsScreen.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/components/HomeDestinationsScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 Mifos Initiative + * Copyright 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -7,7 +7,7 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ -package com.mifos.mifosxdroid +package com.mifos.mifosxdroid.components import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.Assignment diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/components/MifosNavigationBar.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/components/MifosNavigationBar.kt index 2fdc7fd2867..164b19c2099 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/components/MifosNavigationBar.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/components/MifosNavigationBar.kt @@ -16,10 +16,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.sp -import com.mifos.mifosxdroid.HomeDestinationsScreen @Composable fun MifosNavigationBar( @@ -36,7 +32,9 @@ fun MifosNavigationBar( ) } - NavigationBar(modifier = modifier) { + NavigationBar( + modifier = modifier, + ) { tabs.forEach { item -> val targetRoute = item.route val selected = route.contains(targetRoute) @@ -46,18 +44,11 @@ fun MifosNavigationBar( Icon( imageVector = it, contentDescription = item.title, - tint = if (selected) Color.Black else Color.Black.copy(0.7f), ) } }, label = { - Text( - text = item.title, - maxLines = 1, - fontSize = 12.sp, - textAlign = TextAlign.Center, - color = if (selected) Color.Black else Color.Black.copy(0.7f), - ) + Text(text = item.title) }, selected = selected, onClick = { onRouteSelected(targetRoute) }, diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/components/Navigation.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/components/Navigation.kt index 66c615de19a..7ded92e1ab3 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/components/Navigation.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/components/Navigation.kt @@ -20,7 +20,6 @@ import com.mifos.core.common.utils.Constants import com.mifos.feature.about.navigation.aboutScreen import com.mifos.feature.activate.navigation.activateScreen import com.mifos.feature.activate.navigation.navigateToActivateScreen -import com.mifos.feature.auth.navigation.navigateToLogin import com.mifos.feature.center.navigation.centerNavGraph import com.mifos.feature.center.navigation.navigateCenterDetailsScreenRoute import com.mifos.feature.center.navigation.navigateCreateCenterScreenRoute @@ -56,9 +55,10 @@ import com.mifos.feature.savings.navigation.navigateToSavingsAccountSummaryScree import com.mifos.feature.savings.navigation.savingsNavGraph import com.mifos.feature.search.navigation.SearchScreens import com.mifos.feature.search.navigation.searchNavGraph +import com.mifos.feature.settings.navigation.navigateToUpdateServerConfig import com.mifos.feature.settings.navigation.settingsScreen import com.mifos.mifosxdroid.R -import com.mifos.utils.MifosResponseHandler +import com.mifos.mifosxdroid.utils.MifosResponseHandler @Composable fun Navigation( @@ -66,6 +66,7 @@ fun Navigation( padding: PaddingValues, modifier: Modifier = Modifier, startDestination: String = SearchScreens.SearchScreenRoute.route, + onUpdateConfig: () -> Unit, ) { val context = LocalContext.current @@ -104,8 +105,8 @@ fun Navigation( }, hasDatatables = navController::navigateDataTableList, onDocumentClicked = navController::navigateToDocumentListScreen, - onCardClicked = { position, survey -> - // TODO + onCardClicked = { _, _ -> + // TODO:: Add Card Click }, ) @@ -148,7 +149,7 @@ fun Navigation( addLoanAccountScreen( onBackPressed = navController::popBackStack, - dataTable = { dataTable, payload -> + dataTable = { _, _ -> // navController.navigateDataTableList(dataTable, payload, Constants.CLIENT_LOAN) // TODO() }, @@ -179,7 +180,6 @@ fun Navigation( addSavingsAccount = { navController.navigateToAddSavingsAccount(it, 0, true) }, - ) reportNavGraph( @@ -196,9 +196,10 @@ fun Navigation( settingsScreen( navigateBack = navController::popBackStack, - navigateToLoginScreen = { navController.navigateToLogin() }, + onUpdateConfig = onUpdateConfig, + onClickUpdateConfig = navController::navigateToUpdateServerConfig, + // TODO:: Add change passcode route changePasscode = { }, - languageChanged = { }, ) aboutScreen( @@ -217,7 +218,13 @@ fun Navigation( clientCreated = { client, userStatus -> navController.popBackStack() navController.popBackStack() - Toast.makeText(context, context.resources.getString(R.string.client) + MifosResponseHandler.getResponse(userStatus), Toast.LENGTH_LONG).show() + Toast.makeText( + context, + context.resources.getString(R.string.client) + MifosResponseHandler.getResponse( + userStatus, + ), + Toast.LENGTH_LONG, + ).show() if (userStatus == Constants.USER_ONLINE) { client.clientId?.let { navController.navigateClientDetailsScreen(it) } diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/components/NavigationConstants.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/components/NavigationConstants.kt index f1825a6b9f1..fa6bff81314 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/components/NavigationConstants.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/components/NavigationConstants.kt @@ -9,8 +9,6 @@ */ package com.mifos.mifosxdroid.components -import com.mifos.mifosxdroid.HomeDestinationsScreen - object NavigationConstants { private val NavScreenRoutes = listOf( @@ -23,4 +21,14 @@ object NavigationConstants { fun isNavScreen(route: String?): Boolean { return NavScreenRoutes.contains(route) } + + fun getNavTitle(route: String?): String { + return when (route) { + HomeDestinationsScreen.SearchScreen.route -> "Dashboard" // Override the title + HomeDestinationsScreen.ClientListScreen.route -> HomeDestinationsScreen.ClientListScreen.title + HomeDestinationsScreen.CenterListScreen.route -> HomeDestinationsScreen.CenterListScreen.title + HomeDestinationsScreen.GroupListScreen.route -> HomeDestinationsScreen.GroupListScreen.title + else -> "" + } + } } diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/injection/module/RepositoryModule.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/injection/module/RepositoryModule.kt deleted file mode 100644 index 2b55f418ebf..00000000000 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/injection/module/RepositoryModule.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.mifosxdroid.injection.module - -import com.mifos.core.data.repository.CreateNewClientRepository -import com.mifos.core.data.repository.DocumentDialogRepository -import com.mifos.core.data.repository.NoteRepository -import com.mifos.core.data.repository.OfflineDashboardRepository -import com.mifos.core.data.repository.SavingsAccountActivateRepository -import com.mifos.core.data.repository.SavingsAccountApprovalRepository -import com.mifos.core.data.repository.SavingsAccountRepository -import com.mifos.core.data.repository.SavingsAccountSummaryRepository -import com.mifos.core.data.repository.SavingsAccountTransactionRepository -import com.mifos.core.data.repository.SyncCenterPayloadsRepository -import com.mifos.core.data.repository.SyncCentersDialogRepository -import com.mifos.core.data.repository.SyncClientPayloadsRepository -import com.mifos.core.data.repository.SyncClientsDialogRepository -import com.mifos.core.data.repository.SyncGroupPayloadsRepository -import com.mifos.core.data.repository.SyncGroupsDialogRepository -import com.mifos.core.data.repository.SyncLoanRepaymentTransactionRepository -import com.mifos.core.data.repository.SyncSavingsAccountTransactionRepository -import com.mifos.core.data.repositoryImp.CreateNewClientRepositoryImp -import com.mifos.core.data.repositoryImp.DocumentDialogRepositoryImp -import com.mifos.core.data.repositoryImp.NoteRepositoryImp -import com.mifos.core.data.repositoryImp.OfflineDashboardRepositoryImp -import com.mifos.core.data.repositoryImp.SavingsAccountActivateRepositoryImp -import com.mifos.core.data.repositoryImp.SavingsAccountApprovalRepositoryImp -import com.mifos.core.data.repositoryImp.SavingsAccountRepositoryImp -import com.mifos.core.data.repositoryImp.SavingsAccountSummaryRepositoryImp -import com.mifos.core.data.repositoryImp.SavingsAccountTransactionRepositoryImp -import com.mifos.core.data.repositoryImp.SyncCenterPayloadsRepositoryImp -import com.mifos.core.data.repositoryImp.SyncCentersDialogRepositoryImp -import com.mifos.core.data.repositoryImp.SyncClientPayloadsRepositoryImp -import com.mifos.core.data.repositoryImp.SyncClientsDialogRepositoryImp -import com.mifos.core.data.repositoryImp.SyncGroupPayloadsRepositoryImp -import com.mifos.core.data.repositoryImp.SyncGroupsDialogRepositoryImp -import com.mifos.core.data.repositoryImp.SyncLoanRepaymentTransactionRepositoryImp -import com.mifos.core.data.repositoryImp.SyncSavingsAccountTransactionRepositoryImp -import com.mifos.core.network.datamanager.DataManagerCenter -import com.mifos.core.network.datamanager.DataManagerClient -import com.mifos.core.network.datamanager.DataManagerDocument -import com.mifos.core.network.datamanager.DataManagerGroups -import com.mifos.core.network.datamanager.DataManagerLoan -import com.mifos.core.network.datamanager.DataManagerNote -import com.mifos.core.network.datamanager.DataManagerOffices -import com.mifos.core.network.datamanager.DataManagerSavings -import com.mifos.core.network.datamanager.DataManagerStaff -import com.mifos.core.network.datamanager.DataManagerSurveys -import com.mifos.feature.settings.syncSurvey.SyncSurveysDialogRepository -import com.mifos.feature.settings.syncSurvey.SyncSurveysDialogRepositoryImp -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -/** - * Created by Aditya Gupta on 06/08/23. - */ - -@Module -@InstallIn(SingletonComponent::class) -class RepositoryModule { - - @Provides - fun providesSavingsAccountSummaryRepository(dataManagerSavings: DataManagerSavings): SavingsAccountSummaryRepository { - return SavingsAccountSummaryRepositoryImp(dataManagerSavings) - } - - @Provides - fun providesNoteRepository(dataManagerNote: DataManagerNote): NoteRepository { - return NoteRepositoryImp(dataManagerNote) - } - - @Provides - fun providesSavingAccountRepository(dataManagerSavings: DataManagerSavings): SavingsAccountRepository { - return SavingsAccountRepositoryImp(dataManagerSavings) - } - - @Provides - fun providesCreateNewClientRepository( - dataManagerClient: DataManagerClient, - dataManagerOffices: DataManagerOffices, - dataManagerStaff: DataManagerStaff, - ): CreateNewClientRepository { - return CreateNewClientRepositoryImp(dataManagerClient, dataManagerOffices, dataManagerStaff) - } - - @Provides - fun providesSavingsAccountTransactionRepository(dataManagerSavings: DataManagerSavings): SavingsAccountTransactionRepository { - return SavingsAccountTransactionRepositoryImp(dataManagerSavings) - } - - @Provides - fun providesSavingsAccountActivateRepository(dataManagerSavings: DataManagerSavings): SavingsAccountActivateRepository { - return SavingsAccountActivateRepositoryImp(dataManagerSavings) - } - - @Provides - fun providesSavingsAccountApprovalRepository(dataManagerSavings: DataManagerSavings): SavingsAccountApprovalRepository { - return SavingsAccountApprovalRepositoryImp(dataManagerSavings) - } - - @Provides - fun providesDocumentDialogRepository(dataManagerDocument: DataManagerDocument): DocumentDialogRepository { - return DocumentDialogRepositoryImp(dataManagerDocument) - } - - @Provides - fun providesSyncSurveysDialogRepository(dataManagerSurvey: DataManagerSurveys): SyncSurveysDialogRepository { - return SyncSurveysDialogRepositoryImp(dataManagerSurvey) - } - - @Provides - fun providesSyncGroupsDialogRepository( - dataManagerGroups: DataManagerGroups, - dataManagerLoan: DataManagerLoan, - dataManagerSavings: DataManagerSavings, - dataManagerClient: DataManagerClient, - ): SyncGroupsDialogRepository { - return SyncGroupsDialogRepositoryImp( - dataManagerGroups, - dataManagerLoan, - dataManagerSavings, - dataManagerClient, - ) - } - - @Provides - fun providesSyncClientsDialogRepository( - dataManagerClient: DataManagerClient, - dataManagerLoan: DataManagerLoan, - dataManagerSavings: DataManagerSavings, - ): SyncClientsDialogRepository { - return SyncClientsDialogRepositoryImp( - dataManagerClient, - dataManagerLoan, - dataManagerSavings, - ) - } - - @Provides - fun providesSyncCentersDialogRepository( - dataManagerCenter: DataManagerCenter, - dataManagerLoan: DataManagerLoan, - dataManagerSavings: DataManagerSavings, - dataManagerGroups: DataManagerGroups, - dataManagerClient: DataManagerClient, - ): SyncCentersDialogRepository { - return SyncCentersDialogRepositoryImp( - dataManagerCenter, - dataManagerLoan, - dataManagerSavings, - dataManagerGroups, - dataManagerClient, - ) - } - - @Provides - fun providesOfflineDashboardRepository( - dataManagerClient: DataManagerClient, - dataManagerGroups: DataManagerGroups, - dataManagerCenter: DataManagerCenter, - dataManagerLoan: DataManagerLoan, - dataManagerSavings: DataManagerSavings, - ): OfflineDashboardRepository { - return OfflineDashboardRepositoryImp( - dataManagerClient, - dataManagerGroups, - dataManagerCenter, - dataManagerLoan, - dataManagerSavings, - ) - } - - @Provides - fun providesSyncCenterPayloadsRepository(dataManagerCenter: DataManagerCenter): SyncCenterPayloadsRepository { - return SyncCenterPayloadsRepositoryImp(dataManagerCenter) - } - - @Provides - fun providesSyncSavingsAccountTransactionRepository( - dataManagerSavings: DataManagerSavings, - dataManagerLoan: DataManagerLoan, - ): SyncSavingsAccountTransactionRepository { - return SyncSavingsAccountTransactionRepositoryImp(dataManagerSavings, dataManagerLoan) - } - - @Provides - fun providesSyncLoanRepaymentTransactionRepository(dataManagerLoan: DataManagerLoan): SyncLoanRepaymentTransactionRepository { - return SyncLoanRepaymentTransactionRepositoryImp(dataManagerLoan) - } - - @Provides - fun providesSyncGroupPayloadsRepository(dataManagerGroups: DataManagerGroups): SyncGroupPayloadsRepository { - return SyncGroupPayloadsRepositoryImp(dataManagerGroups) - } - - @Provides - fun providesSyncClientPayloadsRepository(dataManagerClient: DataManagerClient): SyncClientPayloadsRepository { - return SyncClientPayloadsRepositoryImp(dataManagerClient) - } -} diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/navigation/HomeNavigation.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/navigation/HomeNavigation.kt index 149504b8315..b004240cf8b 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/navigation/HomeNavigation.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/navigation/HomeNavigation.kt @@ -40,7 +40,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -60,48 +59,52 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navigation -import com.mifos.core.designsystem.theme.Black -import com.mifos.core.designsystem.theme.White -import com.mifos.feature.splash.navigation.SplashScreens -import com.mifos.mifosxdroid.HomeDestinationsScreen import com.mifos.mifosxdroid.R +import com.mifos.mifosxdroid.components.HomeDestinationsScreen import com.mifos.mifosxdroid.components.MifosNavigationBar import com.mifos.mifosxdroid.components.Navigation import com.mifos.mifosxdroid.components.NavigationConstants import kotlinx.coroutines.launch -fun NavGraphBuilder.homeGraph() { +fun NavGraphBuilder.homeGraph( + onClickLogout: () -> Unit, + onUpdateConfig: () -> Unit, +) { navigation( startDestination = HomeScreens.HomeScreen.route, - route = "home_screen_route", + route = MifosNavGraph.MAIN_GRAPH, ) { - homeNavigate() + homeNavigate( + onClickLogout = onClickLogout, + onUpdateConfig = onUpdateConfig, + ) } } -fun NavGraphBuilder.homeNavigate() { +private fun NavGraphBuilder.homeNavigate( + onClickLogout: () -> Unit, + onUpdateConfig: () -> Unit, +) { composable( route = HomeScreens.HomeScreen.route, ) { - HomeNavigation() - } -} - -fun NavController.navigateHome() { - navigate(HomeScreens.HomeScreen.route) { - popBackStack(route = SplashScreens.SplashScreenRoute.route, inclusive = true) + HomeNavigation( + onClickLogout = onClickLogout, + onUpdateConfig = onUpdateConfig, + ) } } @Composable -fun HomeNavigation( +private fun HomeNavigation( + onClickLogout: () -> Unit, modifier: Modifier = Modifier, + onUpdateConfig: () -> Unit, ) { val navController = rememberNavController() val navBackStackEntry by navController.currentBackStackEntryAsState() @@ -189,15 +192,7 @@ fun HomeNavigation( navigationDrawerTabs.forEachIndexed { index, item -> NavigationDrawerItem( label = { - Text( - text = item.title, - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - fontStyle = FontStyle.Normal, - ), - color = Black, - ) + Text(text = item.title) }, selected = index == selectedItemIndex, onClick = { @@ -231,7 +226,8 @@ fun HomeNavigation( ) } }, - modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), + modifier = Modifier + .padding(NavigationDrawerItemDefaults.ItemPadding), ) if (index == (navigationDrawerTabs.size - 2)) { HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) @@ -244,18 +240,24 @@ fun HomeNavigation( Scaffold( topBar = { if (isNavScreen) { + val title = NavigationConstants.getNavTitle(route) TopAppBar( title = { - Text("Dashboard") + Text( + text = title, + fontWeight = FontWeight.SemiBold, + ) }, navigationIcon = { - IconButton(onClick = { - scope.launch { - drawerState.apply { - if (isClosed) open() else close() + IconButton( + onClick = { + scope.launch { + drawerState.apply { + if (isClosed) open() else close() + } } - } - }) { + }, + ) { Icon( imageVector = Icons.Default.Menu, contentDescription = "Menu", @@ -263,30 +265,27 @@ fun HomeNavigation( } }, actions = { - IconButton(onClick = { }) { + IconButton(onClick = onClickLogout) { Icon( imageVector = Icons.AutoMirrored.Rounded.Logout, contentDescription = null, ) } }, - colors = TopAppBarDefaults.topAppBarColors(White), ) } }, bottomBar = { if (isNavScreen) { - Column { - route?.let { - MifosNavigationBar(route = it) { target -> - navController.apply { - navigate(target) { - restoreState = true - launchSingleTop = true - graph.startDestinationRoute?.let { - popUpTo(route = HomeDestinationsScreen.SearchScreen.route) { - saveState = true - } + route?.let { + MifosNavigationBar(route = it) { target -> + navController.apply { + navigate(target) { + restoreState = true + launchSingleTop = true + graph.startDestinationRoute?.let { + popUpTo(route = HomeDestinationsScreen.SearchScreen.route) { + saveState = true } } } @@ -295,9 +294,12 @@ fun HomeNavigation( } } }, - containerColor = White, ) { paddingValues -> - Navigation(navController = navController, padding = paddingValues) + Navigation( + navController = navController, + padding = paddingValues, + onUpdateConfig = onUpdateConfig, + ) } } } diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/navigation/MifosNavGraph.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/navigation/MifosNavGraph.kt index 43125acce06..305a5e926d4 100644 --- a/mifosng-android/src/main/java/com/mifos/mifosxdroid/navigation/MifosNavGraph.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/navigation/MifosNavGraph.kt @@ -14,4 +14,5 @@ internal object MifosNavGraph { const val AUTH_GRAPH = "auth_graph" const val PASSCODE_GRAPH = "passcode_graph" const val MAIN_GRAPH = "home_screen_route" + const val SETTINGS_GRAPH = "settings_screen_route" } diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/navigation/RootNavGraph.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/navigation/RootNavGraph.kt new file mode 100644 index 00000000000..3ec1b39fa15 --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/navigation/RootNavGraph.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.mifosxdroid.navigation + +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.navigation +import com.mifos.feature.auth.navigation.authNavGraph +import com.mifos.feature.settings.updateServer.UpdateServerConfigScreenRoute +import org.mifos.library.passcode.navigateToPasscodeScreen + +@Composable +fun RootNavGraph( + navController: NavHostController, + startDestination: String, + onClickLogout: () -> Unit, + onUpdateConfig: () -> Unit, +) { + NavHost( + navController = navController, + startDestination = startDestination, + route = MifosNavGraph.ROOT_GRAPH, + ) { + authNavGraph( + route = MifosNavGraph.AUTH_GRAPH, + navigatePasscode = navController::navigateToPasscodeScreen, + updateServerConfig = navController::navigateToServerConfigGraph, + ) + + serverConfigGraph( + navigateBack = navController::popBackStack, + onUpdateConfig = onUpdateConfig, + ) + + passcodeNavGraph( + navController = navController, + ) + + homeGraph( + onClickLogout = onClickLogout, + onUpdateConfig = onUpdateConfig, + ) + } +} + +// Creating a new navigation graph to update server config in login screen +private const val SERVER_CONFIG_ROUTE = "update_server_config" + +private fun NavGraphBuilder.serverConfigGraph( + navigateBack: () -> Unit, + onUpdateConfig: () -> Unit, +) { + navigation( + startDestination = SERVER_CONFIG_ROUTE, + route = MifosNavGraph.SETTINGS_GRAPH, + ) { + composable( + route = SERVER_CONFIG_ROUTE, + ) { + UpdateServerConfigScreenRoute( + onBackClick = navigateBack, + onSuccessful = onUpdateConfig, + ) + } + } +} + +private fun NavController.navigateToServerConfigGraph() { + navigate(SERVER_CONFIG_ROUTE) +} diff --git a/mifosng-android/src/main/java/com/mifos/utils/MifosResponseHandler.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/utils/MifosResponseHandler.kt similarity index 94% rename from mifosng-android/src/main/java/com/mifos/utils/MifosResponseHandler.kt rename to mifosng-android/src/main/java/com/mifos/mifosxdroid/utils/MifosResponseHandler.kt index 89b35270a27..b33fae1c5e4 100644 --- a/mifosng-android/src/main/java/com/mifos/utils/MifosResponseHandler.kt +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/utils/MifosResponseHandler.kt @@ -7,7 +7,7 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ -package com.mifos.utils +package com.mifos.mifosxdroid.utils /** * Created by Rajan Maurya on 08/07/16. diff --git a/mifosng-android/src/main/java/com/mifos/mifosxdroid/utils/UiExtensions.kt b/mifosng-android/src/main/java/com/mifos/mifosxdroid/utils/UiExtensions.kt new file mode 100644 index 00000000000..3fe998f745c --- /dev/null +++ b/mifosng-android/src/main/java/com/mifos/mifosxdroid/utils/UiExtensions.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.mifosxdroid.utils + +import android.content.res.Configuration +import androidx.activity.ComponentActivity +import androidx.core.util.Consumer +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged + +/** + * Convenience wrapper for dark mode checking + */ +val Configuration.isSystemInDarkTheme + get() = (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + +/** + * Registers listener for configuration changes to retrieve whether system is in dark theme or not. + * Immediately upon subscribing, it sends the current value and then registers listener for changes. + */ +fun ComponentActivity.isSystemInDarkTheme() = callbackFlow { + channel.trySend(resources.configuration.isSystemInDarkTheme) + + val listener = Consumer { + channel.trySend(it.isSystemInDarkTheme) + } + + addOnConfigurationChangedListener(listener) + + awaitClose { removeOnConfigurationChangedListener(listener) } +} + .distinctUntilChanged() + .conflate() diff --git a/mifosng-android/src/main/java/com/mifos/utils/ThemeHelper.kt b/mifosng-android/src/main/java/com/mifos/utils/ThemeHelper.kt deleted file mode 100644 index e2f0fcc0487..00000000000 --- a/mifosng-android/src/main/java/com/mifos/utils/ThemeHelper.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.utils - -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.os.BuildCompat - -object ThemeHelper { - const val LIGHT_MODE = "light" - const val DARK_MODE = "dark" - const val DEFAULT_MODE = "default" - fun applyTheme(themePref: String) { - when (themePref) { - LIGHT_MODE -> { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - } - - DARK_MODE -> { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) - } - - else -> { - if (BuildCompat.isAtLeastQ()) { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) - } - } - } - } -} diff --git a/feature/splash/src/main/res/drawable/feature_splash_icon.png b/mifosng-android/src/main/res/drawable/ic_splash.png similarity index 100% rename from feature/splash/src/main/res/drawable/feature_splash_icon.png rename to mifosng-android/src/main/res/drawable/ic_splash.png diff --git a/mifosng-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mifosng-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000000..0222c3909a8 --- /dev/null +++ b/mifosng-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/mifosng-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mifosng-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000000..0222c3909a8 --- /dev/null +++ b/mifosng-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/mifosng-android/src/main/res/mipmap-hdpi/ic_launcher.webp b/mifosng-android/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 00000000000..e00c549e695 Binary files /dev/null and b/mifosng-android/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/mifosng-android/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/mifosng-android/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 00000000000..37a873cb147 Binary files /dev/null and b/mifosng-android/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/mifosng-android/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/mifosng-android/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 00000000000..a9ad10b14df Binary files /dev/null and b/mifosng-android/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/mifosng-android/src/main/res/mipmap-mdpi/ic_launcher.webp b/mifosng-android/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 00000000000..24ca1f1e631 Binary files /dev/null and b/mifosng-android/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/mifosng-android/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/mifosng-android/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 00000000000..536e30fa699 Binary files /dev/null and b/mifosng-android/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/mifosng-android/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/mifosng-android/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 00000000000..5a021c2a22f Binary files /dev/null and b/mifosng-android/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/mifosng-android/src/main/res/mipmap-xhdpi/ic_launcher.webp b/mifosng-android/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 00000000000..22e94c907c4 Binary files /dev/null and b/mifosng-android/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/mifosng-android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/mifosng-android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 00000000000..6e6abfad3be Binary files /dev/null and b/mifosng-android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/mifosng-android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/mifosng-android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 00000000000..0599ac9828c Binary files /dev/null and b/mifosng-android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/mifosng-android/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/mifosng-android/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 00000000000..8ce59f02744 Binary files /dev/null and b/mifosng-android/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/mifosng-android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/mifosng-android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 00000000000..94e4ab2b6bf Binary files /dev/null and b/mifosng-android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/mifosng-android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/mifosng-android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000000..594acdbca0c Binary files /dev/null and b/mifosng-android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/mifosng-android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/mifosng-android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 00000000000..bb159f14af5 Binary files /dev/null and b/mifosng-android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/mifosng-android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/mifosng-android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 00000000000..94daae17186 Binary files /dev/null and b/mifosng-android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/mifosng-android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/mifosng-android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000000..3d035014d5b Binary files /dev/null and b/mifosng-android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/mifosng-android/src/main/res/values-night/themes.xml b/mifosng-android/src/main/res/values-night/themes.xml new file mode 100644 index 00000000000..1eb3c68a634 --- /dev/null +++ b/mifosng-android/src/main/res/values-night/themes.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/feature/splash/src/main/AndroidManifest.xml b/mifosng-android/src/main/res/values/ic_launcher_background.xml similarity index 73% rename from feature/splash/src/main/AndroidManifest.xml rename to mifosng-android/src/main/res/values/ic_launcher_background.xml index 1dc76da0f7e..f15b9016317 100644 --- a/feature/splash/src/main/AndroidManifest.xml +++ b/mifosng-android/src/main/res/values/ic_launcher_background.xml @@ -1,6 +1,6 @@ - - - \ No newline at end of file + + #FFFFFF + \ No newline at end of file diff --git a/mifosng-android/src/main/res/values/themes.xml b/mifosng-android/src/main/res/values/themes.xml index 55fadec52af..2e4aa51b13c 100755 --- a/mifosng-android/src/main/res/values/themes.xml +++ b/mifosng-android/src/main/res/values/themes.xml @@ -8,6 +8,25 @@ See https://github.com/openMF/android-client/blob/master/LICENSE.md --> - - + + + + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 3c7c2b63bbc..ea92af864ff 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,5 @@ +import org.ajoberstar.reckon.gradle.ReckonExtension + pluginManagement { includeBuild("build-logic") repositories { @@ -17,6 +19,20 @@ dependencyResolutionManagement { } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version("0.8.0") + id("org.ajoberstar.reckon.settings") version("0.18.3") +} + +extensions.configure { + setDefaultInferredScope("patch") + stages("beta", "final") + setScopeCalc { java.util.Optional.of(org.ajoberstar.reckon.core.Scope.PATCH) } + setScopeCalc(calcScopeFromProp().or(calcScopeFromCommitMessages())) + setStageCalc(calcStageFromProp()) + setTagWriter { it.toString() } +} + rootProject.name = "AndroidClient" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") @@ -59,5 +75,3 @@ include(":feature:report") include(":feature:savings") include(":feature:search") include(":feature:settings") -include(":feature:splash") -//include(":feature:passcode")