diff --git a/.github/workflows/master_dev_ci.yml b/.github/workflows/master_dev_ci.yml index ba41c9883..88fe178ef 100644 --- a/.github/workflows/master_dev_ci.yml +++ b/.github/workflows/master_dev_ci.yml @@ -75,7 +75,7 @@ jobs: - name: Check Dependency Guard id: dependencyguard_verify continue-on-error: true - run: ./gradlew :mifospay-android:dependencyGuard + run: ./gradlew dependencyGuard - name: Prevent updating Dependency Guard baselines if this is a fork id: checkfork_dependencyguard @@ -88,7 +88,7 @@ jobs: id: dependencyguard_baseline if: steps.dependencyguard_verify.outcome == 'failure' && github.event_name == 'pull_request' run: | - ./gradlew :mifospay-android:dependencyGuardBaseline + ./gradlew dependencyGuardBaseline - name: Push new Dependency Guard baselines if available uses: stefanzweifel/git-auto-commit-action@v5 @@ -109,8 +109,7 @@ jobs: java-version: 17 - name: Run tests run: | - ./gradlew :mifospay-android:testDemoDebug -# ./gradlew testDemoDebug :lint:test :mifospay-android:lintProdRelease :lint:lint + ./gradlew testDemoDebug :lint:test :mifospay:lintProdRelease :lint:lint - name: Upload reports if: always() uses: actions/upload-artifact@v4 @@ -131,12 +130,12 @@ jobs: java-version: 17 - name: Build APKs - run: ./gradlew :mifospay-android:assembleDemoDebug + run: ./gradlew :mifospay:assembleDemoDebug - name: Check badging # This step is allowed to fail, as it's not critical for the build continue-on-error: true - run: ./gradlew :mifospay-android:checkProdReleaseBadging + run: ./gradlew :mifospay:checkProdReleaseBadging - name: Upload APKs uses: actions/upload-artifact@v4 diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 2f8cf70e2..0dd037feb 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -23,8 +23,11 @@ dependencies { compileOnly(libs.android.gradlePlugin) compileOnly(libs.android.tools.common) compileOnly(libs.compose.gradlePlugin) + compileOnly(libs.firebase.crashlytics.gradlePlugin) + compileOnly(libs.firebase.performance.gradlePlugin) compileOnly(libs.kotlin.gradlePlugin) compileOnly(libs.ksp.gradlePlugin) + compileOnly(libs.room.gradlePlugin) compileOnly(libs.detekt.gradlePlugin) compileOnly(libs.ktlint.gradlePlugin) compileOnly(libs.spotless.gradle) @@ -40,7 +43,6 @@ tasks { gradlePlugin { plugins { - // Android Plugins register("androidApplicationCompose") { id = "mifospay.android.application.compose" implementationClass = "AndroidApplicationComposeConventionPlugin" @@ -49,28 +51,46 @@ gradlePlugin { id = "mifospay.android.application" implementationClass = "AndroidApplicationConventionPlugin" } - + register("androidLibraryCompose") { + id = "mifospay.android.library.compose" + implementationClass = "AndroidLibraryComposeConventionPlugin" + } + register("androidLibrary") { + id = "mifospay.android.library" + implementationClass = "AndroidLibraryConventionPlugin" + } + register("androidFeature") { + id = "mifospay.android.feature" + implementationClass = "AndroidFeatureConventionPlugin" + } + register("androidTest") { + id = "mifospay.android.test" + implementationClass = "AndroidTestConventionPlugin" + } + register("androidRoom") { + id = "mifospay.android.room" + implementationClass = "AndroidRoomConventionPlugin" + } + register("androidFirebase") { + id = "mifospay.android.application.firebase" + implementationClass = "AndroidApplicationFirebaseConventionPlugin" + } + register("androidLint") { + id = "mifospay.android.lint" + implementationClass = "AndroidLintConventionPlugin" + } + register("jvmLibrary") { + id = "mifospay.jvm.library" + implementationClass = "JvmLibraryConventionPlugin" + } register("androidFlavors") { id = "mifospay.android.application.flavors" implementationClass = "AndroidApplicationFlavorsConventionPlugin" } - - // KMP & CMP Plugins - register("cmpFeature") { - id = "mifospay.cmp.feature" - implementationClass = "CMPFeatureConventionPlugin" + register("androidKoin") { + id = "mifospay.android.koin" + implementationClass = "AndroidKoinConventionPlugin" } - - register("kmpKoin") { - id = "mifospay.kmp.koin" - implementationClass = "KMPKoinConventionPlugin" - } - register("kmpLibrary") { - id = "mifospay.kmp.library" - implementationClass = "KMPLibraryConventionPlugin" - } - - // Static Analysis & Formatting Plugins register("detekt") { id = "mifos.detekt.plugin" implementationClass = "MifosDetektConventionPlugin" diff --git a/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt new file mode 100644 index 000000000..52e08e684 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt @@ -0,0 +1,66 @@ + +import com.android.build.gradle.LibraryExtension +import com.google.devtools.ksp.gradle.KspExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.kotlin +import org.mifospay.libs + +class AndroidFeatureConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply { + apply("mifospay.android.library") + apply("mifospay.android.koin") + } + + extensions.configure { + defaultConfig { + // set custom test runner + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + testOptions.animationsDisabled = true + } + + extensions.configure { + arg("KOIN_USE_COMPOSE_VIEWMODEL","true") + } + + dependencies { + add("implementation", project(":core:ui")) + add("implementation", project(":core:designsystem")) + add("implementation", project(":core:data")) + + add("implementation", project(":libs:material3-navigation")) + + add("implementation", libs.findLibrary("androidx.navigation.compose").get()) + add("implementation", libs.findLibrary("kotlinx.collections.immutable").get()) + add("implementation", libs.findLibrary("androidx.lifecycle.runtimeCompose").get()) + add("implementation", libs.findLibrary("androidx.lifecycle.viewModelCompose").get()) + add("implementation", libs.findLibrary("androidx.tracing.ktx").get()) + + add("implementation", platform(libs.findLibrary("koin-bom").get())) + add("implementation", libs.findLibrary("koin-android").get()) + add("implementation", libs.findLibrary("koin.androidx.compose").get()) + + add("implementation", libs.findLibrary("koin.android").get()) + add("implementation", libs.findLibrary("koin.androidx.navigation").get()) + add("implementation", libs.findLibrary("koin.androidx.compose").get()) + add("implementation", libs.findLibrary("koin.core.viewmodel").get()) + + add("androidTestImplementation", libs.findLibrary("androidx.lifecycle.runtimeTesting").get()) + + add("testImplementation", kotlin("test")) + + add("testImplementation", libs.findLibrary("koin.test").get()) + add("testImplementation", libs.findLibrary("koin.test.junit4").get()) + + add("debugImplementation", libs.findLibrary("androidx.compose.ui.test.manifest").get()) + add("androidTestImplementation", libs.findLibrary("androidx.navigation.testing").get()) + add("androidTestImplementation", libs.findLibrary("androidx.compose.ui.test").get()) + } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt index 23d0a8913..edea58609 100644 --- a/build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KMPLibraryConventionPlugin.kt @@ -1,44 +1,48 @@ - +import com.android.build.api.variant.LibraryAndroidComponentsExtension import com.android.build.gradle.LibraryExtension import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.kotlin import org.mifospay.configureFlavors import org.mifospay.configureKotlinAndroid -import org.mifospay.configureKotlinMultiplatform +import org.mifospay.configurePrintApksTask +import org.mifospay.disableUnnecessaryAndroidTests import org.mifospay.libs -class KMPLibraryConventionPlugin: Plugin { +class AndroidLibraryConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { with(pluginManager) { apply("com.android.library") - apply("org.jetbrains.kotlin.multiplatform") - apply("mifospay.kmp.koin") + apply("org.jetbrains.kotlin.android") + apply("mifospay.android.lint") apply("mifos.detekt.plugin") apply("mifos.spotless.plugin") + apply("mifos.ktlint.plugin") + apply("mifospay.android.koin") } - configureKotlinMultiplatform() - extensions.configure { configureKotlinAndroid(this) defaultConfig.targetSdk = 34 + testOptions.animationsDisabled = true configureFlavors(this) // The resource prefix is derived from the module name, // so resources inside ":core:module1" must be prefixed with "core_module1_" - resourcePrefix = path - .split("""\W""".toRegex()) - .drop(1).distinct() - .joinToString(separator = "_") - .lowercase() + "_" + resourcePrefix = path.split("""\W""".toRegex()).drop(1).distinct().joinToString(separator = "_").lowercase() + "_" + } + + extensions.configure { + configurePrintApksTask(this) + disableUnnecessaryAndroidTests(target) } dependencies { - add("commonTestImplementation", libs.findLibrary("kotlin.test").get()) - add("commonTestImplementation", libs.findLibrary("kotlinx.coroutines.test").get()) + add("testImplementation", kotlin("test")) + add("implementation", libs.findLibrary("androidx.tracing.ktx").get()) } } } -} \ No newline at end of file +} diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts index 0059b3e4d..6df4ddd5f 100644 --- a/core/analytics/build.gradle.kts +++ b/core/analytics/build.gradle.kts @@ -8,23 +8,17 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.kmp.library) - alias(libs.plugins.jetbrainsCompose) - alias(libs.plugins.compose.compiler) + alias(libs.plugins.mifospay.android.library) + alias(libs.plugins.mifospay.android.library.compose) } android { namespace = "org.mifospay.core.analytics" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.runtime) - } - androidMain.dependencies { - implementation(project.dependencies.platform(libs.firebase.bom)) - implementation(libs.firebase.analytics) - } - } -} \ No newline at end of file +dependencies { + implementation(libs.androidx.compose.runtime) + + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.analytics) +} diff --git a/core/analytics/src/main/kotlin/org/mifospay/core/analytics/di/AnalyticsModule.kt b/core/analytics/src/main/kotlin/org/mifospay/core/analytics/di/AnalyticsModule.kt new file mode 100644 index 000000000..472f15c95 --- /dev/null +++ b/core/analytics/src/main/kotlin/org/mifospay/core/analytics/di/AnalyticsModule.kt @@ -0,0 +1,25 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.analytics.di + +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.ktx.Firebase +import org.koin.dsl.module +import org.mifospay.core.analytics.AnalyticsHelper + +val AnalyticsModule = module { + + single { + Firebase.analytics + } + single { + FirebaseAnalyticsHelper(firebaseAnalytics = get()) + } +} diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 35745924c..27f8f6fac 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -8,56 +8,14 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.kmp.library) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.library) } android { namespace = "org.mifospay.common" } -kotlin { - - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64(), - ).forEach { - it.binaries.framework { - isStatic = false - export(libs.kermit.simple) - } - } - - sourceSets { - commonMain.dependencies { - implementation(libs.kotlinx.coroutines.core) - api(libs.coil.kt) - api(libs.coil.core) - api(libs.coil.svg) - api(libs.coil.network.ktor) - api(libs.kermit.logging) - api(libs.squareup.okio) - api(libs.jb.kotlin.stdlib) - api(libs.kotlinx.datetime) - } - - androidMain.dependencies { - implementation(libs.kotlinx.coroutines.android) - } - commonTest.dependencies { - implementation(libs.kotlinx.coroutines.test) - } - iosMain.dependencies { - api(libs.kermit.simple) - } - desktopMain.dependencies { - implementation(libs.kotlinx.coroutines.swing) - implementation(libs.kotlin.reflect) - } - jsMain.dependencies { - api(libs.jb.kotlin.stdlib.js) - api(libs.jb.kotlin.dom) - } - } -} \ No newline at end of file +dependencies { + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) +} diff --git a/core/common/src/main/kotlin/org/mifospay/common/Utils.kt b/core/common/src/main/kotlin/org/mifospay/common/Utils.kt new file mode 100644 index 000000000..c4acdc226 --- /dev/null +++ b/core/common/src/main/kotlin/org/mifospay/common/Utils.kt @@ -0,0 +1,47 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.common + +import java.text.NumberFormat +import java.util.Currency + +object Utils { + + @JvmStatic + fun getFormattedAccountBalance( + balance: Double?, + currencyCode: String?, + maximumFractionDigits: Int? = 0, + ): String { + val accountBalanceFormatter = NumberFormat.getCurrencyInstance() + accountBalanceFormatter.maximumFractionDigits = maximumFractionDigits ?: 0 + accountBalanceFormatter.currency = Currency.getInstance(currencyCode) + return accountBalanceFormatter.format(balance) + } + + // returns in "$ 10,000.00" format + fun getNewCurrencyFormatter( + balance: Double, + currencySymbol: String, + minimumFractionDigit: Int = 0, + ): String { + val accountBalanceFormatter = NumberFormat.getNumberInstance().apply { + maximumFractionDigits = 2 + minimumFractionDigits = minimumFractionDigit + } + return currencySymbol + " " + accountBalanceFormatter.format(balance) + } + + fun List.toArrayList(): ArrayList { + val array: ArrayList = ArrayList() + for (index in this) array.add(index) + return array + } +} diff --git a/core/common/src/main/kotlin/org/mifospay/core/network/di/CoroutineScopesModule.kt b/core/common/src/main/kotlin/org/mifospay/core/network/di/CoroutineScopesModule.kt new file mode 100644 index 000000000..501cf3b80 --- /dev/null +++ b/core/common/src/main/kotlin/org/mifospay/core/network/di/CoroutineScopesModule.kt @@ -0,0 +1,23 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.network.di + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val CoroutineScopesModule = module { + + single(named("ApplicationScope")) { + CoroutineScope(SupervisorJob() + Dispatchers.Default) + } +} diff --git a/core/common/src/main/kotlin/org/mifospay/core/network/di/DispatchersModule.kt b/core/common/src/main/kotlin/org/mifospay/core/network/di/DispatchersModule.kt new file mode 100644 index 000000000..9ad058f96 --- /dev/null +++ b/core/common/src/main/kotlin/org/mifospay/core/network/di/DispatchersModule.kt @@ -0,0 +1,21 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.network.di + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import org.koin.core.qualifier.named +import org.koin.dsl.module +import org.mifospay.core.network.MifosDispatchers + +val DispatchersModule = module { + single(named(MifosDispatchers.IO.name)) { Dispatchers.IO } + single(named(MifosDispatchers.Default.name)) { Dispatchers.Default } +} diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 23a406caa..0531b1e9f 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -8,7 +8,7 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.kmp.library) + alias(libs.plugins.mifospay.android.library) alias(libs.plugins.kotlin.parcelize) id("kotlinx-serialization") } @@ -23,26 +23,26 @@ android { } } -kotlin { - sourceSets { - commonMain.dependencies { - api(projects.core.common) - api(projects.core.datastore) - api(projects.core.model) - implementation(projects.core.network) - implementation(projects.core.analytics) - implementation(libs.kotlinx.serialization.json) - } - - commonTest.dependencies { - implementation(libs.multiplatform.settings) - implementation(libs.multiplatform.settings.test) - } +dependencies { + api(projects.core.common) + api(projects.core.model) + api(projects.core.network) - androidMain.dependencies { - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.tracing.ktx) - implementation(libs.koin.android) - } + implementation(libs.squareup.retrofit2) { + // exclude Retrofit’s OkHttp peer-dependency module and define your own module import + exclude(module = "okhttp") } -} \ No newline at end of file + implementation(libs.squareup.retrofit.adapter.rxjava) + implementation(libs.squareup.retrofit.converter.gson) + implementation(libs.squareup.okhttp) + implementation(libs.squareup.logging.interceptor) + + implementation(libs.reactivex.rxjava.android) + implementation(libs.reactivex.rxjava) + + testImplementation(libs.junit) + androidTestImplementation(libs.espresso.core) + + implementation(libs.kotlinx.serialization.json) + implementation(libs.koin.android) +} diff --git a/core/data/src/androidMain/kotlin/org/mifospay/core/data/util/ConnectivityManagerNetworkMonitor.kt b/core/data/src/androidMain/kotlin/org/mifospay/core/data/util/ConnectivityManagerNetworkMonitor.kt index 5d1e606e9..8e0986c79 100644 --- a/core/data/src/androidMain/kotlin/org/mifospay/core/data/util/ConnectivityManagerNetworkMonitor.kt +++ b/core/data/src/androidMain/kotlin/org/mifospay/core/data/util/ConnectivityManagerNetworkMonitor.kt @@ -16,17 +16,16 @@ import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest import android.net.NetworkRequest.Builder +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES import androidx.core.content.getSystemService -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.conflate -import kotlinx.coroutines.flow.flowOn internal class ConnectivityManagerNetworkMonitor( private val context: Context, - ioDispatcher: CoroutineDispatcher, ) : NetworkMonitor { override val isOnline: Flow = callbackFlow { val connectivityManager = context.getSystemService() @@ -54,10 +53,12 @@ internal class ConnectivityManagerNetworkMonitor( channel.trySend(networks.isNotEmpty()) } } + val request = Builder() .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .build() connectivityManager.registerNetworkCallback(request, callback) + /** * Sends the latest connectivity status to the underlying channel. */ @@ -67,10 +68,15 @@ internal class ConnectivityManagerNetworkMonitor( connectivityManager.unregisterNetworkCallback(callback) } } - .flowOn(ioDispatcher) .conflate() - private fun ConnectivityManager.isCurrentlyConnected() = activeNetwork - ?.let(::getNetworkCapabilities) - ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false + @Suppress("DEPRECATION") + private fun ConnectivityManager.isCurrentlyConnected() = when { + VERSION.SDK_INT >= VERSION_CODES.M -> + activeNetwork + ?.let(::getNetworkCapabilities) + ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + + else -> activeNetworkInfo?.isConnected + } ?: false } diff --git a/core/data/src/androidMain/kotlin/org/mifospay/core/data/util/TimeZoneBroadcastMonitor.kt b/core/data/src/androidMain/kotlin/org/mifospay/core/data/util/TimeZoneBroadcastMonitor.kt index 012bd5376..bd92230cd 100644 --- a/core/data/src/androidMain/kotlin/org/mifospay/core/data/util/TimeZoneBroadcastMonitor.kt +++ b/core/data/src/androidMain/kotlin/org/mifospay/core/data/util/TimeZoneBroadcastMonitor.kt @@ -19,6 +19,7 @@ import androidx.tracing.trace import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.callbackFlow @@ -30,10 +31,19 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.toKotlinTimeZone import java.time.ZoneId +/** + * Utility for reporting current timezone the device has set. + * It always emits at least once with default setting and then for each TZ change. + */ + +interface TimeZoneMonitor { + val currentTimeZone: Flow +} + internal class TimeZoneBroadcastMonitor( private val context: Context, appScope: CoroutineScope, - ioDispatcher: CoroutineDispatcher, + private val ioDispatcher: CoroutineDispatcher, ) : TimeZoneMonitor { override val currentTimeZone: SharedFlow = diff --git a/core/data/src/main/java/org/mifospay/core/data/base/TaskLooper.kt b/core/data/src/main/java/org/mifospay/core/data/base/TaskLooper.kt new file mode 100644 index 000000000..6fa7abbf7 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/base/TaskLooper.kt @@ -0,0 +1,62 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.base + +import org.mifospay.core.data.base.UseCase.UseCaseCallback + +class TaskLooper( + private val mUseCaseHandler: UseCaseHandler, +) { + var isFailed = false + private var tasksPending: Long = 0 + private var listener: Listener? = null + + fun addTask( + useCase: UseCase, + values: T, + taskData: TaskData, + ) { + tasksPending++ + mUseCaseHandler.execute( + useCase, + values, + object : UseCaseCallback { + override fun onSuccess(response: R) { + if (isFailed) return + listener!!.onTaskSuccess(taskData, response) + tasksPending-- + if (isCompleted) { + listener!!.onComplete() + } + } + + override fun onError(message: String) { + isFailed = true + listener!!.onFailure(message) + } + }, + ) + } + + private val isCompleted: Boolean + get() = tasksPending == 0L + + fun listen(listener: Listener?) { + this.listener = listener + } + + interface Listener { + fun onTaskSuccess(taskData: TaskData, response: R) + fun onComplete() + fun onFailure(message: String?) + } + + class TaskData(var taskName: String, var taskId: Int) +} diff --git a/core/data/src/main/java/org/mifospay/core/data/base/UseCaseFactory.kt b/core/data/src/main/java/org/mifospay/core/data/base/UseCaseFactory.kt new file mode 100644 index 000000000..d7f6b510b --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/base/UseCaseFactory.kt @@ -0,0 +1,33 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.base + +import org.mifospay.core.data.domain.usecase.account.FetchAccountTransfer +import org.mifospay.core.data.domain.usecase.client.FetchClientDetails +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants + +class UseCaseFactory( + private val mFineractRepository: FineractRepository, +) { + fun getUseCase(useCase: String): UseCase<*, *>? { + return when (useCase) { + Constants.FETCH_ACCOUNT_TRANSFER_USECASE -> { + FetchAccountTransfer(mFineractRepository) + } + + Constants.FETCH_CLIENT_DETAILS_USE_CASE -> { + FetchClientDetails(mFineractRepository) + } + + else -> null + } + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/di/DataModule.kt b/core/data/src/main/java/org/mifospay/core/data/di/DataModule.kt new file mode 100644 index 000000000..48a560509 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/di/DataModule.kt @@ -0,0 +1,214 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.di + +import org.koin.android.ext.koin.androidContext +import org.koin.core.qualifier.named +import org.koin.dsl.module +import org.mifospay.core.data.base.TaskLooper +import org.mifospay.core.data.base.UseCaseFactory +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.base.UseCaseScheduler +import org.mifospay.core.data.base.UseCaseThreadPoolScheduler +import org.mifospay.core.data.domain.usecase.account.BlockUnblockCommand +import org.mifospay.core.data.domain.usecase.account.DownloadTransactionReceipt +import org.mifospay.core.data.domain.usecase.account.FetchAccount +import org.mifospay.core.data.domain.usecase.account.FetchAccountTransaction +import org.mifospay.core.data.domain.usecase.account.FetchAccountTransactions +import org.mifospay.core.data.domain.usecase.account.FetchAccountTransfer +import org.mifospay.core.data.domain.usecase.account.FetchAccounts +import org.mifospay.core.data.domain.usecase.account.FetchMerchants +import org.mifospay.core.data.domain.usecase.account.TransferFunds +import org.mifospay.core.data.domain.usecase.client.CreateClient +import org.mifospay.core.data.domain.usecase.client.FetchClientData +import org.mifospay.core.data.domain.usecase.client.FetchClientDetails +import org.mifospay.core.data.domain.usecase.client.FetchClientImage +import org.mifospay.core.data.domain.usecase.client.SearchClient +import org.mifospay.core.data.domain.usecase.client.UpdateClient +import org.mifospay.core.data.domain.usecase.history.TransactionsHistory +import org.mifospay.core.data.domain.usecase.invoice.FetchInvoice +import org.mifospay.core.data.domain.usecase.invoice.FetchInvoices +import org.mifospay.core.data.domain.usecase.kyc.FetchKYCLevel1Details +import org.mifospay.core.data.domain.usecase.kyc.UpdateKYCLevel1Details +import org.mifospay.core.data.domain.usecase.kyc.UploadKYCDocs +import org.mifospay.core.data.domain.usecase.kyc.UploadKYCLevel1Details +import org.mifospay.core.data.domain.usecase.notification.FetchNotifications +import org.mifospay.core.data.domain.usecase.savedcards.AddCard +import org.mifospay.core.data.domain.usecase.savedcards.DeleteCard +import org.mifospay.core.data.domain.usecase.savedcards.EditCard +import org.mifospay.core.data.domain.usecase.savedcards.FetchSavedCards +import org.mifospay.core.data.domain.usecase.standinginstruction.CreateStandingTransaction +import org.mifospay.core.data.domain.usecase.standinginstruction.DeleteStandingInstruction +import org.mifospay.core.data.domain.usecase.standinginstruction.FetchStandingInstruction +import org.mifospay.core.data.domain.usecase.standinginstruction.GetAllStandingInstructions +import org.mifospay.core.data.domain.usecase.standinginstruction.UpdateStandingInstruction +import org.mifospay.core.data.domain.usecase.twofactor.FetchDeliveryMethods +import org.mifospay.core.data.domain.usecase.twofactor.RequestOTP +import org.mifospay.core.data.domain.usecase.twofactor.ValidateOTP +import org.mifospay.core.data.domain.usecase.user.AuthenticateUser +import org.mifospay.core.data.domain.usecase.user.CreateUser +import org.mifospay.core.data.domain.usecase.user.DeleteUser +import org.mifospay.core.data.domain.usecase.user.FetchUserDetails +import org.mifospay.core.data.domain.usecase.user.FetchUsers +import org.mifospay.core.data.domain.usecase.user.RegisterUser +import org.mifospay.core.data.domain.usecase.user.UpdateUser +import org.mifospay.core.data.domain.usecase.user.VerifyUser +import org.mifospay.core.data.fineract.entity.mapper.AccountMapper +import org.mifospay.core.data.fineract.entity.mapper.ClientDetailsMapper +import org.mifospay.core.data.fineract.entity.mapper.CurrencyMapper +import org.mifospay.core.data.fineract.entity.mapper.SearchedEntitiesMapper +import org.mifospay.core.data.fineract.entity.mapper.TransactionMapper +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.repository.auth.AuthenticationUserRepository +import org.mifospay.core.data.repository.auth.UserDataRepository +import org.mifospay.core.data.repository.local.LocalAssetRepository +import org.mifospay.core.data.repository.local.LocalRepository +import org.mifospay.core.data.repository.local.MifosLocalAssetRepository +import org.mifospay.core.data.util.ConnectivityManagerNetworkMonitor +import org.mifospay.core.data.util.NetworkMonitor +import org.mifospay.core.data.util.TimeZoneBroadcastMonitor +import org.mifospay.core.data.util.TimeZoneMonitor +import org.mifospay.core.datastore.PreferencesHelper +import org.mifospay.core.network.MifosDispatchers +import org.mifospay.core.network.di.LocalModule +import org.mifospay.core.network.localAssets.LocalAssetDataSource +import org.mifospay.core.network.localAssets.MifosLocalAssetDataSource + +val DataModule = module { + includes(LocalModule) + + single { UseCaseThreadPoolScheduler() } + single { UseCaseHandler(get()) } + single { TaskLooper(get()) } + single { UseCaseFactory(get()) } + single { FetchClientData(get(), get()) } + single { ClientDetailsMapper() } + single { SearchClient(get(), get()) } + single { UpdateClient(get()) } + single { CreateClient(get()) } + single { FetchClientDetails(get()) } + single { FetchClientImage(get()) } + single { BlockUnblockCommand(get()) } + single { DownloadTransactionReceipt(get()) } + single { AccountMapper(get()) } + single { FetchAccount(get(), get()) } + single { FetchAccounts(get(), get()) } + single { FetchAccountTransaction(get(), get()) } + single { FetchAccountTransactions(get(), get()) } + single { FetchAccountTransfer(get()) } + single { FetchMerchants(get()) } + single { TransferFunds(get()) } + single { + TransactionsHistory( + mUseCaseHandler = get(), + fetchAccountTransactionsUseCase = get(), + ) + } + + // Invoice UseCase + single { FetchInvoice(get()) } + single { FetchInvoices(get()) } + + // KYC UseCase + single { FetchKYCLevel1Details(get()) } + single { UpdateKYCLevel1Details(get()) } + single { UploadKYCDocs(get()) } + single { UploadKYCLevel1Details(get()) } + + // Notifications + single { FetchNotifications(get()) } + + // Saved Cards + single { AddCard(get()) } + single { DeleteCard(get()) } + single { EditCard(get()) } + single { FetchSavedCards(get()) } + + // Standing Instructions + single { CreateStandingTransaction(get()) } + single { DeleteStandingInstruction(get()) } + single { FetchStandingInstruction(get()) } + single { GetAllStandingInstructions(get()) } + single { UpdateStandingInstruction(get()) } + + // Two-Factor + single { FetchDeliveryMethods(get()) } + single { RequestOTP(get()) } + single { ValidateOTP(get()) } + + // User + single { AuthenticateUser(get()) } + single { CreateUser(get()) } + single { DeleteUser(get()) } + single { FetchUserDetails(get()) } + single { FetchUsers(get()) } + single { RegisterUser(get()) } + single { UpdateUser(get()) } + single { VerifyUser(get()) } + + // Fineract Entity Mappers + single { CurrencyMapper() } + single { SearchedEntitiesMapper() } + single { TransactionMapper(get()) } + single { AccountMapper(get()) } + single { ClientDetailsMapper() } + + // Fineract Repository + + single { + FineractRepository( + fineractApiManager = get(), + selfApiManager = get(), + ktorAuthenticationService = get(), + ) + } + + // Fineract Repository Auth + + single { AuthenticationUserRepository(get()) } + + // Fineract Repository Local + + single { + val preferencesHelper: PreferencesHelper = get() + LocalRepository(preferencesHelper) + } + + factory { + MifosLocalAssetDataSource( + ioDispatcher = get( + named(MifosDispatchers.IO.name), + ), + networkJson = get(), + assets = get(), + ) + } + + factory { + MifosLocalAssetRepository( + ioDispatcher = get( + named(MifosDispatchers.IO.name), + ), + datasource = get(), + ) + } + + // Util + + single { ConnectivityManagerNetworkMonitor(context = androidContext()) } + + single { + TimeZoneBroadcastMonitor( + context = androidContext(), + appScope = get(named("ApplicationScope")), + ioDispatcher = get(named(MifosDispatchers.IO.name)), + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/di/LocalDataModule.kt b/core/data/src/main/java/org/mifospay/core/data/di/LocalDataModule.kt new file mode 100644 index 000000000..87be810d2 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/di/LocalDataModule.kt @@ -0,0 +1,19 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.di + +import androidx.lifecycle.SavedStateHandle +import org.koin.dsl.module + +val LocalDataModule = module { + factory { + SavedStateHandle() + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/BlockUnblockCommand.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/BlockUnblockCommand.kt new file mode 100644 index 000000000..45075fd22 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/BlockUnblockCommand.kt @@ -0,0 +1,46 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.account + +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository + +class BlockUnblockCommand( + private val mFineractRepository: FineractRepository, +) : UseCase() { + + override fun executeUseCase(requestValues: RequestValues) { + CoroutineScope(Dispatchers.IO).launch { + try { + val res = mFineractRepository.blockUnblockAccount( + requestValues.accountId, + requestValues.command, + ) + withContext(Dispatchers.Main) { + Log.d("BlockUnblockCommand@@@@", "$res") + useCaseCallback.onSuccess(ResponseValue) + } + } catch (e: Exception) { + Log.d("BlockUnblockCommand@@@@", "${e.message}") + useCaseCallback.onError( + "Error " + requestValues.command + "ing account", + ) + } + } + } + + data class RequestValues(val accountId: Long, val command: String) : UseCase.RequestValues + data object ResponseValue : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/DownloadTransactionReceipt.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/DownloadTransactionReceipt.kt new file mode 100644 index 000000000..36d955040 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/DownloadTransactionReceipt.kt @@ -0,0 +1,45 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.account + +import okhttp3.ResponseBody +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class DownloadTransactionReceipt( + private val mFineractRepository: FineractRepository, +) : UseCase() { + override fun executeUseCase(requestValues: RequestValues) { + requestValues.transactionId?.let { + mFineractRepository.getTransactionReceipt(Constants.PDF, it) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(e.toString()) + } + + override fun onNext(t: ResponseBody) { + useCaseCallback.onSuccess(ResponseValue(t)) + } + }, + ) + } + } + + data class RequestValues(val transactionId: String?) : UseCase.RequestValues + data class ResponseValue(val responseBody: ResponseBody) : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchAccount.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchAccount.kt new file mode 100644 index 000000000..4bc88a5e1 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchAccount.kt @@ -0,0 +1,66 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.account + +import com.mifospay.core.model.domain.Account +import com.mifospay.core.model.entity.client.ClientAccounts +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.entity.mapper.AccountMapper +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchAccount( + private val fineractRepository: FineractRepository, + private val accountMapper: AccountMapper, +) : UseCase() { + + override fun executeUseCase(requestValues: RequestValues) { + fineractRepository.getSelfAccounts(requestValues.clientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_ACCOUNTS) + } + + override fun onNext(clientAccounts: ClientAccounts) { + val accounts = accountMapper.transform(clientAccounts) + + if (accounts.isNotEmpty()) { + var walletAccount: Account? = null + for (account in accounts) { + if (account.productId.toInt() == Constants.WALLET_ACCOUNT_SAVINGS_PRODUCT_ID) { + walletAccount = account + break + } + } + if (walletAccount != null) { + useCaseCallback.onSuccess(ResponseValue(walletAccount)) + } else { + useCaseCallback.onError(Constants.NO_ACCOUNT_FOUND) + } + } else { + useCaseCallback.onError(Constants.NO_ACCOUNTS_FOUND) + } + } + }, + ) + } + + data class RequestValues(val clientId: Long) : UseCase.RequestValues + + data class ResponseValue(val account: Account) : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchAccountTransaction.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchAccountTransaction.kt new file mode 100644 index 000000000..6ad743a05 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchAccountTransaction.kt @@ -0,0 +1,62 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.account + +import android.util.Log +import com.mifospay.core.model.domain.Transaction +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.entity.mapper.TransactionMapper +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants + +class FetchAccountTransaction( + private val fineractRepository: FineractRepository, + private val transactionMapper: TransactionMapper, +) : + UseCase() { + override fun executeUseCase(requestValues: RequestValues) { + CoroutineScope(Dispatchers.IO).launch { + try { + val res = fineractRepository.getSelfAccountTransactionFromId( + requestValues.accountId, + requestValues.transactionId, + ) + withContext(Dispatchers.Main) { + Log.d("FetchTransactions@@@@", "$res") + useCaseCallback.onSuccess( + ResponseValue(transactionMapper.transformInvoice(res)), + ) + } + } catch (e: Exception) { + Log.d("FetchTransactions@@@@", "${e.message}") + withContext(Dispatchers.Main) { + if (e.message == "HTTP 401 Unauthorized") { + useCaseCallback.onError(Constants.UNAUTHORIZED_ERROR) + } else { + useCaseCallback.onError( + Constants.ERROR_FETCHING_REMOTE_ACCOUNT_TRANSACTIONS, + ) + } + } + } + } + } + + data class RequestValues( + val accountId: Long, + val transactionId: Long, + ) : UseCase.RequestValues + + data class ResponseValue(val transaction: Transaction) : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchAccountTransactions.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchAccountTransactions.kt new file mode 100644 index 000000000..7ab0830d4 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchAccountTransactions.kt @@ -0,0 +1,53 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.account + +import android.util.Log +import com.mifospay.core.model.domain.Transaction +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.entity.mapper.TransactionMapper +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants + +class FetchAccountTransactions( + private val fineractRepository: FineractRepository, + private val transactionMapper: TransactionMapper, +) : UseCase() { + + override fun executeUseCase(requestValues: RequestValues) { + CoroutineScope(Dispatchers.IO).launch { + try { + val api = fineractRepository.getSelfAccountTransactions(requestValues.accountId) + withContext(Dispatchers.Main) { + Log.d("FetchTransactions@@@@", "$api") + useCaseCallback.onSuccess( + ResponseValue( + transactionMapper.transformTransactionList(api), + ), + ) + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + Log.d("FetchTransactions@@@@", "${e.message}") + useCaseCallback.onError( + Constants.ERROR_FETCHING_REMOTE_ACCOUNT_TRANSACTIONS, + ) + } + } + } + } + + data class RequestValues(var accountId: Long) : UseCase.RequestValues + data class ResponseValue(val transactions: List) : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchAccountTransfer.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchAccountTransfer.kt new file mode 100644 index 000000000..78cc231a7 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchAccountTransfer.kt @@ -0,0 +1,40 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.account + +import com.mifospay.core.model.entity.accounts.savings.TransferDetail +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchAccountTransfer( + private val mFineractRepository: FineractRepository, +) : UseCase() { + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.getAccountTransfer(requestValues.transferId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe(object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(e.toString()) + } + + override fun onNext(transferDetail: TransferDetail) { + useCaseCallback.onSuccess(ResponseValue(transferDetail)) + } + }) + } + + data class RequestValues(var transferId: Long) : UseCase.RequestValues + data class ResponseValue(val transferDetail: TransferDetail) : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchAccounts.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchAccounts.kt new file mode 100644 index 000000000..9b3b4732f --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchAccounts.kt @@ -0,0 +1,55 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.account + +import com.mifospay.core.model.domain.Account +import com.mifospay.core.model.entity.client.ClientAccounts +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.entity.mapper.AccountMapper +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchAccounts( + private val fineractRepository: FineractRepository, + private val accountMapper: AccountMapper, +) : UseCase() { + + override fun executeUseCase(requestValues: RequestValues) { + fineractRepository.getAccounts(requestValues.clientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_ACCOUNTS) + } + + override fun onNext(t: ClientAccounts?) { + if (t != null) { + useCaseCallback.onSuccess( + ResponseValue( + accountMapper.transform(t), + ), + ) + } else { + useCaseCallback.onError(Constants.NO_ACCOUNTS_FOUND) + } + } + }, + ) + } + + data class RequestValues(val clientId: Long) : UseCase.RequestValues + data class ResponseValue(val accountList: List) : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchMerchants.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchMerchants.kt new file mode 100644 index 000000000..57bdbba0a --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/FetchMerchants.kt @@ -0,0 +1,53 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.account + +import android.util.Log +import com.mifospay.core.model.entity.accounts.savings.SavingsWithAssociations +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants + +class FetchMerchants( + private val mFineractRepository: FineractRepository, +) : UseCase() { + override fun executeUseCase(requestValues: RequestValues) { + CoroutineScope(Dispatchers.IO).launch { + try { + val res = mFineractRepository.savingsAccounts() + withContext(Dispatchers.Main) { + Log.d("FetchMerchants@@@@", "$res") + val savingsWithAssociationsList = res.pageItems + val merchantsList: MutableList = ArrayList() + for (i in savingsWithAssociationsList.indices) { + if (savingsWithAssociationsList[i].savingsProductId == + Constants.MIFOS_MERCHANT_SAVINGS_PRODUCT_ID + ) { + merchantsList.add(savingsWithAssociationsList[i]) + } + } + useCaseCallback.onSuccess(ResponseValue(merchantsList)) + } + } catch (e: Exception) { + Log.d("FetchTransactions@@@@", "${e.message}") + e.message?.let { useCaseCallback.onError(it) } + } + } + } + + class RequestValues : UseCase.RequestValues + data class ResponseValue( + val savingsWithAssociationsList: List, + ) : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/TransferFunds.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/TransferFunds.kt new file mode 100644 index 000000000..ec3efb669 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/account/TransferFunds.kt @@ -0,0 +1,275 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.account + +import com.mifospay.core.model.entity.TPTResponse +import com.mifospay.core.model.entity.accounts.savings.SavingAccount +import com.mifospay.core.model.entity.beneficary.Beneficiary +import com.mifospay.core.model.entity.beneficary.BeneficiaryPayload +import com.mifospay.core.model.entity.beneficary.BeneficiaryUpdatePayload +import com.mifospay.core.model.entity.client.Client +import com.mifospay.core.model.entity.client.ClientAccounts +import com.mifospay.core.model.entity.payload.TransferPayload +import com.mifospay.core.model.utils.DateHelper +import okhttp3.ResponseBody +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +@Suppress("UnusedPrivateMember") +class TransferFunds( + private val apiRepository: FineractRepository, +) : UseCase() { + + // override var requestValues: RequestValues + private lateinit var fromClient: Client + private lateinit var toClient: Client + private lateinit var fromAccount: SavingAccount + private lateinit var toAccount: SavingAccount + + override fun executeUseCase(requestValues: RequestValues) { + this.walletRequestValues = requestValues + apiRepository.getSelfClientDetails(requestValues.fromClientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_CLIENT_DATA) + } + + override fun onNext(client: Client) { + fromClient = client + fetchToClientDetails() + } + }, + ) + } + + private fun fetchToClientDetails() { + apiRepository.getClientDetails(walletRequestValues.toClientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_CLIENT_DATA) + } + + override fun onNext(client: Client) { + toClient = client + fetchFromAccountDetails() + } + }, + ) + } + + private fun fetchFromAccountDetails() { + apiRepository.getSelfAccounts(walletRequestValues.fromClientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_FROM_ACCOUNT) + } + + override fun onNext(clientAccounts: ClientAccounts) { + val accounts = clientAccounts.savingsAccounts + if (accounts.isNotEmpty()) { + var walletAccount: SavingAccount? = null + for (account in accounts) { + if (account.productId == Constants.WALLET_ACCOUNT_SAVINGS_PRODUCT_ID) { + walletAccount = account + break + } + } + if (walletAccount != null) { + fromAccount = walletAccount + fetchToAccountDetails() + } else { + useCaseCallback.onError(Constants.NO_WALLET_FOUND) + } + } else { + useCaseCallback.onError(Constants.ERROR_FETCHING_FROM_ACCOUNT) + } + } + }, + ) + } + + private fun fetchToAccountDetails() { + apiRepository.getAccounts(walletRequestValues.toClientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_TO_ACCOUNT) + } + + override fun onNext(clientAccounts: ClientAccounts) { + val accounts = clientAccounts.savingsAccounts + if (accounts.isNotEmpty()) { + var walletAccount: SavingAccount? = null + for (account in accounts) { + if (account.productId == Constants.WALLET_ACCOUNT_SAVINGS_PRODUCT_ID) { + walletAccount = account + break + } + } + if (walletAccount != null) { + toAccount = walletAccount + makeTransfer() + } else { + useCaseCallback.onError(Constants.NO_WALLET_FOUND) + } + } else { + useCaseCallback.onError(Constants.ERROR_FETCHING_TO_ACCOUNT) + } + } + }, + ) + } + + private fun checkBeneficiary() { + apiRepository.beneficiaryList + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber>() { + override fun onCompleted() {} + + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_BENEFICIARIES) + } + + override fun onNext(beneficiaries: List) { + var exists = false + beneficiaries.forEach { beneficiary -> + if (beneficiary.accountNumber == toAccount.accountNo) { + exists = true + if (beneficiary.transferLimit >= walletRequestValues.amount) { + makeTransfer() + } else { + updateTransferLimit(beneficiary.id?.toLong()!!) + } + return@forEach + } + } + if (!exists) { + addBeneficiary() + } + } + }, + ) + } + + private fun addBeneficiary() { + val payload = BeneficiaryPayload().apply { + accountNumber = toAccount.accountNo + name = toClient.displayName + officeName = toClient.officeName + transferLimit = walletRequestValues.amount.toInt() + accountType = 2 + } + + apiRepository.createBeneficiary(payload) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_ADDING_BENEFICIARY) + } + + override fun onNext(responseBody: ResponseBody) { + makeTransfer() + } + }, + ) + } + + private fun updateTransferLimit(beneficiaryId: Long) { + val updatePayload = BeneficiaryUpdatePayload().apply { + transferLimit = walletRequestValues.amount.toInt() + } + apiRepository.updateBeneficiary(beneficiaryId, updatePayload) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_ADDING_BENEFICIARY) + } + + override fun onNext(responseBody: ResponseBody) { + makeTransfer() + } + }, + ) + } + + private fun makeTransfer() { + val transferPayload = TransferPayload().apply { + fromAccountId = fromAccount.id.toInt() + fromClientId = fromClient.id.toLong() + fromAccountType = 2 + fromOfficeId = fromClient.officeId + toOfficeId = toClient.officeId + toAccountId = toAccount.id.toInt() + toClientId = toClient.id.toLong() + toAccountType = 2 + transferDate = DateHelper.getDateAsStringFromLong(System.currentTimeMillis()) + transferAmount = walletRequestValues.amount + transferDescription = Constants.WALLET_TRANSFER + } + + apiRepository.makeThirdPartyTransfer(transferPayload) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_MAKING_TRANSFER) + } + + override fun onNext(responseBody: TPTResponse) { + useCaseCallback.onSuccess(ResponseValue()) + } + }, + ) + } + + data class RequestValues( + val fromClientId: Long, + val toClientId: Long, + val amount: Double, + ) : UseCase.RequestValues + + class ResponseValue : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/CreateClient.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/CreateClient.kt new file mode 100644 index 000000000..fd84e1477 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/CreateClient.kt @@ -0,0 +1,54 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.client + +import com.mifospay.core.model.domain.client.NewClient +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.ErrorJsonMessageHelper.getUserMessage +import retrofit2.HttpException +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class CreateClient( + private val apiRepository: FineractRepository, +) : UseCase() { + + data class RequestValues(val client: NewClient) : UseCase.RequestValues + + data class ResponseValue(val clientId: Int) : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + apiRepository.createClient(requestValues.client) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + var message: String + try { + message = + (e as HttpException).response()?.errorBody()?.string().toString() + message = getUserMessage(message) + } catch (e1: Exception) { + message = e1.message.toString() + } + useCaseCallback.onError(message) + } + + override fun onNext(genericResponse: ResponseValue) { + useCaseCallback.onSuccess(genericResponse) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/FetchClientData.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/FetchClientData.kt new file mode 100644 index 000000000..ad588cd33 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/FetchClientData.kt @@ -0,0 +1,75 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.client + +import com.mifospay.core.model.entity.Page +import com.mifospay.core.model.entity.client.Client +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.entity.mapper.ClientDetailsMapper +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchClientData( + private val fineractRepository: FineractRepository, + private val clientDetailsMapper: ClientDetailsMapper, +) : UseCase() { + + override fun executeUseCase(requestValues: RequestValues) { + requestValues.clientId?.let { clientId -> + fineractRepository.getSelfClientDetails(clientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_CLIENT_DATA) + } + + override fun onNext(client: Client) { + useCaseCallback.onSuccess( + ResponseValue(clientDetailsMapper.transform(client)), + ) + } + }, + ) + } ?: run { + fineractRepository.selfClientDetails + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber>() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_CLIENT_DATA) + } + + override fun onNext(client: Page) { + if (client.pageItems.size != 0) { + useCaseCallback.onSuccess( + ResponseValue(clientDetailsMapper.transform(client.pageItems[0])), + ) + } else { + useCaseCallback.onError(Constants.NO_CLIENT_FOUND) + } + } + }, + ) + } + } + + data class RequestValues(val clientId: Long?) : UseCase.RequestValues + data class ResponseValue( + val clientDetails: com.mifospay.core.model.domain.client.Client, + ) : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/FetchClientDetails.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/FetchClientDetails.kt new file mode 100644 index 000000000..3ab9b074f --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/FetchClientDetails.kt @@ -0,0 +1,44 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.client + +import com.mifospay.core.model.entity.client.Client +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchClientDetails( + private val mFineractRepository: FineractRepository, +) : UseCase() { + + data class RequestValues(val clientId: Long) : UseCase.RequestValues + data class ResponseValue(val client: Client) : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.getClientDetails(requestValues.clientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_CLIENT_DATA) + } + + override fun onNext(client: Client) { + useCaseCallback.onSuccess(ResponseValue(client)) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/FetchClientImage.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/FetchClientImage.kt new file mode 100644 index 000000000..90bd05665 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/FetchClientImage.kt @@ -0,0 +1,45 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.client + +import okhttp3.ResponseBody +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.ErrorJsonMessageHelper.getUserMessage +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchClientImage( + private val mFineractRepository: FineractRepository, +) : + UseCase() { + + data class RequestValues(val clientId: Long) : UseCase.RequestValues + data class ResponseValue(val responseBody: ResponseBody) : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.getClientImage(requestValues.clientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + getUserMessage(e)?.let { useCaseCallback.onError(it) } + } + + override fun onNext(responseBody: ResponseBody) { + useCaseCallback.onSuccess(ResponseValue(responseBody)) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/SearchClient.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/SearchClient.kt new file mode 100644 index 000000000..05b882da0 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/SearchClient.kt @@ -0,0 +1,55 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.client + +import com.mifospay.core.model.domain.SearchResult +import com.mifospay.core.model.entity.SearchedEntity +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.entity.mapper.SearchedEntitiesMapper +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class SearchClient( + private val apiRepository: FineractRepository, + private val searchedEntitiesMapper: SearchedEntitiesMapper, +) : UseCase() { + + data class RequestValues(val externalId: String) : UseCase.RequestValues + data class ResponseValue(val results: List) : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + apiRepository.searchResources(requestValues.externalId, Constants.CLIENTS, false) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber>() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_SEARCHING_CLIENTS) + } + + override fun onNext(results: List) { + if (results.isNotEmpty()) { + useCaseCallback.onSuccess( + ResponseValue( + searchedEntitiesMapper.transformList(results), + ), + ) + } else { + useCaseCallback.onError(Constants.NO_CLIENTS_FOUND) + } + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/UpdateClient.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/UpdateClient.kt new file mode 100644 index 000000000..6badef3e6 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/client/UpdateClient.kt @@ -0,0 +1,55 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.client + +import okhttp3.ResponseBody +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.ErrorJsonMessageHelper.getUserMessage +import retrofit2.HttpException +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class UpdateClient( + private val fineractRepository: FineractRepository, +) : UseCase() { + + data class RequestValues(val updateClientEntity: Any, val clientId: Long) : + UseCase.RequestValues + + data class ResponseValue(val responseBody: ResponseBody) : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + fineractRepository.updateClient(requestValues.clientId, requestValues.updateClientEntity) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + var message: String + try { + message = + (e as HttpException).response()?.errorBody()?.string().toString() + message = getUserMessage(message) + } catch (e1: Exception) { + message = e1.message.toString() + } + useCaseCallback.onError(message) + } + + override fun onNext(responseBody: ResponseBody) { + useCaseCallback.onSuccess(ResponseValue(responseBody)) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/history/TransactionsHistory.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/history/TransactionsHistory.kt new file mode 100644 index 000000000..10e105b76 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/history/TransactionsHistory.kt @@ -0,0 +1,44 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.history + +import com.mifospay.core.model.domain.Transaction +import org.mifospay.core.data.base.UseCase.UseCaseCallback +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.account.FetchAccountTransactions + +class TransactionsHistory( + private val mUseCaseHandler: UseCaseHandler, + private val fetchAccountTransactionsUseCase: FetchAccountTransactions, +) { + var delegate: HistoryContract.TransactionsHistoryAsync? = null + private var transactions: List? + + init { + transactions = ArrayList() + } + + fun fetchTransactionsHistory(accountId: Long) { + mUseCaseHandler.execute( + fetchAccountTransactionsUseCase, + FetchAccountTransactions.RequestValues(accountId), + object : UseCaseCallback { + override fun onSuccess(response: FetchAccountTransactions.ResponseValue?) { + transactions = response?.transactions + delegate!!.onTransactionsFetchCompleted(transactions) + } + + override fun onError(message: String) { + transactions = null + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/invoice/FetchInvoice.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/invoice/FetchInvoice.kt new file mode 100644 index 000000000..162f9a97d --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/invoice/FetchInvoice.kt @@ -0,0 +1,62 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.invoice + +import android.net.Uri +import android.util.Log +import com.mifospay.core.model.entity.invoice.Invoice +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchInvoice( + private val mFineractRepository: FineractRepository, +) : UseCase() { + + class RequestValues(val uniquePaymentLink: Uri?) : UseCase.RequestValues + class ResponseValue( + val invoices: List, + ) : UseCase.ResponseValue + override fun executeUseCase(requestValues: RequestValues) { + val paymentLink = requestValues.uniquePaymentLink + try { + val params = paymentLink?.pathSegments + val clientId = params?.get(0)?.toLong() // "clientId" + val invoiceId = params?.get(1)?.toLong() // "invoiceId + if (clientId != null && invoiceId != null) { + mFineractRepository.fetchInvoice(clientId, invoiceId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber?>() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.INVALID_UPL) + } + + override fun onNext(invoices: List?) { + if (invoices?.isNotEmpty() == true) { + useCaseCallback.onSuccess(ResponseValue(invoices)) + } else { + useCaseCallback.onError(Constants.INVOICE_DOES_NOT_EXIST) + } + } + }, + ) + } + } catch (e: IndexOutOfBoundsException) { + Log.e("Error", e.message.toString()) + useCaseCallback.onError("Invalid link used to open the App") + } + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/invoice/FetchInvoices.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/invoice/FetchInvoices.kt new file mode 100644 index 000000000..262a05383 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/invoice/FetchInvoices.kt @@ -0,0 +1,49 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.invoice + +import android.util.Log +import com.mifospay.core.model.entity.invoice.Invoice +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchInvoices( + private val mFineractRepository: FineractRepository, +) : + UseCase() { + + class RequestValues(val clientId: String) : UseCase.RequestValues + class ResponseValue( + val invoiceList: List, + ) : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.fetchInvoices(requestValues.clientId.toLong()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber>() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + Log.e("Invoices", e.message.toString()) + useCaseCallback.onError(e.toString()) + } + + override fun onNext(invoices: List) { + Log.d("invoice@@@", invoices.toString()) + useCaseCallback.onSuccess(ResponseValue(invoices)) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/kyc/FetchKYCLevel1Details.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/kyc/FetchKYCLevel1Details.kt new file mode 100644 index 000000000..42e9d19c9 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/kyc/FetchKYCLevel1Details.kt @@ -0,0 +1,47 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.kyc + +import com.mifospay.core.model.entity.kyc.KYCLevel1Details +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchKYCLevel1Details( + private val mFineractRepository: FineractRepository, +) : UseCase() { + + class RequestValues(val clientId: Int) : UseCase.RequestValues + class ResponseValue( + val kycLevel1DetailsList: List, + ) : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.fetchKYCLevel1Details(requestValues.clientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber>() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(e.toString()) + } + + override fun onNext(kycLevel1Details: List) { + useCaseCallback.onSuccess( + ResponseValue(kycLevel1Details), + ) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/kyc/UpdateKYCLevel1Details.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/kyc/UpdateKYCLevel1Details.kt new file mode 100644 index 000000000..6f232ca29 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/kyc/UpdateKYCLevel1Details.kt @@ -0,0 +1,51 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.kyc + +import com.mifospay.core.model.entity.kyc.KYCLevel1Details +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.network.GenericResponse +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class UpdateKYCLevel1Details( + private val mFineractRepository: FineractRepository, +) : UseCase() { + + class RequestValues( + val clientId: Int, + val kycLevel1Details: KYCLevel1Details, + ) : UseCase.RequestValues + + class ResponseValue : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.updateKYCLevel1Details( + requestValues.clientId, + requestValues.kycLevel1Details, + ) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(e.toString()) + } + + override fun onNext(t: GenericResponse) { + useCaseCallback.onSuccess(ResponseValue()) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/kyc/UploadKYCDocs.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/kyc/UploadKYCDocs.kt new file mode 100644 index 000000000..6eb539c1f --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/kyc/UploadKYCDocs.kt @@ -0,0 +1,57 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.kyc + +import okhttp3.MultipartBody +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.network.GenericResponse +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class UploadKYCDocs( + private val apiRepository: FineractRepository, +) : UseCase() { + + class RequestValues( + val entityType: String, + val clientId: Long, + val docname: String, + val identityType: String, + val file: MultipartBody.Part, + ) : UseCase.RequestValues + + class ResponseValue : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + apiRepository.uploadKYCDocs( + requestValues.entityType, + requestValues.clientId, + requestValues.docname, + requestValues.identityType, + requestValues.file, + ) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(e.toString()) + } + + override fun onNext(t: GenericResponse) { + useCaseCallback.onSuccess(ResponseValue()) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/kyc/UploadKYCLevel1Details.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/kyc/UploadKYCLevel1Details.kt new file mode 100644 index 000000000..74ace32cb --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/kyc/UploadKYCLevel1Details.kt @@ -0,0 +1,50 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.kyc + +import com.mifospay.core.model.entity.kyc.KYCLevel1Details +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.network.GenericResponse +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class UploadKYCLevel1Details( + var mFineractRepository: FineractRepository, +) : UseCase() { + class RequestValues( + val clientId: Int, + val mKYCLevel1Details: KYCLevel1Details, + ) : UseCase.RequestValues + + class ResponseValue : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.uploadKYCLevel1Details( + requestValues.clientId, + requestValues.mKYCLevel1Details, + ) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(e.toString()) + } + + override fun onNext(t: GenericResponse) { + useCaseCallback.onSuccess(ResponseValue()) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/notification/FetchNotifications.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/notification/FetchNotifications.kt new file mode 100644 index 000000000..4237dc12a --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/notification/FetchNotifications.kt @@ -0,0 +1,46 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.notification + +import com.mifospay.core.model.domain.NotificationPayload +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchNotifications( + private val mFineractRepository: FineractRepository, +) : UseCase() { + + class RequestValues(val clientId: Long) : UseCase.RequestValues + class ResponseValue( + val notificationPayloadList: List?, + ) : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.fetchNotifications(requestValues.clientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber>() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_NOTIFICATIONS) + } + + override fun onNext(notificationPayloads: List) { + useCaseCallback.onSuccess(ResponseValue(notificationPayloads)) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/savedcards/AddCard.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/savedcards/AddCard.kt new file mode 100644 index 000000000..73bb2bbcc --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/savedcards/AddCard.kt @@ -0,0 +1,43 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.savedcards + +import com.mifospay.core.model.entity.savedcards.Card +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.network.GenericResponse +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class AddCard( + private val mFineractRepository: FineractRepository, +) : UseCase() { + class RequestValues(val clientId: Long, val card: Card) : UseCase.RequestValues + class ResponseValue : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.addSavedCards(requestValues.clientId, requestValues.card) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(e.toString()) + } + + override fun onNext(t: GenericResponse?) { + useCaseCallback.onSuccess(ResponseValue()) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/savedcards/DeleteCard.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/savedcards/DeleteCard.kt new file mode 100644 index 000000000..c11cb90f0 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/savedcards/DeleteCard.kt @@ -0,0 +1,43 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.savedcards + +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.network.GenericResponse +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class DeleteCard( + private val mFineractRepository: FineractRepository, +) : UseCase() { + + class RequestValues(val clientId: Int, val cardId: Int) : UseCase.RequestValues + class ResponseValue : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.deleteSavedCard(requestValues.clientId, requestValues.cardId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(e.toString()) + } + + override fun onNext(t: GenericResponse?) { + useCaseCallback.onSuccess(ResponseValue()) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/savedcards/EditCard.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/savedcards/EditCard.kt new file mode 100644 index 000000000..7f0440b18 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/savedcards/EditCard.kt @@ -0,0 +1,44 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.savedcards + +import com.mifospay.core.model.entity.savedcards.Card +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.network.GenericResponse +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class EditCard( + private val mFineractRepository: FineractRepository, +) : UseCase() { + + class RequestValues(val clientId: Int, val card: Card) : UseCase.RequestValues + class ResponseValue : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.editSavedCard(requestValues.clientId, requestValues.card) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(e.toString()) + } + + override fun onNext(t: GenericResponse?) { + useCaseCallback.onSuccess(ResponseValue()) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/savedcards/FetchSavedCards.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/savedcards/FetchSavedCards.kt new file mode 100644 index 000000000..8750008de --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/savedcards/FetchSavedCards.kt @@ -0,0 +1,48 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.savedcards + +import com.mifospay.core.model.entity.savedcards.Card +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchSavedCards( + private val mFineractRepository: FineractRepository, +) : UseCase() { + + class RequestValues(val clientId: Long) : UseCase.RequestValues + class ResponseValue(val cardList: List) : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.fetchSavedCards(requestValues.clientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber>() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(e.toString()) + } + + override fun onNext(cards: List) { + if (cards.isNotEmpty()) { + useCaseCallback.onSuccess(ResponseValue(cards)) + } else { + useCaseCallback.onError(Constants.NO_SAVED_CARDS) + } + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/standinginstruction/CreateStandingTransaction.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/standinginstruction/CreateStandingTransaction.kt new file mode 100644 index 000000000..5e4dfd300 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/standinginstruction/CreateStandingTransaction.kt @@ -0,0 +1,206 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.standinginstruction + +import com.mifospay.core.model.entity.accounts.savings.SavingAccount +import com.mifospay.core.model.entity.client.Client +import com.mifospay.core.model.entity.client.ClientAccounts +import com.mifospay.core.model.entity.payload.StandingInstructionPayload +import com.mifospay.core.model.entity.standinginstruction.SDIResponse +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class CreateStandingTransaction( + private val apiRepository: FineractRepository, +) : UseCase() { + + lateinit var fromClient: Client + lateinit var toClient: Client + lateinit var fromAccount: SavingAccount + lateinit var toAccount: SavingAccount + + override fun executeUseCase(requestValues: RequestValues) { + apiRepository.getSelfClientDetails(requestValues.fromClientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + + override fun onCompleted() { + } + + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_CLIENT_DATA) + } + + override fun onNext(client: Client) { + fromClient = client + fetchToClientData() + } + }, + ) + } + + private fun fetchToClientData() { + apiRepository.getClientDetails(walletRequestValues.toClientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + + override fun onCompleted() { + } + + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_CLIENT_DATA) + } + + override fun onNext(client: Client) { + toClient = client + fetchFromAccountDetails() + } + }, + ) + } + + private fun fetchFromAccountDetails() { + apiRepository.getSelfAccounts(walletRequestValues.fromClientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() { + } + + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_FROM_ACCOUNT) + } + + override fun onNext(clientAccounts: ClientAccounts) { + val accounts = clientAccounts.savingsAccounts + if (accounts.isNotEmpty()) { + var walletAccount: SavingAccount? = null + for (account in accounts) { + if (account.productId == + Constants.WALLET_ACCOUNT_SAVINGS_PRODUCT_ID + ) { + walletAccount = account + break + } + } + walletAccount?.let { + fromAccount = walletAccount + fetchToAccountDetails() + } ?: useCaseCallback.onError(Constants.NO_WALLET_FOUND) + } else { + useCaseCallback.onError(Constants.ERROR_FETCHING_FROM_ACCOUNT) + } + } + }, + ) + } + + private fun fetchToAccountDetails() { + apiRepository.getAccounts(walletRequestValues.toClientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_TO_ACCOUNT) + } + + override fun onNext(clientAccounts: ClientAccounts) { + val accounts = clientAccounts.savingsAccounts + if (accounts.isNotEmpty()) { + var walletAccount: SavingAccount? = null + for (account in accounts) { + if (account.productId == + Constants.WALLET_ACCOUNT_SAVINGS_PRODUCT_ID + ) { + walletAccount = account + break + } + } + walletAccount?.let { + toAccount = walletAccount + createNewStandingInstruction() + } ?: useCaseCallback.onError(Constants.NO_WALLET_FOUND) + } else { + useCaseCallback.onError(Constants.ERROR_FETCHING_TO_ACCOUNT) + } + } + }, + ) + } + + private fun createNewStandingInstruction() { + val standingInstructionPayload = StandingInstructionPayload( + fromClient.officeId, + fromClient.id, + 2, + "wallet standing transaction", + 1, + 2, + 1, + fromAccount.id, + toClient.officeId, + toClient.id, + 2, + toAccount.id, + 1, + walletRequestValues.amount, + walletRequestValues.validFrom, + 1, + walletRequestValues.recurrenceInterval, + 2, + "en", + "dd MM yyyy", + walletRequestValues.validTill, + walletRequestValues.recurrenceOnDayMonth, + "dd MM", + ) + apiRepository.createStandingInstruction(standingInstructionPayload) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + + override fun onCompleted() { + } + + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_MAKING_TRANSFER) + } + + override fun onNext(sdiResponse: SDIResponse) { + useCaseCallback.onSuccess(ResponseValue()) + } + }, + ) + } + + class RequestValues( + val validTill: String, + val validFrom: String, + val recurrenceInterval: Int, + val recurrenceOnDayMonth: String, + val fromClientId: Long, + val toClientId: Long, + val amount: Double, + ) : UseCase.RequestValues + + class ResponseValue : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/standinginstruction/DeleteStandingInstruction.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/standinginstruction/DeleteStandingInstruction.kt new file mode 100644 index 000000000..569a9149b --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/standinginstruction/DeleteStandingInstruction.kt @@ -0,0 +1,47 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.standinginstruction + +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.network.GenericResponse +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class DeleteStandingInstruction( + private val apiRepository: FineractRepository, +) : UseCase() { + + override fun executeUseCase(requestValues: RequestValues) { + apiRepository.deleteStandingInstruction(requestValues.standingInstructionId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + + override fun onCompleted() { + } + + override fun onError(e: Throwable) { + e.message?.let { useCaseCallback.onError(it) } + } + + override fun onNext(genericResponse: GenericResponse) = + useCaseCallback.onSuccess(ResponseValue()) + }, + ) + } + + class RequestValues(val standingInstructionId: Long) : + UseCase.RequestValues + + class ResponseValue : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/standinginstruction/FetchStandingInstruction.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/standinginstruction/FetchStandingInstruction.kt new file mode 100644 index 000000000..8e165f333 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/standinginstruction/FetchStandingInstruction.kt @@ -0,0 +1,46 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.standinginstruction + +import com.mifospay.core.model.entity.standinginstruction.StandingInstruction +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchStandingInstruction( + private val apiRepository: FineractRepository, +) : UseCase() { + + override fun executeUseCase(requestValues: RequestValues) { + apiRepository.getStandingInstruction(requestValues.standingInstructionId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + + override fun onCompleted() { + } + + override fun onError(e: Throwable) { + e.message?.let { useCaseCallback.onError(it) } + } + + override fun onNext(standingInstruction: StandingInstruction) = + useCaseCallback.onSuccess(ResponseValue(standingInstruction)) + }, + ) + } + + class RequestValues(val standingInstructionId: Long) : UseCase.RequestValues + + class ResponseValue(val standingInstruction: StandingInstruction) : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/standinginstruction/GetAllStandingInstructions.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/standinginstruction/GetAllStandingInstructions.kt new file mode 100644 index 000000000..1e4338704 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/standinginstruction/GetAllStandingInstructions.kt @@ -0,0 +1,51 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.standinginstruction + +import com.mifospay.core.model.entity.Page +import com.mifospay.core.model.entity.standinginstruction.StandingInstruction +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class GetAllStandingInstructions( + private val apiRepository: FineractRepository, +) : UseCase() { + + override fun executeUseCase(requestValues: RequestValues) { + apiRepository.getAllStandingInstructions(requestValues.clientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber>() { + + override fun onCompleted() { + } + + override fun onError(e: Throwable) { + e.message?.let { useCaseCallback.onError(it) } + } + + override fun onNext(standingInstructionPage: Page) { + return useCaseCallback.onSuccess( + ResponseValue(standingInstructionPage.pageItems), + ) + } + }, + ) + } + + class RequestValues(val clientId: Long) : UseCase.RequestValues + + class ResponseValue(val standingInstructionsList: List) : + UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/standinginstruction/UpdateStandingInstruction.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/standinginstruction/UpdateStandingInstruction.kt new file mode 100644 index 000000000..8eef580b2 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/standinginstruction/UpdateStandingInstruction.kt @@ -0,0 +1,89 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.standinginstruction + +import com.mifospay.core.model.entity.payload.StandingInstructionPayload +import com.mifospay.core.model.entity.standinginstruction.StandingInstruction +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.network.GenericResponse +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class UpdateStandingInstruction( + private val apiRepository: FineractRepository, +) : UseCase() { + + override fun executeUseCase(requestValues: RequestValues) { + val validTillString = "${requestValues.standingInstruction.validTill?.get(2)} " + + "${requestValues.standingInstruction.validTill?.get(1)} " + + "${requestValues.standingInstruction.validTill?.get(0)}" + val validFromString = "${requestValues.standingInstruction.validFrom[2]} " + + "${requestValues.standingInstruction.validFrom[1]} " + + "${requestValues.standingInstruction.validFrom[0]}" + val recurrenceOnMonthDayString = "${requestValues.standingInstruction.validFrom[2]} " + + "${requestValues.standingInstruction.validFrom[1]}" + + val standingInstructionPayload = StandingInstructionPayload( + requestValues.standingInstruction.fromClient.officeId, + requestValues.standingInstruction.fromClient.id, + 2, + "wallet standing transaction", + 1, + 2, + 1, + requestValues.standingInstruction.fromAccount.id, + requestValues.standingInstruction.toClient.officeId, + requestValues.standingInstruction.toClient.id, + 2, + requestValues.standingInstruction.toAccount.id, + 1, + requestValues.standingInstruction.amount, + validFromString, + 1, + 1, + 2, + "en", + "dd MM yyyy", + validTillString, + recurrenceOnMonthDayString, + "dd MM", + ) + + apiRepository.updateStandingInstruction( + requestValues.standingInstructionId, + standingInstructionPayload, + ) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + + override fun onCompleted() { + } + + override fun onError(e: Throwable) { + e.message?.let { useCaseCallback.onError(it) } + } + + override fun onNext(genericResponse: GenericResponse) = + useCaseCallback.onSuccess(ResponseValue()) + }, + ) + } + + class RequestValues( + val standingInstructionId: Long, + val standingInstruction: StandingInstruction, + ) : UseCase.RequestValues + + class ResponseValue : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/twofactor/FetchDeliveryMethods.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/twofactor/FetchDeliveryMethods.kt new file mode 100644 index 000000000..29cf2bb7c --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/twofactor/FetchDeliveryMethods.kt @@ -0,0 +1,45 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.twofactor + +import com.mifospay.core.model.domain.twofactor.DeliveryMethod +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchDeliveryMethods( + private val mFineractRepository: FineractRepository, +) : UseCase() { + + class RequestValues : UseCase.RequestValues + class ResponseValue( + val deliveryMethodList: List, + ) : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.deliveryMethods + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber>() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(e.toString()) + } + + override fun onNext(deliveryMethods: List) { + useCaseCallback.onSuccess(ResponseValue(deliveryMethods)) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/twofactor/RequestOTP.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/twofactor/RequestOTP.kt new file mode 100644 index 000000000..004806f96 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/twofactor/RequestOTP.kt @@ -0,0 +1,42 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.twofactor + +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class RequestOTP( + private val mFineractRepository: FineractRepository, +) : UseCase() { + + class RequestValues(val deliveryMethod: String) : UseCase.RequestValues + class ResponseValue(val response: String) : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.requestOTP(requestValues.deliveryMethod) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(e.toString()) + } + + override fun onNext(response: String) { + useCaseCallback.onSuccess(ResponseValue(response)) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/twofactor/ValidateOTP.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/twofactor/ValidateOTP.kt new file mode 100644 index 000000000..1249e58be --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/twofactor/ValidateOTP.kt @@ -0,0 +1,43 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.twofactor + +import com.mifospay.core.model.domain.twofactor.AccessToken +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class ValidateOTP( + private val mFineractRepository: FineractRepository, +) : UseCase() { + + class RequestValues(val token: String) : UseCase.RequestValues + class ResponseValue(val accessToken: AccessToken) : UseCase.ResponseValue + + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.validateToken(requestValues.token) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(e.toString()) + } + + override fun onNext(accessToken: AccessToken) { + useCaseCallback.onSuccess(ResponseValue(accessToken)) + } + }, + ) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/AuthenticateUser.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/AuthenticateUser.kt new file mode 100644 index 000000000..c849b1261 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/AuthenticateUser.kt @@ -0,0 +1,45 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.user + +import com.mifospay.core.model.domain.user.User +import com.mifospay.core.model.entity.authentication.AuthenticationPayload +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants + +class AuthenticateUser( + private val apiRepository: FineractRepository, +) : UseCase() { + + override fun executeUseCase(requestValues: RequestValues) { + CoroutineScope(Dispatchers.IO).launch { + try { + val user = apiRepository.loginSelf( + AuthenticationPayload(requestValues.username, requestValues.password), + ) + withContext(Dispatchers.Main) { + useCaseCallback.onSuccess(ResponseValue(user)) + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + useCaseCallback.onError(Constants.ERROR_LOGGING_IN) + } + } + } + } + + data class RequestValues(val username: String, val password: String) : UseCase.RequestValues + data class ResponseValue(val user: User) : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/CreateUser.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/CreateUser.kt new file mode 100644 index 000000000..79f056b70 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/CreateUser.kt @@ -0,0 +1,51 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.user + +import com.mifospay.core.model.domain.user.NewUser +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.ErrorJsonMessageHelper.getUserMessage +import retrofit2.HttpException +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class CreateUser(private val apiRepository: FineractRepository) : + UseCase() { + override fun executeUseCase(requestValues: RequestValues) { + apiRepository.createUser(requestValues.user) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + getUserMessage(e) + var message: String + try { + message = (e as HttpException).response()!!.errorBody()!!.string() + message = getUserMessage(message) + } catch (e1: Exception) { + message = e1.message.toString() + } + useCaseCallback.onError(message) + } + + override fun onNext(genericResponse: ResponseValue) { + useCaseCallback.onSuccess(genericResponse) + } + }, + ) + } + + class RequestValues(val user: NewUser) : UseCase.RequestValues + class ResponseValue(val userId: Int) : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/DeleteUser.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/DeleteUser.kt new file mode 100644 index 000000000..2a537345f --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/DeleteUser.kt @@ -0,0 +1,42 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.user + +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.network.GenericResponse +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class DeleteUser( + private val mFineractRepository: FineractRepository, +) : UseCase() { + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.deleteUser(requestValues.userId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + e.message?.let { useCaseCallback.onError(it) } + } + + override fun onNext(genericResponse: GenericResponse) { + useCaseCallback.onSuccess(ResponseValue()) + } + }, + ) + } + + class RequestValues(val userId: Int) : UseCase.RequestValues + class ResponseValue : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/FetchUserDetails.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/FetchUserDetails.kt new file mode 100644 index 000000000..153e46ee0 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/FetchUserDetails.kt @@ -0,0 +1,42 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.user + +import com.mifospay.core.model.entity.UserWithRole +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchUserDetails( + private val mFineractRepository: FineractRepository, +) : UseCase() { + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.getUser() + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + e.message?.let { useCaseCallback.onError(it) } + } + + override fun onNext(userWithRole: UserWithRole) { + useCaseCallback.onSuccess(ResponseValue(userWithRole)) + } + }, + ) + } + + class RequestValues(val userId: Long) : UseCase.RequestValues + class ResponseValue(val userWithRole: UserWithRole) : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/FetchUsers.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/FetchUsers.kt new file mode 100644 index 000000000..61340673a --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/FetchUsers.kt @@ -0,0 +1,51 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.user + +import com.mifospay.core.model.entity.UserWithRole +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchUsers( + private val mFineractRepository: FineractRepository, +) : UseCase() { + + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.users + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe(object : Subscriber>() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(e.toString()) + } + + override fun onNext(userWithRoles: List) { + val tbp: MutableList = ArrayList() + for (userWithRole in userWithRoles) { + for ((_, name) in userWithRole.selectedRoles!!) { + if (name == Constants.MERCHANT) { + tbp.add(userWithRole) + break + } + } + } + useCaseCallback.onSuccess(ResponseValue(tbp)) + } + }) + } + + class RequestValues : UseCase.RequestValues + data class ResponseValue(val userWithRoleList: List) : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/RegisterUser.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/RegisterUser.kt new file mode 100644 index 000000000..f37d1df98 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/RegisterUser.kt @@ -0,0 +1,45 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.user + +import com.mifospay.core.model.entity.register.RegisterPayload +import okhttp3.ResponseBody +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class RegisterUser( + private val apiRepository: FineractRepository, +) : UseCase() { + + override fun executeUseCase(requestValues: RequestValues) { + apiRepository.registerUser(requestValues.registerPayload) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_REGISTERING_USER) + } + + override fun onNext(t: ResponseBody?) { + useCaseCallback.onSuccess(ResponseValue()) + } + }, + ) + } + + data class RequestValues(val registerPayload: RegisterPayload) : UseCase.RequestValues + class ResponseValue : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/UpdateUser.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/UpdateUser.kt new file mode 100644 index 000000000..9fac23150 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/UpdateUser.kt @@ -0,0 +1,52 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.user + +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.ErrorJsonMessageHelper.getUserMessage +import org.mifospay.core.network.GenericResponse +import retrofit2.HttpException +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class UpdateUser( + private val mFineractRepository: FineractRepository, +) : UseCase() { + override fun executeUseCase(requestValues: RequestValues) { + mFineractRepository.updateUser(requestValues.updateUserEntity, requestValues.userId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + var message: String + try { + message = (e as HttpException).response()!!.errorBody()!!.string() + message = getUserMessage(message) + } catch (e1: Exception) { + message = e1.message.toString() + } + useCaseCallback.onError(message) + } + + override fun onNext(genericResponse: GenericResponse?) { + useCaseCallback.onSuccess(ResponseValue()) + } + }, + ) + } + + class RequestValues(val updateUserEntity: Any, val userId: Int) : UseCase.RequestValues + + class ResponseValue : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/VerifyUser.kt b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/VerifyUser.kt new file mode 100644 index 000000000..d946eff90 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/domain/usecase/user/VerifyUser.kt @@ -0,0 +1,44 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.domain.usecase.user + +import com.mifospay.core.model.entity.register.UserVerify +import okhttp3.ResponseBody +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class VerifyUser( + private val apiRepository: FineractRepository, +) : UseCase() { + override fun executeUseCase(requestValues: RequestValues) { + apiRepository.verifyUser(requestValues.userVerify) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe( + object : Subscriber() { + override fun onCompleted() {} + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_VERIFYING_USER) + } + + override fun onNext(t: ResponseBody?) { + useCaseCallback.onSuccess(ResponseValue()) + } + }, + ) + } + + class RequestValues(val userVerify: UserVerify) : UseCase.RequestValues + class ResponseValue : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/AccountMapper.kt b/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/AccountMapper.kt new file mode 100644 index 000000000..672f1a393 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/AccountMapper.kt @@ -0,0 +1,35 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.fineract.entity.mapper + +import com.mifospay.core.model.domain.Account +import com.mifospay.core.model.entity.client.ClientAccounts + +class AccountMapper( + private val currencyMapper: CurrencyMapper, +) { + + fun transform(clientAccounts: ClientAccounts?): List { + val accountList = mutableListOf() + + clientAccounts?.savingsAccounts?.forEach { savingAccount -> + val account = Account( + name = savingAccount.productName, + number = savingAccount.accountNo, + id = savingAccount.id, + balance = savingAccount.accountBalance, + currency = currencyMapper.transform(savingAccount.currency), + productId = savingAccount.productId.toLong(), + ) + accountList.add(account) + } + return accountList + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/ClientDetailsMapper.kt b/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/ClientDetailsMapper.kt new file mode 100644 index 000000000..eccd4a613 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/ClientDetailsMapper.kt @@ -0,0 +1,34 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.fineract.entity.mapper + +import com.mifospay.core.model.entity.client.Client +import com.mifospay.core.model.domain.client.Client as DomainClient + +class ClientDetailsMapper { + fun transformList(clients: List?): List { + val clientList: MutableList = ArrayList() + clients?.forEach { client -> + clientList.add(transform(client)) + } + return clientList + } + + fun transform(client: Client?): DomainClient { + val clientDetails = DomainClient() + if (client != null) { + clientDetails.name = client.displayName + clientDetails.clientId = client.id.toLong() + clientDetails.externalId = client.externalId + clientDetails.mobileNo = client.mobileNo + } + return clientDetails + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/CurrencyMapper.kt b/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/CurrencyMapper.kt new file mode 100644 index 000000000..21270ee2d --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/CurrencyMapper.kt @@ -0,0 +1,23 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.fineract.entity.mapper + +import com.mifospay.core.model.entity.accounts.savings.Currency +import com.mifospay.core.model.domain.Currency as DomainCurrency + +class CurrencyMapper { + fun transform(savingsCurrency: Currency): DomainCurrency { + val currency = DomainCurrency() + currency.code = savingsCurrency.code + currency.displayLabel = savingsCurrency.displayLabel + currency.displaySymbol = savingsCurrency.displaySymbol + return currency + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/FetchAccount.kt b/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/FetchAccount.kt new file mode 100644 index 000000000..d21aa0b8e --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/FetchAccount.kt @@ -0,0 +1,62 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.fineract.entity.mapper + +import com.mifospay.core.model.domain.Account +import com.mifospay.core.model.entity.client.ClientAccounts +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.fineract.repository.FineractRepository +import org.mifospay.core.data.util.Constants +import rx.Subscriber +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers + +class FetchAccount( + private val fineractRepository: FineractRepository, + private val accountMapper: AccountMapper, +) : UseCase() { + + override fun executeUseCase(requestValues: RequestValues) { + fineractRepository.getSelfAccounts(requestValues.clientId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .subscribe(object : Subscriber() { + override fun onCompleted() {} + + override fun onError(e: Throwable) { + useCaseCallback.onError(Constants.ERROR_FETCHING_ACCOUNTS) + } + + override fun onNext(clientAccounts: ClientAccounts) { + val accounts: List = accountMapper.transform(clientAccounts) + if (accounts.isNotEmpty()) { + var walletAccount: Account? = null + for (account in accounts) { + if (account.productId.toInt() == Constants.WALLET_ACCOUNT_SAVINGS_PRODUCT_ID) { + walletAccount = account + break + } + } + if (walletAccount != null) { + useCaseCallback.onSuccess(ResponseValue(walletAccount)) + } else { + useCaseCallback.onError(Constants.NO_ACCOUNT_FOUND) + } + } else { + useCaseCallback.onError(Constants.NO_ACCOUNTS_FOUND) + } + } + }) + } + + data class RequestValues(val clientId: Long) : UseCase.RequestValues + + data class ResponseValue(val account: Account) : UseCase.ResponseValue +} diff --git a/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/SearchedEntitiesMapper.kt b/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/SearchedEntitiesMapper.kt new file mode 100644 index 000000000..affc21632 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/SearchedEntitiesMapper.kt @@ -0,0 +1,33 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.fineract.entity.mapper + +import com.mifospay.core.model.domain.SearchResult +import com.mifospay.core.model.entity.SearchedEntity + +class SearchedEntitiesMapper { + fun transformList(searchedEntities: List?): List { + val searchResults: MutableList = ArrayList() + + searchedEntities?.forEach { entity -> + searchResults.add(transform(entity)) + } + + return searchResults + } + + fun transform(searchedEntity: SearchedEntity): SearchResult { + val searchResult = SearchResult() + searchResult.resultId = searchedEntity.entityId + searchResult.resultName = searchedEntity.entityName + searchResult.resultType = searchedEntity.entityType + return searchResult + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/TransactionMapper.kt b/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/TransactionMapper.kt new file mode 100644 index 000000000..340298508 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/fineract/entity/mapper/TransactionMapper.kt @@ -0,0 +1,60 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.fineract.entity.mapper + +import com.mifospay.core.model.domain.Transaction +import com.mifospay.core.model.domain.TransactionType +import com.mifospay.core.model.entity.accounts.savings.SavingsWithAssociations +import com.mifospay.core.model.entity.accounts.savings.Transactions +import com.mifospay.core.model.utils.DateHelper + +class TransactionMapper( + private val currencyMapper: CurrencyMapper, +) { + + fun transformTransactionList(savingsWithAssociations: SavingsWithAssociations?): List { + val transactionList = ArrayList() + + savingsWithAssociations?.transactions?.forEach { transaction -> + transactionList.add(transformInvoice(transaction)) + } + return transactionList + } + + fun transformInvoice(transactions: Transactions?): Transaction { + val transaction = Transaction() + + if (transactions != null) { + transaction.transactionId = transactions.id.toString() + transactions.paymentDetailData?.let { + transaction.receiptId = it.receiptNumber + } + transaction.amount = transactions.amount + transactions.submittedOnDate.let { + transaction.date = DateHelper.getDateAsString(it) + } + transaction.currency = currencyMapper.transform(transactions.currency) + transaction.transactionType = TransactionType.OTHER + + if (transactions.transactionType.deposit) { + transaction.transactionType = TransactionType.CREDIT + } + + if (transactions.transactionType.withdrawal) { + transaction.transactionType = TransactionType.DEBIT + } + + transactions.transfer.let { + transaction.transferId = it.id + } + } + return transaction + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/fineract/repository/FineractRepository.kt b/core/data/src/main/java/org/mifospay/core/data/fineract/repository/FineractRepository.kt new file mode 100644 index 000000000..89e7e7a53 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/fineract/repository/FineractRepository.kt @@ -0,0 +1,327 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.fineract.repository + +import com.mifospay.core.model.domain.NewAccount +import com.mifospay.core.model.domain.NotificationPayload +import com.mifospay.core.model.domain.client.NewClient +import com.mifospay.core.model.domain.twofactor.AccessToken +import com.mifospay.core.model.domain.twofactor.DeliveryMethod +import com.mifospay.core.model.domain.user.NewUser +import com.mifospay.core.model.domain.user.User +import com.mifospay.core.model.entity.Page +import com.mifospay.core.model.entity.SearchedEntity +import com.mifospay.core.model.entity.TPTResponse +import com.mifospay.core.model.entity.UserWithRole +import com.mifospay.core.model.entity.accounts.savings.SavingsWithAssociations +import com.mifospay.core.model.entity.accounts.savings.Transactions +import com.mifospay.core.model.entity.accounts.savings.TransferDetail +import com.mifospay.core.model.entity.authentication.AuthenticationPayload +import com.mifospay.core.model.entity.beneficary.Beneficiary +import com.mifospay.core.model.entity.beneficary.BeneficiaryPayload +import com.mifospay.core.model.entity.beneficary.BeneficiaryUpdatePayload +import com.mifospay.core.model.entity.client.Client +import com.mifospay.core.model.entity.client.ClientAccounts +import com.mifospay.core.model.entity.invoice.Invoice +import com.mifospay.core.model.entity.invoice.InvoiceEntity +import com.mifospay.core.model.entity.kyc.KYCLevel1Details +import com.mifospay.core.model.entity.payload.StandingInstructionPayload +import com.mifospay.core.model.entity.payload.TransferPayload +import com.mifospay.core.model.entity.register.RegisterPayload +import com.mifospay.core.model.entity.register.UserVerify +import com.mifospay.core.model.entity.savedcards.Card +import com.mifospay.core.model.entity.standinginstruction.SDIResponse +import com.mifospay.core.model.entity.standinginstruction.StandingInstruction +import okhttp3.MultipartBody +import okhttp3.ResponseBody +import org.mifospay.core.data.domain.usecase.client.CreateClient +import org.mifospay.core.data.domain.usecase.user.CreateUser +import org.mifospay.core.data.util.Constants +import org.mifospay.core.network.FineractApiManager +import org.mifospay.core.network.GenericResponse +import org.mifospay.core.network.SelfServiceApiManager +import org.mifospay.core.network.services.KtorAuthenticationService +import rx.Observable + +@Suppress("TooManyFunctions") +class FineractRepository( + private val fineractApiManager: FineractApiManager, + private val selfApiManager: SelfServiceApiManager, + private val ktorAuthenticationService: KtorAuthenticationService, +) { + fun createClient(newClient: NewClient): Observable { + return fineractApiManager.clientsApi.createClient(newClient) + } + + fun createUser(user: NewUser): Observable { + return fineractApiManager.userApi.createUser(user) + } + + fun updateUser(updateUserEntity: Any, userId: Int): Observable { + return fineractApiManager.userApi.updateUser(userId, updateUserEntity) + } + + fun registerUser(registerPayload: RegisterPayload): Observable { + return fineractApiManager.registrationAPi.registerUser(registerPayload) + } + + fun deleteUser(userId: Int): Observable { + return fineractApiManager.userApi.deleteUser(userId) + } + + fun verifyUser(userVerify: UserVerify): Observable { + return fineractApiManager.registrationAPi.verifyUser(userVerify) + } + + fun searchResources( + query: String, + resources: String, + exactMatch: Boolean, + ): Observable> { + return fineractApiManager.searchApi.searchResources(query, resources, exactMatch) + } + + fun updateClient(clientId: Long, payload: Any): Observable { + return fineractApiManager.clientsApi.updateClient(clientId, payload) + .map { responseBody -> responseBody } + } + + fun createSavingsAccount(newAccount: NewAccount?): Observable { + return fineractApiManager.clientsApi.createAccount(newAccount) + } + + fun getAccounts(clientId: Long): Observable { + return fineractApiManager.clientsApi.getAccounts(clientId, Constants.SAVINGS) + } + + suspend fun savingsAccounts(): Page = + fineractApiManager.ktorSavingsAccountApi.getSavingsAccounts(-1) + + suspend fun blockUnblockAccount(accountId: Long, command: String?): GenericResponse { + return fineractApiManager.ktorSavingsAccountApi.blockUnblockAccount( + accountId, + command, + ) + } + + fun getClientDetails(clientId: Long): Observable { + return fineractApiManager.clientsApi.getClientForId(clientId) + } + + fun getClientImage(clientId: Long): Observable { + return fineractApiManager.clientsApi.getClientImage(clientId) + } + + fun addSavedCards( + clientId: Long, + card: Card, + ): Observable { + return fineractApiManager.savedCardApi.addSavedCard(clientId.toInt(), card) + } + + fun fetchSavedCards(clientId: Long): Observable> { + return fineractApiManager.savedCardApi.getSavedCards(clientId.toInt()) + } + + fun editSavedCard(clientId: Int, card: Card): Observable { + return fineractApiManager.savedCardApi.updateCard(clientId, card.id, card) + } + + fun deleteSavedCard(clientId: Int, cardId: Int): Observable { + return fineractApiManager.savedCardApi.deleteCard(clientId, cardId) + } + + fun uploadKYCDocs( + entityType: String, + entityId: Long, + name: String, + desc: String, + file: MultipartBody.Part, + ): Observable { + return fineractApiManager.documentApi.createDocument( + entityType, + entityId, + name, + desc, + file, + ) + } + + fun getAccountTransfer(transferId: Long): Observable { + return fineractApiManager.accountTransfersApi.getAccountTransfer(transferId) + } + + fun uploadKYCLevel1Details( + clientId: Int, + kycLevel1Details: KYCLevel1Details, + ): Observable { + return fineractApiManager.kycLevel1Api.addKYCLevel1Details( + clientId, + kycLevel1Details, + ) + } + + fun fetchKYCLevel1Details(clientId: Int): Observable> { + return fineractApiManager.kycLevel1Api.fetchKYCLevel1Details(clientId) + } + + fun updateKYCLevel1Details( + clientId: Int, + kycLevel1Details: KYCLevel1Details, + ): Observable { + return fineractApiManager.kycLevel1Api.updateKYCLevel1Details( + clientId, + kycLevel1Details, + ) + } + + fun fetchNotifications(clientId: Long): Observable> { + return fineractApiManager.notificationApi.fetchNotifications(clientId) + } + + val deliveryMethods: Observable> + get() = fineractApiManager.twoFactorAuthApi.deliveryMethods + + fun requestOTP(deliveryMethod: String): Observable { + return fineractApiManager.twoFactorAuthApi.requestOTP(deliveryMethod) + } + + fun validateToken(token: String): Observable { + return fineractApiManager.twoFactorAuthApi.validateToken(token) + } + + fun getTransactionReceipt( + outputType: String, + transactionId: String, + ): Observable { + return fineractApiManager.runReportApi.getTransactionReceipt( + outputType, + transactionId, + ) + } + + fun addInvoice(clientId: Long, invoice: InvoiceEntity?): Observable { + return Observable.fromCallable { + fineractApiManager.invoiceApi.addInvoice(clientId, invoice) + } + } + + fun fetchInvoices(clientId: Long): Observable> { + return fineractApiManager.invoiceApi.getInvoices(clientId) + } + + fun fetchInvoice(clientId: Long, invoiceId: Long): Observable> { + return fineractApiManager.invoiceApi.getInvoice(clientId, invoiceId) + } + + fun editInvoice(clientId: Long, invoice: Invoice): Observable { + return Observable.fromCallable { + fineractApiManager.invoiceApi.updateInvoice(clientId, invoice.id, invoice) + } + } + + fun deleteInvoice(clientId: Long, invoiceId: Long): Observable { + return Observable.fromCallable { + fineractApiManager.invoiceApi.deleteInvoice(clientId, invoiceId) + } + } + + val users: Observable> + get() = fineractApiManager.userApi.users + + fun getUser(): Observable { + return fineractApiManager.userApi.getUser() + } + + fun makeThirdPartyTransfer(transferPayload: TransferPayload): Observable { + return fineractApiManager.thirdPartyTransferApi.makeTransfer(transferPayload) + } + + fun createStandingInstruction( + standingInstructionPayload: StandingInstructionPayload, + ): Observable { + return fineractApiManager.standingInstructionApi + .createStandingInstruction(standingInstructionPayload) + } + + fun getAllStandingInstructions(clientId: Long): Observable> { + return fineractApiManager.standingInstructionApi.getAllStandingInstructions(clientId) + } + + fun getStandingInstruction(standingInstructionId: Long): Observable { + return fineractApiManager.standingInstructionApi + .getStandingInstruction(standingInstructionId) + } + + fun updateStandingInstruction( + standingInstructionId: Long, + data: StandingInstructionPayload, + ): Observable { + return fineractApiManager.standingInstructionApi.updateStandingInstruction( + standingInstructionId, + data, + "update", + ) + } + + fun deleteStandingInstruction(standingInstruction: Long): Observable { + return fineractApiManager.standingInstructionApi.deleteStandingInstruction( + standingInstruction, + "delete", + ) + } + + // self user apis + suspend fun loginSelf(payload: AuthenticationPayload): User { + return ktorAuthenticationService.authenticate(payload) + } + + fun getSelfClientDetails(clientId: Long): Observable { + return selfApiManager.clientsApi.getClientForId(clientId) + } + + val selfClientDetails: Observable> + get() = selfApiManager.clientsApi.clients + + suspend fun getSelfAccountTransactions(accountId: Long): SavingsWithAssociations { + return selfApiManager.ktorSavingsAccountApi.getSavingsWithAssociations( + accountId, + Constants.TRANSACTIONS, + ) + } + + suspend fun getSelfAccountTransactionFromId( + accountId: Long, + transactionId: Long, + ): Transactions { + return selfApiManager.ktorSavingsAccountApi.getSavingAccountTransaction( + accountId, + transactionId, + ) + } + + fun getSelfAccounts(clientId: Long): Observable { + return selfApiManager.clientsApi.getAccounts(clientId, Constants.SAVINGS) + } + + val beneficiaryList: Observable> + get() = selfApiManager.beneficiaryApi.beneficiaryList + + fun createBeneficiary(beneficiaryPayload: BeneficiaryPayload): Observable { + return selfApiManager.beneficiaryApi.createBeneficiary(beneficiaryPayload) + } + + fun updateBeneficiary( + beneficiaryId: Long, + payload: BeneficiaryUpdatePayload, + ): Observable { + return selfApiManager.beneficiaryApi.updateBeneficiary(beneficiaryId, payload) + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/repository/auth/AuthenticationUserRepository.kt b/core/data/src/main/java/org/mifospay/core/data/repository/auth/AuthenticationUserRepository.kt new file mode 100644 index 000000000..70cc1a2c3 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/repository/auth/AuthenticationUserRepository.kt @@ -0,0 +1,35 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.repository.auth + +import com.mifospay.core.model.UserData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import org.mifospay.core.datastore.PreferencesHelper + +class AuthenticationUserRepository( + private val preferencesHelper: PreferencesHelper, +) : UserDataRepository { + + override val userData: Flow = flow { + emit( + UserData( + isAuthenticated = !preferencesHelper.token.isNullOrEmpty(), + userName = preferencesHelper.username, + // user = preferencesHelper.user, + clientId = preferencesHelper.clientId, + ), + ) + } + + override fun logOut() { + preferencesHelper.clear() + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/repository/local/LocalRepository.kt b/core/data/src/main/java/org/mifospay/core/data/repository/local/LocalRepository.kt new file mode 100644 index 000000000..de55915ed --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/repository/local/LocalRepository.kt @@ -0,0 +1,33 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.repository.local + +import com.mifospay.core.model.domain.client.Client +import org.mifospay.core.datastore.PreferencesHelper + +class LocalRepository( + val preferencesHelper: PreferencesHelper, +) { + + val clientDetails: Client + get() { + val details = Client() + details.name = preferencesHelper.fullName + details.clientId = preferencesHelper.clientId + details.externalId = preferencesHelper.clientVpa + return details + } + + fun saveClientData(client: Client) { + preferencesHelper.saveFullName(client.name) + preferencesHelper.clientId = client.clientId + preferencesHelper.clientVpa = client.externalId + } +} diff --git a/core/data/src/main/java/org/mifospay/core/data/repository/local/MifosLocalAssetRepository.kt b/core/data/src/main/java/org/mifospay/core/data/repository/local/MifosLocalAssetRepository.kt new file mode 100644 index 000000000..4456a8363 --- /dev/null +++ b/core/data/src/main/java/org/mifospay/core/data/repository/local/MifosLocalAssetRepository.kt @@ -0,0 +1,47 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.data.repository.local + +import com.mifospay.core.model.City +import com.mifospay.core.model.Country +import com.mifospay.core.model.State +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import org.mifospay.core.network.localAssets.LocalAssetDataSource + +/** + * Local implementation of the [LocalAssetRepository] that retrieves the countries, banks, cities + * and state list from a JSON String. + * + */ + +class MifosLocalAssetRepository( + private val ioDispatcher: CoroutineDispatcher, + private val datasource: LocalAssetDataSource, +) : LocalAssetRepository { + + override fun getCountries(): Flow> = flow { + emit(datasource.getCountries()) + }.flowOn(ioDispatcher) + + override fun getStateList(): Flow> = flow { + emit(datasource.getStateList()) + }.flowOn(ioDispatcher) + + override fun getBanks(): Flow> = flow { + emit(datasource.getBanks()) + }.flowOn(ioDispatcher) + + override fun getCities(): Flow> = flow { + emit(datasource.getCities()) + }.flowOn(ioDispatcher) +} diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts index 154a4d55b..1deb96ab2 100644 --- a/core/datastore/build.gradle.kts +++ b/core/datastore/build.gradle.kts @@ -8,9 +8,10 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.kmp.library) + alias(libs.plugins.mifospay.android.library) } + android { namespace = "org.mifospay.core.datastore" defaultConfig { @@ -23,25 +24,13 @@ android { } } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(libs.multiplatform.settings) - implementation(libs.multiplatform.settings.serialization) - implementation(libs.multiplatform.settings.coroutines) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.serialization.core) - implementation(projects.core.model) - implementation(projects.core.common) - implementation(projects.core.datastoreProto) - } - - commonTest.dependencies { - implementation(libs.multiplatform.settings.test) - } +dependencies { + api(libs.kotlinx.datetime) + api(libs.androidx.dataStore.core) + api(projects.core.datastoreProto) + api(projects.core.common) + api(projects.core.model) - desktopMain.dependencies { - implementation(libs.kotlinx.coroutines.swing) - } - } + implementation(libs.squareup.retrofit.converter.gson) + implementation(libs.koin.android) } \ No newline at end of file diff --git a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/IconBox.kt b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/IconBox.kt index cb8345d1e..b6d35f999 100644 --- a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/IconBox.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/IconBox.kt @@ -19,8 +19,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview import org.mifospay.core.designsystem.icon.MifosIcons import org.mifospay.core.designsystem.theme.MifosTheme diff --git a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/MifosTab.kt b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/MifosTab.kt index cdd34932b..5a06560d4 100644 --- a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/MifosTab.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/MifosTab.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Tab import androidx.compose.material3.Text -import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -28,20 +27,27 @@ fun MifosTab( selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, - selectedColor: Color = MaterialTheme.colorScheme.primary, - unselectedColor: Color = MaterialTheme.colorScheme.primaryContainer, + selectedContentColor: Color = MaterialTheme.colorScheme.primary, + unselectedContentColor: Color = MaterialTheme.colorScheme.primaryContainer, ) { Tab( text = { - Text(text = text) + Text( + text = text, + color = if (selected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurface + }, + ) }, selected = selected, - onClick = onClick, - selectedContentColor = contentColorFor(selectedColor), - unselectedContentColor = contentColorFor(unselectedColor), modifier = modifier .clip(RoundedCornerShape(25.dp)) - .background(if (selected) selectedColor else unselectedColor) + .background(if (selected) selectedContentColor else unselectedContentColor) .padding(horizontal = 20.dp), + selectedContentColor = selectedContentColor, + unselectedContentColor = unselectedContentColor, + onClick = onClick, ) } diff --git a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/MifosTopBar.kt b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/MifosTopBar.kt index 164e59097..d85d0ae47 100644 --- a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/MifosTopBar.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/MifosTopBar.kt @@ -18,12 +18,13 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import org.mifospay.core.designsystem.icon.MifosIcons @OptIn(ExperimentalMaterial3Api::class) @Composable fun MifosTopBar( - topBarTitle: String, + topBarTitle: Int, backPress: () -> Unit, modifier: Modifier = Modifier, actions: @Composable RowScope.() -> Unit = {}, @@ -33,7 +34,7 @@ fun MifosTopBar( CenterAlignedTopAppBar( title = { Text( - text = topBarTitle, + text = stringResource(id = topBarTitle), style = MaterialTheme.typography.titleMedium, color = titleColor ?: MaterialTheme.colorScheme.onSurface, ) diff --git a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/Navigation.kt b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/Navigation.kt index e532dbe3f..61b198126 100644 --- a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/Navigation.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/Navigation.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview import org.mifospay.core.designsystem.icon.MifosIcons import org.mifospay.core.designsystem.theme.MifosTheme @@ -86,7 +85,7 @@ fun MifosNavigationBar( ) { NavigationBar( modifier = modifier, - containerColor = MaterialTheme.colorScheme.background, + containerColor = MaterialTheme.colorScheme.onPrimary, contentColor = MifosNavigationDefaults.navigationContentColor(), tonalElevation = 0.dp, content = content, @@ -160,24 +159,22 @@ fun MifosNavigationRail( ) } -@Preview +@ThemePreviews @Composable fun MifosNavigationBarPreview() { val items = listOf("Home", "Payments", "Finance", "Profile") - val icons = - listOf( - MifosIcons.Home, - MifosIcons.Payment, - MifosIcons.Finance, - MifosIcons.Profile, - ) - val selectedIcons = - listOf( - MifosIcons.HomeBoarder, - MifosIcons.Payment, - MifosIcons.Finance, - MifosIcons.ProfileBoarder, - ) + val icons = listOf( + MifosIcons.Home, + MifosIcons.Payment, + MifosIcons.Finance, + MifosIcons.Profile, + ) + val selectedIcons = listOf( + MifosIcons.HomeBoarder, + MifosIcons.Payment, + MifosIcons.Finance, + MifosIcons.ProfileBoarder, + ) MifosTheme { MifosNavigationBar { @@ -204,24 +201,22 @@ fun MifosNavigationBarPreview() { } } -@Preview +@ThemePreviews @Composable fun MifosNavigationRailPreview() { val items = listOf("Home", "Payments", "Finance", "Profile") - val icons = - listOf( - MifosIcons.Home, - MifosIcons.Payment, - MifosIcons.Finance, - MifosIcons.Profile, - ) - val selectedIcons = - listOf( - MifosIcons.HomeBoarder, - MifosIcons.Payment, - MifosIcons.Finance, - MifosIcons.ProfileBoarder, - ) + val icons = listOf( + MifosIcons.Home, + MifosIcons.Payment, + MifosIcons.Finance, + MifosIcons.Profile, + ) + val selectedIcons = listOf( + MifosIcons.HomeBoarder, + MifosIcons.Payment, + MifosIcons.Finance, + MifosIcons.ProfileBoarder, + ) MifosTheme { MifosNavigationRail { @@ -256,8 +251,8 @@ object MifosNavigationDefaults { fun navigationContentColor() = MaterialTheme.colorScheme.onSurfaceVariant @Composable - fun navigationSelectedItemColor() = MaterialTheme.colorScheme.onPrimaryContainer + fun navigationSelectedItemColor() = MaterialTheme.colorScheme.onSurface @Composable - fun navigationIndicatorColor() = MaterialTheme.colorScheme.primaryContainer + fun navigationIndicatorColor() = MaterialTheme.colorScheme.onPrimary } diff --git a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/TopAppBar.kt b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/TopAppBar.kt index 11c641dc1..5c01fc963 100644 --- a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/TopAppBar.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/TopAppBar.kt @@ -11,12 +11,15 @@ package org.mifospay.core.designsystem.component +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.RowScope import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -24,14 +27,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag -import org.jetbrains.compose.ui.tooling.preview.Preview +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import org.mifospay.core.designsystem.icon.MifosIcons import org.mifospay.core.designsystem.theme.MifosTheme @OptIn(ExperimentalMaterial3Api::class) @Composable fun MifosTopAppBar( - titleRes: String, + @StringRes titleRes: Int, modifier: Modifier = Modifier, navigationIcon: ImageVector? = null, navigationIconContentDescription: String? = null, @@ -42,7 +46,7 @@ fun MifosTopAppBar( onActionClick: (() -> Unit)? = null, ) { CenterAlignedTopAppBar( - title = { Text(text = titleRes) }, + title = { Text(text = stringResource(id = titleRes)) }, navigationIcon = { navigationIcon?.let { IconButton(onClick = onNavigationClick!!) { @@ -70,15 +74,38 @@ fun MifosTopAppBar( ) } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MifosTopAppBar( + @StringRes titleRes: Int, + modifier: Modifier = Modifier, + actions: + @Composable() + (RowScope.() -> Unit) = {}, + colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent, + ), +) { + TopAppBar( + title = { Text(text = stringResource(id = titleRes)) }, + actions = actions, + colors = colors, + modifier = modifier.testTag("mifosTopAppBar"), + ) +} + @Composable fun MifosNavigationTopAppBar( - titleRes: String, + @StringRes titleRes: Int, onNavigationClick: (() -> Unit)?, ) { MifosTopAppBar( titleRes = titleRes, navigationIcon = MifosIcons.Back, - navigationIconContentDescription = titleRes, + navigationIconContentDescription = + stringResource( + id = titleRes, + ), colors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = Color.Transparent, @@ -88,12 +115,12 @@ fun MifosNavigationTopAppBar( } @OptIn(ExperimentalMaterial3Api::class) -@Preview +@Preview("Top App Bar") @Composable private fun MifosTopAppBarPreview() { MifosTheme { MifosTopAppBar( - titleRes = "Demo Preview", + titleRes = android.R.string.untitled, navigationIcon = MifosIcons.Search, navigationIconContentDescription = "Navigation icon", actionIcon = MifosIcons.MoreVert, diff --git a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt index 96fbabe19..553fe7e3c 100644 --- a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/icon/MifosIcons.kt @@ -14,45 +14,32 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.ArrowOutward import androidx.compose.material.icons.filled.AttachMoney -import androidx.compose.material.icons.filled.Badge -import androidx.compose.material.icons.filled.CalendarMonth import androidx.compose.material.icons.filled.Camera import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.CheckCircleOutline import androidx.compose.material.icons.filled.ChevronLeft import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Description import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.FlashOff import androidx.compose.material.icons.filled.FlashOn import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Photo import androidx.compose.material.icons.filled.PhotoLibrary import androidx.compose.material.icons.filled.QrCode import androidx.compose.material.icons.filled.QrCode2 -import androidx.compose.material.icons.filled.RadioButtonChecked -import androidx.compose.material.icons.filled.RadioButtonUnchecked +import androidx.compose.material.icons.filled.RemoveCircleOutline import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.outlined.AccountCircle import androidx.compose.material.icons.outlined.Cancel -import androidx.compose.material.icons.outlined.DeleteOutline -import androidx.compose.material.icons.outlined.DoneAll import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Home -import androidx.compose.material.icons.outlined.Info -import androidx.compose.material.icons.outlined.Lock -import androidx.compose.material.icons.outlined.Notifications -import androidx.compose.material.icons.outlined.QrCodeScanner import androidx.compose.material.icons.outlined.Settings -import androidx.compose.material.icons.outlined.Share -import androidx.compose.material.icons.outlined.Visibility -import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material.icons.outlined.Wallet import androidx.compose.material.icons.rounded.AccountBalance import androidx.compose.material.icons.rounded.AccountCircle @@ -72,15 +59,10 @@ import androidx.compose.ui.graphics.vector.ImageVector * Mifos icons. Material icons are [ImageVector]s, custom icons are drawable resource IDs. */ object MifosIcons { - val OutlinedInfo = Icons.Outlined.Info - val OutlinedLock = Icons.Outlined.Lock - val OutlinedNotifications = Icons.Outlined.Notifications val ChevronRight: ImageVector = Icons.Filled.ChevronRight val QrCode: ImageVector = Icons.Filled.QrCode val Close: ImageVector = Icons.Filled.Close val AttachMoney: ImageVector = Icons.Filled.AttachMoney - val OutlinedVisibilityOff: ImageVector = Icons.Outlined.VisibilityOff - val OutlinedVisibility: ImageVector = Icons.Outlined.Visibility val VisibilityOff: ImageVector = Icons.Filled.VisibilityOff val Visibility: ImageVector = Icons.Filled.Visibility val Check: ImageVector = Icons.Default.Check @@ -98,7 +80,6 @@ object MifosIcons { val Back = Icons.AutoMirrored.Outlined.ArrowBack val Copy = Icons.Filled.ContentCopy val Share = Icons.Filled.Share - val OutlinedShare = Icons.Outlined.Share val ArrowBack = Icons.AutoMirrored.Filled.ArrowBack val ArrowBack2 = Icons.Filled.ChevronLeft val Cancel = Icons.Outlined.Cancel @@ -108,7 +89,6 @@ object MifosIcons { val Camera = Icons.Filled.Camera val PhotoLibrary = Icons.Filled.PhotoLibrary val Delete = Icons.Filled.Delete - val OutlinedDelete = Icons.Outlined.DeleteOutline val RoundedInfo = Icons.Rounded.Info val Contact = Icons.Rounded.Contacts val Settings = Icons.Rounded.Settings @@ -121,12 +101,6 @@ object MifosIcons { val QrCode2 = Icons.Filled.QrCode2 val Edit = Icons.Filled.Edit val Edit2 = Icons.Outlined.Edit - val CalenderMonth = Icons.Filled.CalendarMonth - val OutlinedDoneAll = Icons.Outlined.DoneAll - val Person = Icons.Filled.Person - val Badge = Icons.Filled.Badge - val DataInfo = Icons.Filled.Description - val Scan = Icons.Outlined.QrCodeScanner - val RadioButtonUnchecked = Icons.Default.RadioButtonUnchecked - val RadioButtonChecked = Icons.Filled.RadioButtonChecked + val CheckCircle = Icons.Default.CheckCircleOutline + val CheckCircle2 = Icons.Default.RemoveCircleOutline } diff --git a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/theme/Type.kt b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/theme/Type.kt index cf918ca06..c5e9e810b 100644 --- a/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/theme/Type.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/theme/Type.kt @@ -10,79 +10,65 @@ package org.mifospay.core.designsystem.theme import androidx.compose.material3.Typography -import androidx.compose.runtime.Composable import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.unit.sp -import mobile_wallet.core.designsystem.generated.resources.Res -import mobile_wallet.core.designsystem.generated.resources.outfit_black -import mobile_wallet.core.designsystem.generated.resources.outfit_bold -import mobile_wallet.core.designsystem.generated.resources.outfit_extra_bold -import mobile_wallet.core.designsystem.generated.resources.outfit_extra_light -import mobile_wallet.core.designsystem.generated.resources.outfit_light -import mobile_wallet.core.designsystem.generated.resources.outfit_medium -import mobile_wallet.core.designsystem.generated.resources.outfit_regular -import mobile_wallet.core.designsystem.generated.resources.outfit_semi_bold -import mobile_wallet.core.designsystem.generated.resources.outfit_thin -import org.jetbrains.compose.resources.Font +import org.mifospay.core.designsystem.R -@Composable -private fun fontFamily(): FontFamily { - return FontFamily( - Font(Res.font.outfit_black, FontWeight.Black), - Font(Res.font.outfit_bold, FontWeight.Bold), - Font(Res.font.outfit_semi_bold, FontWeight.SemiBold), - Font(Res.font.outfit_medium, FontWeight.Medium), - Font(Res.font.outfit_regular, FontWeight.Normal), - Font(Res.font.outfit_light, FontWeight.Light), - Font(Res.font.outfit_thin, FontWeight.Thin), - Font(Res.font.outfit_extra_light, FontWeight.ExtraLight), - Font(Res.font.outfit_extra_bold, FontWeight.ExtraBold), - ) -} +private val fontFamily = FontFamily( + Font(R.font.outfit_black, FontWeight.Black), + Font(R.font.outfit_bold, FontWeight.Bold), + Font(R.font.outfit_semi_bold, FontWeight.SemiBold), + Font(R.font.outfit_medium, FontWeight.Medium), + Font(R.font.outfit_regular, FontWeight.Normal), + Font(R.font.outfit_light, FontWeight.Light), + Font(R.font.outfit_thin, FontWeight.Thin), + Font(R.font.outfit_extra_light, FontWeight.ExtraLight), + Font(R.font.outfit_extra_bold, FontWeight.ExtraBold), +) // Set of Material typography styles to start with -@Composable -internal fun mifosTypography() = Typography( +internal val MifosTypography = Typography( displayLarge = TextStyle( - fontFamily = fontFamily(), + fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 57.sp, lineHeight = 64.sp, letterSpacing = (-0.25).sp, ), displayMedium = TextStyle( - fontFamily = fontFamily(), + fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 45.sp, lineHeight = 52.sp, letterSpacing = 0.sp, ), displaySmall = TextStyle( - fontFamily = fontFamily(), + fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 36.sp, lineHeight = 44.sp, letterSpacing = 0.sp, ), headlineLarge = TextStyle( - fontFamily = fontFamily(), + fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 32.sp, lineHeight = 40.sp, letterSpacing = 0.sp, ), headlineMedium = TextStyle( - fontFamily = fontFamily(), + fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 28.sp, lineHeight = 36.sp, letterSpacing = 0.sp, ), headlineSmall = TextStyle( - fontFamily = fontFamily(), + fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 24.sp, lineHeight = 32.sp, @@ -93,20 +79,20 @@ internal fun mifosTypography() = Typography( ), ), titleLarge = TextStyle( - fontFamily = fontFamily(), + fontFamily = fontFamily, fontWeight = FontWeight.SemiBold, fontSize = 24.sp, lineHeight = 30.24.sp, ), titleMedium = TextStyle( - fontFamily = fontFamily(), + fontFamily = fontFamily, fontWeight = FontWeight.SemiBold, fontSize = 20.sp, lineHeight = 28.sp, letterSpacing = 0.1.sp, ), titleSmall = TextStyle( - fontFamily = fontFamily(), + fontFamily = fontFamily, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 20.sp, @@ -114,7 +100,7 @@ internal fun mifosTypography() = Typography( ), // Default text style bodyLarge = TextStyle( - fontFamily = fontFamily(), + fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, @@ -125,14 +111,14 @@ internal fun mifosTypography() = Typography( ), ), bodyMedium = TextStyle( - fontFamily = fontFamily(), + fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 20.sp, letterSpacing = 0.25.sp, ), bodySmall = TextStyle( - fontFamily = fontFamily(), + fontFamily = fontFamily, fontWeight = FontWeight.Normal, fontSize = 12.sp, lineHeight = 16.sp, @@ -140,7 +126,7 @@ internal fun mifosTypography() = Typography( ), // Used for Button labelLarge = TextStyle( - fontFamily = fontFamily(), + fontFamily = fontFamily, fontWeight = FontWeight.Medium, fontSize = 16.sp, lineHeight = 20.sp, @@ -148,7 +134,7 @@ internal fun mifosTypography() = Typography( ), // Used for Navigation items labelMedium = TextStyle( - fontFamily = fontFamily(), + fontFamily = fontFamily, fontWeight = FontWeight.Medium, fontSize = 12.sp, lineHeight = 16.sp, @@ -160,7 +146,7 @@ internal fun mifosTypography() = Typography( ), // Used for Tag labelSmall = TextStyle( - fontFamily = fontFamily(), + fontFamily = fontFamily, fontWeight = FontWeight.Medium, fontSize = 10.sp, lineHeight = 14.sp, diff --git a/core/designsystem/src/main/kotlin/org/mifospay/core/designsystem/component/MifosScaffold.kt b/core/designsystem/src/main/kotlin/org/mifospay/core/designsystem/component/MifosScaffold.kt new file mode 100644 index 000000000..f1826e291 --- /dev/null +++ b/core/designsystem/src/main/kotlin/org/mifospay/core/designsystem/component/MifosScaffold.kt @@ -0,0 +1,66 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.designsystem.component + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@Composable +fun MifosScaffold( + backPress: () -> Unit, + modifier: Modifier = Modifier, + topBarTitle: Int? = null, + titleColor: Color? = MaterialTheme.colorScheme.onSurface, + iconTint: Color? = null, + floatingActionButtonContent: FloatingActionButtonContent? = null, + snackbarHost: @Composable () -> Unit = {}, + scaffoldContent: @Composable (PaddingValues) -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, +) { + Scaffold( + topBar = { + if (topBarTitle != null) { + MifosTopBar( + topBarTitle = topBarTitle, + backPress = backPress, + actions = actions, + titleColor = titleColor, + iconTint = iconTint, + ) + } + }, + floatingActionButton = { + floatingActionButtonContent?.let { content -> + FloatingActionButton( + onClick = content.onClick, + contentColor = content.contentColor, + content = content.content, + containerColor = MaterialTheme.colorScheme.primary, + ) + } + }, + snackbarHost = snackbarHost, + content = scaffoldContent, + modifier = modifier, + containerColor = Color.Transparent, + ) +} + +data class FloatingActionButtonContent( + val onClick: (() -> Unit), + val contentColor: Color, + val content: (@Composable () -> Unit), +) diff --git a/core/designsystem/src/main/kotlin/org/mifospay/core/designsystem/component/TextField.kt b/core/designsystem/src/main/kotlin/org/mifospay/core/designsystem/component/TextField.kt new file mode 100644 index 000000000..f1c90fd8d --- /dev/null +++ b/core/designsystem/src/main/kotlin/org/mifospay/core/designsystem/component/TextField.kt @@ -0,0 +1,339 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.designsystem.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +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 +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.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.mifospay.core.designsystem.theme.MifosTheme + +@Composable +fun MfOutlinedTextField( + value: String, + label: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + isError: Boolean = false, + errorMessage: String = "", + singleLine: Boolean = false, + onKeyboardActions: (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, +) { + OutlinedTextField( + modifier = modifier, + value = value, + onValueChange = onValueChange, + label = { Text(label) }, + supportingText = { + if (isError) { + Text(text = errorMessage) + } + }, + singleLine = singleLine, + trailingIcon = trailingIcon, + keyboardActions = KeyboardActions { + onKeyboardActions?.invoke() + }, + keyboardOptions = keyboardOptions, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.onSurface, + focusedLabelColor = MaterialTheme.colorScheme.onSurface, + ), + textStyle = LocalDensity.current.run { + TextStyle(fontSize = 18.sp, color = MaterialTheme.colorScheme.onSurface) + }, + ) +} + +@Composable +fun MfPasswordTextField( + password: String, + label: String, + isError: Boolean, + isPasswordVisible: Boolean, + onTogglePasswordVisibility: () -> Unit, + onPasswordChange: (String) -> Unit, + modifier: Modifier = Modifier, + errorMessage: String? = null, +) { + OutlinedTextField( + modifier = modifier, + value = password, + onValueChange = onPasswordChange, + label = { Text(label) }, + isError = isError, + visualTransformation = if (isPasswordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + supportingText = { + errorMessage?.let { Text(text = it) } + }, + trailingIcon = { + IconButton(onClick = onTogglePasswordVisibility) { + Icon( + if (isPasswordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff, + contentDescription = "Show password", + ) + } + }, + ) +} + +@Composable +fun MifosOutlinedTextField( + label: Int, + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + modifier: Modifier = Modifier, + maxLines: Int = 1, + singleLine: Boolean = true, + icon: Int? = null, + visualTransformation: VisualTransformation = VisualTransformation.None, + trailingIcon: @Composable (() -> Unit)? = null, + keyboardActions: KeyboardActions = KeyboardActions.Default, + error: Boolean = false, +) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(stringResource(id = label)) }, + modifier = modifier, + leadingIcon = if (icon != null) { + { + Image( + painter = painterResource(id = icon), + contentDescription = null, + colorFilter = ColorFilter.tint( + MaterialTheme.colorScheme.onSurface, + ), + ) + } + } else { + null + }, + trailingIcon = trailingIcon, + maxLines = maxLines, + singleLine = singleLine, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.onSurface, + focusedLabelColor = MaterialTheme.colorScheme.onSurface, + ), + textStyle = LocalDensity.current.run { + TextStyle(fontSize = 18.sp, color = MaterialTheme.colorScheme.onSurface) + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = keyboardActions, + visualTransformation = visualTransformation, + isError = error, + ) +} + +@Composable +fun MifosTextField( + value: String, + onValueChange: (String) -> Unit, + label: String, + modifier: Modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = LocalTextStyle.current, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = true, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + minLines: Int = 1, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + keyboardOptions: KeyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + trailingIcon: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + indicatorColor: Color? = null, +) { + var isFocused by rememberSaveable { mutableStateOf(false) } + + BasicTextField( + value = value, + onValueChange = onValueChange, + textStyle = textStyle, + modifier = modifier + .fillMaxWidth() + .padding(top = 10.dp) + .onFocusChanged { focusState -> + isFocused = focusState.isFocused + } + .semantics(mergeDescendants = true) {}, + enabled = enabled, + readOnly = readOnly, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + interactionSource = interactionSource, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + decorationBox = { innerTextField -> + Column { + Text( + text = label, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.align(alignment = Alignment.Start), + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + if (leadingIcon != null) { + leadingIcon() + } + + Box(modifier = Modifier.weight(1f)) { + innerTextField() + } + + if (trailingIcon != null) { + trailingIcon() + } + } + indicatorColor?.let { color -> + HorizontalDivider( + thickness = 1.dp, + color = if (isFocused) { + color + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f) + }, + ) + } ?: run { + HorizontalDivider( + thickness = 1.dp, + color = if (isFocused) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f) + }, + ) + } + } + }, + + ) +} + +@Composable +@Preview +fun MfTextFieldPreview(modifier: Modifier = Modifier) { + MifosTheme { + Box( + modifier = modifier.background(color = Color.White), + ) { + MifosTextField( + value = "Text Field Value", + onValueChange = {}, + label = "Text Field", + ) + } + } +} + +@Preview +@Composable +fun MfOutlinedTextFieldPreview() { + MifosTheme { + Box( + modifier = Modifier.background(color = MaterialTheme.colorScheme.surface), + ) { + MfOutlinedTextField( + value = "Text Field Value", + label = "Text Field", + onValueChange = { }, + modifier = Modifier, + isError = true, + errorMessage = "Error Message", + onKeyboardActions = { }, + ) + } + } +} + +@Preview +@Composable +fun MfPasswordTextFieldPreview() { + MifosTheme { + val password = " " + Box( + modifier = Modifier.background(color = Color.White), + ) { + MfPasswordTextField( + password = password, + label = "Password", + isError = true, + isPasswordVisible = true, + onTogglePasswordVisibility = { }, + onPasswordChange = { }, + modifier = Modifier.fillMaxWidth(), + errorMessage = "Password must be at least 6 characters", + ) + } + } +} diff --git a/core/network/build.gradle.kts b/core/network/build.gradle.kts index f9bfe9672..d10961924 100644 --- a/core/network/build.gradle.kts +++ b/core/network/build.gradle.kts @@ -1,3 +1,13 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ + /* * Copyright 2024 Mifos Initiative * @@ -8,10 +18,8 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.kmp.library) - alias(libs.plugins.ktrofit) + alias(libs.plugins.mifospay.android.library) id("kotlinx-serialization") - id("com.google.devtools.ksp") } android { @@ -27,54 +35,36 @@ android { } } -kotlin { - sourceSets { - commonMain.dependencies { - api(projects.core.common) - implementation(projects.core.model) - implementation(projects.core.datastore) - implementation(libs.kotlinx.serialization.json) - implementation(libs.ktor.client.core) - implementation(libs.ktor.client.json) - implementation(libs.ktor.client.logging) - implementation(libs.ktor.client.serialization) - implementation(libs.ktor.client.content.negotiation) - implementation(libs.ktor.client.auth) - implementation(libs.ktor.serialization.kotlinx.json) - implementation(libs.ktorfit.lib) - implementation(libs.squareup.okio) - } +dependencies { + api(libs.kotlinx.datetime) + api(projects.core.common) + api(projects.core.model) + api(projects.core.datastore) - androidMain.dependencies { - implementation(libs.ktor.client.okhttp) - implementation(libs.koin.android) - } + implementation(libs.squareup.okhttp) + implementation(libs.squareup.logging.interceptor) - nativeMain.dependencies { - implementation(libs.ktor.client.darwin) - } + implementation(libs.squareup.retrofit2) + implementation(libs.retrofit.kotlin.serialization) + implementation(libs.squareup.retrofit.adapter.rxjava) + implementation(libs.squareup.retrofit.converter.gson) - val desktopMain by getting { - dependencies { - implementation(libs.ktor.client.okhttp) - } - } - jsMain.dependencies { - implementation(libs.ktor.client.js) - } - wasmJsMain.dependencies { - implementation(libs.ktor.client.js) - } - } -} + implementation(libs.reactivex.rxjava.android) + implementation(libs.reactivex.rxjava) -dependencies { - add("kspCommonMainMetadata", libs.ktorfit.ksp) - add("kspAndroid", libs.ktorfit.ksp) - add("kspJs", libs.ktorfit.ksp) - add("kspWasmJs", libs.ktorfit.ksp) - add("kspDesktop", libs.ktorfit.ksp) - add("kspIosX64", libs.ktorfit.ksp) - add("kspIosArm64", libs.ktorfit.ksp) - add("kspIosSimulatorArm64", libs.ktorfit.ksp) -} \ No newline at end of file + implementation(libs.jetbrains.kotlin.stdlib) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.android) + implementation(libs.ktor.client.serialization) + implementation(libs.ktor.client.logging) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.client.json) + implementation(libs.ktor.client.websockets) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.logback.classic) + + implementation(libs.kotlinx.serialization.json) + testImplementation(libs.kotlinx.coroutines.test) + + implementation(libs.koin.android) +} diff --git a/core/network/src/androidMain/AndroidManifest.xml b/core/network/src/androidMain/AndroidManifest.xml index 31df960ef..f9567b8ac 100644 --- a/core/network/src/androidMain/AndroidManifest.xml +++ b/core/network/src/androidMain/AndroidManifest.xml @@ -8,6 +8,7 @@ See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md --> - - - \ No newline at end of file + + Loading + Start sending money tax free! + \ No newline at end of file diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/TestingApiInterceptor.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/TestingApiInterceptor.kt new file mode 100644 index 000000000..1592ab43a --- /dev/null +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/TestingApiInterceptor.kt @@ -0,0 +1,41 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.network + +import okhttp3.Credentials +import okhttp3.Interceptor +import okhttp3.Response +import java.io.IOException + +class TestingApiInterceptor( + private val username: String, + private val password: String, +) : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val chainRequest = chain.request() + + val basicAuthCredentials = Credentials.basic(username, password) + + val builder = chainRequest.newBuilder() + .header(HEADER_TENANT, DEFAULT) + .header(HEADER_AUTH, basicAuthCredentials) + + val request = builder.build() + return chain.proceed(request) + } + + companion object { + const val HEADER_TENANT = "Fineract-Platform-TenantId" + const val HEADER_AUTH = "Authorization" + const val DEFAULT = "venus" + } +} diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/di/Qualifier.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/di/Qualifier.kt index 9e5dc7469..ff398c153 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/di/Qualifier.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/di/Qualifier.kt @@ -11,7 +11,17 @@ package org.mifospay.core.network.di import org.koin.core.qualifier.named -val SelfClient = named("SelfClient") -val BaseClient = named("BaseClient") -val KtorClient = named("KtorClient") -val KtorBaseClient = named("KtorBaseClient") +val SelfServiceApi = named("SelfServiceApi") +val FineractApi = named("FineractApi") +val Testing = named("Testing") +val FineractAuthenticationService = named("FineractAuthenticationService") +val FineractClientService = named("FineractClientService") +val FineractSavingsAccountsService = named("FineractSavingsAccountsService") +val FineractRegistrationService = named("FineractRegistrationService") +val FineractThirdPartyTransferService = named("FineractThirdPartyTransferService") + +val SelfServiceAuthenticationService = named("SelfServiceAuthenticationService") +val SelfServiceClientService = named("SelfServiceClientService") +val SelfServiceSavingsAccountsService = named("SelfServiceSavingsAccountsService") +val SelfServiceRegistrationService = named("SelfServiceRegistrationService") +val SelfServiceThirdPartyTransferService = named("SelfServiceThirdPartyTransferService") diff --git a/core/network/src/commonMain/kotlin/org/mifospay/core/network/localAssets/JvmLocalAssetManager.kt b/core/network/src/commonMain/kotlin/org/mifospay/core/network/localAssets/JvmLocalAssetManager.kt index feed1e7e6..e7403cf44 100644 --- a/core/network/src/commonMain/kotlin/org/mifospay/core/network/localAssets/JvmLocalAssetManager.kt +++ b/core/network/src/commonMain/kotlin/org/mifospay/core/network/localAssets/JvmLocalAssetManager.kt @@ -7,10 +7,36 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.core.network.localAssets +package org.mifospay -internal object JvmLocalAssetManager : LocalAssetManager { - override fun open(fileName: String): String { - return "" +import android.app.Application +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin +import org.koin.core.logger.Level +import org.mifospay.di.KoinModules + +class MifosPayApp : Application() { + override fun onCreate() { + super.onCreate() + val koinModules = KoinModules() + + startKoin { + printLogger(Level.ERROR) + androidContext(this@MifosPayApp) + modules( + listOf( + koinModules.dataModules, + koinModules.mifosPayModule, + koinModules + .coreDataStoreModules, + koinModules.featureModules, + koinModules.networkModules, + koinModules + .analyticsModules, + koinModules.commonModules, + koinModules.libsModule, + ), + ) + } } } diff --git a/core/network/src/main/kotlin/org/mifospay/core/network/FineractApiManager.kt b/core/network/src/main/kotlin/org/mifospay/core/network/FineractApiManager.kt new file mode 100644 index 000000000..b42a185f4 --- /dev/null +++ b/core/network/src/main/kotlin/org/mifospay/core/network/FineractApiManager.kt @@ -0,0 +1,100 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.network + +import org.mifospay.core.network.services.AccountTransfersService +import org.mifospay.core.network.services.AuthenticationService +import org.mifospay.core.network.services.ClientService +import org.mifospay.core.network.services.DocumentService +import org.mifospay.core.network.services.InvoiceService +import org.mifospay.core.network.services.KYCLevel1Service +import org.mifospay.core.network.services.KtorSavingsAccountService +import org.mifospay.core.network.services.NotificationService +import org.mifospay.core.network.services.RegistrationService +import org.mifospay.core.network.services.RunReportService +import org.mifospay.core.network.services.SavedCardService +import org.mifospay.core.network.services.SavingsAccountsService +import org.mifospay.core.network.services.SearchService +import org.mifospay.core.network.services.StandingInstructionService +import org.mifospay.core.network.services.ThirdPartyTransferService +import org.mifospay.core.network.services.TwoFactorAuthService +import org.mifospay.core.network.services.UserService + +class FineractApiManager( + private val authenticationService: AuthenticationService, + private val clientService: ClientService, + private val savingsAccountsService: SavingsAccountsService, + private val registrationService: RegistrationService, + private val searchService: SearchService, + private val documentService: DocumentService, + private val runReportService: RunReportService, + private val twoFactorAuthService: TwoFactorAuthService, + private val accountTransfersService: AccountTransfersService, + private val savedCardService: SavedCardService, + private val kYCLevel1Service: KYCLevel1Service, + private val invoiceService: InvoiceService, + private val userService: UserService, + private val thirdPartyTransferService: ThirdPartyTransferService, + private val standingInstructionService: StandingInstructionService, + private val notificationService: NotificationService, + private val ktorSavingsAccountService: KtorSavingsAccountService, +) { + + val authenticationApi: AuthenticationService + get() = authenticationService + + val clientsApi: ClientService + get() = clientService + + val registrationAPi: RegistrationService + get() = registrationService + + val searchApi: SearchService + get() = searchService + + val documentApi: DocumentService + get() = documentService + + val runReportApi: RunReportService + get() = runReportService + + val twoFactorAuthApi: TwoFactorAuthService + get() = twoFactorAuthService + + val accountTransfersApi: AccountTransfersService + get() = accountTransfersService + + val savedCardApi: SavedCardService + get() = savedCardService + + val kycLevel1Api: KYCLevel1Service + get() = kYCLevel1Service + + val invoiceApi: InvoiceService + get() = invoiceService + + val userApi: UserService + get() = userService + + val thirdPartyTransferApi: ThirdPartyTransferService + get() = thirdPartyTransferService + + val notificationApi: NotificationService + get() = notificationService + + val savingsAccountsApi: SavingsAccountsService + get() = savingsAccountsService + + val standingInstructionApi: StandingInstructionService + get() = standingInstructionService + + val ktorSavingsAccountApi: KtorSavingsAccountService + get() = ktorSavingsAccountService +} diff --git a/core/network/src/main/kotlin/org/mifospay/core/network/JvmLocalAssetManager.kt b/core/network/src/main/kotlin/org/mifospay/core/network/JvmLocalAssetManager.kt new file mode 100644 index 000000000..37cc92891 --- /dev/null +++ b/core/network/src/main/kotlin/org/mifospay/core/network/JvmLocalAssetManager.kt @@ -0,0 +1,37 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.network + +import androidx.annotation.VisibleForTesting +import org.mifospay.core.network.localAssets.LocalAssetManager +import java.io.File +import java.io.InputStream + +/** + * This class helps with loading Android `/assets` files, especially when running JVM unit tests. + * It must remain on the root package for an easier [Class.getResource] with relative paths. + * @see https://developer.android.com/reference/tools/gradle-api/7.3/com/android/build/api/dsl/UnitTestOptions + */ + +@VisibleForTesting +internal object JvmLocalAssetManager : LocalAssetManager { +// private val config = +// requireNotNull(javaClass.getResource("com/android/tools/test_config.properties")) { +// """ +// Missing Android resources properties file. +// Did you forget to enable the feature in the gradle build file? +// android.testOptions.unitTests.isIncludeAndroidResources = true +// """.trimIndent() +// } +// private val properties = Properties().apply { config.openStream().use(::load) } +// private val assets = File(properties["android_merged_assets"].toString()) + + override fun open(fileName: String): InputStream = File(fileName).inputStream() +} diff --git a/core/network/src/main/kotlin/org/mifospay/core/network/MifosWalletOkHttpClient.kt b/core/network/src/main/kotlin/org/mifospay/core/network/MifosWalletOkHttpClient.kt new file mode 100644 index 000000000..aaab8f9b5 --- /dev/null +++ b/core/network/src/main/kotlin/org/mifospay/core/network/MifosWalletOkHttpClient.kt @@ -0,0 +1,93 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.network + +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.mifospay.core.datastore.PreferencesHelper +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 + +class MifosWalletOkHttpClient( + private val preferences: PreferencesHelper, + private val username: String? = null, + private val password: String? = null, + private val isTesting: Boolean = false, +) { + // Create a trust manager that does not validate certificate chains + val mifosOkHttpClient: OkHttpClient + // 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 { _, _ -> true } + } catch (e: Exception) { + throw RuntimeException(e) + } + + // Enable Full Body Logging + val logger = HttpLoggingInterceptor() + logger.level = HttpLoggingInterceptor.Level.BODY + + // Setting Timeout 30 Seconds + builder.connectTimeout(60, TimeUnit.SECONDS) + builder.readTimeout(60, TimeUnit.SECONDS) + + // Interceptor :> Full Body Logger and ApiRequest Header + builder.addInterceptor(logger) + if (isTesting && username != null && password != null) { + builder.addInterceptor(TestingApiInterceptor(username, password)) + } else { + builder.addInterceptor(ApiInterceptor(preferences)) + } + return builder.build() + } +} diff --git a/core/network/src/main/kotlin/org/mifospay/core/network/SelfServiceApiManager.kt b/core/network/src/main/kotlin/org/mifospay/core/network/SelfServiceApiManager.kt new file mode 100644 index 000000000..54ea9d17d --- /dev/null +++ b/core/network/src/main/kotlin/org/mifospay/core/network/SelfServiceApiManager.kt @@ -0,0 +1,43 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.network + +import org.mifospay.core.network.services.AuthenticationService +import org.mifospay.core.network.services.BeneficiaryService +import org.mifospay.core.network.services.ClientService +import org.mifospay.core.network.services.KtorSavingsAccountService +import org.mifospay.core.network.services.RegistrationService +import org.mifospay.core.network.services.SavingsAccountsService +import org.mifospay.core.network.services.ThirdPartyTransferService + +class SelfServiceApiManager( + private val authenticationService: AuthenticationService, + private val clientService: ClientService, + private val savingsAccountsService: SavingsAccountsService, + private val registrationService: RegistrationService, + private val beneficiaryService: BeneficiaryService, + private val thirdPartyTransferService: ThirdPartyTransferService, + private val ktorSavingsAccountService: KtorSavingsAccountService, +) { + val authenticationApi: AuthenticationService + get() = authenticationService + val clientsApi: ClientService + get() = clientService + val savingAccountsListApi: SavingsAccountsService + get() = savingsAccountsService + val registrationAPi: RegistrationService + get() = registrationService + val beneficiaryApi: BeneficiaryService + get() = beneficiaryService + val thirdPartyTransferApi: ThirdPartyTransferService + get() = thirdPartyTransferService + val ktorSavingsAccountApi: KtorSavingsAccountService + get() = ktorSavingsAccountService +} diff --git a/core/network/src/main/kotlin/org/mifospay/core/network/di/LocalModule.kt b/core/network/src/main/kotlin/org/mifospay/core/network/di/LocalModule.kt new file mode 100644 index 000000000..484d2effe --- /dev/null +++ b/core/network/src/main/kotlin/org/mifospay/core/network/di/LocalModule.kt @@ -0,0 +1,20 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.network.di + +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module +import org.mifospay.core.network.localAssets.LocalAssetManager + +val LocalModule = module { + single { + LocalAssetManager { fileName -> androidContext().assets.open(fileName) } + } +} diff --git a/core/network/src/main/kotlin/org/mifospay/core/network/di/NetworkModule.kt b/core/network/src/main/kotlin/org/mifospay/core/network/di/NetworkModule.kt new file mode 100644 index 000000000..41774c371 --- /dev/null +++ b/core/network/src/main/kotlin/org/mifospay/core/network/di/NetworkModule.kt @@ -0,0 +1,247 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.network.di + +import io.ktor.client.HttpClient +import io.ktor.client.engine.android.Android +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.plugins.websocket.WebSockets +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json +import org.koin.dsl.module +import org.mifospay.core.datastore.PreferencesHelper +import org.mifospay.core.network.BaseURL +import org.mifospay.core.network.FineractApiManager +import org.mifospay.core.network.KtorInterceptor +import org.mifospay.core.network.MifosWalletOkHttpClient +import org.mifospay.core.network.SelfServiceApiManager +import org.mifospay.core.network.services.AccountTransfersService +import org.mifospay.core.network.services.AuthenticationService +import org.mifospay.core.network.services.BeneficiaryService +import org.mifospay.core.network.services.ClientService +import org.mifospay.core.network.services.DocumentService +import org.mifospay.core.network.services.InvoiceService +import org.mifospay.core.network.services.KYCLevel1Service +import org.mifospay.core.network.services.KtorAuthenticationService +import org.mifospay.core.network.services.KtorSavingsAccountService +import org.mifospay.core.network.services.NotificationService +import org.mifospay.core.network.services.RegistrationService +import org.mifospay.core.network.services.RunReportService +import org.mifospay.core.network.services.SavedCardService +import org.mifospay.core.network.services.SavingsAccountsService +import org.mifospay.core.network.services.SearchService +import org.mifospay.core.network.services.StandingInstructionService +import org.mifospay.core.network.services.ThirdPartyTransferService +import org.mifospay.core.network.services.TwoFactorAuthService +import org.mifospay.core.network.services.UserService +import retrofit2.Retrofit +import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory + +@Suppress("TooManyFunctions") +val NetworkModule = module { + + single { + Json { + ignoreUnknownKeys = true + } + } + + single(SelfServiceApi) { + val preferencesHelper: PreferencesHelper = get() + Retrofit.Builder() + .baseUrl(BaseURL().selfServiceUrl) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + .client(MifosWalletOkHttpClient(preferencesHelper).mifosOkHttpClient) + .build() + } + + single(FineractApi) { + val preferencesHelper: PreferencesHelper = get() + Retrofit.Builder() + .baseUrl(BaseURL().url) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + .client(MifosWalletOkHttpClient(preferencesHelper).mifosOkHttpClient) + .build() + } + + // This can be removed as it for testing purpose + single(Testing) { + val preferencesHelper: PreferencesHelper = get() + Retrofit.Builder() + .baseUrl(BaseURL().url) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + .client( + MifosWalletOkHttpClient(preferencesHelper, "mifos", "password", true) + .mifosOkHttpClient, + ) + .build() + } + + single { + FineractApiManager( + authenticationService = get(FineractAuthenticationService), + clientService = get(FineractClientService), + savingsAccountsService = get(FineractSavingsAccountsService), + registrationService = get(FineractRegistrationService), + searchService = get(), + documentService = get(), + runReportService = get(), + twoFactorAuthService = get(), + accountTransfersService = get(), + savedCardService = get(), + kYCLevel1Service = get(), + invoiceService = get(), + userService = get(), + thirdPartyTransferService = get(FineractThirdPartyTransferService), + standingInstructionService = get(), + notificationService = get(), + ktorSavingsAccountService = get(), + ) + } + + single { + SelfServiceApiManager( + authenticationService = get(SelfServiceAuthenticationService), + clientService = get(SelfServiceClientService), + savingsAccountsService = get(SelfServiceSavingsAccountsService), + registrationService = get(SelfServiceRegistrationService), + beneficiaryService = get(), + thirdPartyTransferService = get(SelfServiceThirdPartyTransferService), + ktorSavingsAccountService = get(), + ) + } + +// Http client for Ktor + + single { + HttpClient(Android) { + install(WebSockets) + install(KtorInterceptor) { + this.preferencesHelper = get() + } + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + isLenient = true + }, + ) + } + install(HttpTimeout) { + requestTimeoutMillis = 15000 + } + install(Logging) { + level = LogLevel.ALL + } + } + } + + single { KtorAuthenticationService(client = get()) } + single { KtorSavingsAccountService(client = get()) } + + // -----Fineract API Service---------// + + single(FineractAuthenticationService) { + get(FineractApi).create(AuthenticationService::class.java) + } + + single(FineractClientService) { + get(FineractApi).create(ClientService::class.java) + } + + single(FineractSavingsAccountsService) { + get(FineractApi).create(SavingsAccountsService::class.java) + } + + single(FineractRegistrationService) { + get(FineractApi).create(RegistrationService::class.java) + } + + single { + get(FineractApi).create(SearchService::class.java) + } + + single { + get(FineractApi).create(SavedCardService::class.java) + } + + single { + get(FineractApi).create(DocumentService::class.java) + } + + single { + get(FineractApi).create(TwoFactorAuthService::class.java) + } + + single { + get(FineractApi).create(AccountTransfersService::class.java) + } + + single { + get(FineractApi).create(RunReportService::class.java) + } + + single { + get(FineractApi).create(KYCLevel1Service::class.java) + } + + single { + get(FineractApi).create(InvoiceService::class.java) + } + + single { + get(FineractApi).create(UserService::class.java) + } + + single(FineractThirdPartyTransferService) { + get(FineractApi).create(ThirdPartyTransferService::class.java) + } + + single { + get(FineractApi).create(NotificationService::class.java) + } + + single { + get(FineractApi).create(StandingInstructionService::class.java) + } + + // -------SelfService API Service-------// + + single(SelfServiceAuthenticationService) { + get(SelfServiceApi).create(AuthenticationService::class.java) + } + + single(SelfServiceClientService) { + get(SelfServiceApi).create(ClientService::class.java) + } + + single(SelfServiceSavingsAccountsService) { + get(SelfServiceApi).create(SavingsAccountsService::class.java) + } + + single(SelfServiceRegistrationService) { + get(SelfServiceApi).create(RegistrationService::class.java) + } + + single { + get(SelfServiceApi).create(BeneficiaryService::class.java) + } + + single(SelfServiceThirdPartyTransferService) { + get(SelfServiceApi).create(ThirdPartyTransferService::class.java) + } +} diff --git a/core/network/src/main/kotlin/org/mifospay/core/network/localAssets/MifosLocalAssetDataSource.kt b/core/network/src/main/kotlin/org/mifospay/core/network/localAssets/MifosLocalAssetDataSource.kt new file mode 100644 index 000000000..1bb530e4d --- /dev/null +++ b/core/network/src/main/kotlin/org/mifospay/core/network/localAssets/MifosLocalAssetDataSource.kt @@ -0,0 +1,63 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.network.localAssets + +import com.mifospay.core.model.City +import com.mifospay.core.model.Country +import com.mifospay.core.model.State +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream + +class MifosLocalAssetDataSource( + private val ioDispatcher: CoroutineDispatcher, + private val networkJson: Json, + private val assets: LocalAssetManager, +) : LocalAssetDataSource { + + @OptIn(ExperimentalSerializationApi::class) + override suspend fun getCountries(): List { + return withContext(ioDispatcher) { + assets.open(COUNTRIES_ASSET).use(networkJson::decodeFromStream) + } + } + + @OptIn(ExperimentalSerializationApi::class) + override suspend fun getStateList(): List { + return withContext(ioDispatcher) { + assets.open(STATES_ASSET).use(networkJson::decodeFromStream) + } + } + + @OptIn(ExperimentalSerializationApi::class) + override suspend fun getBanks(): List { + return withContext(ioDispatcher) { + assets.open(BANKS_ASSET).use(networkJson::decodeFromStream) + } + } + + @OptIn(ExperimentalSerializationApi::class) + override suspend fun getCities(): List { + return withContext(ioDispatcher) { + assets.open(CITIES_ASSET).use(networkJson::decodeFromStream) + } + } + + @Suppress("UnusedPrivateProperty") + companion object { + private const val COUNTRIES_ASSET = "countries.json" + private const val STATES_ASSET = "states.json" + private const val CITIES_ASSET = "cities.json" + private const val BANKS_ASSET = "banks.json" + private const val COUNTRIES_TO_CITIES_ASSET = "countriesToCities.json" + } +} diff --git a/core/network/src/main/kotlin/org/mifospay/core/network/services/InvoiceService.kt b/core/network/src/main/kotlin/org/mifospay/core/network/services/InvoiceService.kt new file mode 100644 index 000000000..45cd13429 --- /dev/null +++ b/core/network/src/main/kotlin/org/mifospay/core/network/services/InvoiceService.kt @@ -0,0 +1,54 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.network.services + +import com.mifospay.core.model.entity.invoice.Invoice +import com.mifospay.core.model.entity.invoice.InvoiceEntity +import org.mifospay.core.network.ApiEndPoints +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import rx.Observable + +/** + * Created by ankur on 07/June/2018 + */ +interface InvoiceService { + @POST(ApiEndPoints.DATATABLES + "/invoice/{clientId}") + fun addInvoice( + @Path("clientId") clientId: Long, + @Body invoice: InvoiceEntity?, + ) + + @GET(ApiEndPoints.DATATABLES + "/invoice/{clientId}") + fun getInvoices(@Path("clientId") clientId: Long): Observable> + + @GET(ApiEndPoints.DATATABLES + "/invoice/{clientId}/{invoiceId}") + fun getInvoice( + @Path("clientId") clientId: Long, + @Path("invoiceId") invoiceId: Long, + ): Observable> + + @DELETE(ApiEndPoints.DATATABLES + "/invoice/{clientId}/{invoiceId}") + fun deleteInvoice( + @Path("clientId") clientId: Long, + @Path("invoiceId") invoiceId: Long, + ) + + @PUT(ApiEndPoints.DATATABLES + "/invoice/{clientId}/{invoiceId}") + fun updateInvoice( + @Path("clientId") clientId: Long, + @Path("invoiceId") invoiceId: Long, + @Body invoice: Invoice?, + ) +} diff --git a/core/network/src/main/kotlin/org/mifospay/core/network/services/KtorAuthenticationService.kt b/core/network/src/main/kotlin/org/mifospay/core/network/services/KtorAuthenticationService.kt new file mode 100644 index 000000000..4df4b550c --- /dev/null +++ b/core/network/src/main/kotlin/org/mifospay/core/network/services/KtorAuthenticationService.kt @@ -0,0 +1,32 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.network.services + +import com.mifospay.core.model.domain.user.User +import com.mifospay.core.model.entity.authentication.AuthenticationPayload +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType +import org.mifospay.core.network.BaseURL + +class KtorAuthenticationService( + private val client: HttpClient, +) { + + suspend fun authenticate(authPayload: AuthenticationPayload): User { + return client.post("${BaseURL().selfServiceUrl}authentication") { + contentType(ContentType.Application.Json) + setBody(authPayload) + }.body() + } +} diff --git a/core/network/src/main/kotlin/org/mifospay/core/network/services/KtorSavingsAccountService.kt b/core/network/src/main/kotlin/org/mifospay/core/network/services/KtorSavingsAccountService.kt new file mode 100644 index 000000000..141088cd7 --- /dev/null +++ b/core/network/src/main/kotlin/org/mifospay/core/network/services/KtorSavingsAccountService.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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.network.services + +import com.mifospay.core.model.entity.Page +import com.mifospay.core.model.entity.accounts.savings.SavingAccount +import com.mifospay.core.model.entity.accounts.savings.SavingsWithAssociations +import com.mifospay.core.model.entity.accounts.savings.Transactions +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType +import org.mifospay.core.network.ApiEndPoints.SAVINGS_ACCOUNTS +import org.mifospay.core.network.ApiEndPoints.TRANSACTIONS +import org.mifospay.core.network.BaseURL +import org.mifospay.core.network.GenericResponse + +class KtorSavingsAccountService( + private val client: HttpClient, +) { + suspend fun getSavingsWithAssociations( + accountId: Long, + associationType: String, + ): SavingsWithAssociations { + return client.get("${BaseURL().selfServiceUrl}$SAVINGS_ACCOUNTS/$accountId") { + url { + parameters.append("associations", associationType) + } + }.body() + } + + suspend fun getSavingsAccounts(limit: Int): Page { + return client.get("${BaseURL().selfServiceUrl}$SAVINGS_ACCOUNTS") { + url { + parameters.append("limit", limit.toString()) + } + }.body() + } + + suspend fun createSavingsAccount(savingAccount: SavingAccount): GenericResponse { + return client.post("${BaseURL().selfServiceUrl}$SAVINGS_ACCOUNTS") { + contentType(ContentType.Application.Json) + setBody(savingAccount) + }.body() + } + + suspend fun blockUnblockAccount(accountId: Long, command: String?): GenericResponse { + return client.post("${BaseURL().selfServiceUrl}$SAVINGS_ACCOUNTS/$accountId") { + url { + parameters.append("command", command ?: "") + } + }.body() + } + + suspend fun getSavingAccountTransaction(accountId: Long, transactionId: Long): Transactions { + return client.get( + urlString = "${BaseURL().selfServiceUrl}$SAVINGS_ACCOUNTS/" + + "$accountId/$TRANSACTIONS/$transactionId", + ).body() + } + + suspend fun payViaMobile(accountId: Long): Transactions { + return client.post( + urlString = "${BaseURL().selfServiceUrl}$SAVINGS_ACCOUNTS/$accountId/$TRANSACTIONS", + ) { + url { + parameters.append("command", "deposit") + } + }.body() + } +} diff --git a/core/ui/src/commonMain/composeResources/drawable/checker.webp b/core/ui/src/commonMain/composeResources/drawable/checker.webp new file mode 100644 index 000000000..c2fc44379 Binary files /dev/null and b/core/ui/src/commonMain/composeResources/drawable/checker.webp differ diff --git a/core/ui/src/commonMain/composeResources/drawable/core_ui_ic_dp_placeholder.png b/core/ui/src/commonMain/composeResources/drawable/core_ui_ic_dp_placeholder.png new file mode 100644 index 000000000..4f7005f77 Binary files /dev/null and b/core/ui/src/commonMain/composeResources/drawable/core_ui_ic_dp_placeholder.png differ diff --git a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/AvatarBox.kt b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/AvatarBox.kt index 9a9591b3f..3b9749a32 100644 --- a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/AvatarBox.kt +++ b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/AvatarBox.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -23,43 +22,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -@Composable -fun AvatarBox( - name: String, - size: Int = 40, - backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainer, -) { - val initials = name.split(" ") - .mapNotNull { it.firstOrNull()?.toString() } - .take(2) - .joinToString("") - .uppercase() - - Box( - modifier = Modifier - .size(size.dp) - .clip(CircleShape) - .background(backgroundColor), - contentAlignment = Alignment.Center, - ) { - Text( - text = initials, - color = contentColorFor(backgroundColor), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.SemiBold, - ) - } -} - @Composable fun AvatarBox( icon: ImageVector, - size: Int = 40, modifier: Modifier = Modifier, - backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainer, + size: Int = 40, + backgroundColor: Color = MaterialTheme.colorScheme.onPrimary, contentColor: Color = contentColorFor(backgroundColor), ) { Box( @@ -73,7 +43,6 @@ fun AvatarBox( imageVector = icon, contentDescription = "Avatar", tint = contentColor, - modifier = Modifier.size((size / 2).dp), ) } } diff --git a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/EmptyContentScreen.kt b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/EmptyContentScreen.kt index 9b8a2e215..e400fc834 100644 --- a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/EmptyContentScreen.kt +++ b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/EmptyContentScreen.kt @@ -28,15 +28,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource 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.unit.dp -import mobile_wallet.core.ui.generated.resources.Res -import mobile_wallet.core.ui.generated.resources.artwork -import mobile_wallet.core.ui.generated.resources.core_ui_money_in -import org.jetbrains.compose.resources.DrawableResource -import org.jetbrains.compose.resources.painterResource -import org.mifospay.core.designsystem.component.MifosButton import org.mifospay.core.designsystem.icon.MifosIcons import org.mifospay.core.designsystem.theme.MifosTheme @@ -70,6 +66,7 @@ fun EmptyContentScreen( .fillMaxWidth() .padding(start = 24.dp, end = 24.dp), textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, ) @@ -80,79 +77,11 @@ fun EmptyContentScreen( text = subTitle, modifier = Modifier .fillMaxWidth() - .padding(start = 24.dp, end = 24.dp), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } - } -} - -@Composable -fun EmptyContentScreen( - title: String, - subTitle: String, - btnText: String, - btnIcon: ImageVector, - modifier: Modifier = Modifier, - onClick: () -> Unit, - imageContent: @Composable () -> Unit, -) { - Column( - modifier = modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Column( - modifier = Modifier - .padding(16.dp) - .fillMaxSize() - .testTag("mifos:empty"), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - imageContent() - - Spacer(modifier = Modifier.height(48.dp)) - - Text( - text = title, - modifier = Modifier - .fillMaxWidth() - .padding(start = 24.dp, end = 24.dp), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = subTitle, - modifier = Modifier - .fillMaxWidth() - .padding(start = 24.dp, end = 24.dp), + .padding(start = 37.dp, end = 37.dp), textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface, ) - - Spacer(modifier = Modifier.height(48.dp)) - - MifosButton( - text = { - Text(text = btnText) - }, - leadingIcon = { - Icon( - imageVector = btnIcon, - contentDescription = "BtnIcon", - ) - }, - onClick = onClick, - modifier = Modifier.fillMaxWidth(), - ) } } } @@ -161,31 +90,8 @@ fun EmptyContentScreen( fun EmptyContentScreen( title: String, subTitle: String, - iconDrawable: DrawableResource, - modifier: Modifier = Modifier, - iconTint: Color = MaterialTheme.colorScheme.surfaceTint, -) { - EmptyContentScreen( - title = title, - subTitle = subTitle, - imageContent = { - Image( - modifier = Modifier.size(64.dp), - painter = painterResource(iconDrawable), - colorFilter = if (iconTint != Color.Unspecified) ColorFilter.tint(iconTint) else null, - contentDescription = null, - ) - }, - modifier = modifier, - ) -} - -@Composable -fun EmptyContentScreen( - title: String, - subTitle: String, + iconDrawable: Int, modifier: Modifier = Modifier, - drawableResource: DrawableResource = Res.drawable.artwork, iconTint: Color = MaterialTheme.colorScheme.surfaceTint, ) { EmptyContentScreen( @@ -194,7 +100,7 @@ fun EmptyContentScreen( imageContent = { Image( modifier = Modifier.size(200.dp), - painter = painterResource(drawableResource), + painter = painterResource(id = iconDrawable), colorFilter = if (iconTint != Color.Unspecified) ColorFilter.tint(iconTint) else null, contentDescription = null, ) @@ -207,12 +113,9 @@ fun EmptyContentScreen( fun EmptyContentScreen( title: String, subTitle: String, - btnText: String, - btnIcon: ImageVector, modifier: Modifier = Modifier, - icon: ImageVector = MifosIcons.Info, iconTint: Color = MaterialTheme.colorScheme.surfaceTint, - onClick: () -> Unit, + iconImageVector: ImageVector = MifosIcons.Search, ) { EmptyContentScreen( title = title, @@ -220,33 +123,30 @@ fun EmptyContentScreen( imageContent = { Icon( modifier = Modifier.size(64.dp), - imageVector = icon, - tint = iconTint, + imageVector = iconImageVector, contentDescription = null, + tint = iconTint, ) }, - btnText = btnText, - btnIcon = btnIcon, - onClick = onClick, modifier = modifier, ) } -@DevicePreviews +@Preview(device = "id:pixel_5") @Composable fun EmptyContentScreenDrawableImagePreview() { MifosTheme { EmptyContentScreen( title = "No data found", subTitle = "Please check you connection or try again", - iconDrawable = Res.drawable.core_ui_money_in, + iconDrawable = R.drawable.core_ui_baseline_info_outline_24, modifier = Modifier, iconTint = MaterialTheme.colorScheme.primary, ) } } -@DevicePreviews +@Preview(device = "id:pixel_5") @Composable fun EmptyContentScreenImageVectorPreview() { MifosTheme { @@ -255,6 +155,7 @@ fun EmptyContentScreenImageVectorPreview() { subTitle = "Please check you connection or try again", modifier = Modifier, iconTint = MaterialTheme.colorScheme.primary, + iconImageVector = MifosIcons.Search, ) } } diff --git a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/MifosUserImage.kt b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/MifosUserImage.kt index 9db2430d2..9950beb5e 100644 --- a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/MifosUserImage.kt +++ b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/MifosUserImage.kt @@ -9,6 +9,7 @@ */ package org.mifospay.core.ui +import android.graphics.Bitmap import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.shape.CircleShape @@ -16,29 +17,42 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale -import org.mifospay.core.designsystem.component.MifosTextUserImage +import androidx.compose.ui.res.painterResource +import org.mifospay.core.designsystem.theme.MifosTheme @Composable fun MifosUserImage( modifier: Modifier = Modifier, - bitmap: ImageBitmap? = null, - username: String? = null, + bitmap: Bitmap? = null, ) { if (bitmap == null) { - MifosTextUserImage( - text = username?.firstOrNull()?.toString() ?: "J", - modifier = modifier, + Image( + modifier = modifier + .clip(CircleShape), + painter = painterResource(id = R.drawable.core_ui_ic_dp_placeholder), + contentDescription = "Empty profile Image", + contentScale = ContentScale.Fit, ) } else { Image( modifier = modifier .clip(CircleShape) .background(MaterialTheme.colorScheme.primary), - bitmap = bitmap, + bitmap = bitmap.asImageBitmap(), contentDescription = "Profile Image", contentScale = ContentScale.Crop, ) } } + +@DevicePreviews +@Composable +private fun MifosUserImagePreview( + modifier: Modifier = Modifier, +) { + MifosTheme { + MifosUserImage(modifier) + } +} diff --git a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/ScrollableTabRow.kt b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/ScrollableTabRow.kt index c31c9d3b2..a7ad03f32 100644 --- a/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/ScrollableTabRow.kt +++ b/core/ui/src/commonMain/kotlin/org/mifospay/core/ui/ScrollableTabRow.kt @@ -9,8 +9,6 @@ */ package org.mifospay.core.ui -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PagerState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ScrollableTabRow import androidx.compose.runtime.Composable @@ -19,6 +17,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.PagerState import kotlinx.coroutines.launch import org.mifospay.core.designsystem.component.MifosTab import org.mifospay.core.ui.utility.TabContent @@ -48,8 +48,8 @@ fun MifosScrollableTabRow( MifosTab( text = currentTab.tabName, selected = pagerState.currentPage == index, - selectedColor = selectedContentColor, - unselectedColor = unselectedContentColor, + selectedContentColor = selectedContentColor, + unselectedContentColor = unselectedContentColor, onClick = { scope.launch { pagerState.animateScrollToPage(index) @@ -60,6 +60,7 @@ fun MifosScrollableTabRow( } HorizontalPager( + count = tabContents.size, state = pagerState, ) { tabContents[it].content.invoke() diff --git a/core/ui/src/main/kotlin/org/mifospay/core/ui/FaqItemScreen.kt b/core/ui/src/main/kotlin/org/mifospay/core/ui/FaqItemScreen.kt new file mode 100644 index 000000000..4420572f9 --- /dev/null +++ b/core/ui/src/main/kotlin/org/mifospay/core/ui/FaqItemScreen.kt @@ -0,0 +1,122 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +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.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.NewUi + +@Composable +fun FaqItemScreen( + modifier: Modifier = Modifier, + question: String? = null, + answer: String? = null, +) { + var isSelected by remember { mutableStateOf(false) } + val density = LocalDensity.current + + Card( + modifier = modifier.fillMaxWidth(), + onClick = { isSelected = !isSelected }, + colors = CardDefaults.cardColors( + containerColor = Color.Transparent, + ), + shape = RoundedCornerShape(0.dp), + ) { + Column( + modifier = Modifier.padding( + horizontal = 20.dp, + vertical = 25.dp, + ), + ) { + Row { + Text( + text = question.orEmpty(), + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight(500), + modifier = Modifier + .fillMaxWidth() + .weight(1f), + ) + + Icon( + imageVector = MifosIcons.KeyboardArrowDown, + contentDescription = "drop down", + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.scale(1f, if (isSelected) -1f else 1f), + ) + } + Row { + AnimatedVisibility( + modifier = Modifier.weight(1f), + visible = isSelected, + enter = slideInVertically { + with(density) { -40.dp.roundToPx() } + } + expandVertically( + expandFrom = Alignment.Top, + ) + fadeIn( + initialAlpha = 0.3f, + ), + exit = slideOutVertically() + shrinkVertically() + fadeOut(), + ) { + Text( + text = answer.orEmpty(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp) + .weight(1f), + ) + } + + Spacer(modifier = Modifier.weight(.1f)) + } + } + + HorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + thickness = 1.dp, + color = NewUi.onSurface.copy(alpha = 0.05f), + ) + } +} diff --git a/core/ui/src/main/kotlin/org/mifospay/core/ui/ProfileConcentricImage.kt b/core/ui/src/main/kotlin/org/mifospay/core/ui/ProfileConcentricImage.kt new file mode 100644 index 000000000..60990a4fc --- /dev/null +++ b/core/ui/src/main/kotlin/org/mifospay/core/ui/ProfileConcentricImage.kt @@ -0,0 +1,55 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.ui + +import android.graphics.Bitmap +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.mifospay.core.designsystem.theme.MifosBlue + +@Composable +fun ProfileImage( + modifier: Modifier = Modifier, + bitmap: Bitmap? = null, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(top = 64.dp, bottom = 12.dp), + horizontalArrangement = Arrangement.Center, + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(150.dp) + .border( + width = 4.dp, + color = MifosBlue, + shape = CircleShape, + ), + ) { + MifosUserImage( + modifier = Modifier + .size(150.dp), + bitmap = bitmap, + ) + } + } +} diff --git a/core/ui/src/main/kotlin/org/mifospay/core/ui/TransactionItemScreen.kt b/core/ui/src/main/kotlin/org/mifospay/core/ui/TransactionItemScreen.kt new file mode 100644 index 000000000..08b48d5e7 --- /dev/null +++ b/core/ui/src/main/kotlin/org/mifospay/core/ui/TransactionItemScreen.kt @@ -0,0 +1,116 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.core.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowOutward +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.mifospay.core.model.domain.Transaction +import com.mifospay.core.model.domain.TransactionType +import org.mifospay.common.Utils.getNewCurrencyFormatter +import org.mifospay.core.designsystem.theme.green +import org.mifospay.core.designsystem.theme.red + +@Preview +@Composable +fun ItemTransactionPreview() { + TransactionItemScreen(Transaction(), modifier = Modifier) +} + +@Composable +fun TransactionItemScreen( + transaction: Transaction, + modifier: Modifier = Modifier, +) { + Column { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column { + Text( + text = transaction.transactionType.toString(), + fontWeight = FontWeight(400), + style = MaterialTheme.typography.bodySmall, + ) + Text( + text = transaction.date.toString(), + fontWeight = FontWeight(300), + style = MaterialTheme.typography.bodySmall, + ) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = when (transaction.transactionType) { + TransactionType.DEBIT -> Icons.Filled.ArrowOutward + TransactionType.CREDIT -> Icons.Filled.ArrowOutward + else -> Icons.Filled.ArrowOutward + }, + modifier = when (transaction.transactionType) { + TransactionType.DEBIT -> Modifier.size(16.dp) + TransactionType.CREDIT -> + Modifier + .graphicsLayer(rotationZ = 180f) + .size(16.dp) + + else -> + Modifier + .graphicsLayer(rotationZ = 180f) + .size(16.dp) + }, + tint = when (transaction.transactionType) { + TransactionType.CREDIT -> green + TransactionType.DEBIT -> red + else -> Color.Black + }, + contentDescription = null, + ) + val amount = getNewCurrencyFormatter( + balance = transaction.amount, + currencySymbol = transaction.currency.displaySymbol, + minimumFractionDigit = 2, + ) + Text( + text = " $amount", + fontWeight = FontWeight(400), + ) + } + } + + HorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + thickness = 1.dp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.13f), + ) + } +} diff --git a/feature/accounts/build.gradle.kts b/feature/accounts/build.gradle.kts index d4a4a02b3..a0ddb8c38 100644 --- a/feature/accounts/build.gradle.kts +++ b/feature/accounts/build.gradle.kts @@ -8,23 +8,17 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { - namespace = "org.mifospay.feature.accounts" + namespace = "org.mifospay.feature.bank.accounts" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - implementation(libs.kotlinx.serialization.json) - } - } +dependencies { + implementation(projects.core.data) + + implementation(projects.libs.pullrefresh) + implementation(libs.play.services.auth) } \ No newline at end of file diff --git a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountViewModel.kt b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountViewModel.kt new file mode 100644 index 000000000..1512449d8 --- /dev/null +++ b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountViewModel.kt @@ -0,0 +1,141 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.bank.accounts + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifospay.core.model.domain.BankAccountDetails +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.util.Random + +class AccountViewModel : ViewModel() { + + private val _bankAccountDetailsList = MutableStateFlow>(emptyList()) + val bankAccountDetailsList: StateFlow> = _bankAccountDetailsList + + private val _accountsUiState = MutableStateFlow(AccountsUiState.Loading) + val accountsUiState: StateFlow = _accountsUiState + + init { + fetchLinkedAccount() + } + + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow get() = _isRefreshing.asStateFlow() + + fun refresh() { + viewModelScope.launch { + _isRefreshing.emit(true) + fetchLinkedAccount() + _isRefreshing.emit(false) + } + } + + private val mRandom = Random() + + private fun fetchLinkedAccount() { + viewModelScope.launch { + _accountsUiState.value = AccountsUiState.Loading + delay(2000) + val linkedAccounts = fetchSampleLinkedAccounts() + _bankAccountDetailsList.value = linkedAccounts + _accountsUiState.value = if (linkedAccounts.isEmpty()) { + AccountsUiState.Empty + } else { + AccountsUiState.LinkedAccounts(linkedAccounts) + } + } + } + + private fun fetchSampleLinkedAccounts(): List { + return listOf( + BankAccountDetails( + "SBI", + "Ankur Sharma", + "New Delhi", + mRandom.nextInt().toString() + " ", + "Savings", + ), + BankAccountDetails( + "HDFC", + "Mandeep Singh", + "Uttar Pradesh", + mRandom.nextInt().toString() + " ", + "Savings", + ), + BankAccountDetails( + "ANDHRA", + "Rakesh anna", + "Telegana", + mRandom.nextInt().toString() + " ", + "Savings", + ), + BankAccountDetails( + "PNB", + "luv Pro", + "Gujrat", + mRandom.nextInt().toString() + " ", + "Savings", + ), + BankAccountDetails( + "HDF", + "Harry potter", + "Hogwarts", + mRandom.nextInt().toString() + " ", + "Savings", + ), + BankAccountDetails( + "GCI", + "JIGME", + "JAMMU", + mRandom.nextInt().toString() + " ", + "Savings", + ), + BankAccountDetails( + "FCI", + "NISHU BOII", + "ASSAM", + mRandom.nextInt().toString() + " ", + "Savings", + ), + ) + } + + fun addBankAccount(bankAccountDetails: BankAccountDetails) { + viewModelScope.launch { + val updatedList = _bankAccountDetailsList.value.toMutableList().apply { + add(bankAccountDetails) + } + _bankAccountDetailsList.value = updatedList + _accountsUiState.value = AccountsUiState.LinkedAccounts(updatedList) + } + } + + fun updateBankAccount(index: Int, bankAccountDetails: BankAccountDetails) { + viewModelScope.launch { + val updatedList = _bankAccountDetailsList.value.toMutableList().apply { + this[index] = bankAccountDetails + } + _bankAccountDetailsList.value = updatedList + _accountsUiState.value = AccountsUiState.LinkedAccounts(updatedList) + } + } +} + +sealed class AccountsUiState { + data object Loading : AccountsUiState() + data object Empty : AccountsUiState() + data object Error : AccountsUiState() + data class LinkedAccounts(val linkedAccounts: List) : AccountsUiState() +} diff --git a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountsScreen.kt b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountsScreen.kt new file mode 100644 index 000000000..f2b2c5fcb --- /dev/null +++ b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/AccountsScreen.kt @@ -0,0 +1,231 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.bank.accounts + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifos.library.pullrefresh.PullRefreshIndicator +import com.mifos.library.pullrefresh.pullRefresh +import com.mifos.library.pullrefresh.rememberPullRefreshState +import com.mifospay.core.model.domain.BankAccountDetails +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.ui.EmptyContentScreen +import org.mifospay.core.ui.utility.AddCardChip + +@Composable +fun AccountsScreen( + navigateToBankAccountDetailScreen: (BankAccountDetails, Int) -> Unit, + navigateToLinkBankAccountScreen: () -> Unit, + modifier: Modifier = Modifier, + viewModel: AccountViewModel = koinViewModel(), +) { + val accountsUiState by viewModel.accountsUiState.collectAsStateWithLifecycle() + val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() + val bankAccountDetailsList by viewModel.bankAccountDetailsList.collectAsStateWithLifecycle() + + AccountScreen( + modifier = modifier, + accountsUiState = accountsUiState, + onAddAccount = { + navigateToLinkBankAccountScreen.invoke() + }, + bankAccountDetailsList = bankAccountDetailsList, + isRefreshing = isRefreshing, + onRefresh = { + viewModel.refresh() + }, + onUpdateAccount = { bankAccountDetails, index -> + viewModel.updateBankAccount(index, bankAccountDetails) + navigateToBankAccountDetailScreen.invoke(bankAccountDetails, index) + }, + ) +} + +@Composable +private fun AccountScreen( + accountsUiState: AccountsUiState, + onAddAccount: () -> Unit, + bankAccountDetailsList: List, + isRefreshing: Boolean, + onRefresh: () -> Unit, + onUpdateAccount: (BankAccountDetails, Int) -> Unit, + modifier: Modifier = Modifier, +) { + val pullRefreshState = rememberPullRefreshState(isRefreshing, onRefresh) + Box(modifier.pullRefresh(pullRefreshState)) { + Column(modifier = Modifier.fillMaxSize()) { + when (accountsUiState) { + AccountsUiState.Empty -> { + NoLinkedAccountsScreen( + onAddBtn = onAddAccount, + ) + } + + AccountsUiState.Error -> { + EmptyContentScreen( + title = stringResource(id = R.string.feature_accounts_error_oops), + subTitle = stringResource(id = R.string.feature_accounts_unexpected_error_subtitle), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.onSurface, + iconImageVector = MifosIcons.Info, + ) + } + + is AccountsUiState.LinkedAccounts -> { + LazyColumn( + modifier = Modifier + .weight(1f) + .fillMaxSize() + .padding(horizontal = 25.dp, vertical = 7.dp), + ) { + item { + Text( + text = stringResource(id = R.string.feature_accounts_linked_bank_account), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(vertical = 25.dp), + ) + } + items(bankAccountDetailsList) { bankAccountDetails -> + val index = bankAccountDetailsList.indexOf(bankAccountDetails) + AccountsItem( + bankAccountDetails = bankAccountDetails, + onAccountClicked = { + onUpdateAccount(bankAccountDetails, index) + }, + ) + HorizontalDivider(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f)) + } + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .background(Color.Transparent), + ) { + AddCardChip( + text = R.string.feature_accounts_add_account, + btnText = R.string.feature_accounts_add_cards, + onAddBtn = onAddAccount, + modifier = Modifier.align(Alignment.Center), + ) + } + } + } + } + + AccountsUiState.Loading -> { + MfLoadingWheel( + contentDesc = stringResource(R.string.feature_accounts_loading), + backgroundColor = MaterialTheme.colorScheme.surface, + ) + } + } + } + PullRefreshIndicator( + refreshing = isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) + } +} + +@Composable +private fun NoLinkedAccountsScreen( + onAddBtn: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text(text = stringResource(R.string.feature_accounts_no_linked_bank_accounts)) + AddCardChip( + text = R.string.feature_accounts_add_account, + btnText = R.string.feature_accounts_add_cards, + onAddBtn = onAddBtn, + modifier = Modifier, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun AccountScreenLoadingPreview() { + AccountScreen( + accountsUiState = AccountsUiState.Loading, + {}, + emptyList(), + false, + {}, + { _, _ -> }, + ) +} + +@Preview(showBackground = true) +@Composable +private fun AccountEmptyScreenPreview() { + AccountScreen(accountsUiState = AccountsUiState.Empty, {}, emptyList(), false, {}, { _, _ -> }) +} + +@Preview(showBackground = true) +@Composable +private fun AccountListScreenPreview() { + AccountScreen( + accountsUiState = AccountsUiState.LinkedAccounts(sampleLinkedAccount), + {}, + sampleLinkedAccount, + false, + {}, + { _, _ -> }, + ) +} + +@Preview(showBackground = true) +@Composable +private fun AccountErrorScreenPreview() { + AccountScreen(accountsUiState = AccountsUiState.Error, {}, emptyList(), false, {}, { _, _ -> }) +} + +val sampleLinkedAccount = List(10) { + BankAccountDetails( + "SBI", + "Ankur Sharma", + "New Delhi", + "XXXXXXXX9990XXX " + " ", + "Savings", + ) +} diff --git a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankAccountScreen.kt b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankAccountScreen.kt new file mode 100644 index 000000000..d51bfce27 --- /dev/null +++ b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankAccountScreen.kt @@ -0,0 +1,347 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.bank.accounts.link + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowColumn +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +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.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifospay.core.model.domain.Bank +import com.mifospay.core.model.domain.BankType +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.component.MfOverlayLoadingWheel +import org.mifospay.core.designsystem.component.MifosCard +import org.mifospay.core.designsystem.component.MifosOutlinedTextField +import org.mifospay.core.designsystem.component.MifosTopAppBar +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.core.ui.DevicePreviews +import org.mifospay.feature.bank.accounts.R +import org.mifospay.feature.bank.accounts.choose.sim.ChooseSimDialogSheet + +@Composable +internal fun LinkBankAccountRoute( + viewModel: LinkBankAccountViewModel = koinViewModel(), + onBackClick: () -> Unit, +) { + val bankUiState by viewModel.bankListUiState.collectAsStateWithLifecycle() + var showSimBottomSheet by rememberSaveable { mutableStateOf(false) } + var showOverlyProgressBar by rememberSaveable { mutableStateOf(false) } + + if (showSimBottomSheet) { + ChooseSimDialogSheet( + onSimSelected = { selectedSim -> + showSimBottomSheet = false + if (selectedSim != -1) { + showOverlyProgressBar = true + viewModel.fetchBankAccountDetails { + showOverlyProgressBar = false + onBackClick() + } + } + }, + ) + } + + LinkBankAccountScreen( + bankUiState = bankUiState, + showOverlyProgressBar = showOverlyProgressBar, + onBankSearch = { query -> + viewModel.updateSearchQuery(query) + }, + onBankSelected = { + viewModel.updateSelectedBank(it) + showSimBottomSheet = true + }, + onBackClick = onBackClick, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun LinkBankAccountScreen( + bankUiState: BankUiState, + showOverlyProgressBar: Boolean, + onBankSearch: (String) -> Unit, + onBankSelected: (Bank) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier + .background(color = MaterialTheme.colorScheme.surface), + topBar = { + MifosTopAppBar( + titleRes = R.string.feature_accounts_link_bank_account, + navigationIcon = MifosIcons.Back, + navigationIconContentDescription = "Back icon", + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + onNavigationClick = onBackClick, + ) + }, + ) { paddingValues -> + Box( + modifier = Modifier + .padding(paddingValues), + ) { + when (bankUiState) { + is BankUiState.Loading -> { + MfLoadingWheel( + contentDesc = stringResource(R.string.feature_accounts_loading), + backgroundColor = MaterialTheme.colorScheme.surface, + ) + } + + is BankUiState.Success -> { + BankListScreenContent( + banks = bankUiState.banks, + onBankSearch = onBankSearch, + onBankSelected = onBankSelected, + ) + } + } + + if (showOverlyProgressBar) { + MfOverlayLoadingWheel() + } + } + } +} + +@Composable +private fun BankListScreenContent( + banks: List, + onBankSearch: (String) -> Unit, + onBankSelected: (Bank) -> Unit, + modifier: Modifier = Modifier, +) { + var searchQuery by rememberSaveable { mutableStateOf("") } + Column( + modifier = modifier + .fillMaxSize() + .background(color = MaterialTheme.colorScheme.surface) + .verticalScroll(rememberScrollState()), + ) { + MifosOutlinedTextField( + label = R.string.feature_accounts_search, + value = searchQuery, + onValueChange = { + searchQuery = it + onBankSearch(it) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + trailingIcon = { + Icon(imageVector = MifosIcons.Search, contentDescription = null) + }, + ) + + if (searchQuery.isBlank()) { + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = stringResource(id = R.string.feature_accounts_popular_banks), + style = TextStyle( + MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Medium, + ), + modifier = Modifier.padding(start = 16.dp), + ) + Spacer(modifier = Modifier.height(12.dp)) + PopularBankGridBody( + banks = banks.filter { it.bankType == BankType.POPULAR }, + onBankSelected = onBankSelected, + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = stringResource(id = R.string.feature_accounts_other_banks), + style = TextStyle( + MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Medium, + ), + modifier = Modifier.padding(start = 16.dp), + ) + Spacer(modifier = Modifier.height(12.dp)) + } + + BankListBody( + banks = if (searchQuery.isBlank()) { + banks.filter { it.bankType == BankType.OTHER } + } else { + banks + }, + onBankSelected = onBankSelected, + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun PopularBankGridBody( + banks: List, + onBankSelected: (Bank) -> Unit, + modifier: Modifier = Modifier, +) { + MifosCard( + modifier = modifier, + shape = RoundedCornerShape(0.dp), + elevation = 2.dp, + colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surface), + ) { + FlowRow( + modifier = Modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + maxItemsInEachRow = 3, + ) { + banks.forEach { + PopularBankItemBody( + modifier = Modifier.weight(1f), + bank = it, + onBankSelected = onBankSelected, + ) + } + } + } +} + +@Composable +private fun PopularBankItemBody( + bank: Bank, + onBankSelected: (Bank) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = modifier + .fillMaxSize() + .clickable { + onBankSelected(bank) + }, + ) { + Image( + modifier = Modifier + .size(58.dp) + .padding(bottom = 4.dp, top = 16.dp), + painter = painterResource(id = bank.image), + contentDescription = bank.name, + ) + Text( + text = bank.name, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(top = 4.dp, bottom = 16.dp), + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun BankListBody( + banks: List, + onBankSelected: (Bank) -> Unit, + modifier: Modifier = Modifier, +) { + FlowColumn(modifier) { + banks.forEach { bank -> + BankListItemBody(bank = bank, onBankSelected = onBankSelected) + } + } +} + +@Composable +private fun BankListItemBody( + bank: Bank, + onBankSelected: (Bank) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .clickable { onBankSelected(bank) }, + ) { + HorizontalDivider() + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(start = 24.dp, top = 8.dp, bottom = 8.dp), + ) { + Image( + modifier = Modifier.size(32.dp), + painter = painterResource(id = bank.image), + contentDescription = bank.name, + ) + Text( + modifier = Modifier.padding(start = 16.dp, end = 16.dp), + text = bank.name, + style = TextStyle(fontSize = 14.sp), + ) + } + } +} + +@DevicePreviews +@Composable +private fun LinkBankAccountScreenPreview( + @PreviewParameter(LinkBankUiStatePreviewParameterProvider::class) + bankUiState: BankUiState, +) { + MifosTheme { + LinkBankAccountScreen( + bankUiState = bankUiState, + showOverlyProgressBar = false, + onBankSelected = { }, + onBankSearch = { }, + onBackClick = { }, + ) + } +} diff --git a/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankAccountViewModel.kt b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankAccountViewModel.kt new file mode 100644 index 000000000..2d23eb372 --- /dev/null +++ b/feature/accounts/src/main/kotlin/org/mifospay/feature/bank/accounts/link/LinkBankAccountViewModel.kt @@ -0,0 +1,106 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.bank.accounts.link + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifospay.core.model.domain.Bank +import com.mifospay.core.model.domain.BankAccountDetails +import com.mifospay.core.model.domain.BankType +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import org.mifospay.core.data.repository.local.LocalAssetRepository +import org.mifospay.feature.bank.accounts.R +import java.util.Random + +class LinkBankAccountViewModel( + localAssetRepository: LocalAssetRepository, +) : ViewModel() { + + private val searchQuery = MutableStateFlow("") + private var selectedBank by mutableStateOf(null) + + private val accountDetails: MutableStateFlow = MutableStateFlow(null) + val bankAccountDetails: StateFlow = accountDetails.asStateFlow() + + fun updateSearchQuery(query: String) { + searchQuery.update { query } + } + + fun updateSelectedBank(bank: Bank) { + selectedBank = bank + } + + val bankListUiState: StateFlow = combine( + searchQuery, + localAssetRepository.getBanks(), + ::Pair, + ).map { searchQueryAndBanks -> + val searchQuery = searchQueryAndBanks.first + val localBanks = searchQueryAndBanks.second.map { + Bank(it, R.drawable.feature_accounts_ic_bank, BankType.OTHER) + } + val banks = ArrayList().apply { + addAll(popularBankList()) + addAll(localBanks) + }.distinctBy { it.name } + BankUiState.Success( + banks.filter { it.name.contains(searchQuery.lowercase(), ignoreCase = true) }, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = BankUiState.Loading, + ) + + private fun popularBankList(): List { + return listOf( + Bank("RBL Bank", R.drawable.feature_accounts_logo_rbl, BankType.POPULAR), + Bank("SBI Bank", R.drawable.feature_accounts_logo_sbi, BankType.POPULAR), + Bank("PNB Bank", R.drawable.feature_accounts_logo_pnb, BankType.POPULAR), + Bank("HDFC Bank", R.drawable.feature_accounts_logo_hdfc, BankType.POPULAR), + Bank("ICICI Bank", R.drawable.feature_accounts_logo_icici, BankType.POPULAR), + Bank("AXIS Bank", R.drawable.feature_accounts_logo_axis, BankType.POPULAR), + ) + } + + fun fetchBankAccountDetails(onBankDetailsSuccess: () -> Unit) { + // TODO:: UPI API implement, Implement with real API, + // It revert back to Account Screen after successful BankAccount Add + accountDetails.update { + BankAccountDetails( + selectedBank?.name, + "Ankur Sharma", + "New Delhi", + mRandom.nextInt().toString() + " ", + "Savings", + ) + } + onBankDetailsSuccess.invoke() + } + + companion object { + private val mRandom = Random() + } +} + +sealed interface BankUiState { + data class Success(val banks: List = emptyList()) : BankUiState + data object Loading : BankUiState +} diff --git a/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts index 637fd9a73..ae4c6d07b 100644 --- a/feature/auth/build.gradle.kts +++ b/feature/auth/build.gradle.kts @@ -8,9 +8,8 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) - alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { @@ -20,30 +19,15 @@ android { } } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(projects.core.domain) - implementation(compose.material3) - implementation(compose.foundation) - implementation(compose.ui) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - implementation(libs.koin.compose.viewmodel) - implementation(libs.koin.compose) - implementation(libs.jb.kotlin.stdlib) - implementation(libs.kotlin.reflect) - } +dependencies { + implementation(projects.libs.countryCodePicker) - androidMain.dependencies { - // Credentials Manager - implementation(libs.androidx.credentials) - // optional - needed for credentials support from play services, for devices running - // Android 13 and below. - implementation(libs.androidx.credentials.play.services.auth) - implementation(libs.googleid) + // Credentials Manager + implementation(libs.androidx.credentials) + // optional - needed for credentials support from play services, for devices running + // Android 13 and below. + implementation(libs.androidx.credentials.play.services.auth) + implementation(libs.googleid) - implementation(libs.play.services.auth) - } - } + implementation(libs.play.services.auth) } \ No newline at end of file diff --git a/feature/auth/src/main/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt b/feature/auth/src/main/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt new file mode 100644 index 000000000..4cddd202e --- /dev/null +++ b/feature/auth/src/main/kotlin/org/mifospay/feature/auth/login/LoginScreen.kt @@ -0,0 +1,251 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.auth.login + +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfOverlayLoadingWheel +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosOutlinedTextField +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.core.designsystem.theme.grey +import org.mifospay.core.designsystem.theme.styleNormal18sp +import org.mifospay.feature.auth.R +import org.mifospay.feature.auth.socialSignup.SocialSignupMethodContentScreen + +@Composable +internal fun LoginScreen( + navigateToPasscodeScreen: () -> Unit, + modifier: Modifier = Modifier, + viewModel: LoginViewModel = koinViewModel(), + navigateToSignupScreen: () -> Unit, +) { + val context = LocalContext.current + val showProgress by viewModel.showProgress.collectAsStateWithLifecycle() + val isLoginSuccess by viewModel.isLoginSuccess.collectAsStateWithLifecycle() + + LoginScreenContent( + modifier = modifier, + showProgress = showProgress, + login = { username, password -> + viewModel.loginUser( + username = username, + password = password, + onLoginFailed = { message -> + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + }, + ) + }, + navigateToSignupScreen = navigateToSignupScreen, + ) + + if (isLoginSuccess) { + navigateToPasscodeScreen() + } +} + +@Composable +@Suppress("LongMethod") +private fun LoginScreenContent( + showProgress: Boolean, + login: (username: String, password: String) -> Unit, + modifier: Modifier = Modifier, + navigateToSignupScreen: () -> Unit, +) { + var showSignUpScreen by rememberSaveable { mutableStateOf(false) } + + var userName by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf( + TextFieldValue(""), + ) + } + var password by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf( + TextFieldValue(""), + ) + } + var passwordVisibility: Boolean by remember { mutableStateOf(false) } + + if (showSignUpScreen) { + SocialSignupMethodContentScreen( + navigateToSignupScreen = navigateToSignupScreen, + ) { + showSignUpScreen = false + } + } + + Box( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface), + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(top = 100.dp, start = 48.dp, end = 48.dp), + horizontalAlignment = Alignment.Start, + ) { + Text( + text = stringResource(id = R.string.feature_auth_login), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary, + ) + Text( + modifier = Modifier + .padding(top = 32.dp), + text = stringResource(id = R.string.feature_auth_welcome_back), + style = styleNormal18sp.copy(color = grey), + ) + Spacer(modifier = Modifier.padding(top = 32.dp)) + MifosOutlinedTextField( + label = R.string.feature_auth_username, + value = userName, + onValueChange = { + userName = it + }, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(modifier = Modifier.padding(top = 16.dp)) + MifosOutlinedTextField( + label = R.string.feature_auth_password, + value = password, + onValueChange = { + password = it + }, + modifier = Modifier.fillMaxWidth(), + visualTransformation = if (passwordVisibility) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + trailingIcon = { + val image = if (passwordVisibility) { + MifosIcons.Visibility + } else { + MifosIcons.VisibilityOff + } + IconButton(onClick = { passwordVisibility = !passwordVisibility }) { + Icon(imageVector = image, null) + } + }, + ) + MifosButton( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + enabled = userName.text.isNotEmpty() && password.text.isNotEmpty(), + onClick = { + login.invoke(userName.text, password.text) + }, + contentPadding = PaddingValues(12.dp), + ) { + Text( + text = stringResource(id = R.string.feature_auth_login).uppercase(), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onPrimary, + ) + } + // Hide reset password for now + /*Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 32.dp), + text = "Forgot Password", + textAlign = TextAlign.Center, + style = styleMedium16sp.copy( + textDecoration = TextDecoration.Underline, + ) + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp), + text = "OR", + textAlign = TextAlign.Center, + style = styleMedium16sp.copy(color = grey) + )*/ + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = "Don’t have an account yet? ", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + modifier = Modifier.clickable { + showSignUpScreen = true + }, + text = stringResource(id = R.string.feature_auth_sign_up), + style = MaterialTheme.typography.titleMedium.copy( + textDecoration = TextDecoration.Underline, + ), + ) + } + } + + if (showProgress) { + MfOverlayLoadingWheel( + contentDesc = stringResource(id = R.string.feature_auth_logging_in), + ) + } + } +} + +@Preview(showSystemUi = true, device = "id:pixel_5") +@Composable +private fun LoanScreenPreview() { + MifosTheme { + LoginScreenContent( + showProgress = false, + login = { _, _ -> }, + navigateToSignupScreen = {}, + ) + } +} diff --git a/feature/auth/src/main/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt b/feature/auth/src/main/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt new file mode 100644 index 000000000..8ed1a1cec --- /dev/null +++ b/feature/auth/src/main/kotlin/org/mifospay/feature/auth/login/LoginViewModel.kt @@ -0,0 +1,157 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.auth.login + +import android.util.Log +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.domain.client.Client +import com.mifospay.core.model.domain.user.User +import com.mifospay.core.model.entity.UserWithRole +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import org.mifospay.core.data.base.UseCase.UseCaseCallback +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.client.FetchClientData +import org.mifospay.core.data.domain.usecase.user.AuthenticateUser +import org.mifospay.core.data.domain.usecase.user.FetchUserDetails +import org.mifospay.core.datastore.PreferencesHelper + +class LoginViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val authenticateUserUseCase: AuthenticateUser, + private val fetchClientDataUseCase: FetchClientData, + private val fetchUserDetailsUseCase: FetchUserDetails, + private val preferencesHelper: PreferencesHelper, +) : ViewModel() { + + private val _showProgress = MutableStateFlow(false) + val showProgress: StateFlow = _showProgress + + private val _isLoginSuccess = MutableStateFlow(false) + val isLoginSuccess: StateFlow = _isLoginSuccess + + fun updateProgressState(isVisible: Boolean) { + _showProgress.update { isVisible } + } + + fun updateIsLoginSuccess(isLoginSuccess: Boolean) { + _isLoginSuccess.update { isLoginSuccess } + } + + /** + * Authenticate User with username and password + * @param username + * @param password + * Note: username and password can't be empty or null when we pass to API + */ + fun loginUser( + username: String, + password: String, + onLoginFailed: (String) -> Unit, + ) { + updateProgressState(true) + authenticateUserUseCase.walletRequestValues = + AuthenticateUser.RequestValues(username, password) + + val requestValue = authenticateUserUseCase.walletRequestValues + mUseCaseHandler.execute( + authenticateUserUseCase, + requestValue, + object : UseCaseCallback { + override fun onSuccess(response: AuthenticateUser.ResponseValue) { + saveAuthTokenInPref(response.user) + fetchClientData(response.user) + fetchUserDetails(response.user) + } + + override fun onError(message: String) { + updateProgressState(false) + onLoginFailed(message) + } + }, + ) + } + + /** + * Fetch user details return by authenticated user + * @param user + */ + private fun fetchUserDetails(user: User) { + mUseCaseHandler.execute( + fetchUserDetailsUseCase, + FetchUserDetails.RequestValues(user.userId), + object : UseCaseCallback { + override fun onSuccess(response: FetchUserDetails.ResponseValue) { + saveUserDetails(user, response.userWithRole) + } + + override fun onError(message: String) { + updateProgressState(false) + Log.d("Login User Detailed: ", message) + } + }, + ) + } + + /** + * Fetch client details return by authenticated user + * Client Id: user.clients.firstOrNull() ?: 0 + * @param user + */ + private fun fetchClientData(user: User) { + mUseCaseHandler.execute( + fetchClientDataUseCase, + FetchClientData.RequestValues(user.clients.firstOrNull()), + object : UseCaseCallback { + override fun onSuccess(response: FetchClientData.ResponseValue) { + saveClientDetails(response.clientDetails) + updateProgressState(false) + if (response.clientDetails.name != "") { + updateIsLoginSuccess(true) + } + } + + override fun onError(message: String) { + updateProgressState(false) + } + }, + ) + } + + private fun saveAuthTokenInPref(user: User) { + preferencesHelper.saveToken("Basic " + user.base64EncodedAuthenticationKey) + } + + /** + * TODO remove userName, userId and Email from pref and use from saved User + */ + private fun saveUserDetails( + user: User, + userWithRole: UserWithRole, + ) { + val userName = user.username + val userID = user.userId + preferencesHelper.saveUsername(userName) + preferencesHelper.userId = userID + preferencesHelper.saveEmail(userWithRole.email) + preferencesHelper.user = user + } + + /** + * TODO remove name, clientId and mobileNo from pref and use from saved Client + */ + private fun saveClientDetails(client: Client?) { + preferencesHelper.saveFullName(client?.name) + preferencesHelper.clientId = client?.clientId!! + preferencesHelper.saveMobile(client.mobileNo) + preferencesHelper.client = client + } +} diff --git a/feature/auth/src/main/kotlin/org/mifospay/feature/auth/mobileVerify/MobileVerificationScreen.kt b/feature/auth/src/main/kotlin/org/mifospay/feature/auth/mobileVerify/MobileVerificationScreen.kt new file mode 100644 index 000000000..db9ec6611 --- /dev/null +++ b/feature/auth/src/main/kotlin/org/mifospay/feature/auth/mobileVerify/MobileVerificationScreen.kt @@ -0,0 +1,289 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.auth.mobileVerify + +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.focusable +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifos.library.countrycodepicker.CountryCodePicker +import org.koin.androidx.compose.koinViewModel +import org.mifospay.common.Constants +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosLoadingWheel +import org.mifospay.core.designsystem.component.MifosOutlinedTextField +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.feature.auth.R + +@Composable +internal fun MobileVerificationScreen( + onOtpVerificationSuccess: (String) -> Unit, + modifier: Modifier = Modifier, + viewModel: MobileVerificationViewModel = koinViewModel(), +) { + val context = LocalContext.current + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + MobileVerificationScreen( + uiState = uiState, + showProgressState = viewModel.showProgress, + verifyMobileAndRequestOtp = { phone, fullPhone -> + viewModel.verifyMobileAndRequestOtp(fullPhone, phone) { + it?.let { + Toast.makeText(context, it, Toast.LENGTH_SHORT).show() + } + } + }, + verifyOtp = { validatedOtp, fullNumber -> + viewModel.verifyOTP(validatedOtp) { + onOtpVerificationSuccess(fullNumber) + } + }, + modifier = modifier, + ) +} + +@Composable +private fun MobileVerificationScreen( + uiState: MobileVerificationUiState, + verifyMobileAndRequestOtp: (String, String) -> Unit, + verifyOtp: (String, String) -> Unit, + modifier: Modifier = Modifier, + showProgressState: Boolean = false, +) { + var phoneNumber by rememberSaveable { mutableStateOf("") } + var fullPhoneNumber by rememberSaveable { mutableStateOf("") } + var isNumberValid: Boolean by rememberSaveable { mutableStateOf(false) } + + var isOtpValidated by rememberSaveable { mutableStateOf(false) } + var validatedOtp by rememberSaveable { mutableStateOf("") } + + fun verifyMobileOrOtp() { + if (uiState == MobileVerificationUiState.VerifyPhone && isNumberValid) { + verifyMobileAndRequestOtp(phoneNumber, fullPhoneNumber) + } else if (isOtpValidated) { + verifyOtp(validatedOtp, fullPhoneNumber) + } + } + + Box(modifier) { + Column( + modifier = Modifier + .fillMaxSize() + .background(color = Color.White) + .focusable(!showProgressState), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = MaterialTheme.colorScheme.primary), + verticalArrangement = Arrangement.Top, + ) { + Text( + modifier = Modifier.padding(top = 48.dp, start = 24.dp, end = 24.dp), + text = if (uiState == MobileVerificationUiState.VerifyPhone) { + stringResource(id = R.string.feature_auth_enter_mobile_number) + } else { + stringResource(id = R.string.feature_auth_enter_otp) + }, + style = MaterialTheme.typography.titleLarge.copy(color = MaterialTheme.colorScheme.onPrimary), + ) + Text( + modifier = Modifier.padding( + top = 4.dp, + bottom = 32.dp, + start = 24.dp, + end = 24.dp, + ), + text = if (uiState == MobileVerificationUiState.VerifyPhone) { + stringResource(id = R.string.feature_auth_enter_mobile_number_description) + } else { + stringResource(id = R.string.feature_auth_enter_otp_received_on_your_registered_device) + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimary, + ) + } + + when (uiState) { + MobileVerificationUiState.VerifyPhone -> { + EnterPhoneScreen( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 16.dp), + onNumberUpdated = { phone, fullPhone, valid -> + phoneNumber = phone + fullPhoneNumber = fullPhone + isNumberValid = valid + }, + ) + } + + MobileVerificationUiState.VerifyOtp -> { + EnterOtpScreen( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 48.dp, vertical = 24.dp), + onOtpValidated = { isValidated, otp -> + isOtpValidated = isValidated + validatedOtp = otp + }, + ) + } + } + + MifosButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp, vertical = 16.dp), + color = MaterialTheme.colorScheme.primary, + enabled = if (uiState == MobileVerificationUiState.VerifyPhone) { + isNumberValid + } else { + isOtpValidated + }, + onClick = { verifyMobileOrOtp() }, + contentPadding = PaddingValues(12.dp), + ) { + Text( + text = if (uiState == MobileVerificationUiState.VerifyPhone) { + stringResource(id = R.string.feature_auth_verify_phone).uppercase() + } else { + stringResource(id = R.string.feature_auth_verify_otp).uppercase() + }, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onPrimary, + ) + } + } + + if (showProgressState) { + ShowProgressScreen(uiState = uiState) + } + } +} + +@Composable +private fun EnterPhoneScreen( + onNumberUpdated: (String, String, Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val keyboardController = LocalSoftwareKeyboardController.current + CountryCodePicker( + modifier = modifier, + shape = RoundedCornerShape(8.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + ), + onValueChange = { (code, phone), isValid -> + onNumberUpdated(phone, code + phone, isValid) + }, + label = { Text(stringResource(id = R.string.feature_auth_phone_number)) }, + keyboardActions = KeyboardActions { keyboardController?.hide() }, + ) +} + +@Composable +private fun EnterOtpScreen( + onOtpValidated: (Boolean, String) -> Unit, + modifier: Modifier = Modifier, +) { + val keyboardController = LocalSoftwareKeyboardController.current + var otp by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + + MifosOutlinedTextField( + label = R.string.feature_auth_enter_otp, + value = otp, + onValueChange = { + otp = it + onOtpValidated(otp.text.length == 6, otp.text) + }, + modifier = modifier, + keyboardActions = KeyboardActions { keyboardController?.hide() }, + ) +} + +@Composable +private fun ShowProgressScreen( + uiState: MobileVerificationUiState, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxSize() + .background(color = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)) + .focusable(), + contentAlignment = Alignment.Center, + ) { + MifosLoadingWheel( + modifier = Modifier.wrapContentSize(), + contentDesc = if (uiState == MobileVerificationUiState.VerifyPhone) { + Constants.SENDING_OTP_TO_YOUR_MOBILE_NUMBER + } else { + Constants.VERIFYING_OTP + }, + ) + } +} + +@Preview +@Composable +private fun MobileVerificationScreenVerifyPhonePreview() { + MifosTheme { + MobileVerificationScreen( + uiState = MobileVerificationUiState.VerifyPhone, + showProgressState = false, + verifyMobileAndRequestOtp = { _, _ -> }, + verifyOtp = { _, _ -> }, + ) + } +} + +@Preview +@Composable +private fun MobileVerificationScreenVerifyOtpPreview() { + MifosTheme { + MobileVerificationScreen( + uiState = MobileVerificationUiState.VerifyOtp, + showProgressState = false, + verifyMobileAndRequestOtp = { _, _ -> }, + verifyOtp = { _, _ -> }, + ) + } +} diff --git a/feature/auth/src/main/kotlin/org/mifospay/feature/auth/mobileVerify/MobileVerificationViewModel.kt b/feature/auth/src/main/kotlin/org/mifospay/feature/auth/mobileVerify/MobileVerificationViewModel.kt new file mode 100644 index 000000000..fa0e3b7ab --- /dev/null +++ b/feature/auth/src/main/kotlin/org/mifospay/feature/auth/mobileVerify/MobileVerificationViewModel.kt @@ -0,0 +1,92 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.auth.mobileVerify + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.client.SearchClient + +@Suppress("UnusedParameter") +class MobileVerificationViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val searchClientUseCase: SearchClient, +) : ViewModel() { + + private val _uiState = + MutableStateFlow(MobileVerificationUiState.VerifyPhone) + val uiState: StateFlow = _uiState + + var showProgress by mutableStateOf(false) + + /** + * Verify Mobile number that it already exist or not then request otp + */ + fun verifyMobileAndRequestOtp( + fullNumber: String, + mobileNo: String, + onError: (String?) -> Unit, + ) { + showProgress = true + mUseCaseHandler.execute( + searchClientUseCase, + fullNumber.let { SearchClient.RequestValues(it) }, + object : UseCase.UseCaseCallback { + override fun onSuccess(response: SearchClient.ResponseValue) { + onError("Mobile number already exists.") + showProgress = false + } + + override fun onError(message: String) { + requestOtp(fullNumber) + } + }, + ) + } + + /** + * Request Otp from server + */ + fun requestOtp(fullNumber: String) { + viewModelScope.launch { + delay(2000) + showProgress = false + _uiState.update { + MobileVerificationUiState.VerifyOtp + } + } + } + + /** + * Verify Otp + */ + fun verifyOTP(otp: String?, onOtpVerifySuccess: () -> Unit) { + showProgress = true + viewModelScope.launch { + delay(2000) + showProgress = false + onOtpVerifySuccess() + } + } +} + +sealed interface MobileVerificationUiState { + data object VerifyOtp : MobileVerificationUiState + data object VerifyPhone : MobileVerificationUiState +} diff --git a/feature/auth/src/main/kotlin/org/mifospay/feature/auth/signup/SignupScreen.kt b/feature/auth/src/main/kotlin/org/mifospay/feature/auth/signup/SignupScreen.kt new file mode 100644 index 000000000..d52642be0 --- /dev/null +++ b/feature/auth/src/main/kotlin/org/mifospay/feature/auth/signup/SignupScreen.kt @@ -0,0 +1,531 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.auth.signup + +import android.widget.Toast +import androidx.compose.foundation.background +import androidx.compose.foundation.focusable +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +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.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifospay.core.model.State +import com.mifospay.core.model.signup.PasswordStrength +import com.mifospay.core.model.signup.SignupData +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.data.util.Constants.MIFOS_MERCHANT_SAVINGS_PRODUCT_ID +import org.mifospay.core.designsystem.component.MfOutlinedTextField +import org.mifospay.core.designsystem.component.MfOverlayLoadingWheel +import org.mifospay.core.designsystem.component.MfPasswordTextField +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.theme.styleMedium16sp +import org.mifospay.feature.auth.R +import org.mifospay.feature.auth.utils.ValidateUtil.isValidEmail +import java.util.Locale + +@Composable +internal fun SignupScreen( + savingProductId: Int, + mobileNumber: String, + country: String, + email: String, + firstName: String, + lastName: String, + businessName: String, + onLoginSuccess: () -> Unit, + modifier: Modifier = Modifier, + viewModel: SignupViewModel = koinViewModel(), +) { + val context = LocalContext.current + + val stateList by viewModel.states.collectAsStateWithLifecycle() + + LaunchedEffect(key1 = true) { + viewModel.initSignupData( + savingProductId = savingProductId, + mobileNumber = mobileNumber, + countryName = country, + email = email, + firstName = firstName, + lastName = lastName, + businessName = businessName, + ) + } + LaunchedEffect(viewModel.isLoginSuccess) { + if (viewModel.isLoginSuccess) { + onLoginSuccess.invoke() + } + } + + SignupScreenContent( + modifier = modifier, + showProgressState = viewModel.showProgress, + data = viewModel.signupData, + stateList = stateList, + onCompleteRegistration = { + viewModel.registerUser(it) { message -> + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + }, + ) +} + +@Composable +@Suppress("LongMethod", "CyclomaticComplexMethod") +private fun SignupScreenContent( + data: SignupData, + stateList: List, + onCompleteRegistration: (SignupData) -> Unit, + modifier: Modifier = Modifier, + showProgressState: Boolean = false, +) { + val context = LocalContext.current + + var firstName by rememberSaveable { mutableStateOf(data.firstName ?: "") } + var lastName by rememberSaveable { mutableStateOf(data.lastName ?: "") } + var email by rememberSaveable { mutableStateOf(data.email ?: "") } + var userName by rememberSaveable { + mutableStateOf( + data.email?.ifEmpty { "" } + ?: data.email?.let { it.substring(0, it.indexOf('@')) } ?: "", + ) + } + var addressLine1 by rememberSaveable { mutableStateOf("") } + var addressLine2 by rememberSaveable { mutableStateOf("") } + var pinCode by rememberSaveable { mutableStateOf("") } + var nameOfBusiness by rememberSaveable { mutableStateOf(data.businessName ?: "") } + + var password by rememberSaveable { mutableStateOf("") } + var confirmPassword by rememberSaveable { mutableStateOf("") } + var isPasswordVisible by rememberSaveable { mutableStateOf(false) } + var isConfirmPasswordVisible by rememberSaveable { mutableStateOf(false) } + + var selectedState by rememberSaveable { mutableStateOf(null) } + + fun validateAllFields() { + val isAnyFieldEmpty = + firstName.isEmpty() || + lastName.isEmpty() || + email.isEmpty() || + userName.isEmpty() || + addressLine1.isEmpty() || + addressLine2.isEmpty() || + pinCode.isEmpty() || + password.isEmpty() || + confirmPassword.isEmpty() || + selectedState == null + + val isNameOfBusinessEmpty = + data.mifosSavingsProductId == MIFOS_MERCHANT_SAVINGS_PRODUCT_ID && + nameOfBusiness.isEmpty() + + if (!email.isValidEmail()) { + Toast + .makeText( + context, + context.getString(R.string.feature_auth_validate_email), + Toast.LENGTH_SHORT, + ).show() + return + } + + if (isAnyFieldEmpty || isNameOfBusinessEmpty) { + Toast + .makeText( + context, + context.getString(R.string.feature_auth_all_fields_are_mandatory), + Toast.LENGTH_SHORT, + ).show() + return + } + } + + fun completeRegistration() { + val signUpData = + data.copy( + firstName = firstName, + lastName = lastName, + email = email, + userName = userName, + addressLine1 = addressLine1, + addressLine2 = addressLine2, + pinCode = pinCode, + businessName = nameOfBusiness, + password = password, + stateId = selectedState?.id, + ) + onCompleteRegistration.invoke(signUpData) + } + + Box(modifier) { + Column( + modifier = + Modifier + .fillMaxSize() + .background(color = MaterialTheme.colorScheme.surface) + .verticalScroll(rememberScrollState()) + .focusable(!showProgressState), + ) { + Column( + modifier = + Modifier + .fillMaxWidth() + .background(color = MaterialTheme.colorScheme.primary), + verticalArrangement = Arrangement.Top, + ) { + Text( + modifier = Modifier.padding(top = 48.dp, start = 24.dp, end = 24.dp), + text = stringResource(id = R.string.feature_auth_complete_your_registration), + style = MaterialTheme.typography.titleLarge.copy(color = MaterialTheme.colorScheme.onPrimary), + ) + Text( + modifier = + Modifier.padding( + top = 4.dp, + bottom = 32.dp, + start = 24.dp, + end = 24.dp, + ), + text = stringResource(id = R.string.feature_auth_all_fields_are_mandatory), + style = MaterialTheme.typography.bodySmall.copy(color = Color.White), + ) + } + + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp) + .focusable(!showProgressState), + ) { + UserInfoTextField( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 16.dp), + label = stringResource(id = R.string.feature_auth_first_name), + value = firstName, + ) { + firstName = it + } + UserInfoTextField( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), + label = stringResource(id = R.string.feature_auth_last_name), + value = lastName, + ) { + lastName = it + } + UserInfoTextField( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), + label = stringResource(id = R.string.feature_auth_username), + value = userName, + ) { + userName = it + } + PasswordAndConfirmPassword( + password = password, + onPasswordChange = { password = it }, + confirmPassword = confirmPassword, + onConfirmPasswordChange = { confirmPassword = it }, + isPasswordVisible = isPasswordVisible, + onTogglePasswordVisibility = { isPasswordVisible = !isPasswordVisible }, + isConfirmPasswordVisible = isConfirmPasswordVisible, + onConfirmTogglePasswordVisibility = { + isConfirmPasswordVisible = !isConfirmPasswordVisible + }, + ) + UserInfoTextField( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), + label = stringResource(id = R.string.feature_auth_email), + value = email, + ) { + email = it + } + if (data.mifosSavingsProductId == MIFOS_MERCHANT_SAVINGS_PRODUCT_ID) { + UserInfoTextField( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), + label = stringResource(id = R.string.feature_auth_name_of_business), + value = nameOfBusiness, + ) { + nameOfBusiness = it + } + } + UserInfoTextField( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), + label = stringResource(id = R.string.feature_auth_address_line_1), + value = addressLine1, + ) { + addressLine1 = it + } + UserInfoTextField( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), + label = stringResource(id = R.string.feature_auth_address_line_2), + value = addressLine2, + ) { + addressLine2 = it + } + UserInfoTextField( + modifier = Modifier.padding(top = 8.dp), + label = stringResource(id = R.string.feature_auth_pin_code), + value = pinCode, + ) { + pinCode = it + } + HorizontalDivider(thickness = 8.dp, color = Color.White) + MifosStateDropDownOutlinedTextField( + value = selectedState?.name ?: "", + label = stringResource(id = R.string.feature_auth_state), + stateList = stateList, + ) { + selectedState = it + } + HorizontalDivider(thickness = 24.dp, color = Color.White) + MifosButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + color = MaterialTheme.colorScheme.primary, + enabled = true, + onClick = { + validateAllFields() + completeRegistration() + }, + contentPadding = PaddingValues(12.dp), + ) { + Text( + text = stringResource(id = R.string.feature_auth_complete), + style = styleMedium16sp.copy(color = MaterialTheme.colorScheme.onPrimary), + ) + } + } + } + + if (showProgressState) { + MfOverlayLoadingWheel( + contentDesc = stringResource(id = R.string.feature_auth_please_wait), + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun MifosStateDropDownOutlinedTextField( + value: String, + label: String, + stateList: List, + modifier: Modifier = Modifier, + onSelectedState: (State) -> Unit = {}, +) { + var expanded by rememberSaveable { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { + expanded = !expanded + }, + ) { + OutlinedTextField( + modifier = modifier.menuAnchor(), + value = value, + onValueChange = { }, + readOnly = true, + label = { Text(label) }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) + }, + ) + DropdownMenu( + expanded = expanded, + onDismissRequest = { + expanded = false + }, + ) { + stateList.forEach { + DropdownMenuItem( + text = { Text(text = it.name) }, + onClick = { + expanded = false + onSelectedState(it) + }, + ) + } + } + } +} + +@Composable +private fun UserInfoTextField( + label: String, + value: String, + modifier: Modifier = Modifier, + onValueChange: (String) -> Unit = {}, +) { + MfOutlinedTextField( + value = value, + label = label, + onValueChange = onValueChange, + modifier = modifier, + isError = value.isEmpty(), + errorMessage = stringResource(id = R.string.feature_auth_mandatory), + ) +} + +@Composable +private fun PasswordAndConfirmPassword( + password: String, + onPasswordChange: (String) -> Unit, + confirmPassword: String, + onConfirmPasswordChange: (String) -> Unit, + isPasswordVisible: Boolean, + onTogglePasswordVisibility: () -> Unit, + isConfirmPasswordVisible: Boolean, + onConfirmTogglePasswordVisibility: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier) { + MfPasswordTextField( + password = password, + label = stringResource(id = R.string.feature_auth_password), + isError = password.isEmpty() || password.length < 6, + isPasswordVisible = isPasswordVisible, + onTogglePasswordVisibility = onTogglePasswordVisibility, + onPasswordChange = onPasswordChange, + modifier = Modifier.fillMaxWidth(), + errorMessage = + if (password.isEmpty()) { + stringResource(id = R.string.feature_auth_password_cannot_be_empty) + } else if (password.length < 6) { + stringResource(id = R.string.feature_auth_password_must_be_least_6_characters) + } else { + null + }, + ) + MfPasswordTextField( + password = confirmPassword, + label = stringResource(id = R.string.feature_auth_confirm_password), + isError = confirmPassword.isEmpty() || password != confirmPassword, + isPasswordVisible = isConfirmPasswordVisible, + onTogglePasswordVisibility = onConfirmTogglePasswordVisibility, + onPasswordChange = onConfirmPasswordChange, + modifier = Modifier.fillMaxWidth(), + errorMessage = + if (confirmPassword.isEmpty()) { + stringResource(id = R.string.feature_auth_confirm_password_cannot_empty) + } else if (password != confirmPassword) { + stringResource(id = R.string.feature_auth_passwords_do_not_match) + } else { + null + }, + ) + if (password.length >= 6) { + Text( + modifier = Modifier.padding(top = 8.dp), + text = "${stringResource(id = R.string.feature_auth_password_strength)}${ + getPasswordStrength(password).replaceFirstChar { + if (it.isLowerCase()) { + it.titlecase( + Locale.ENGLISH, + ) + } else { + it.toString() + } + } + }", + color = getPasswordStrengthColor(password), + ) + } + } +} + +private fun getPasswordStrength(password: String): String { + val hasUpperCase = password.any { it.isUpperCase() } + val hasLowerCase = password.any { it.isLowerCase() } + val hasNumbers = password.any { it.isDigit() } + val hasSymbols = password.any { !it.isLetterOrDigit() } + + val numTypesPresent = + intArrayOf( + hasUpperCase.toInt(), + hasLowerCase.toInt(), + hasNumbers.toInt(), + hasSymbols.toInt(), + ).sum() + return PasswordStrength.entries[numTypesPresent].name +} + +private fun Boolean.toInt() = if (this) 1 else 0 + +private fun getPasswordStrengthColor(password: String): Color { + val strength = getPasswordStrength(password) + return when (PasswordStrength.valueOf(strength)) { + PasswordStrength.WEAK -> Color.Red + PasswordStrength.MODERATE -> Color.DarkGray + PasswordStrength.STRONG -> Color.Green + PasswordStrength.VERY_STRONG -> Color.Blue + PasswordStrength.EXCELLENT -> Color.Magenta + else -> Color.Black + } +} + +@Preview +@Composable +private fun SignupScreenPreview() { + SignupScreenContent( + showProgressState = false, + data = SignupData(), + stateList = listOf(), + onCompleteRegistration = { }, + ) +} diff --git a/feature/auth/src/main/kotlin/org/mifospay/feature/auth/signup/SignupViewModel.kt b/feature/auth/src/main/kotlin/org/mifospay/feature/auth/signup/SignupViewModel.kt new file mode 100644 index 000000000..c9e3aeb77 --- /dev/null +++ b/feature/auth/src/main/kotlin/org/mifospay/feature/auth/signup/SignupViewModel.kt @@ -0,0 +1,294 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.auth.signup + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifospay.core.model.State +import com.mifospay.core.model.domain.user.NewUser +import com.mifospay.core.model.domain.user.UpdateUserEntityClients +import com.mifospay.core.model.domain.user.User +import com.mifospay.core.model.entity.UserWithRole +import com.mifospay.core.model.signup.SignupData +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import org.mifospay.common.Constants +import org.mifospay.common.DebugUtil +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.client.CreateClient +import org.mifospay.core.data.domain.usecase.client.FetchClientData +import org.mifospay.core.data.domain.usecase.client.SearchClient +import org.mifospay.core.data.domain.usecase.user.AuthenticateUser +import org.mifospay.core.data.domain.usecase.user.CreateUser +import org.mifospay.core.data.domain.usecase.user.DeleteUser +import org.mifospay.core.data.domain.usecase.user.FetchUserDetails +import org.mifospay.core.data.domain.usecase.user.UpdateUser +import org.mifospay.core.data.repository.local.LocalAssetRepository +import org.mifospay.core.datastore.PreferencesHelper + +class SignupViewModel( + localAssetRepository: LocalAssetRepository, + private val useCaseHandler: UseCaseHandler, + private val preferencesHelper: PreferencesHelper, + private val searchClientUseCase: SearchClient, + private val createClientUseCase: CreateClient, + private val createUserUseCase: CreateUser, + private val updateUserUseCase: UpdateUser, + private val authenticateUserUseCase: AuthenticateUser, + private val fetchClientDataUseCase: FetchClientData, + private val deleteUserUseCase: DeleteUser, + private val fetchUserDetailsUseCase: FetchUserDetails, +) : ViewModel() { + + var showProgress by mutableStateOf(false) + var isLoginSuccess by mutableStateOf(false) + + var signupData by mutableStateOf(SignupData()) + var state by mutableStateOf(null) + + fun initSignupData( + savingProductId: Int, + mobileNumber: String, + countryName: String?, + email: String?, + firstName: String?, + lastName: String?, + businessName: String?, + ) { + signupData = signupData.copy( + mifosSavingsProductId = savingProductId, + mobileNumber = mobileNumber, + countryName = countryName, + email = email, + firstName = firstName!!, + lastName = lastName!!, + businessName = businessName, + ) + } + + val states: StateFlow> = combine( + localAssetRepository.getCountries(), + localAssetRepository.getStateList(), + ::Pair, + ) + .map { + val countries = it.first + signupData = signupData.copy( + countryId = countries.find { it.name == signupData.countryName }?.id ?: "", + ) + it.second.filter { it.countryId == signupData.countryId } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList(), + ) + + fun registerUser(data: SignupData, showToastMessage: (String) -> Unit) { + signupData = data + + // 0. Unique Mobile Number (checked in MOBILE VERIFICATION ACTIVITY) + // 1. Check for unique external id and username + // 2. Create user + // 3. Create Client + // 4. Update User and connect client with user + useCaseHandler.execute( + searchClientUseCase, + SearchClient.RequestValues("${signupData.userName}@mifos"), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: SearchClient.ResponseValue) { + showToastMessage("Username already exists.") + } + + override fun onError(message: String) { + createUser(showToastMessage) + } + }, + ) + } + + private fun createUser(showToastMessage: (String) -> Unit) { + val newUser = NewUser( + signupData.userName, + signupData.firstName, + signupData.lastName, + signupData.email, + signupData.password, + ) + useCaseHandler.execute( + createUserUseCase, + CreateUser.RequestValues(newUser), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: CreateUser.ResponseValue) { + createClient(response.userId, showToastMessage) + } + + override fun onError(message: String) { + DebugUtil.log(message) + showToastMessage(message) + } + }, + ) + } + + private fun createClient(userId: Int, showToastMessage: (String) -> Unit) { + val newClient = com.mifospay.core.model.domain.client.NewClient( + signupData.businessName, signupData.userName, signupData.addressLine1, + signupData.addressLine2, signupData.city, signupData.pinCode, signupData.stateId, + signupData.countryId, signupData.mobileNumber, signupData.mifosSavingsProductId, + ) + useCaseHandler.execute( + createClientUseCase, + CreateClient.RequestValues(newClient), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: CreateClient.ResponseValue) { + response.clientId.let { DebugUtil.log(it) } + val clients = ArrayList() + response.clientId.let { clients.add(it) } + updateClient(clients, userId, showToastMessage) + } + + override fun onError(message: String) { + // delete user + DebugUtil.log(message) + showToastMessage(message) + deleteUser(userId) + } + }, + ) + } + + private fun updateClient( + clients: ArrayList, + userId: Int, + showToastMessage: (String) -> Unit, + ) { + useCaseHandler.execute( + updateUserUseCase, + UpdateUser.RequestValues(UpdateUserEntityClients(clients), userId), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: UpdateUser.ResponseValue?) { + loginUser(signupData.userName, signupData.password, showToastMessage) + } + + override fun onError(message: String) { + // connect client later + DebugUtil.log(message) + showToastMessage("update client error") + } + }, + ) + } + + private fun loginUser( + username: String?, + password: String?, + showToastMessage: (String) -> Unit, + ) { + authenticateUserUseCase.walletRequestValues = AuthenticateUser.RequestValues(username!!, password!!) + val requestValue = authenticateUserUseCase.walletRequestValues + useCaseHandler.execute( + authenticateUserUseCase, + requestValue, + object : UseCase.UseCaseCallback { + override fun onSuccess(response: AuthenticateUser.ResponseValue) { + createAuthenticatedService(response.user) + fetchClientData(showToastMessage) + fetchUserDetails(response.user) + } + + override fun onError(message: String) { + showToastMessage("Login Failed") + } + }, + ) + } + + private fun fetchUserDetails(user: User) { + useCaseHandler.execute( + fetchUserDetailsUseCase, + FetchUserDetails.RequestValues(user.userId), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchUserDetails.ResponseValue) { + saveUserDetails(user, response.userWithRole) + } + + override fun onError(message: String) { + DebugUtil.log(message) + } + }, + ) + } + + private fun fetchClientData(showToastMessage: (String) -> Unit) { + useCaseHandler.execute( + fetchClientDataUseCase, + null, + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchClientData.ResponseValue) { + saveClientDetails(response.clientDetails) + if (response.clientDetails.name != "") { + isLoginSuccess = true + } + } + + override fun onError(message: String) { + showToastMessage("Fetch Client Error") + } + }, + ) + } + + private fun createAuthenticatedService(user: User) { + val authToken = Constants.BASIC + user.base64EncodedAuthenticationKey + preferencesHelper.saveToken(authToken) + } + + private fun saveUserDetails( + user: User, + userWithRole: UserWithRole, + ) { + val userName = user.username + val userID = user.userId + preferencesHelper.saveUsername(userName) + preferencesHelper.userId = userID + preferencesHelper.saveEmail(userWithRole.email) + } + + private fun saveClientDetails(client: com.mifospay.core.model.domain.client.Client) { + preferencesHelper.saveFullName(client.name) + preferencesHelper.clientId = client.clientId + preferencesHelper.saveMobile(client.mobileNo) + } + + private fun deleteUser(userId: Int) { + useCaseHandler.execute( + deleteUserUseCase, + DeleteUser.RequestValues(userId), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: DeleteUser.ResponseValue) {} + override fun onError(message: String) {} + }, + ) + } +} + +sealed interface SignupUiState { + data object None : SignupUiState + data object Success : SignupUiState + data class Error(val exception: String) : SignupUiState +} diff --git a/feature/editpassword/build.gradle.kts b/feature/editpassword/build.gradle.kts index 1f20fe47b..34b701da8 100644 --- a/feature/editpassword/build.gradle.kts +++ b/feature/editpassword/build.gradle.kts @@ -8,22 +8,12 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { namespace = "org.mifospay.feature.editpassword" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - } - } -} \ No newline at end of file +dependencies {} \ No newline at end of file diff --git a/feature/editpassword/src/main/kotlin/org/mifospay/feature/editpassword/EditPasswordScreen.kt b/feature/editpassword/src/main/kotlin/org/mifospay/feature/editpassword/EditPasswordScreen.kt new file mode 100644 index 000000000..e49c4ede1 --- /dev/null +++ b/feature/editpassword/src/main/kotlin/org/mifospay/feature/editpassword/EditPasswordScreen.kt @@ -0,0 +1,225 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.editpassword + +import androidx.compose.foundation.layout.Column +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.padding +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.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfPasswordTextField +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.theme.MifosTheme + +@Composable +internal fun EditPasswordScreen( + onBackPress: () -> Unit, + onCancelChanges: () -> Unit, + modifier: Modifier = Modifier, + viewModel: EditPasswordViewModel = koinViewModel(), +) { + val editPasswordUiState by viewModel.editPasswordUiState.collectAsStateWithLifecycle() + EditPasswordScreen( + modifier = modifier, + editPasswordUiState = editPasswordUiState, + onCancelChanges = onCancelChanges, + onBackPress = onBackPress, + onSave = { currentPass, newPass, confirmPass -> + viewModel.updatePassword( + currentPassword = currentPass, + newPassword = newPass, + newPasswordRepeat = confirmPass, + ) + }, + ) +} + +@Composable +private fun EditPasswordScreen( + editPasswordUiState: EditPasswordUiState, + onCancelChanges: () -> Unit, + onBackPress: () -> Unit, + onSave: (currentPass: String, newPass: String, confirmPass: String) -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + var currentPassword by rememberSaveable { mutableStateOf("") } + var newPassword by rememberSaveable { mutableStateOf("") } + var confirmNewPassword by rememberSaveable { mutableStateOf("") } + var isConfirmPasswordVisible by rememberSaveable { mutableStateOf(false) } + var isNewPasswordVisible by rememberSaveable { mutableStateOf(false) } + var isConfirmNewPasswordVisible by rememberSaveable { mutableStateOf(false) } + + val snackbarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + + val currentSnackbarHostState by rememberUpdatedState(snackbarHostState) + + LaunchedEffect(editPasswordUiState) { + when (editPasswordUiState) { + is EditPasswordUiState.Error -> { + val errorMessage = editPasswordUiState.message + coroutineScope.launch { + currentSnackbarHostState.showSnackbar(errorMessage) + } + } + + EditPasswordUiState.Loading -> {} + EditPasswordUiState.Success -> { + coroutineScope.launch { + currentSnackbarHostState.showSnackbar( + context.getString(R.string.feature_editpassword_password_changed_successfully), + ) + } + } + } + } + + MifosScaffold( + modifier = modifier, + topBarTitle = R.string.feature_editpassword_change_password, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + backPress = onBackPress, + scaffoldContent = { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + ) { + MfPasswordTextField( + password = currentPassword, + label = stringResource(R.string.feature_editpassword_current_password), + isError = false, + isPasswordVisible = isConfirmPasswordVisible, + onTogglePasswordVisibility = { + isConfirmPasswordVisible = !isConfirmPasswordVisible + }, + onPasswordChange = { currentPassword = it }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + MfPasswordTextField( + password = newPassword, + + label = stringResource(id = R.string.feature_editpassword_new_password), + isError = newPassword.isNotEmpty() && newPassword.length < 6, + isPasswordVisible = isNewPasswordVisible, + onTogglePasswordVisibility = { isNewPasswordVisible = !isNewPasswordVisible }, + onPasswordChange = { newPassword = it }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + errorMessage = if (newPassword.isNotEmpty() && newPassword.length < 6) { + stringResource( + id = R.string.feature_editpassword_password_length_error, + ) + } else { + null + }, + ) + MfPasswordTextField( + password = confirmNewPassword, + label = stringResource(id = R.string.feature_editpassword_confirm_new_password), + isError = newPassword != confirmNewPassword && confirmNewPassword.isNotEmpty(), + isPasswordVisible = isConfirmNewPasswordVisible, + onTogglePasswordVisibility = { + isConfirmNewPasswordVisible = !isConfirmNewPasswordVisible + }, + onPasswordChange = { confirmNewPassword = it }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + errorMessage = if (newPassword != + confirmNewPassword && confirmNewPassword.isNotEmpty() + ) { + stringResource( + id = R.string.feature_editpassword_password_mismatch_error, + ) + } else { + null + }, + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp, start = 16.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + MifosButton( + onClick = { onCancelChanges.invoke() }, + modifier = Modifier + .weight(1f) + .padding(8.dp), + contentPadding = PaddingValues(16.dp), + content = { Text(text = stringResource(id = R.string.feature_editpassword_cancel)) }, + ) + MifosButton( + modifier = Modifier + .weight(1f) + .padding(8.dp), + onClick = { + onSave.invoke(currentPassword, newPassword, confirmNewPassword) + }, + contentPadding = PaddingValues(16.dp), + content = { Text(text = stringResource(id = R.string.feature_editpassword_save)) }, + ) + } + } + }, + ) +} + +class EditPasswordUiStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + EditPasswordUiState.Loading, + EditPasswordUiState.Success, + EditPasswordUiState.Error("Some Error Occurred"), + ) +} + +@Preview +@Composable +private fun EditPasswordScreenPreview( + @PreviewParameter(EditPasswordUiStateProvider::class) editPasswordUiState: EditPasswordUiState, +) { + MifosTheme { + EditPasswordScreen(editPasswordUiState = editPasswordUiState, {}, {}, { _, _, _ -> }) + } +} diff --git a/feature/editpassword/src/main/kotlin/org/mifospay/feature/editpassword/EditPasswordViewModel.kt b/feature/editpassword/src/main/kotlin/org/mifospay/feature/editpassword/EditPasswordViewModel.kt new file mode 100644 index 000000000..f905b6e28 --- /dev/null +++ b/feature/editpassword/src/main/kotlin/org/mifospay/feature/editpassword/EditPasswordViewModel.kt @@ -0,0 +1,124 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.editpassword + +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.domain.user.UpdateUserEntityPassword +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.mifospay.common.Constants +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.user.AuthenticateUser +import org.mifospay.core.data.domain.usecase.user.UpdateUser +import org.mifospay.core.datastore.PreferencesHelper + +@Suppress("NestedBlockDepth") +class EditPasswordViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val mPreferencesHelper: PreferencesHelper, + private val authenticateUserUseCase: AuthenticateUser, + private val updateUserUseCase: UpdateUser, +) : ViewModel() { + + private val _editPasswordUiState = + MutableStateFlow(EditPasswordUiState.Loading) + val editPasswordUiState: StateFlow = _editPasswordUiState + + fun updatePassword( + currentPassword: String?, + newPassword: String?, + newPasswordRepeat: String?, + ) { + _editPasswordUiState.value = EditPasswordUiState.Loading + if (isNotEmpty(currentPassword) && isNotEmpty(newPassword) && + isNotEmpty(newPasswordRepeat) + ) { + when { + currentPassword == newPassword -> { + _editPasswordUiState.value = + EditPasswordUiState.Error(Constants.ERROR_PASSWORDS_CANT_BE_SAME) + } + + newPassword?.let { + newPasswordRepeat?.let { it1 -> + isNewPasswordValid( + it, + it1, + ) + } + } == true -> { + if (currentPassword != null) { + updatePassword(currentPassword, newPassword) + } + } + + else -> { + _editPasswordUiState.value = + EditPasswordUiState.Error(Constants.ERROR_VALIDATING_PASSWORD) + } + } + } else { + _editPasswordUiState.value = + EditPasswordUiState.Error(Constants.ERROR_FIELDS_CANNOT_BE_EMPTY) + } + } + + private fun isNotEmpty(str: String?): Boolean { + return !str.isNullOrEmpty() + } + + private fun isNewPasswordValid(newPassword: String, newPasswordRepeat: String): Boolean { + return newPassword == newPasswordRepeat + } + + private fun updatePassword(currentPassword: String, newPassword: String) { + // authenticate and then update + mUseCaseHandler.execute( + authenticateUserUseCase, + AuthenticateUser.RequestValues( + mPreferencesHelper.username, + currentPassword, + ), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: AuthenticateUser.ResponseValue) { + mUseCaseHandler.execute( + updateUserUseCase, + UpdateUser.RequestValues( + UpdateUserEntityPassword( + newPassword, + ), + mPreferencesHelper.userId.toInt(), + ), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: UpdateUser.ResponseValue?) { + _editPasswordUiState.value = EditPasswordUiState.Success + } + + override fun onError(message: String) { + _editPasswordUiState.value = EditPasswordUiState.Error(message) + } + }, + ) + } + + override fun onError(message: String) { + _editPasswordUiState.value = EditPasswordUiState.Error("Wrong Password") + } + }, + ) + } +} + +sealed interface EditPasswordUiState { + data object Loading : EditPasswordUiState + data object Success : EditPasswordUiState + data class Error(val message: String) : EditPasswordUiState +} diff --git a/feature/faq/build.gradle.kts b/feature/faq/build.gradle.kts index d152c1e63..053183d86 100644 --- a/feature/faq/build.gradle.kts +++ b/feature/faq/build.gradle.kts @@ -8,22 +8,12 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { namespace = "org.mifospay.feature.faq" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - } - } -} \ No newline at end of file +dependencies { } \ No newline at end of file diff --git a/feature/faq/src/commonMain/composeResources/values/strings.xml b/feature/faq/src/commonMain/composeResources/values/strings.xml index 34625452c..99ed8f863 100644 --- a/feature/faq/src/commonMain/composeResources/values/strings.xml +++ b/feature/faq/src/commonMain/composeResources/values/strings.xml @@ -17,5 +17,5 @@ Navigate to Payments section. You will find your payment history under the History tab. To make a transfer, navigate to Payments section.Under the Send tab, from there you can choose the type of transfer, add the amount and submit. Navigate to Finance section. Click on Add Card under the Cards tab. - FAQ's + FAQ\'s \ No newline at end of file diff --git a/feature/faq/src/main/kotlin/org/mifospay/feature/faq/FAQViewModel.kt b/feature/faq/src/main/kotlin/org/mifospay/feature/faq/FAQViewModel.kt new file mode 100644 index 000000000..1a5376313 --- /dev/null +++ b/feature/faq/src/main/kotlin/org/mifospay/feature/faq/FAQViewModel.kt @@ -0,0 +1,34 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.faq + +import androidx.lifecycle.ViewModel + +internal class FAQViewModel : ViewModel() { + + /** + * Retrieves a list of Frequently Asked Questions (FAQs). + * + * Currently, the FAQs are statically defined within this function. This is a temporary + * implementation for demonstration or testing purposes. In the future, this method will + * be updated to fetch the FAQ data from a backend service once the backend functionality + * is implemented. This will allow for dynamic and up-to-date FAQ content. + * + * @return A list of [FAQ] objects containing the questions and answers. + */ + fun getFAQ(): List { + return listOf( + FAQ(R.string.feature_faq_question1, R.string.feature_faq_answer1), + FAQ(R.string.feature_faq_question2, R.string.feature_faq_answer2), + FAQ(R.string.feature_faq_question3, R.string.feature_faq_answer3), + FAQ(R.string.feature_faq_question4, R.string.feature_faq_answer4), + ) + } +} diff --git a/feature/faq/src/main/kotlin/org/mifospay/feature/faq/FaqScreen.kt b/feature/faq/src/main/kotlin/org/mifospay/feature/faq/FaqScreen.kt new file mode 100644 index 000000000..d7b7a5a97 --- /dev/null +++ b/feature/faq/src/main/kotlin/org/mifospay/feature/faq/FaqScreen.kt @@ -0,0 +1,79 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.faq + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MifosTopBar +import org.mifospay.core.ui.FaqItemScreen + +@Composable +internal fun FaqScreenRoute( + navigateBack: () -> Unit, + modifier: Modifier = Modifier, + faqViewModel: FAQViewModel = koinViewModel(), +) { + FaqScreen( + modifier = modifier, + navigateBack = navigateBack, + faqList = faqViewModel.getFAQ(), + ) +} + +@Composable +private fun FaqScreen( + navigateBack: () -> Unit, + faqList: List, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize(), + ) { + MifosTopBar( + topBarTitle = R.string.feature_faq, + backPress = { navigateBack.invoke() }, + ) + LazyColumn( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + ) { + itemsIndexed(items = faqList) { _, faqItem -> + FaqItemScreen( + question = stringResource(id = faqItem.question), + answer = faqItem.answer?.let { stringResource(id = it) }, + ) + } + } + } +} + +@Preview(showSystemUi = true) +@Composable +private fun FaqScreenPreview() { + FaqScreen( + {}, + listOf( + FAQ(R.string.feature_faq_question1, R.string.feature_faq_answer1), + FAQ(R.string.feature_faq_question2, R.string.feature_faq_answer2), + FAQ(R.string.feature_faq_question3, R.string.feature_faq_answer3), + FAQ(R.string.feature_faq_question4, R.string.feature_faq_answer4), + ), + ) +} diff --git a/feature/history/build.gradle.kts b/feature/history/build.gradle.kts index 8c814abc7..77b20cc0b 100644 --- a/feature/history/build.gradle.kts +++ b/feature/history/build.gradle.kts @@ -8,22 +8,15 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { namespace = "org.mifospay.feature.history" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - } - } +dependencies { + + implementation(projects.libs.pullrefresh) } \ No newline at end of file diff --git a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/navigation/HistoryNavigation.kt b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/navigation/HistoryNavigation.kt index a6e79165c..4e0381e22 100644 --- a/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/navigation/HistoryNavigation.kt +++ b/feature/history/src/commonMain/kotlin/org/mifospay/feature/history/navigation/HistoryNavigation.kt @@ -7,26 +7,22 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.feature.history.navigation +package org.mifospay.feature.profile.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable -import org.mifospay.feature.history.HistoryScreen +import org.mifospay.feature.profile.ProfileRoute -const val HISTORY_ROUTE = "history_route" +const val PROFILE_ROUTE = "profile_route" -fun NavGraphBuilder.historyNavigation( - viewTransactionDetail: (Long) -> Unit, +fun NavController.navigateToProfile(navOptions: NavOptions) = navigate(PROFILE_ROUTE, navOptions) + +fun NavGraphBuilder.profileScreen( + onEditProfile: () -> Unit, ) { - composable(HISTORY_ROUTE) { - HistoryScreen( - viewTransferDetail = viewTransactionDetail, - ) + composable(route = PROFILE_ROUTE) { + ProfileRoute(onLinkAccount = onEditProfile) } } - -fun NavController.navigateToHistory(navOptions: NavOptions? = null) { - navigate(HISTORY_ROUTE, navOptions) -} diff --git a/feature/history/src/main/kotlin/org/mifospay/feature/history/HistoryScreen.kt b/feature/history/src/main/kotlin/org/mifospay/feature/history/HistoryScreen.kt new file mode 100644 index 000000000..1e6becd02 --- /dev/null +++ b/feature/history/src/main/kotlin/org/mifospay/feature/history/HistoryScreen.kt @@ -0,0 +1,239 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.history + +import android.widget.Toast +import androidx.compose.foundation.border +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +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.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifospay.core.model.domain.Currency +import com.mifospay.core.model.domain.Transaction +import com.mifospay.core.model.domain.TransactionType +import com.mifospay.core.model.entity.accounts.savings.TransferDetail +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MifosBottomSheet +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosLoadingWheel +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.ui.EmptyContentScreen +import org.mifospay.core.ui.TransactionItemScreen +import org.mifospay.feature.transaction.detail.TransactionDetailScreen + +@Composable +fun HistoryScreen( + viewReceipt: (String) -> Unit, + accountClicked: (String, ArrayList) -> Unit, + modifier: Modifier = Modifier, + viewModel: HistoryViewModel = koinViewModel(), +) { + val historyUiState by viewModel.historyUiState.collectAsStateWithLifecycle() + + HistoryScreen( + historyUiState = historyUiState, + viewReceipt = viewReceipt, + accountClicked = accountClicked, + modifier = modifier, + ) +} + +@Composable +private fun HistoryScreen( + historyUiState: HistoryUiState, + viewReceipt: (String) -> Unit, + accountClicked: (String, ArrayList) -> Unit, + modifier: Modifier = Modifier, +) { + var selectedChip by remember { mutableStateOf(TransactionType.OTHER) } + var filteredTransactions by remember { mutableStateOf(emptyList()) } + var transactionsList by remember { mutableStateOf(emptyList()) } + var transactionDetailState by remember { mutableStateOf(null) } + + when (historyUiState) { + HistoryUiState.Empty -> { + EmptyContentScreen( + title = stringResource(id = R.string.feature_history_error_oops), + subTitle = stringResource(id = R.string.feature_history_empty_no_transaction_history_title), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.primary, + iconImageVector = MifosIcons.Info, + ) + } + + is HistoryUiState.Error -> { + EmptyContentScreen( + title = stringResource(id = R.string.feature_history_error_oops), + subTitle = stringResource(id = R.string.feature_history_unexpected_error_subtitle), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.primary, + iconImageVector = MifosIcons.Info, + ) + } + + is HistoryUiState.HistoryList -> { + LaunchedEffect(selectedChip) { + transactionsList = historyUiState.list + filteredTransactions = when (selectedChip) { + TransactionType.OTHER -> historyUiState.list + else -> historyUiState.list.filter { it.transactionType == selectedChip } + } + } + Column(modifier = Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Chip( + selected = selectedChip == TransactionType.OTHER, + onClick = { selectedChip = TransactionType.OTHER }, + label = stringResource(R.string.feature_history_all), + ) + Chip( + selected = selectedChip == TransactionType.CREDIT, + onClick = { selectedChip = TransactionType.CREDIT }, + label = stringResource(R.string.feature_history_credits), + ) + Chip( + selected = selectedChip == TransactionType.DEBIT, + onClick = { selectedChip = TransactionType.DEBIT }, + label = stringResource(R.string.feature_history_debits), + ) + } + Spacer(modifier = Modifier.height(12.dp)) + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(filteredTransactions) { + Column { + TransactionItemScreen( + transaction = it, + modifier = Modifier.clickable { transactionDetailState = it }, + ) + } + } + } + } + } + + HistoryUiState.Loading -> { + MifosLoadingWheel( + modifier = Modifier.fillMaxWidth(), + contentDesc = stringResource(R.string.feature_history_loading), + ) + } + } + + if (transactionDetailState != null) { + MifosBottomSheet( + modifier = modifier, + content = { + TransactionDetailScreen( + transaction = transactionDetailState!!, + viewReceipt = { transactionDetailState?.transactionId?.let { viewReceipt(it) } }, + accountClicked = { accountClicked(it, ArrayList(transactionsList)) }, + ) + }, + onDismiss = { transactionDetailState = null }, + ) + } +} + +@Composable +private fun Chip( + selected: Boolean, + onClick: () -> Unit, + label: String, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val backgroundColor = MaterialTheme.colorScheme.onPrimaryContainer + MifosButton( + modifier = modifier.then( + if (selected) { + Modifier.border( + width = 1.dp, + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(25.dp), + ) + } else { + Modifier + }, + ), + onClick = { + onClick() + Toast.makeText(context, label, Toast.LENGTH_SHORT).show() + }, + color = backgroundColor, + ) { + Text( + modifier = Modifier.padding(top = 4.dp, bottom = 4.dp, start = 16.dp, end = 16.dp), + text = label, + color = MaterialTheme.colorScheme.onSurface, + ) + } +} + +internal class HistoryPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + HistoryUiState.Empty, + HistoryUiState.Loading, + HistoryUiState.Error("Error Screen"), + HistoryUiState.HistoryList(sampleHistoryList), + ) +} + +internal val sampleHistoryList = List(10) { index -> + Transaction( + transactionId = "txn_123456789", + clientId = 1001L, + accountId = index.toLong(), + amount = 1500.0, + date = "2024-03-23", + currency = Currency(), + transactionType = TransactionType.CREDIT, + transferId = 3003L, + transferDetail = TransferDetail(), + receiptId = "receipt_123456789", + ) +} + +@Preview(showBackground = true) +@Composable +private fun HistoryScreenPreview( + @PreviewParameter(HistoryPreviewProvider::class) historyUiState: HistoryUiState, +) { + HistoryScreen(historyUiState = historyUiState, viewReceipt = {}, accountClicked = { _, _ -> }) +} diff --git a/feature/history/src/main/kotlin/org/mifospay/feature/history/HistoryViewModel.kt b/feature/history/src/main/kotlin/org/mifospay/feature/history/HistoryViewModel.kt new file mode 100644 index 000000000..16bc6e090 --- /dev/null +++ b/feature/history/src/main/kotlin/org/mifospay/feature/history/HistoryViewModel.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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.history + +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.domain.Transaction +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.account.FetchAccount +import org.mifospay.core.data.domain.usecase.account.FetchAccountTransactions +import org.mifospay.core.data.repository.local.LocalRepository + +class HistoryViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val mLocalRepository: LocalRepository, + private val mFetchAccountUseCase: FetchAccount, + private val fetchAccountTransactionsUseCase: FetchAccountTransactions, +) : ViewModel() { + + private val _historyUiState = MutableStateFlow(HistoryUiState.Loading) + val historyUiState: StateFlow = _historyUiState + + private fun fetchTransactions() { + _historyUiState.value = HistoryUiState.Loading + mUseCaseHandler.execute( + mFetchAccountUseCase, + FetchAccount.RequestValues(mLocalRepository.clientDetails.clientId), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchAccount.ResponseValue) { + response.account.id.let { + fetchTransactionsHistory(it) + } + } + + override fun onError(message: String) { + _historyUiState.value = HistoryUiState.Error(message) + } + }, + ) + } + + fun fetchTransactionsHistory(accountId: Long) { + mUseCaseHandler.execute( + fetchAccountTransactionsUseCase, + FetchAccountTransactions.RequestValues(accountId), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchAccountTransactions.ResponseValue?) { + if (response?.transactions?.isNotEmpty() == true) { + _historyUiState.value = HistoryUiState.HistoryList(response.transactions) + } else { + _historyUiState.value = HistoryUiState.Empty + } + } + + override fun onError(message: String) { + _historyUiState.value = HistoryUiState.Error(message) + } + }, + ) + } + + init { + fetchTransactions() + } +} + +sealed class HistoryUiState { + data object Loading : HistoryUiState() + data object Empty : HistoryUiState() + data class Error(val message: String) : HistoryUiState() + data class HistoryList(val list: List) : HistoryUiState() +} diff --git a/feature/history/src/main/kotlin/org/mifospay/feature/specific/transactions/SpecificTransactionsScreen.kt b/feature/history/src/main/kotlin/org/mifospay/feature/specific/transactions/SpecificTransactionsScreen.kt new file mode 100644 index 000000000..83cc4297b --- /dev/null +++ b/feature/history/src/main/kotlin/org/mifospay/feature/specific/transactions/SpecificTransactionsScreen.kt @@ -0,0 +1,265 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.specific.transactions + +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +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.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifospay.core.model.domain.Transaction +import com.mifospay.core.model.domain.TransactionType +import com.mifospay.core.model.domain.client.Client +import com.mifospay.core.model.entity.accounts.savings.SavingAccount +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.core.designsystem.theme.creditTextColor +import org.mifospay.core.designsystem.theme.debitTextColor +import org.mifospay.core.designsystem.theme.otherTextColor +import org.mifospay.core.ui.EmptyContentScreen +import org.mifospay.core.ui.ErrorScreenContent +import org.mifospay.feature.history.R + +@Composable +internal fun SpecificTransactionsScreen( + backPress: () -> Unit, + transactionItemClicked: (String) -> Unit, + modifier: Modifier = Modifier, + viewModel: SpecificTransactionsViewModel = koinViewModel(), +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + SpecificTransactionsScreen( + uiState = uiState, + backPress = backPress, + transactionItemClicked = transactionItemClicked, + modifier = modifier, + ) +} + +@Composable +@VisibleForTesting +internal fun SpecificTransactionsScreen( + uiState: SpecificTransactionsUiState, + backPress: () -> Unit, + transactionItemClicked: (String) -> Unit, + modifier: Modifier = Modifier, +) { + MifosScaffold( + modifier = modifier, + topBarTitle = R.string.feature_history_specific_transactions_history, + backPress = backPress, + scaffoldContent = { paddingValues -> + Box(modifier = Modifier.padding(paddingValues)) { + when (uiState) { + SpecificTransactionsUiState.Error -> { + ErrorScreenContent( + modifier = Modifier, + title = stringResource(id = R.string.feature_history_error_oops), + subTitle = stringResource(id = R.string.feature_history_unexpected_error_subtitle), + ) + } + + SpecificTransactionsUiState.Loading -> { + MfLoadingWheel( + contentDesc = stringResource(R.string.feature_history_loading), + backgroundColor = MaterialTheme.colorScheme.surface, + ) + } + + is SpecificTransactionsUiState.Success -> { + if (uiState.transactionsList.isEmpty()) { + EmptyContentScreen( + title = stringResource(id = R.string.feature_history_error_oops), + subTitle = stringResource(id = R.string.feature_history_no_transactions_found), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.onSurface, + iconImageVector = MifosIcons.Info, + ) + } else { + SpecificTransactionsContent( + transactionList = uiState.transactionsList, + transactionItemClicked = transactionItemClicked, + ) + } + } + } + } + }, + ) +} + +@Composable +private fun SpecificTransactionsContent( + transactionList: List, + transactionItemClicked: (String) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn(modifier = modifier) { + itemsIndexed( + items = transactionList, + ) { index, transaction -> + SpecificTransactionItem( + transaction = transaction, + modifier = Modifier + .padding(12.dp) + .clickable { + transaction.transactionId?.let { transactionItemClicked(it) } + }, + ) + Spacer(modifier = Modifier.height(4.dp)) + if (index != transactionList.lastIndex) { + HorizontalDivider() + } + Spacer(modifier = Modifier.height(4.dp)) + } + } +} + +@Composable +private fun SpecificTransactionItem( + transaction: Transaction, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.padding(horizontal = 12.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + SpecificTransactionAccountInfo( + account = transaction.transferDetail.fromAccount, + client = transaction.transferDetail.fromClient, + modifier = Modifier.weight(1f), + ) + Icon(imageVector = MifosIcons.SendRightTilted, contentDescription = null) + SpecificTransactionAccountInfo( + account = transaction.transferDetail.toAccount, + client = transaction.transferDetail.toClient, + modifier = Modifier.weight(1f), + ) + } + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.padding(horizontal = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column { + Text( + text = stringResource(id = R.string.feature_history_transaction_id) + transaction.transactionId, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.primary, + ) + Text( + text = stringResource(id = R.string.feature_history_transaction_date) + transaction.date, + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = when (transaction.transactionType) { + TransactionType.DEBIT -> stringResource(id = R.string.feature_history_debits) + TransactionType.CREDIT -> stringResource(id = R.string.feature_history_credits) + TransactionType.OTHER -> stringResource(id = R.string.feature_history_other) + }, + style = MaterialTheme.typography.bodyLarge, + ) + } + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "${transaction.currency.code}${transaction.amount}", + style = MaterialTheme.typography.displaySmall, + color = when (transaction.transactionType) { + TransactionType.DEBIT -> debitTextColor + TransactionType.CREDIT -> creditTextColor + TransactionType.OTHER -> otherTextColor + }, + ) + } + } +} + +@Composable +internal fun SpecificTransactionAccountInfo( + account: SavingAccount, + client: Client, + modifier: Modifier = Modifier, + accountClicked: (String) -> Unit = {}, +) { + Column( + modifier = modifier.clickable { + accountClicked(account.accountNo) + }, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon(imageVector = MifosIcons.AccountCircle, contentDescription = null) + Text( + text = client.displayName, + style = MaterialTheme.typography.titleSmall, + ) + Text( + text = account.accountNo, + style = MaterialTheme.typography.bodyMedium, + ) + } +} + +internal class SpecificTransactionsUiStateProvider : + PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + SpecificTransactionsUiState.Success(arrayListOf()), + SpecificTransactionsUiState.Success(arrayListOf(Transaction(), Transaction())), + SpecificTransactionsUiState.Error, + SpecificTransactionsUiState.Loading, + ) +} + +@Preview(showSystemUi = true) +@Composable +private fun ShowQrScreenPreview( + @PreviewParameter(SpecificTransactionsUiStateProvider::class) + uiState: SpecificTransactionsUiState, +) { + MifosTheme { + SpecificTransactionsScreen( + uiState = uiState, + backPress = {}, + transactionItemClicked = {}, + ) + } +} + +@Preview +@Composable +private fun SpecificTransactionItemPreview() { + Surface { + SpecificTransactionItem(transaction = Transaction()) + } +} diff --git a/feature/history/src/main/kotlin/org/mifospay/feature/specific/transactions/SpecificTransactionsViewModel.kt b/feature/history/src/main/kotlin/org/mifospay/feature/specific/transactions/SpecificTransactionsViewModel.kt new file mode 100644 index 000000000..ad53eef1d --- /dev/null +++ b/feature/history/src/main/kotlin/org/mifospay/feature/specific/transactions/SpecificTransactionsViewModel.kt @@ -0,0 +1,103 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.specific.transactions + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.domain.Transaction +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.mifospay.core.data.base.TaskLooper +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseFactory +import org.mifospay.core.data.domain.usecase.account.FetchAccountTransfer +import org.mifospay.core.data.util.Constants +import org.mifospay.feature.specific.transactions.SpecificTransactionsUiState.Loading +import org.mifospay.feature.specific.transactions.navigation.ACCOUNT_NUMBER_ARG +import org.mifospay.feature.specific.transactions.navigation.TRANSACTIONS_ARG + +class SpecificTransactionsViewModel( + private val mUseCaseFactory: UseCaseFactory, + private var mTaskLooper: TaskLooper? = null, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + + private val _uiState: MutableStateFlow = MutableStateFlow(Loading) + val uiState = _uiState.asStateFlow() + + init { + savedStateHandle.get(ACCOUNT_NUMBER_ARG)?.let { accountNumber -> + savedStateHandle.get>(TRANSACTIONS_ARG)?.let { transactions -> + getSpecificTransactions(accountNumber, transactions) + } + } + } + + private fun getSpecificTransactions( + accountNumber: String, + transactions: ArrayList, + ): ArrayList { + val specificTransactions = ArrayList() + if (transactions.size > 0) { + for (i in transactions.indices) { + val transaction = transactions[i] + val transferId = transaction.transferId + mTaskLooper?.addTask( + useCase = mUseCaseFactory.getUseCase(Constants.FETCH_ACCOUNT_TRANSFER_USECASE) + as UseCase, + values = FetchAccountTransfer.RequestValues(transferId), + taskData = TaskLooper.TaskData( + org.mifospay.common.Constants.TRANSFER_DETAILS, + i, + ), + ) + } + mTaskLooper!!.listen( + object : TaskLooper.Listener { + override fun onTaskSuccess( + taskData: TaskLooper.TaskData, + response: R, + ) { + when (taskData.taskName) { + org.mifospay.common.Constants.TRANSFER_DETAILS -> { + val responseValue = response as FetchAccountTransfer.ResponseValue + val index = taskData.taskId + transactions[index].transferDetail = responseValue.transferDetail + } + } + } + + override fun onComplete() { + for (transaction in transactions) { + if ( + transaction.transferDetail.fromAccount.accountNo == accountNumber || + transaction.transferDetail.toAccount.accountNo == accountNumber + ) { + specificTransactions.add(transaction) + } + } + _uiState.value = SpecificTransactionsUiState.Success(specificTransactions) + } + + override fun onFailure(message: String?) { + _uiState.value = SpecificTransactionsUiState.Error + } + }, + ) + } + return specificTransactions + } +} + +sealed class SpecificTransactionsUiState { + data object Loading : SpecificTransactionsUiState() + data object Error : SpecificTransactionsUiState() + data class Success(val transactionsList: ArrayList) : SpecificTransactionsUiState() +} diff --git a/feature/history/src/main/kotlin/org/mifospay/feature/transaction/detail/TransactionDetailScreen.kt b/feature/history/src/main/kotlin/org/mifospay/feature/transaction/detail/TransactionDetailScreen.kt new file mode 100644 index 000000000..d57010eb1 --- /dev/null +++ b/feature/history/src/main/kotlin/org/mifospay/feature/transaction/detail/TransactionDetailScreen.kt @@ -0,0 +1,208 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.transaction.detail + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifospay.core.model.domain.Transaction +import com.mifospay.core.model.domain.TransactionType +import com.mifospay.core.model.entity.accounts.savings.TransferDetail +import org.koin.androidx.compose.koinViewModel +import org.mifospay.common.Constants +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.core.ui.ErrorScreenContent +import org.mifospay.feature.history.R +import org.mifospay.feature.specific.transactions.SpecificTransactionAccountInfo + +@Composable +internal fun TransactionDetailScreen( + transaction: Transaction, + viewReceipt: () -> Unit, + accountClicked: (String) -> Unit, + modifier: Modifier = Modifier, + viewModel: TransactionDetailViewModel = koinViewModel(), +) { + val uiState = viewModel.transactionDetailUiState.collectAsStateWithLifecycle() + + LaunchedEffect(key1 = transaction) { + viewModel.getTransferDetail(transaction.transferId) + } + + TransactionDetailScreen( + uiState = uiState.value, + transaction = transaction, + viewReceipt = viewReceipt, + accountClicked = accountClicked, + modifier = modifier, + ) +} + +@Composable +private fun TransactionDetailScreen( + uiState: TransactionDetailUiState, + transaction: Transaction, + viewReceipt: () -> Unit, + accountClicked: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .padding(20.dp) + .height(300.dp), + ) { + when (uiState) { + is TransactionDetailUiState.Error -> { + ErrorScreenContent() + } + + is TransactionDetailUiState.Loading -> { + MfLoadingWheel( + backgroundColor = Color.Black.copy(alpha = 0.6f), + ) + } + + is TransactionDetailUiState.Success -> { + TransactionsDetailContent( + transaction = transaction, + viewReceipt = viewReceipt, + accountClicked = accountClicked, + ) + } + } + } +} + +@Composable +private fun TransactionsDetailContent( + transaction: Transaction, + viewReceipt: () -> Unit, + accountClicked: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.padding(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + modifier = Modifier + .padding(horizontal = 12.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column { + Text( + text = stringResource(id = R.string.feature_history_transaction_id) + transaction.transactionId, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.primary, + ) + Text( + text = stringResource(id = R.string.feature_history_transaction_date) + transaction.date, + style = MaterialTheme.typography.bodyLarge, + ) + if (transaction.receiptId != null) { + Text( + text = stringResource(id = R.string.feature_history_pan_id) + transaction.receiptId, + style = MaterialTheme.typography.bodyLarge, + ) + } + Text( + text = when (transaction.transactionType) { + TransactionType.DEBIT -> Constants.DEBIT + TransactionType.CREDIT -> Constants.CREDIT + TransactionType.OTHER -> Constants.OTHER + }, + style = MaterialTheme.typography.bodyLarge, + ) + } + } + + HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + SpecificTransactionAccountInfo( + modifier = Modifier.weight(1f), + account = transaction.transferDetail.fromAccount, + client = transaction.transferDetail.fromClient, + accountClicked = accountClicked, + ) + Image( + painter = painterResource(id = R.drawable.feature_history_ic_send), + contentDescription = null, + ) + SpecificTransactionAccountInfo( + modifier = Modifier.weight(1f), + account = transaction.transferDetail.toAccount, + client = transaction.transferDetail.toClient, + accountClicked = accountClicked, + ) + } + + HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp)) + + Text( + text = stringResource(id = R.string.feature_history_view_Receipt), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.primary, + textAlign = TextAlign.Center, + modifier = Modifier.clickable { viewReceipt() }, + ) + } +} + +class TransactionDetailUiStateProvider : + PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + TransactionDetailUiState.Success(TransferDetail()), + TransactionDetailUiState.Error, + TransactionDetailUiState.Loading, + ) +} + +@Preview(showSystemUi = true) +@Composable +private fun ShowQrScreenPreview( + @PreviewParameter(TransactionDetailUiStateProvider::class) + uiState: TransactionDetailUiState, +) { + MifosTheme { + TransactionDetailScreen( + uiState = uiState, + transaction = Transaction(), + viewReceipt = {}, + accountClicked = {}, + ) + } +} diff --git a/feature/history/src/main/kotlin/org/mifospay/feature/transaction/detail/TransactionDetailViewModel.kt b/feature/history/src/main/kotlin/org/mifospay/feature/transaction/detail/TransactionDetailViewModel.kt new file mode 100644 index 000000000..5543b2228 --- /dev/null +++ b/feature/history/src/main/kotlin/org/mifospay/feature/transaction/detail/TransactionDetailViewModel.kt @@ -0,0 +1,50 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.transaction.detail + +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.entity.accounts.savings.TransferDetail +import kotlinx.coroutines.flow.MutableStateFlow +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.account.FetchAccountTransfer + +class TransactionDetailViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val mFetchAccountTransferUseCase: FetchAccountTransfer, +) : ViewModel() { + + private val _transactionDetailUiState: MutableStateFlow = + MutableStateFlow(TransactionDetailUiState.Loading) + val transactionDetailUiState get() = _transactionDetailUiState + + fun getTransferDetail(transferId: Long) { + mUseCaseHandler.execute( + mFetchAccountTransferUseCase, + FetchAccountTransfer.RequestValues(transferId), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchAccountTransfer.ResponseValue?) { + _transactionDetailUiState.value = + TransactionDetailUiState.Success(response?.transferDetail) + } + + override fun onError(message: String) { + _transactionDetailUiState.value = TransactionDetailUiState.Error + } + }, + ) + } +} + +sealed class TransactionDetailUiState { + data object Loading : TransactionDetailUiState() + data object Error : TransactionDetailUiState() + data class Success(val transferDetail: TransferDetail?) : TransactionDetailUiState() +} diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts index 6f819bc4b..0fe68be92 100644 --- a/feature/home/build.gradle.kts +++ b/feature/home/build.gradle.kts @@ -8,26 +8,14 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) - alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { namespace = "org.mifospay.feature.home" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) +dependencies { - implementation(libs.koin.compose.viewmodel) - implementation(libs.koin.compose) - } - } } \ No newline at end of file diff --git a/feature/home/src/main/kotlin/org/mifospay/feature/home/HomeScreen.kt b/feature/home/src/main/kotlin/org/mifospay/feature/home/HomeScreen.kt new file mode 100644 index 000000000..8145b8b69 --- /dev/null +++ b/feature/home/src/main/kotlin/org/mifospay/feature/home/HomeScreen.kt @@ -0,0 +1,453 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowOutward +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.MoreHoriz +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +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.lifecycle.compose.collectAsStateWithLifecycle +import com.mifospay.core.model.domain.Account +import com.mifospay.core.model.domain.Currency +import com.mifospay.core.model.domain.Transaction +import com.mifospay.core.model.domain.TransactionType +import org.koin.androidx.compose.koinViewModel +import org.mifospay.common.Utils +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.theme.NewUi +import org.mifospay.core.ui.ErrorScreenContent +import org.mifospay.core.ui.TransactionItemScreen + +@Composable +internal fun HomeRoute( + onRequest: (String) -> Unit, + onPay: () -> Unit, + modifier: Modifier = Modifier, + homeViewModel: HomeViewModel = koinViewModel(), +) { + val homeUIState by homeViewModel + .homeUIState + .collectAsStateWithLifecycle() + + when (homeUIState) { + is HomeUiState.Loading -> { + MfLoadingWheel( + contentDesc = stringResource(R.string.feature_home_loading), + backgroundColor = MaterialTheme.colorScheme.surface, + ) + } + + is HomeUiState.Success -> { + val successState = homeUIState as HomeUiState.Success + HomeScreen( + successState.account, + successState.transactions, + onRequest = { + onRequest.invoke(successState.vpa ?: "") + }, + onPay = onPay, + modifier = modifier, + ) + } + + is HomeUiState.Error -> { + ErrorScreenContent( + onClickRetry = { + homeViewModel.fetchAccountDetails() + }, + ) + } + } +} + +@Composable +private fun HomeScreen( + account: Account?, + transactions: List, + onRequest: () -> Unit, + onPay: () -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface), + contentPadding = PaddingValues(horizontal = 20.dp), + ) { + item { + MifosWalletCard( + modifier = Modifier.padding(top = 20.dp), + account = account, + ) + } + + item { + PayRequestScreen( + modifier = Modifier.padding(vertical = 20.dp), + onRequest = onRequest, + onSend = onPay, + ) + } + + item { + MifosSendMoneyFreeCard() + } + + if (transactions.isNotEmpty()) { + item { + TransactionHistoryCard( + modifier = Modifier.padding(vertical = 20.dp), + transactions = transactions, + ) + } + } + } +} + +@Composable +private fun MifosWalletCard( + modifier: Modifier = Modifier, + account: Account? = null, +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(200.dp) + .background( + brush = Brush.linearGradient( + colors = listOf( + NewUi.walletColor1, + NewUi.walletColor2, + ), + ), + shape = RoundedCornerShape(16.dp), + ), + ) { + Card( + modifier = Modifier + .fillMaxSize(), + colors = CardDefaults.cardColors( + containerColor = Color.Transparent, + ), + ) { + Column( + modifier = Modifier.fillMaxSize(), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column(modifier = Modifier.padding(20.dp)) { + Text( + text = "Client Name", + fontWeight = FontWeight(300), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.surface, + ) + + Text( + text = account?.name ?: "Username", + fontWeight = FontWeight(400), + color = MaterialTheme.colorScheme.surface, + ) + } + + IconButton(onClick = { }, modifier = Modifier.padding(end = 12.dp)) { + Icon( + imageVector = Icons.Filled.MoreHoriz, + contentDescription = "more", + tint = MaterialTheme.colorScheme.surface, + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + ) { + Column { + Text( + text = "Wallet Balance", + fontWeight = FontWeight(300), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.surface, + ) + + val accountBalance = + if (account != null) { + Utils.getNewCurrencyFormatter( + account.balance, + account.currency.displaySymbol, + ) + } else { + "0" + } + Text( + text = accountBalance, + color = MaterialTheme.colorScheme.surface, + style = MaterialTheme.typography.headlineLarge, + ) + } + + Icon( + modifier = Modifier + .graphicsLayer(rotationZ = 90f) + .padding(4.dp), + imageVector = Icons.Filled.KeyboardArrowUp, + contentDescription = "arrow", + tint = MaterialTheme.colorScheme.surface, + ) + } + } + } + } +} + +@Composable +private fun PayRequestScreen( + onRequest: () -> Unit, + onSend: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + PaymentButton( + modifier = Modifier + .weight(1f) + .height(55.dp), + text = "Request", + onClick = onRequest, + leadingIcon = { + Icon( + modifier = Modifier + .size(26.dp) + .graphicsLayer(rotationZ = 180f), + imageVector = Icons.Filled.ArrowOutward, + contentDescription = "request money", + ) + }, + ) + + Spacer(modifier = Modifier.width(20.dp)) + + PaymentButton( + modifier = Modifier + .weight(1f) + .height(55.dp), + text = "Send", + onClick = onSend, + leadingIcon = { + Icon( + modifier = Modifier.size(26.dp), + imageVector = Icons.Filled.ArrowOutward, + contentDescription = "Send money", + ) + }, + ) + } +} + +@Composable +@Preview(showSystemUi = true) +fun MifosSendMoneyFreeCard(modifier: Modifier = Modifier) { + Card( + modifier = modifier, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier + .padding(start = 20.dp, end = 10.dp, top = 20.dp, bottom = 20.dp) + .weight(7.5f), + ) { + Text( + text = stringResource(id = R.string.start_sending_your_money_tax_free), + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight(500), + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = "Mifos Pay is the best place for users to receive and send money. Start saving money now!", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight(300), + modifier = Modifier.padding(top = 15.dp), + ) + } + + Image( + modifier = Modifier.weight(2.5f), + contentScale = ContentScale.Fit, + painter = painterResource(id = R.drawable.coin_image), + contentDescription = "coin Image", + ) + } + } +} + +@Composable +fun TransactionHistoryCard(transactions: List, modifier: Modifier = Modifier) { + Card( + modifier = modifier + .fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + ) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .padding(top = 20.dp, bottom = 10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "Transaction History", + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight(500), + ) + + Box( + modifier = Modifier.clickable( + onClick = { }, + ), + ) { + Text( + text = "See All", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight(300), + ) + } + } + transactions.forEachIndexed { _, transaction -> + TransactionItemScreen(transaction = transaction) + } + } + } +} + +@Composable +private fun PaymentButton( + text: String, + leadingIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Button( + modifier = modifier, + onClick = onClick, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.onPrimaryContainer, + contentColor = MaterialTheme.colorScheme.onSurface, + ), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + leadingIcon() + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = text, + fontWeight = FontWeight(400), + ) + } + } +} + +@Preview(showSystemUi = true, device = "id:pixel_5") +@Composable +private fun HomeScreenPreview() { + HomeScreen( + account = Account( + image = "", + name = "Mifos", + number = "1234567890", + balance = 10000.0, + id = 1L, + currency = Currency( + code = "USD", + displayLabel = "$", + displaySymbol = "$", + ), + productId = 1223, + ), + transactions = List(25) { index -> + Transaction( + transactionId = index.toString(), + amount = 23004.0, + currency = Currency( + code = "USD", + displayLabel = "$", + displaySymbol = "$", + ), + transactionType = TransactionType.CREDIT, + ) + }, + onPay = {}, + onRequest = {}, + ) +} + +@Preview +@Composable +private fun PayRequestScreenPreview() { + PayRequestScreen({}, {}) +} diff --git a/feature/home/src/main/kotlin/org/mifospay/feature/home/HomeViewModel.kt b/feature/home/src/main/kotlin/org/mifospay/feature/home/HomeViewModel.kt new file mode 100644 index 000000000..eb2d3832a --- /dev/null +++ b/feature/home/src/main/kotlin/org/mifospay/feature/home/HomeViewModel.kt @@ -0,0 +1,86 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.home + +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.domain.Account +import com.mifospay.core.model.domain.Transaction +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import org.mifospay.core.data.base.UseCase.UseCaseCallback +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.account.FetchAccount +import org.mifospay.core.data.domain.usecase.history.HistoryContract +import org.mifospay.core.data.domain.usecase.history.TransactionsHistory +import org.mifospay.core.data.repository.local.LocalRepository +import org.mifospay.core.datastore.PreferencesHelper + +class HomeViewModel( + private val useCaseHandler: UseCaseHandler, + private val localRepository: LocalRepository, + private val preferencesHelper: PreferencesHelper, + private val fetchAccountUseCase: FetchAccount, + private val transactionsHistory: TransactionsHistory, +) : ViewModel(), HistoryContract.TransactionsHistoryAsync { + + // Expose screen UI state + private val _homeUIState: MutableStateFlow = MutableStateFlow(HomeUiState.Loading) + val homeUIState: StateFlow = _homeUIState.asStateFlow() + + init { + transactionsHistory.delegate = this + fetchAccountDetails() + } + + fun fetchAccountDetails() { + useCaseHandler.execute( + fetchAccountUseCase, + FetchAccount.RequestValues(localRepository.clientDetails.clientId), + object : UseCaseCallback { + override fun onSuccess(response: FetchAccount.ResponseValue) { + preferencesHelper.accountId = response.account.id + _homeUIState.update { + HomeUiState.Success( + account = response.account, + vpa = localRepository.clientDetails.externalId, + ) + } + response.account.id.let { + transactionsHistory.fetchTransactionsHistory(it) + } + } + + override fun onError(message: String) { + _homeUIState.update { HomeUiState.Error } + } + }, + ) + } + + override fun onTransactionsFetchCompleted(transactions: List?) { + _homeUIState.update { currentState -> + (currentState as HomeUiState.Success) + .copy(transactions = transactions ?: emptyList()) + } + } +} + +sealed interface HomeUiState { + data object Loading : HomeUiState + data class Success( + val account: Account? = null, + val transactions: List = emptyList(), + val vpa: String? = null, + ) : HomeUiState + + data object Error : HomeUiState +} diff --git a/feature/invoices/build.gradle.kts b/feature/invoices/build.gradle.kts index c7270350a..94232da14 100644 --- a/feature/invoices/build.gradle.kts +++ b/feature/invoices/build.gradle.kts @@ -8,22 +8,12 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { - namespace = "org.mifospay.feature.invoices" + namespace = "org.mifospay.invoices" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - } - } -} \ No newline at end of file +dependencies {} \ No newline at end of file diff --git a/feature/invoices/src/commonMain/kotlin/org/mifospay/feature/invoices/navigation/InvoiceNavigation.kt b/feature/invoices/src/commonMain/kotlin/org/mifospay/feature/invoices/navigation/InvoiceNavigation.kt index 33b4973d6..d3d7cbd71 100644 --- a/feature/invoices/src/commonMain/kotlin/org/mifospay/feature/invoices/navigation/InvoiceNavigation.kt +++ b/feature/invoices/src/commonMain/kotlin/org/mifospay/feature/invoices/navigation/InvoiceNavigation.kt @@ -9,31 +9,32 @@ */ package org.mifospay.feature.invoices.navigation +import android.net.Uri import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType +import androidx.navigation.compose.composable import androidx.navigation.navArgument -import org.mifospay.core.ui.composableWithPushTransitions -import org.mifospay.feature.invoices.details.InvoiceDetailScreen +import org.mifospay.feature.invoices.InvoiceDetailScreen const val INVOICE_ROUTE = "invoice_route" -const val INVOICE_DATA_ARG = "invoiceId" +const val INVOICE_DATA_ARG = "invoiceData" -fun NavController.navigateToInvoiceDetail(invoiceId: Long) { - this.navigate("$INVOICE_ROUTE/$invoiceId") +fun NavController.navigateToInvoiceDetail(invoiceData: String) { + this.navigate("$INVOICE_ROUTE/${Uri.encode(invoiceData)}") } fun NavGraphBuilder.invoiceDetailScreen( - onNavigateBack: () -> Unit, + onBackPress: () -> Unit, ) { - composableWithPushTransitions( + composable( route = "$INVOICE_ROUTE/{$INVOICE_DATA_ARG}", arguments = listOf( - navArgument(INVOICE_DATA_ARG) { type = NavType.LongType }, + navArgument(INVOICE_DATA_ARG) { type = NavType.StringType }, ), ) { InvoiceDetailScreen( - navigateBack = onNavigateBack, + onBackPress = onBackPress, ) } } diff --git a/feature/invoices/src/main/kotlin/org/mifospay/feature/invoices/InvoiceDetailScreen.kt b/feature/invoices/src/main/kotlin/org/mifospay/feature/invoices/InvoiceDetailScreen.kt new file mode 100644 index 000000000..9d02b4c4b --- /dev/null +++ b/feature/invoices/src/main/kotlin/org/mifospay/feature/invoices/InvoiceDetailScreen.kt @@ -0,0 +1,292 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.invoices + +import androidx.compose.foundation.background +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.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifospay.core.model.entity.invoice.Invoice +import org.koin.androidx.compose.koinViewModel +import org.mifospay.common.Constants +import org.mifospay.core.designsystem.component.MfOverlayLoadingWheel +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.core.ui.ErrorScreenContent +import org.mifospay.core.ui.MifosDivider +import org.mifospay.invoices.R + +@Composable +internal fun InvoiceDetailScreen( + onBackPress: () -> Unit, + modifier: Modifier = Modifier, + viewModel: InvoiceDetailViewModel = koinViewModel(), +) { + val invoiceDetailUiState by viewModel.invoiceDetailUiState.collectAsStateWithLifecycle() + + InvoiceDetailScreen( + invoiceDetailUiState = invoiceDetailUiState, + onBackPress = onBackPress, + modifier = modifier, + ) +} + +@Composable +private fun InvoiceDetailScreen( + invoiceDetailUiState: InvoiceDetailUiState, + onBackPress: () -> Unit, + modifier: Modifier = Modifier, +) { + MifosScaffold( + modifier = modifier, + topBarTitle = R.string.feature_invoices_invoice, + backPress = { onBackPress.invoke() }, + scaffoldContent = { + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + .padding(it), + ) { + when (invoiceDetailUiState) { + is InvoiceDetailUiState.Error -> { + ErrorScreenContent() + } + + InvoiceDetailUiState.Loading -> { + MfOverlayLoadingWheel( + contentDesc = stringResource(R.string.feature_invoices_loading), + ) + } + + is InvoiceDetailUiState.Success -> { + InvoiceDetailContent( + invoiceDetailUiState.invoice, + ) + } + } + } + }, + ) +} + +@Composable +private fun InvoiceDetailContent( + invoice: Invoice?, + modifier: Modifier = Modifier, + lazyListState: LazyListState = rememberLazyListState(), +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + state = lazyListState, + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item { + if (invoice != null) { + InvoiceDetailCard( + invoice = invoice, + modifier = Modifier, + ) + } + } + } +} + +@Composable +private fun InvoiceDetailCard( + invoice: Invoice, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth(), + ) { + RowBlock { + Text( + text = stringResource(id = R.string.feature_invoices_merchant_id), + color = + MaterialTheme.colorScheme.onSurface, + ) + Text( + text = invoice.consumerId, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + ) + } + + RowBlock { + Text( + text = stringResource(R.string.feature_invoices_consumer_id), + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = invoice.consumerName, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + ) + } + + RowBlock { + Text( + text = stringResource(R.string.feature_invoices_amount), + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = invoice.amount.toString(), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + ) + } + + RowBlock { + Text( + text = stringResource(R.string.feature_invoices_items_bought), + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = invoice.itemsBought, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + ) + } + + RowBlock { + Text( + text = stringResource(R.string.feature_invoices_status), + color = MaterialTheme.colorScheme.onSurface, + ) + + Text( + text = if (invoice.status == 1L) Constants.DONE else Constants.PENDING, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + ) + } + + RowBlock { + Text( + text = stringResource(R.string.feature_invoices_transaction_id), + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = invoice.transactionId, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + ) + } + + RowBlock(showDivider = false) { + Text( + text = stringResource(R.string.feature_invoices_date), + color = MaterialTheme.colorScheme.onSurface, + ) + + Text( + text = invoice.date, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } +} + +@Composable +private inline fun RowBlock( + modifier: Modifier = Modifier, + showDivider: Boolean = true, + crossinline content: @Composable (RowScope.() -> Unit), +) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + content() + } + + if (showDivider) { + MifosDivider() + } + } +} + +class InvoiceDetailScreenProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + InvoiceDetailUiState.Loading, + InvoiceDetailUiState.Error("Some Error Occurred"), + InvoiceDetailUiState.Success( + Invoice( + id = 1L, + clientId = 2L, + consumerId = "CUST001", + consumerName = "John Doe", + amount = 1500.750000, + itemsBought = "Laptop, Mouse, Keyboard", + status = 1L, + transactionId = "TRX12345", + invoiceId = 1L, + title = "Invoice for Computer Accessories", + date = "19 October 2024", + createdAt = listOf(System.currentTimeMillis()), + updatedAt = listOf(System.currentTimeMillis()), + ), + ), + ) +} + +@Preview(showBackground = true, showSystemUi = true) +@Composable +private fun InvoiceDetailScreenPreview( + @PreviewParameter(InvoiceDetailScreenProvider::class) + invoiceDetailUiState: InvoiceDetailUiState, +) { + MifosTheme { + InvoiceDetailScreen( + invoiceDetailUiState = invoiceDetailUiState, + onBackPress = {}, + ) + } +} diff --git a/feature/invoices/src/main/kotlin/org/mifospay/feature/invoices/InvoiceDetailViewModel.kt b/feature/invoices/src/main/kotlin/org/mifospay/feature/invoices/InvoiceDetailViewModel.kt new file mode 100644 index 000000000..cd2c1d4d1 --- /dev/null +++ b/feature/invoices/src/main/kotlin/org/mifospay/feature/invoices/InvoiceDetailViewModel.kt @@ -0,0 +1,66 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.invoices + +import android.net.Uri +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.entity.invoice.Invoice +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.invoice.FetchInvoice +import org.mifospay.feature.invoices.navigation.INVOICE_DATA_ARG + +class InvoiceDetailViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val fetchInvoiceUseCase: FetchInvoice, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + + private val _invoiceDetailUiState = + MutableStateFlow(InvoiceDetailUiState.Loading) + val invoiceDetailUiState: StateFlow = _invoiceDetailUiState + init { + savedStateHandle.get(INVOICE_DATA_ARG)?.let { invoiceData -> + Uri.decode(invoiceData)?.let { + getInvoiceDetails(Uri.parse(it)) + } + } + } + + private fun getInvoiceDetails(data: Uri?) { + mUseCaseHandler.execute( + fetchInvoiceUseCase, + data?.let { FetchInvoice.RequestValues(it) }, + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchInvoice.ResponseValue) { + _invoiceDetailUiState.value = InvoiceDetailUiState.Success( + response.invoices[0], + ) + } + + override fun onError(message: String) { + _invoiceDetailUiState.value = InvoiceDetailUiState.Error(message) + } + }, + ) + } +} + +sealed interface InvoiceDetailUiState { + data object Loading : InvoiceDetailUiState + data class Success( + val invoice: Invoice?, + ) : InvoiceDetailUiState + + data class Error(val message: String) : InvoiceDetailUiState +} diff --git a/feature/invoices/src/main/kotlin/org/mifospay/feature/invoices/InvoiceItem.kt b/feature/invoices/src/main/kotlin/org/mifospay/feature/invoices/InvoiceItem.kt new file mode 100644 index 000000000..706bb7bc0 --- /dev/null +++ b/feature/invoices/src/main/kotlin/org/mifospay/feature/invoices/InvoiceItem.kt @@ -0,0 +1,103 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.invoices + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.mifospay.core.model.entity.invoice.Invoice +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.ui.AvatarBox + +@Composable +internal fun InvoiceItem( + invoice: Invoice, + onClick: (Long) -> Unit, + modifier: Modifier = Modifier, +) { + OutlinedCard( + modifier = modifier + .fillMaxWidth(), + onClick = { + onClick(invoice.invoiceId) + }, + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.cardColors( + containerColor = Color.Transparent, + ), + ) { + ListItem( + headlineContent = { + Text(text = invoice.title) + }, + supportingContent = { + Text(text = "${invoice.date} | ${invoice.consumerName}") + }, + leadingContent = { + AvatarBox( + icon = if (invoice.status == 1L) { + MifosIcons.CheckCircle + } else { + MifosIcons.CheckCircle2 + }, + contentColor = if (invoice.status == 1L) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.error + }, + ) + }, + trailingContent = { + Text( + text = invoice.amount.toString(), + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.titleSmall, + ) + }, + colors = ListItemDefaults.colors( + containerColor = Color.Transparent, + ), + ) + } +} + +@Preview +@Composable +private fun PreviewInvoiceItem() { + InvoiceItem( + invoice = Invoice( + id = 1L, + clientId = 2L, + consumerId = "CUST001", + consumerName = "John Doe", + amount = 1500.750000, + itemsBought = "Laptop, Mouse, Keyboard", + status = 1L, + transactionId = "TRX12345", + invoiceId = 1L, + title = "Invoice for Computer Accessories", + date = "19 October 2024", + createdAt = listOf(System.currentTimeMillis()), + updatedAt = listOf(System.currentTimeMillis()), + ), + onClick = {}, + ) +} diff --git a/feature/invoices/src/main/kotlin/org/mifospay/feature/invoices/InvoiceScreen.kt b/feature/invoices/src/main/kotlin/org/mifospay/feature/invoices/InvoiceScreen.kt new file mode 100644 index 000000000..d045e4114 --- /dev/null +++ b/feature/invoices/src/main/kotlin/org/mifospay/feature/invoices/InvoiceScreen.kt @@ -0,0 +1,170 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.invoices + +import android.net.Uri +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +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.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifospay.core.model.entity.invoice.Invoice +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MifosLoadingWheel +import org.mifospay.core.designsystem.icon.MifosIcons.Info +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.core.ui.EmptyContentScreen +import org.mifospay.invoices.R + +@Composable +fun InvoiceScreenRoute( + navigateToInvoiceDetailScreen: (Uri) -> Unit, + modifier: Modifier = Modifier, + viewModel: InvoicesViewModel = koinViewModel(), +) { + val invoiceUiState by viewModel.invoiceUiState.collectAsStateWithLifecycle() + InvoiceScreen( + invoiceUiState = invoiceUiState, + getUniqueInvoiceLink = viewModel::getUniqueInvoiceLink, + navigateToInvoiceDetailScreen = navigateToInvoiceDetailScreen, + modifier = modifier, + ) +} + +@Composable +private fun InvoiceScreen( + invoiceUiState: InvoicesUiState, + getUniqueInvoiceLink: (Long) -> Uri?, + navigateToInvoiceDetailScreen: (Uri) -> Unit, + modifier: Modifier = Modifier, +) { + when (invoiceUiState) { + is InvoicesUiState.Error -> { + EmptyContentScreen( + title = stringResource(id = R.string.feature_invoices_error_oops), + subTitle = stringResource(id = R.string.feature_invoices_unexpected_error_subtitle), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.primary, + iconImageVector = Info, + ) + } + + is InvoicesUiState.InvoiceList -> { + InvoicesList( + invoiceList = invoiceUiState.list, + modifier = modifier, + onClickInvoice = { invoiceId -> + val invoiceUri = getUniqueInvoiceLink(invoiceId) + invoiceUri?.let { uri -> + navigateToInvoiceDetailScreen.invoke(uri) + } + }, + ) + } + + InvoicesUiState.Empty -> { + EmptyContentScreen( + title = stringResource(id = R.string.feature_invoices_error_oops), + subTitle = stringResource(id = R.string.feature_invoices_error_no_invoices_found), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + + InvoicesUiState.Loading -> { + MifosLoadingWheel( + modifier = Modifier.fillMaxWidth(), + contentDesc = stringResource(R.string.feature_invoices_loading), + ) + } + } +} + +@Composable +private fun InvoicesList( + invoiceList: List, + onClickInvoice: (Long) -> Unit, + modifier: Modifier = Modifier, + lazyListState: LazyListState = rememberLazyListState(), +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + state = lazyListState, + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items( + items = invoiceList, + key = { it?.invoiceId ?: 0L }, + ) { invoice -> + if (invoice != null) { + InvoiceItem( + invoice = invoice, + onClick = onClickInvoice, + ) + } + } + } +} + +class InvoicesUiStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + InvoicesUiState.Loading, + InvoicesUiState.Empty, + InvoicesUiState.InvoiceList(sampleInvoiceList), + InvoicesUiState.Error("Some Error Occurred"), + ) +} + +@Preview(showBackground = true, showSystemUi = true) +@Composable +private fun InvoiceScreenPreview( + @PreviewParameter(InvoicesUiStateProvider::class) invoiceUiState: InvoicesUiState, +) { + MifosTheme { + InvoiceScreen( + invoiceUiState = invoiceUiState, + getUniqueInvoiceLink = { Uri.EMPTY }, + navigateToInvoiceDetailScreen = {}, + ) + } +} + +val sampleInvoiceList = List(10) { index -> + Invoice( + id = 1L, + clientId = 2L, + consumerId = "CUST001", + consumerName = "John Doe", + amount = 1500.750000, + itemsBought = "Laptop, Mouse, Keyboard", + status = 1L, + transactionId = "TRX12345", + invoiceId = 1L, + title = "Invoice for Computer Accessories", + date = "19 October 2024", + createdAt = listOf(System.currentTimeMillis()), + updatedAt = listOf(System.currentTimeMillis()), + ) +} diff --git a/feature/invoices/src/main/kotlin/org/mifospay/feature/invoices/InvoicesViewModel.kt b/feature/invoices/src/main/kotlin/org/mifospay/feature/invoices/InvoicesViewModel.kt new file mode 100644 index 000000000..d2ce45f87 --- /dev/null +++ b/feature/invoices/src/main/kotlin/org/mifospay/feature/invoices/InvoicesViewModel.kt @@ -0,0 +1,69 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.invoices + +import android.net.Uri +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.entity.invoice.Invoice +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.mifospay.common.Constants.INVOICE_DOMAIN +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.invoice.FetchInvoices +import org.mifospay.core.datastore.PreferencesHelper + +class InvoicesViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val mPreferencesHelper: PreferencesHelper, + private val fetchInvoicesUseCase: FetchInvoices, +) : ViewModel() { + + private val _invoiceUiState = MutableStateFlow(InvoicesUiState.Loading) + val invoiceUiState: StateFlow = _invoiceUiState + + private fun fetchInvoices() { + _invoiceUiState.value = InvoicesUiState.Loading + mUseCaseHandler.execute( + fetchInvoicesUseCase, + FetchInvoices.RequestValues(mPreferencesHelper.clientId.toString() + ""), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchInvoices.ResponseValue) { + if (response.invoiceList.isNotEmpty()) { + _invoiceUiState.value = InvoicesUiState.InvoiceList(response.invoiceList) + } else { + _invoiceUiState.value = InvoicesUiState.Empty + } + } + + override fun onError(message: String) { + _invoiceUiState.value = InvoicesUiState.Error(message) + } + }, + ) + } + + fun getUniqueInvoiceLink(id: Long): Uri? { + return Uri.parse( + INVOICE_DOMAIN + mPreferencesHelper.clientId + "/" + id, + ) + } + + init { + fetchInvoices() + } +} + +sealed class InvoicesUiState { + data object Loading : InvoicesUiState() + data object Empty : InvoicesUiState() + data class Error(val message: String) : InvoicesUiState() + data class InvoiceList(val list: List) : InvoicesUiState() +} diff --git a/feature/kyc/build.gradle.kts b/feature/kyc/build.gradle.kts index 67ca42fc7..d855b9d50 100644 --- a/feature/kyc/build.gradle.kts +++ b/feature/kyc/build.gradle.kts @@ -8,24 +8,21 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { - namespace = "org.mifospay.feature.kyc" + namespace = "org.mifospay.kyc" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - implementation(libs.filekit.compose) - implementation(libs.coil.kt.compose) - } - } +dependencies { + implementation(projects.libs.countryCodePicker) + implementation(projects.libs.pullrefresh) + + implementation(libs.sheets.compose.dialogs.core) + implementation(libs.sheets.compose.dialogs.calender) + + // TODO:: this should be removed + implementation(libs.squareup.okhttp) } \ No newline at end of file diff --git a/feature/kyc/src/commonMain/composeResources/drawable/coin_image.png b/feature/kyc/src/commonMain/composeResources/drawable/coin_image.png new file mode 100644 index 000000000..000c53aed Binary files /dev/null and b/feature/kyc/src/commonMain/composeResources/drawable/coin_image.png differ diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCDescriptionScreen.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCDescriptionScreen.kt new file mode 100644 index 000000000..b8d30baad --- /dev/null +++ b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCDescriptionScreen.kt @@ -0,0 +1,355 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +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.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +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 +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifos.library.pullrefresh.PullRefreshIndicator +import com.mifos.library.pullrefresh.pullRefresh +import com.mifos.library.pullrefresh.rememberPullRefreshState +import com.mifospay.core.model.entity.kyc.KYCLevel1Details +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosOverlayLoadingWheel +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.ui.EmptyContentScreen +import org.mifospay.kyc.R + +@Composable +fun KYCScreen( + onLevel1Clicked: () -> Unit, + onLevel2Clicked: () -> Unit, + onLevel3Clicked: () -> Unit, + modifier: Modifier = Modifier, + viewModel: KYCDescriptionViewModel = koinViewModel(), +) { + val kUiState by viewModel.kycDescriptionState.collectAsState() + val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() + + KYCDescriptionScreen( + modifier = modifier, + kUiState = kUiState, + onLevel1Clicked = { + // Todo : Implement onLevel1Clicked flow + onLevel1Clicked.invoke() + }, + onLevel2Clicked = { + // Todo : Implement onLevel2Clicked flow + onLevel2Clicked.invoke() + }, + onLevel3Clicked = { + // Todo : Implement onLevel3Clicked flow + onLevel3Clicked.invoke() + }, + isRefreshing = isRefreshing, + onRefresh = viewModel::refresh, + ) +} + +@Composable +private fun KYCDescriptionScreen( + kUiState: KYCDescriptionUiState, + isRefreshing: Boolean, + onRefresh: () -> Unit, + onLevel1Clicked: () -> Unit, + onLevel2Clicked: () -> Unit, + onLevel3Clicked: () -> Unit, + modifier: Modifier = Modifier, +) { + val pullRefreshState = rememberPullRefreshState(isRefreshing, onRefresh) + Box( + modifier = modifier + .pullRefresh(pullRefreshState), + ) { + when (kUiState) { + KYCDescriptionUiState.Loading -> { + MifosOverlayLoadingWheel(contentDesc = stringResource(R.string.feature_kyc_loading)) + } + + is KYCDescriptionUiState.Error -> { + EmptyContentScreen( + title = stringResource(id = R.string.feature_kyc_error_oops), + subTitle = stringResource(id = R.string.feature_kyc_unexpected_error_subtitle), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.primary, + iconImageVector = MifosIcons.Info, + ) + } + + is KYCDescriptionUiState.KYCDescription -> { + val kyc = kUiState.kycLevel1Details + if (kyc != null) { + KYCDescriptionScreen( + kyc, + onLevel1Clicked, + onLevel2Clicked, + onLevel3Clicked, + ) + } + } + } + + PullRefreshIndicator( + refreshing = isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) + } +} + +@Composable +private fun KYCDescriptionScreen( + kyc: KYCLevel1Details, + onLevel1Clicked: () -> Unit, + onLevel2Clicked: () -> Unit, + onLevel3Clicked: () -> Unit, + modifier: Modifier = Modifier, +) { + val currentLevel = kyc.currentLevel + + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + ) { + Text( + text = stringResource(R.string.feature_kyc_complete_kyc), + modifier = Modifier.padding(top = 40.dp), + fontSize = 19.sp, + textAlign = TextAlign.Center, + ) + + KYCLevelButton( + level = 1, + enabled = currentLevel >= 0.toString(), + completed = currentLevel >= 1.toString(), + modifier = Modifier.padding(top = 90.dp), + onLevel1Clicked, + ) + + KYCLevelButton( + level = 2, + enabled = currentLevel >= 1.toString(), + completed = currentLevel >= 2.toString(), + modifier = Modifier.padding(top = 80.dp), + onLevel2Clicked, + ) + + KYCLevelButton( + level = 3, + enabled = currentLevel >= 2.toString(), + completed = currentLevel >= 3.toString(), + modifier = Modifier.padding(top = 80.dp), + onLevel3Clicked, + ) + } +} + +@Composable +private fun KYCLevelButton( + level: Int, + enabled: Boolean, + completed: Boolean, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + ButtonComponent( + stringResource(R.string.feature_kyc_level) + "$level", + enabled, + completed, + onClick, + ) + Spacer(modifier = Modifier.weight(0.1f)) + IconComponent( + completed, + modifier = Modifier.weight(0.9f), + ) + } +} + +@Composable +private fun ButtonComponent( + value: String, + enabled: Boolean, + completed: Boolean, + onButtonClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + MifosButton( + modifier = modifier + .width(130.dp) + .heightIn(38.dp) + .shadow(43.dp), + onClick = { + if (enabled) { + onButtonClicked.invoke() + } + }, + contentPadding = PaddingValues(), + colors = ButtonDefaults.buttonColors( + containerColor = when { + completed -> MaterialTheme.colorScheme.onPrimary + enabled -> MaterialTheme.colorScheme.primary + else -> Color.Gray + }, + contentColor = when { + completed -> MaterialTheme.colorScheme.primary + enabled -> MaterialTheme.colorScheme.onPrimary + else -> Color.Gray + }, + ), + shape = RoundedCornerShape(10.dp), + enabled = enabled, + ) { + Text( + text = value, + fontSize = 18.sp, + fontWeight = FontWeight.Normal, + ) + } +} + +@Composable +private fun IconComponent( + completed: Boolean, + modifier: Modifier = Modifier, +) { + if (completed) { + Row( + modifier = modifier + .height(23.dp), + ) { + Icon( + imageVector = MifosIcons.Check, + contentDescription = stringResource(R.string.feature_kyc_check), + modifier = Modifier + .size(20.dp), + ) + Spacer(modifier = Modifier.width(26.dp)) + Text( + text = stringResource(R.string.feature_kyc_completion), + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun KYCDescriptionPreview() { + val onLevel1Clicked: () -> Unit = { } + val onLevel2Clicked: () -> Unit = { } + val onLevel3Clicked: () -> Unit = { } + KYCDescriptionScreen( + kyc = KYCLevel1Details(), + onLevel1Clicked, + onLevel2Clicked, + onLevel3Clicked, + ) +} + +internal class KYCDescriptionUiStatePreviewProvider : + PreviewParameterProvider { + override val values = sequenceOf( + KYCDescriptionUiState.Loading, + KYCDescriptionUiState.Error, + KYCDescriptionUiState.KYCDescription( + KYCLevel1Details().apply { + currentLevel = "0" + }, + ), + KYCDescriptionUiState.KYCDescription( + KYCLevel1Details().apply { + currentLevel = "1" + }, + ), + KYCDescriptionUiState.KYCDescription( + KYCLevel1Details().apply { + currentLevel = "2" + }, + ), + KYCDescriptionUiState.KYCDescription( + KYCLevel1Details().apply { + currentLevel = "3" + }, + ), + ) +} + +@Preview(showBackground = true) +@Composable +private fun KYCDescriptionScreenPreview( + @PreviewParameter(KYCDescriptionUiStatePreviewProvider::class) uiState: KYCDescriptionUiState, +) { + KYCDescriptionScreen( + kUiState = uiState, + onLevel1Clicked = {}, + onLevel2Clicked = {}, + onLevel3Clicked = {}, + isRefreshing = false, + onRefresh = {}, + ) +} + +@Preview(showBackground = true) +@Composable +private fun ButtonComponentPreview() { + Column { + ButtonComponent(value = "Level 1", enabled = true, completed = false, onButtonClicked = {}) + ButtonComponent(value = "Level 2", enabled = true, completed = true, {}) + ButtonComponent(value = "Level 3", enabled = false, completed = false, {}) + } +} + +@Preview(showBackground = true) +@Composable +private fun IconComponentPreview() { + Column { + IconComponent(completed = true) + IconComponent(completed = false) + } +} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCDescriptionViewModel.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCDescriptionViewModel.kt new file mode 100644 index 000000000..a2f9fd98b --- /dev/null +++ b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCDescriptionViewModel.kt @@ -0,0 +1,80 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifospay.core.model.entity.kyc.KYCLevel1Details +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.kyc.FetchKYCLevel1Details +import org.mifospay.core.data.repository.local.LocalRepository +import org.mifospay.feature.kyc.KYCDescriptionUiState.Loading + +class KYCDescriptionViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val mLocalRepository: LocalRepository, + private val fetchKYCLevel1DetailsUseCase: FetchKYCLevel1Details, +) : ViewModel() { + private val descriptionState = MutableStateFlow(Loading) + val kycDescriptionState: StateFlow = descriptionState + + init { + fetchCurrentLevel() + } + + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow get() = _isRefreshing.asStateFlow() + + fun refresh() { + viewModelScope.launch { + _isRefreshing.emit(true) + delay(2000) + fetchCurrentLevel() + _isRefreshing.emit(false) + } + } + + private fun fetchCurrentLevel() { + fetchKYCLevel1DetailsUseCase.walletRequestValues = + FetchKYCLevel1Details.RequestValues(mLocalRepository.clientDetails.clientId.toInt()) + val requestValues = fetchKYCLevel1DetailsUseCase.walletRequestValues + mUseCaseHandler.execute( + fetchKYCLevel1DetailsUseCase, + requestValues, + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchKYCLevel1Details.ResponseValue) { + if (response.kycLevel1DetailsList.size == 1) { + descriptionState.value = KYCDescriptionUiState.KYCDescription( + response.kycLevel1DetailsList.first()!!, + ) + } else { + descriptionState.value = KYCDescriptionUiState.Error + } + } + + override fun onError(message: String) { + descriptionState.value = KYCDescriptionUiState.Error + } + }, + ) + } +} + +sealed interface KYCDescriptionUiState { + data class KYCDescription(val kycLevel1Details: KYCLevel1Details?) : KYCDescriptionUiState + data object Error : KYCDescriptionUiState + data object Loading : KYCDescriptionUiState +} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel1Screen.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel1Screen.kt new file mode 100644 index 000000000..e56cf5a68 --- /dev/null +++ b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel1Screen.kt @@ -0,0 +1,286 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +import android.widget.Toast +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState +import com.maxkeppeler.sheets.calendar.CalendarDialog +import com.maxkeppeler.sheets.calendar.models.CalendarConfig +import com.maxkeppeler.sheets.calendar.models.CalendarSelection +import com.maxkeppeler.sheets.calendar.models.CalendarStyle +import com.mifos.library.countrycodepicker.CountryCodePicker +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfOverlayLoadingWheel +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosOutlinedTextField +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.kyc.R +import java.time.format.DateTimeFormatter + +@Composable +internal fun KYCLevel1Screen( + navigateToKycLevel2: () -> Unit, + modifier: Modifier = Modifier, + viewModel: KYCLevel1ViewModel = koinViewModel(), +) { + val kyc1uiState by viewModel.kyc1uiState.collectAsStateWithLifecycle() + + KYCLevel1Screen( + uiState = kyc1uiState, + submitData = viewModel::submitData, + navigateToKycLevel2 = navigateToKycLevel2, + modifier = modifier, + ) +} + +@Composable +private fun KYCLevel1Screen( + uiState: KYCLevel1UiState, + submitData: (KYCLevel1DetailsState) -> Unit, + navigateToKycLevel2: () -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + Kyc1Form( + submitData = submitData, + modifier = modifier, + ) + + when (uiState) { + KYCLevel1UiState.Loading -> { + MfOverlayLoadingWheel( + contentDesc = stringResource(id = R.string.feature_kyc_submitting), + ) + } + + KYCLevel1UiState.Error -> { + Toast.makeText( + context, + stringResource(R.string.feature_kyc_error_adding_KYC_Level_1_details), + Toast.LENGTH_SHORT, + ).show() + navigateToKycLevel2.invoke() + } + + KYCLevel1UiState.Success -> { + Toast.makeText( + context, + stringResource(R.string.feature_kyc_successkyc1), + Toast.LENGTH_SHORT, + ).show() + navigateToKycLevel2.invoke() + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun Kyc1Form( + submitData: (KYCLevel1DetailsState) -> Unit, + modifier: Modifier = Modifier, +) { + var firstName by rememberSaveable { mutableStateOf("") } + var lastName by rememberSaveable { mutableStateOf("") } + var address1 by rememberSaveable { mutableStateOf("") } + var address2 by rememberSaveable { mutableStateOf("") } + var mobileNumber by rememberSaveable { mutableStateOf("") } + var dateOfBirth by rememberSaveable { mutableStateOf("") } + val dateState = rememberUseCaseState() + val dateFormatter = + DateTimeFormatter.ofPattern(stringResource(R.string.feature_kyc_date_format)) + + val kycDetails = KYCLevel1DetailsState( + firstName = firstName, + lastName = lastName, + addressLine1 = address1, + addressLine2 = address2, + mobileNo = mobileNumber, + dob = dateOfBirth, + ) + + Column( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + ) { + Spacer(modifier = Modifier.height(20.dp)) + MifosOutlinedTextField( + label = R.string.feature_kyc_first_name, + value = firstName, + onValueChange = { + firstName = it + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + ) + MifosOutlinedTextField( + label = R.string.feature_kyc_last_name, + value = lastName, + onValueChange = { + lastName = it + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + ) + MifosOutlinedTextField( + label = R.string.feature_kyc_address_line_1, + value = address1, + onValueChange = { + address1 = it + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + ) + MifosOutlinedTextField( + label = R.string.feature_kyc_address_line_2, + value = address2, + onValueChange = { + address2 = it + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + ) + + Box( + modifier = Modifier + .padding(vertical = 7.dp), + ) { + val keyboardController = LocalSoftwareKeyboardController.current + CountryCodePicker( + modifier = Modifier, + shape = RoundedCornerShape(3.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + ), + onValueChange = { (code, phone), isValid -> + if (isValid) { + mobileNumber = code + phone + } + }, + label = { + Text(stringResource(id = R.string.feature_kyc_phone_number)) + }, + keyboardActions = KeyboardActions { keyboardController?.hide() }, + ) + } + + CalendarDialog( + state = dateState, + config = CalendarConfig( + monthSelection = true, + yearSelection = true, + style = CalendarStyle.MONTH, + ), + selection = CalendarSelection.Date { date -> + dateOfBirth = dateFormatter.format(date) + }, + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(70.dp) + .padding(vertical = 9.dp) + .clickable { dateState.show() } + .border( + width = 1.dp, + color = Color.Black, + ) + .padding(12.dp) + .clip(shape = RoundedCornerShape(8.dp)), + ) { + Text( + text = dateOfBirth.ifEmpty { stringResource(R.string.feature_kyc_select_dob) }, + style = MaterialTheme.typography.bodyLarge, + ) + } + + MifosButton( + onClick = { + submitData(kycDetails) + }, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(vertical = 7.dp), + ) { + Text(text = stringResource(R.string.feature_kyc_submit)) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun Kyc1FormPreview() { + MifosTheme { + KYCLevel1Screen( + uiState = KYCLevel1UiState.Loading, + submitData = { _ -> }, + navigateToKycLevel2 = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun Kyc1PreviewWithError() { + MifosTheme { + KYCLevel1Screen( + uiState = KYCLevel1UiState.Error, + submitData = { _ -> }, + navigateToKycLevel2 = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun Kyc1FormPreviewWithSuccess() { + MifosTheme { + KYCLevel1Screen( + uiState = KYCLevel1UiState.Success, + submitData = { _ -> }, + navigateToKycLevel2 = {}, + ) + } +} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel1ViewModel.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel1ViewModel.kt new file mode 100644 index 000000000..3e7f282d1 --- /dev/null +++ b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel1ViewModel.kt @@ -0,0 +1,85 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.entity.kyc.KYCLevel1Details +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.kyc.UploadKYCLevel1Details +import org.mifospay.core.data.repository.local.LocalRepository +import org.mifospay.feature.kyc.KYCLevel1UiState.Loading + +class KYCLevel1ViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val mLocalRepository: LocalRepository, + private val uploadKYCLevel1DetailsUseCase: UploadKYCLevel1Details, +) : ViewModel() { + + private val kycUiState = MutableStateFlow(Loading) + val kyc1uiState: StateFlow = kycUiState + + fun submitData(kycLevel1Details: KYCLevel1DetailsState) { + uploadKYCLevel1DetailsUseCase.walletRequestValues = UploadKYCLevel1Details.RequestValues( + mLocalRepository.clientDetails.clientId.toInt(), + kycLevel1Details.toModel(), + ) + val requestValues = uploadKYCLevel1DetailsUseCase.walletRequestValues + mUseCaseHandler.execute( + uploadKYCLevel1DetailsUseCase, + requestValues, + object : UseCase.UseCaseCallback { + override fun onSuccess(response: UploadKYCLevel1Details.ResponseValue) { + kycUiState.value = KYCLevel1UiState.Success + } + + override fun onError(message: String) { + kycUiState.value = KYCLevel1UiState.Error + } + }, + ) + } +} + +sealed interface KYCLevel1UiState { + data object Loading : KYCLevel1UiState + data object Success : KYCLevel1UiState + data object Error : KYCLevel1UiState +} + +data class KYCLevel1DetailsState( + val firstName: String, + + val lastName: String, + + val addressLine1: String, + + val addressLine2: String, + + val mobileNo: String, + + val dob: String, + + val currentLevel: String = "1", +) + +internal fun KYCLevel1DetailsState.toModel(): KYCLevel1Details { + return KYCLevel1Details( + firstName = firstName.trim(), + lastName = lastName.trim(), + addressLine1 = addressLine1.trim(), + addressLine2 = addressLine2.trim(), + mobileNo = mobileNo.trim(), + dob = dob.trim(), + currentLevel = currentLevel, + ) +} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel2Screen.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel2Screen.kt new file mode 100644 index 000000000..35d9bb24a --- /dev/null +++ b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel2Screen.kt @@ -0,0 +1,389 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build.VERSION.SDK_INT +import android.provider.Settings +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfOverlayLoadingWheel +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosOutlinedTextField +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.kyc.R + +@Composable +internal fun KYCLevel2Screen( + onSuccessKyc2: () -> Unit, + modifier: Modifier = Modifier, + viewModel: KYCLevel2ViewModel = koinViewModel(), +) { + val kyc2uiState by viewModel.kyc2uiState.collectAsStateWithLifecycle() + + KYCLevel2Screen( + uiState = kyc2uiState, + uploadData = viewModel::uploadKYCDocs, + onSuccessKyc2 = onSuccessKyc2, + modifier = modifier, + ) +} + +@Composable +private fun KYCLevel2Screen( + uiState: KYCLevel2UiState, + uploadData: (String, Uri) -> Unit, + onSuccessKyc2: () -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + Kyc2Form( + modifier = modifier, + uploadData = uploadData, + ) + + when (uiState) { + KYCLevel2UiState.Loading -> { + MfOverlayLoadingWheel( + contentDesc = stringResource(id = R.string.feature_kyc_submitting), + ) + } + + KYCLevel2UiState.Error -> { + Toast.makeText( + context, + stringResource(R.string.feature_kyc_error_adding_KYC_Level_2_details), + Toast.LENGTH_SHORT, + ).show() + } + + KYCLevel2UiState.Success -> { + Toast.makeText( + context, + stringResource(R.string.feature_kyc_successkyc2), + Toast.LENGTH_SHORT, + ).show() + onSuccessKyc2.invoke() + } + } +} + +@Suppress("LongMethod", "CyclomaticComplexMethod") +@Composable +private fun Kyc2Form( + uploadData: (String, Uri) -> Unit, + modifier: Modifier = Modifier, +) { + var idType by rememberSaveable { mutableStateOf("") } + val context = LocalContext.current + var result by rememberSaveable { mutableStateOf(null) } + val lifecycleOwner = LocalLifecycleOwner.current + val scope = rememberCoroutineScope() + val snackBarHostState = remember { SnackbarHostState() } + val docLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { + result = it + } + + var storagePermissionGranted by remember { + mutableStateOf( + if (SDK_INT >= 33) { + ContextCompat.checkSelfPermission(context, Manifest.permission.READ_MEDIA_IMAGES) == + PackageManager.PERMISSION_GRANTED + } else { + ContextCompat.checkSelfPermission( + context, + Manifest.permission.READ_EXTERNAL_STORAGE, + ) == + PackageManager.PERMISSION_GRANTED + }, + ) + } + + var shouldShowPermissionRationale = + if (SDK_INT >= 33) { + shouldShowRequestPermissionRationale( + context as Activity, + Manifest.permission.READ_MEDIA_IMAGES, + ) + } else { + shouldShowRequestPermissionRationale( + context as Activity, + Manifest.permission.READ_EXTERNAL_STORAGE, + ) + } + + var shouldDirectUserToApplicationSettings by remember { + mutableStateOf(false) + } + + val decideCurrentPermissionStatus: (Boolean, Boolean) -> String = + { granted, permissionRationale -> + if (granted) { + "Granted" + } else if (permissionRationale) { + "Rejected" + } else { + "Denied" + } + } + + var currentPermissionStatus by remember { + mutableStateOf( + decideCurrentPermissionStatus( + storagePermissionGranted, + shouldShowPermissionRationale, + ), + ) + } + + val permission = if (SDK_INT >= 33) { + Manifest.permission.READ_MEDIA_IMAGES + } else { + Manifest.permission.READ_EXTERNAL_STORAGE + } + + val storagePermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = { isGranted -> + storagePermissionGranted = isGranted + + if (!isGranted) { + shouldShowPermissionRationale = + if (SDK_INT >= 33) { + shouldShowRequestPermissionRationale( + context, + Manifest.permission.READ_MEDIA_IMAGES, + ) + } else { + shouldShowRequestPermissionRationale( + context, + Manifest.permission.READ_EXTERNAL_STORAGE, + ) + } + } + shouldDirectUserToApplicationSettings = + !shouldShowPermissionRationale && !storagePermissionGranted + currentPermissionStatus = decideCurrentPermissionStatus( + storagePermissionGranted, + shouldShowPermissionRationale, + ) + }, + ) + + DisposableEffect( + key1 = lifecycleOwner, + effect = { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START && + !storagePermissionGranted && + !shouldShowPermissionRationale + ) { + storagePermissionLauncher.launch(permission) + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + }, + ) + + Scaffold( + modifier = modifier, + snackbarHost = { + SnackbarHost(hostState = snackBarHostState) + }, + ) { contentPadding -> + Box( + modifier = Modifier + .padding(contentPadding) + .fillMaxSize(), + contentAlignment = Alignment.TopCenter, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + MifosOutlinedTextField( + label = R.string.feature_kyc_id_type, + value = idType, + onValueChange = { + idType = it + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + ) + + Row { + MifosButton( + onClick = { + if (storagePermissionGranted) { + docLauncher.launch(arrayOf("application/pdf", "image/*")) + } else { + Toast.makeText( + context, + R.string.feature_kyc_approve_permission, + Toast.LENGTH_SHORT, + ).show() + } + }, + ) { + Text(text = stringResource(id = R.string.feature_kyc_browse)) + } + result?.let { doc -> + val fileName = doc.path?.substringAfterLast("/").toString() + Text( + text = stringResource(id = R.string.feature_kyc_file_name) + fileName, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(horizontal = 2.dp), + ) + } + } + + MifosButton( + modifier = Modifier.align(Alignment.CenterHorizontally), + onClick = { + result?.let { uri -> + uploadData(idType, uri) + } + }, + ) { + Text(text = stringResource(id = R.string.feature_kyc_submit)) + } + } + } + + if (shouldShowPermissionRationale) { + LaunchedEffect(Unit) { + scope.launch { + val userAction = snackBarHostState.showSnackbar( + message = R.string.feature_kyc_approve_permission.toString(), + actionLabel = R.string.feature_kyc_approve.toString(), + duration = SnackbarDuration.Indefinite, + withDismissAction = true, + ) + when (userAction) { + SnackbarResult.ActionPerformed -> { + shouldShowPermissionRationale = false + storagePermissionLauncher.launch(permission) + } + + SnackbarResult.Dismissed -> { + shouldShowPermissionRationale = false + } + } + } + } + } + + if (shouldDirectUserToApplicationSettings) { + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.packageName, null), + ).also { + context.startActivity(it) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun EmptyKyc2FormPreview() { + MifosTheme { + Kyc2Form( + modifier = Modifier, + uploadData = { _, _ -> }, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun Kyc2FormPreviewWithLoading() { + MifosTheme { + KYCLevel2Screen( + uiState = KYCLevel2UiState.Loading, + uploadData = { _, _ -> }, + onSuccessKyc2 = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun Kyc2FormPreviewWithError() { + MifosTheme { + KYCLevel2Screen( + uiState = KYCLevel2UiState.Error, + uploadData = { _, _ -> }, + onSuccessKyc2 = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun Kyc2FormPreviewWithSuccess() { + MifosTheme { + KYCLevel2Screen( + uiState = KYCLevel2UiState.Success, + uploadData = { _, _ -> }, + onSuccessKyc2 = {}, + ) + } +} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel2ViewModel.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel2ViewModel.kt new file mode 100644 index 000000000..185589b27 --- /dev/null +++ b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel2ViewModel.kt @@ -0,0 +1,76 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +import android.net.Uri +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import org.mifospay.common.Constants +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.kyc.UploadKYCDocs +import org.mifospay.core.datastore.PreferencesHelper +import org.mifospay.feature.kyc.KYCLevel2UiState.Loading +import java.io.File + +class KYCLevel2ViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val preferencesHelper: PreferencesHelper, + private val uploadKYCDocsUseCase: UploadKYCDocs, +) : ViewModel() { + + private val kycUiState = MutableStateFlow(Loading) + val kyc2uiState: StateFlow = kycUiState + + fun uploadKYCDocs(identityType: String, result: Uri) { + val file = result.path?.let { File(it) } + if (file != null) { + uploadKYCDocsUseCase.walletRequestValues = identityType.let { + UploadKYCDocs.RequestValues( + org.mifospay.core.data.util.Constants.ENTITY_TYPE_CLIENTS, + preferencesHelper.clientId, file.name, it, + getRequestFileBody(file), + ) + } + } + val requestValues = uploadKYCDocsUseCase.walletRequestValues + mUseCaseHandler.execute( + uploadKYCDocsUseCase, + requestValues, + object : UseCase.UseCaseCallback { + override fun onSuccess(response: UploadKYCDocs.ResponseValue) { + kycUiState.value = KYCLevel2UiState.Success + } + + override fun onError(message: String) { + kycUiState.value = KYCLevel2UiState.Error + } + }, + ) + } + + private fun getRequestFileBody(file: File): MultipartBody.Part { + // create RequestBody instance from file + val requestFile = file.asRequestBody(Constants.MULTIPART_FORM_DATA.toMediaTypeOrNull()) + + // MultipartBody.Part is used to send also the actual file name + return MultipartBody.Part.createFormData(Constants.FILE, file.name, requestFile) + } +} + +sealed interface KYCLevel2UiState { + data object Loading : KYCLevel2UiState + data object Success : KYCLevel2UiState + data object Error : KYCLevel2UiState +} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel3Screen.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel3Screen.kt new file mode 100644 index 000000000..0f37d7b17 --- /dev/null +++ b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel3Screen.kt @@ -0,0 +1,111 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfOverlayLoadingWheel +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosOutlinedTextField +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.kyc.R + +@Composable +internal fun KYCLevel3Screen( + modifier: Modifier = Modifier, + viewModel: KYCLevel3ViewModel = koinViewModel(), +) { + val kyc3uiState by viewModel.kyc3uiState.collectAsStateWithLifecycle() + + KYCLevel3Screen( + uiState = kyc3uiState, + modifier = modifier, + ) +} + +@Composable +private fun KYCLevel3Screen( + uiState: KYCLevel3UiState, + modifier: Modifier = Modifier, +) { + Kyc3Form(modifier = modifier) + + when (uiState) { + KYCLevel3UiState.Loading -> { + MfOverlayLoadingWheel(contentDesc = stringResource(id = R.string.feature_kyc_submitting)) + } + + KYCLevel3UiState.Error -> { + // Todo : Implement Error state + } + + KYCLevel3UiState.Success -> { + // Todo : Implement Success state + } + } +} + +@Composable +private fun Kyc3Form( + modifier: Modifier = Modifier, +) { + var panIdValue by rememberSaveable { mutableStateOf("") } + + Column( + modifier = modifier + .fillMaxSize() + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + MifosOutlinedTextField( + label = R.string.feature_kyc_pan_id, + value = panIdValue, + onValueChange = { + panIdValue = it + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + ) + + MifosButton( + onClick = {}, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(16.dp), + ) { + Text(stringResource(R.string.feature_kyc_submit)) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun KYCLevel3ScreenPreview() { + MifosTheme { + Kyc3Form(modifier = Modifier) + } +} diff --git a/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt new file mode 100644 index 000000000..bddd96f58 --- /dev/null +++ b/feature/kyc/src/main/kotlin/org/mifospay/feature/kyc/KYCLevel3ViewModel.kt @@ -0,0 +1,34 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.kyc + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.repository.local.LocalRepository +import org.mifospay.feature.kyc.KYCLevel3UiState.Loading + +@Suppress("UnusedPrivateProperty") +class KYCLevel3ViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val mLocalRepository: LocalRepository, +) : ViewModel() { + private val kycUiState = MutableStateFlow(Loading) + val kyc3uiState: StateFlow = kycUiState + + // Todo: Implement KYCLevel3ViewModel flow +} + +sealed interface KYCLevel3UiState { + data object Loading : KYCLevel3UiState + data object Success : KYCLevel3UiState + data object Error : KYCLevel3UiState +} diff --git a/feature/make-transfer/build.gradle.kts b/feature/make-transfer/build.gradle.kts index 440f1634a..9b2e16cf6 100644 --- a/feature/make-transfer/build.gradle.kts +++ b/feature/make-transfer/build.gradle.kts @@ -8,22 +8,12 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { namespace = "org.mifospay.feature.make.transfer" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - } - } -} +dependencies { } diff --git a/feature/make-transfer/src/main/kotlin/org/mifospay/feature/make/transfer/MakeTransferScreen.kt b/feature/make-transfer/src/main/kotlin/org/mifospay/feature/make/transfer/MakeTransferScreen.kt new file mode 100644 index 000000000..4a6bf03cd --- /dev/null +++ b/feature/make-transfer/src/main/kotlin/org/mifospay/feature/make/transfer/MakeTransferScreen.kt @@ -0,0 +1,401 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.make.transfer + +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +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 +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.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.koin.androidx.compose.koinViewModel +import org.mifospay.common.Constants +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosLoadingWheel + +@Composable +internal fun MakeTransferScreenRoute( + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + viewModel: MakeTransferViewModel = koinViewModel(), +) { + // TODO: commented out because not using it + // val fetchPayeeClient by viewModel.fetchPayeeClient.collectAsStateWithLifecycle() + val makeTransferState by viewModel.makeTransferState.collectAsStateWithLifecycle() + val showTransactionStatus by viewModel.showTransactionStatus.collectAsStateWithLifecycle() + + MakeTransferScreen( + state = makeTransferState, + showTransactionStatus = showTransactionStatus, + makeTransfer = viewModel::makeTransfer, + onDismiss = onDismiss, + modifier = modifier, + ) +} + +@Composable +private fun MakeTransferScreen( + state: MakeTransferState, + showTransactionStatus: ShowTransactionStatus, + makeTransfer: (Long, Double) -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + when (state) { + MakeTransferState.Loading -> { + MifosLoadingWheel( + modifier = Modifier.fillMaxWidth(), + contentDesc = stringResource(R.string.feature_make_transfer_loading), + ) + } + + is MakeTransferState.Error -> { + val message = state.message + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + + is MakeTransferState.Success -> { + MakeTransferBottomSheetContent( + showBottomSheet = state.showBottomSheet, + toClientId = state.toClientId, + resultName = state.resultName, + externalId = state.externalId, + transferAmount = state.transferAmount, + showTransactionStatus = showTransactionStatus, + makeTransfer = makeTransfer, + onDismiss = onDismiss, + modifier = modifier, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun MakeTransferBottomSheetContent( + showBottomSheet: Boolean, + toClientId: Long, + resultName: String, + externalId: String, + transferAmount: Double, + showTransactionStatus: ShowTransactionStatus, + makeTransfer: (Long, Double) -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + val sheetState = rememberModalBottomSheetState() + + var visibleBottomSheet by rememberSaveable { mutableStateOf(showBottomSheet) } + + if (visibleBottomSheet) { + ModalBottomSheet( + modifier = modifier, + sheetState = sheetState, + onDismissRequest = { + visibleBottomSheet = false + onDismiss.invoke() + }, + dragHandle = { BottomSheetDefaults.DragHandle() }, + ) { + if (showTransactionStatus.showErrorStatus || + showTransactionStatus.showSuccessStatus + ) { + TransactionStatusContent(showTransactionStatus) + } else { + MakeTransferContent( + toClientId = toClientId, + resultName = resultName, + externalId = externalId, + transferAmount = transferAmount, + makeTransfer = makeTransfer, + onCloseBottomSheet = { + visibleBottomSheet = false + onDismiss.invoke() + }, + ) + } + } + } +} + +@Composable +private fun MakeTransferContent( + toClientId: Long, + resultName: String, + externalId: String, + transferAmount: Double, + makeTransfer: (Long, Double) -> Unit, + onCloseBottomSheet: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .padding(20.dp) + .fillMaxWidth(), + ) { + Text( + text = stringResource(id = R.string.feature_make_transfer_send_money), + style = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + ), + modifier = Modifier + .padding(vertical = 10.dp) + .align(Alignment.CenterHorizontally), + ) + + Box( + modifier = Modifier + .padding(vertical = 10.dp) + .fillMaxWidth() + .height(180.dp), + ) { + Column( + modifier = Modifier + .padding(20.dp), + ) { + Text( + text = stringResource(id = R.string.feature_make_transfer_sending_to) + Constants.COLON, + style = TextStyle( + MaterialTheme.colorScheme.onSurface, + MaterialTheme.typography.bodyMedium.fontSize, + ), + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Text( + text = resultName, + style = TextStyle( + MaterialTheme.colorScheme.onSurface, + MaterialTheme.typography.bodyLarge.fontSize, + ), + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Text( + text = externalId, + style = TextStyle( + MaterialTheme.colorScheme.onSurface, + MaterialTheme.typography.bodyLarge.fontSize, + ), + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = stringResource(id = R.string.feature_make_transfer_amount) + Constants.COLON, + style = TextStyle( + MaterialTheme.colorScheme.onSurface, + MaterialTheme.typography.bodyMedium.fontSize, + ), + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Text( + text = transferAmount.toString(), + style = TextStyle( + MaterialTheme.colorScheme.onSurface, + MaterialTheme.typography.bodyLarge.fontSize, + ), + ) + } + } + + Row( + modifier = Modifier + .padding(top = 20.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + MifosButton( + onClick = onCloseBottomSheet, + modifier = Modifier + .width(100.dp) + .height(50.dp), + ) { + Text(text = "Cancel") + } + + Spacer(modifier = Modifier.width(20.dp)) + + MifosButton( + onClick = { + makeTransfer( + toClientId, + transferAmount, + ) + }, + modifier = Modifier + .width(100.dp) + .height(50.dp), + ) { + Text(text = "Confirm") + } + } + } +} + +@Composable +private fun TransactionStatusContent(showTransactionStatus: ShowTransactionStatus) { + Column( + modifier = Modifier + .padding(20.dp) + .fillMaxWidth(), + ) { + Text( + text = if (showTransactionStatus.showSuccessStatus) { + stringResource(id = R.string.feature_make_transfer_transaction_success) + } else { + stringResource(id = R.string.feature_make_transfer_transaction_unable_to_process) + }, + style = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + ), + modifier = Modifier + .padding(vertical = 10.dp) + .align(Alignment.CenterHorizontally), + ) + + Box( + modifier = Modifier + .padding(vertical = 10.dp) + .fillMaxWidth() + .height(180.dp), + ) { + Icon( + if (showTransactionStatus.showSuccessStatus) { + painterResource(R.drawable.feature_make_transfer_transfer_success) + } else { + painterResource(R.drawable.feature_make_transfer_transfer_failure) + }, + contentDescription = if (showTransactionStatus.showSuccessStatus) { + stringResource(id = R.string.feature_make_transfer_transaction_success) + } else { + stringResource(id = R.string.feature_make_transfer_transaction_unable_to_process) + }, + modifier = Modifier + .align(Alignment.Center), + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewWithMakeTransferContentLoading() { + MakeTransferScreen( + state = MakeTransferState.Loading, + showTransactionStatus = ShowTransactionStatus( + showSuccessStatus = false, + showErrorStatus = false, + ), + makeTransfer = { _, _ -> }, + onDismiss = { }, + ) +} + +@Preview(showBackground = true) +@Composable +private fun PreviewWithMakeTransferContentSuccess() { + MakeTransferScreen( + state = MakeTransferState.Success( + toClientId = 1234, + resultName = "John Doe", + externalId = "example@example.com", + transferAmount = 100.0, + showBottomSheet = true, + ), + showTransactionStatus = ShowTransactionStatus( + showSuccessStatus = false, + showErrorStatus = false, + ), + makeTransfer = { _, _ -> }, + onDismiss = { }, + ) +} + +@Preview(showBackground = true) +@Composable +private fun PreviewMakeTransferContent() { + MakeTransferContent( + toClientId = 1234, + resultName = "John Doe", + externalId = "example@example.com", + transferAmount = 100.0, + makeTransfer = { _, _ -> }, + onCloseBottomSheet = {}, + ) +} + +@Preview(showBackground = true) +@Composable +private fun PreviewMakeTransferBottomSheetContent() { + MakeTransferBottomSheetContent( + showBottomSheet = true, + toClientId = 1234, + resultName = "John Doe", + externalId = "example@example.com", + transferAmount = 100.0, + showTransactionStatus = ShowTransactionStatus( + showSuccessStatus = false, + showErrorStatus = false, + ), + makeTransfer = { _, _ -> }, + onDismiss = { }, + ) +} + +@Preview(showBackground = true) +@Composable +private fun PreviewWithMakeTransferContentError() { + MakeTransferScreen( + state = MakeTransferState.Error("An error occurred"), + showTransactionStatus = ShowTransactionStatus( + showSuccessStatus = false, + showErrorStatus = false, + ), + makeTransfer = { _, _ -> }, + onDismiss = { }, + ) +} diff --git a/feature/make-transfer/src/main/kotlin/org/mifospay/feature/make/transfer/MakeTransferViewModel.kt b/feature/make-transfer/src/main/kotlin/org/mifospay/feature/make/transfer/MakeTransferViewModel.kt new file mode 100644 index 000000000..1a1ed3df1 --- /dev/null +++ b/feature/make-transfer/src/main/kotlin/org/mifospay/feature/make/transfer/MakeTransferViewModel.kt @@ -0,0 +1,129 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.make.transfer + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import org.mifospay.common.PAYEE_EXTERNAL_ID_ARG +import org.mifospay.common.TRANSFER_AMOUNT_ARG +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.account.TransferFunds +import org.mifospay.core.data.domain.usecase.client.SearchClient +import org.mifospay.core.data.repository.local.LocalRepository + +class MakeTransferViewModel( + savedStateHandle: SavedStateHandle, + private val useCaseHandler: UseCaseHandler, + private val searchClientUseCase: SearchClient, + private val transferFundsUseCase: TransferFunds, + private val localRepository: LocalRepository, +) : ViewModel() { + + private val payeeExternalId: StateFlow = + savedStateHandle.getStateFlow(PAYEE_EXTERNAL_ID_ARG, "") + private val transferAmount: StateFlow = + savedStateHandle.getStateFlow(TRANSFER_AMOUNT_ARG, null) + + private val _makeTransferState = MutableStateFlow(MakeTransferState.Loading) + val makeTransferState: StateFlow = _makeTransferState.asStateFlow() + + private val _showTransactionStatus = MutableStateFlow( + ShowTransactionStatus( + showSuccessStatus = false, + showErrorStatus = false, + ), + ) + val showTransactionStatus: StateFlow = + _showTransactionStatus.asStateFlow() + + // Fetch Payee client details + val fetchPayeeClient = combine(payeeExternalId, transferAmount, ::Pair) + .map { stringPair -> + stringPair.takeIf { it.first.isNotEmpty() }?.let { + fetchClient(it.first, it.second?.toDouble() ?: 0.0) + } + } + .stateIn(scope = viewModelScope, started = SharingStarted.Eagerly, initialValue = null) + + private fun fetchClient(externalId: String, transferAmount: Double) { + useCaseHandler.execute( + searchClientUseCase, + SearchClient.RequestValues(externalId), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: SearchClient.ResponseValue) { + val searchResult = response.results[0] + searchResult.resultId.let { + _makeTransferState.value = MakeTransferState.Success( + it.toLong(), + searchResult.resultName, + externalId, + transferAmount, + true, + ) + } + } + + override fun onError(message: String) { + _makeTransferState.value = MakeTransferState.Error(message) + } + }, + ) + } + + fun makeTransfer(toClientId: Long, amount: Double) { + val fromClientId = localRepository.clientDetails.clientId + useCaseHandler.execute( + transferFundsUseCase, + TransferFunds.RequestValues(fromClientId, toClientId, amount), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: TransferFunds.ResponseValue) { + _showTransactionStatus.value = ShowTransactionStatus( + showSuccessStatus = true, + showErrorStatus = false, + ) + } + + override fun onError(message: String) { + _showTransactionStatus.value = ShowTransactionStatus( + showSuccessStatus = false, + showErrorStatus = true, + ) + } + }, + ) + } +} + +data class ShowTransactionStatus( + val showSuccessStatus: Boolean, + val showErrorStatus: Boolean, +) + +sealed interface MakeTransferState { + data object Loading : MakeTransferState + data class Success( + val toClientId: Long, + val resultName: String, + val externalId: String, + val transferAmount: Double, + val showBottomSheet: Boolean, + ) : MakeTransferState + + data class Error(val message: String) : MakeTransferState +} diff --git a/feature/merchants/build.gradle.kts b/feature/merchants/build.gradle.kts index b2fb61dac..3519fb03a 100644 --- a/feature/merchants/build.gradle.kts +++ b/feature/merchants/build.gradle.kts @@ -8,22 +8,14 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { namespace = "org.mifospay.feature.merchants" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - } - } -} +dependencies { + implementation(projects.libs.pullrefresh) +} \ No newline at end of file diff --git a/feature/merchants/src/commonMain/kotlin/org/mifospay/feature/merchants/ui/MerchantScreen.kt b/feature/merchants/src/commonMain/kotlin/org/mifospay/feature/merchants/ui/MerchantScreen.kt index ef9b3aa28..c1a62dd24 100644 --- a/feature/merchants/src/commonMain/kotlin/org/mifospay/feature/merchants/ui/MerchantScreen.kt +++ b/feature/merchants/src/commonMain/kotlin/org/mifospay/feature/merchants/ui/MerchantScreen.kt @@ -9,6 +9,7 @@ */ package org.mifospay.feature.merchants.ui +import android.widget.Toast import androidx.annotation.VisibleForTesting import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -29,37 +30,25 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.rememberNavController -import mobile_wallet.feature.merchants.generated.resources.Res -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_close -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_empty_no_merchants_subtitle -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_empty_no_merchants_title -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_error_oops -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_loading -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_search -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_unexpected_error_subtitle -import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview -import org.koin.compose.viewmodel.koinViewModel +import com.mifos.library.pullrefresh.PullRefreshIndicator +import com.mifos.library.pullrefresh.pullRefresh +import com.mifos.library.pullrefresh.rememberPullRefreshState +import com.mifospay.core.model.entity.accounts.savings.SavingsWithAssociations +import org.koin.androidx.compose.koinViewModel import org.mifospay.core.designsystem.component.MfLoadingWheel -import org.mifospay.core.designsystem.component.MifosScaffold -import org.mifospay.core.designsystem.component.rememberMifosPullToRefreshState import org.mifospay.core.designsystem.icon.MifosIcons import org.mifospay.core.designsystem.theme.MifosTheme -import org.mifospay.core.model.savingsaccount.Currency -import org.mifospay.core.model.savingsaccount.DepositType -import org.mifospay.core.model.savingsaccount.InterestPeriod -import org.mifospay.core.model.savingsaccount.SavingsWithAssociationsEntity -import org.mifospay.core.model.savingsaccount.Status -import org.mifospay.core.model.savingsaccount.SubStatus -import org.mifospay.core.model.savingsaccount.Summary -import org.mifospay.core.model.savingsaccount.Timeline import org.mifospay.core.ui.EmptyContentScreen import org.mifospay.feature.merchants.MerchantUiState import org.mifospay.feature.merchants.MerchantViewModel +import org.mifospay.feature.merchants.R import org.mifospay.feature.merchants.navigation.navigateToMerchantTransferScreen @Composable @@ -91,43 +80,37 @@ internal fun MerchantScreen( onRefresh: () -> Unit, modifier: Modifier = Modifier, ) { - val pullRefreshState = rememberMifosPullToRefreshState( - isRefreshing = isRefreshing, - onRefresh = onRefresh, - ) + val pullRefreshState = rememberPullRefreshState(isRefreshing, onRefresh) - MifosScaffold( - modifier = modifier.fillMaxSize(), - pullToRefreshState = pullRefreshState, + Box( + modifier = modifier + .pullRefresh(pullRefreshState), ) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(it), - contentAlignment = Alignment.Center, - ) { + Column(modifier = Modifier.fillMaxSize()) { when (merchantUiState) { MerchantUiState.Empty -> { EmptyContentScreen( - title = stringResource(Res.string.feature_merchants_empty_no_merchants_title), - subTitle = stringResource(Res.string.feature_merchants_empty_no_merchants_subtitle), + title = stringResource(id = R.string.feature_merchants_empty_no_merchants_title), + subTitle = stringResource(id = R.string.feature_merchants_empty_no_merchants_subtitle), modifier = Modifier, iconTint = MaterialTheme.colorScheme.primary, + iconImageVector = MifosIcons.Info, ) } is MerchantUiState.Error -> { EmptyContentScreen( - title = stringResource(Res.string.feature_merchants_error_oops), - subTitle = stringResource(Res.string.feature_merchants_unexpected_error_subtitle), + title = stringResource(id = R.string.feature_merchants_error_oops), + subTitle = stringResource(id = R.string.feature_merchants_unexpected_error_subtitle), modifier = Modifier, iconTint = MaterialTheme.colorScheme.primary, + iconImageVector = MifosIcons.Info, ) } MerchantUiState.Loading -> { MfLoadingWheel( - contentDesc = stringResource(Res.string.feature_merchants_loading), + contentDesc = stringResource(R.string.feature_merchants_loading), backgroundColor = MaterialTheme.colorScheme.surface, ) } @@ -140,12 +123,17 @@ internal fun MerchantScreen( } } } + PullRefreshIndicator( + refreshing = isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) } } @Composable private fun MerchantScreenContent( - merchantList: List, + merchantList: List, updateQuery: (String) -> Unit, modifier: Modifier = Modifier, ) { @@ -168,9 +156,10 @@ private fun MerchantScreenContent( @Composable private fun MerchantList( - merchantList: List, + merchantList: List, modifier: Modifier = Modifier, ) { + val context = LocalContext.current val clipboardManager = LocalClipboardManager.current val navController = rememberNavController() @@ -184,7 +173,7 @@ private fun MerchantList( savingsWithAssociations = merchantList[index], onMerchantClicked = { navController.navigateToMerchantTransferScreen( - merchantVPA = merchantList[index].accountNo, + merchantVPA = merchantList[index].externalId, merchantName = merchantList[index].clientName, merchantAccountNumber = merchantList[index].accountNo.toString(), ) @@ -196,6 +185,11 @@ private fun MerchantList( }, onMerchantLongPressed = { clipboardManager.setText(AnnotatedString(it ?: "")) + Toast.makeText( + context, + R.string.feature_merchants_vpa_copy_success, + Toast.LENGTH_LONG, + ).show() }, ) } @@ -221,12 +215,12 @@ private fun SearchBarScreen( active = false, onActiveChange = { }, placeholder = { - Text(text = stringResource(Res.string.feature_merchants_search)) + Text(text = stringResource(R.string.feature_merchants_search)) }, leadingIcon = { Icon( imageVector = MifosIcons.Search, - contentDescription = stringResource(Res.string.feature_merchants_search), + contentDescription = stringResource(R.string.feature_merchants_search), ) }, trailingIcon = { @@ -235,14 +229,14 @@ private fun SearchBarScreen( ) { Icon( imageVector = MifosIcons.Close, - contentDescription = stringResource(Res.string.feature_merchants_close), + contentDescription = stringResource(R.string.feature_merchants_close), ) } }, ) {} } -@Preview +@Preview(showBackground = true) @Composable private fun MerchantLoadingPreview() { MifosTheme { @@ -257,7 +251,7 @@ private fun MerchantLoadingPreview() { } } -@Preview +@Preview(showBackground = true) @Composable private fun MerchantListPreview() { MifosTheme { @@ -272,7 +266,7 @@ private fun MerchantListPreview() { } } -@Preview +@Preview(showBackground = true) @Composable private fun MerchantErrorPreview() { MifosTheme { @@ -287,7 +281,7 @@ private fun MerchantErrorPreview() { } } -@Preview +@Preview(showBackground = true) @Composable private fun MerchantEmptyPreview() { MifosTheme { @@ -303,99 +297,29 @@ private fun MerchantEmptyPreview() { } val sampleMerchantList = List(10) { - SavingsWithAssociationsEntity( + SavingsWithAssociations( id = 1L, accountNo = "123456789", - depositType = DepositType( - id = 9994, - code = "iriure", - value = "liber", - ), + depositType = null, + externalId = "EXT987654", clientId = 101, clientName = "Alice Bob", savingsProductId = 2001, savingsProductName = "Premium Savings Account", fieldOfficerId = 501, - status = Status( - id = 1403, - code = "ornatus", - value = "iaculis", - submittedAndPendingApproval = false, - approved = false, - rejected = false, - withdrawnByApplicant = false, - active = false, - closed = false, - prematureClosed = false, - transferInProgress = false, - transferOnHold = false, - matured = false, - ), - timeline = Timeline( - submittedOnDate = listOf(), - submittedByUsername = "Lemuel Solomon", - submittedByFirstname = "Vivian Henson", - submittedByLastname = "Amalia Booker", - approvedOnDate = listOf(), - approvedByUsername = "Helga Randall", - approvedByFirstname = "Terri Ochoa", - approvedByLastname = "Sheryl Cain", - activatedOnDate = listOf(), - activatedByUsername = "Lela Johnston", - activatedByFirstname = "Raymundo Foley", - activatedByLastname = "Deanne Sosa", - ), - currency = Currency( - code = "USD", - name = "Lessie Lindsey", - decimalPlaces = 7322, - inMultiplesOf = 5447, - displaySymbol = "ut", - nameCode = "Angelina Walls", - displayLabel = "iisque", - ), + status = null, + timeline = null, + currency = null, nominalAnnualInterestRate = 3.5, + minRequiredOpeningBalance = 500.0, + lockinPeriodFrequency = 12.0, withdrawalFeeForTransfers = true, allowOverdraft = false, enforceMinRequiredBalance = false, withHoldTax = true, lastActiveTransactionDate = listOf(2024, 3, 24), - summary = Summary( - currency = Currency( - code = "USD", - name = "Kennith Gray", - decimalPlaces = 6021, - inMultiplesOf = 4636, - displaySymbol = "efficiantur", - nameCode = "Gerardo Deleon", - displayLabel = "mollis", - ), - totalDeposits = 18.19, - totalWithdrawals = 20.21, - totalInterestPosted = 6052, - accountBalance = 22.23, - totalOverdraftInterestDerived = 2232, - interestNotPosted = 5113, - availableBalance = 24.25, - ), + dormancyTrackingActive = true, + summary = null, transactions = listOf(), - subStatus = SubStatus( - id = 2838, - code = "nobis", - value = "mi", - none = false, - inactive = false, - dormant = false, - escheat = false, - block = false, - blockCredit = false, - blockDebit = false, - ), - interestCompoundingPeriodType = InterestPeriod(), - interestPostingPeriodType = InterestPeriod(), - interestCalculationType = InterestPeriod(), - interestCalculationDaysInYearType = InterestPeriod(), - lienAllowed = false, - isDormancyTrackingActive = false, ) } diff --git a/feature/merchants/src/commonMain/kotlin/org/mifospay/feature/merchants/ui/MerchantTransferScreen.kt b/feature/merchants/src/commonMain/kotlin/org/mifospay/feature/merchants/ui/MerchantTransferScreen.kt index b31c1e847..077f64682 100644 --- a/feature/merchants/src/commonMain/kotlin/org/mifospay/feature/merchants/ui/MerchantTransferScreen.kt +++ b/feature/merchants/src/commonMain/kotlin/org/mifospay/feature/merchants/ui/MerchantTransferScreen.kt @@ -9,6 +9,7 @@ */ package org.mifospay.feature.merchants.ui +import android.widget.Toast import androidx.annotation.VisibleForTesting import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -17,7 +18,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -41,34 +41,25 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import mobile_wallet.feature.merchants.generated.resources.Res -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_amount -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_credits -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_debits -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_error_oops -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_loading -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_merchant_transaction -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_no_transactions_found -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_other -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_submit -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_transaction_date -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_transaction_id -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_transfer_money_to_this_merchant -import mobile_wallet.feature.merchants.generated.resources.feature_merchants_unexpected_error_subtitle -import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview -import org.jetbrains.compose.ui.tooling.preview.PreviewParameter -import org.jetbrains.compose.ui.tooling.preview.PreviewParameterProvider -import org.koin.compose.viewmodel.koinViewModel +import com.mifospay.core.model.domain.Transaction +import com.mifospay.core.model.domain.TransactionType +import com.mifospay.core.model.domain.client.Client +import com.mifospay.core.model.entity.accounts.savings.SavingAccount +import org.koin.androidx.compose.koinViewModel import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.component.MfOutlinedTextField import org.mifospay.core.designsystem.component.MifosBottomSheet import org.mifospay.core.designsystem.component.MifosButton -import org.mifospay.core.designsystem.component.MifosOutlinedTextField import org.mifospay.core.designsystem.component.MifosScaffold import org.mifospay.core.designsystem.icon.MifosIcons import org.mifospay.core.designsystem.theme.ElectricViolet @@ -76,13 +67,11 @@ import org.mifospay.core.designsystem.theme.MifosTheme import org.mifospay.core.designsystem.theme.creditTextColor import org.mifospay.core.designsystem.theme.debitTextColor import org.mifospay.core.designsystem.theme.otherTextColor -import org.mifospay.core.model.savingsaccount.Currency -import org.mifospay.core.model.savingsaccount.Transaction -import org.mifospay.core.model.savingsaccount.TransactionType import org.mifospay.core.ui.EmptyContentScreen import org.mifospay.core.ui.ErrorScreenContent import org.mifospay.feature.merchants.MerchantTransferUiState import org.mifospay.feature.merchants.MerchantTransferViewModel +import org.mifospay.feature.merchants.R @Composable internal fun MerchantTransferScreenRoute( @@ -92,13 +81,23 @@ internal fun MerchantTransferScreenRoute( viewModel: MerchantTransferViewModel = koinViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val merchantName by viewModel.merchantName.collectAsStateWithLifecycle() + val merchantVPA by viewModel.merchantVPA.collectAsStateWithLifecycle() MerchantTransferScreen( uiState = uiState, - merchantName = "New User", - merchantVPA = "Sample VPA", + merchantName = merchantName, + merchantVPA = merchantVPA, onBackPressed = onBackPressed, - checkBalanceAvailability = { vpa, transferAmount -> }, + checkBalanceAvailability = { vpa, transferAmount -> + viewModel.checkBalanceAvailability( + proceedWithMakeTransferFlow = { externalId, amount -> + proceedWithMakeTransferFlow.invoke(externalId, amount.toString()) + }, + externalId = vpa, + transferAmount = transferAmount.toDoubleOrNull() ?: 0.0, + ) + }, modifier = modifier, ) } @@ -115,22 +114,20 @@ internal fun MerchantTransferScreen( ) { var showBottomSheet by remember { mutableStateOf(true) } var amount by rememberSaveable { mutableStateOf("") } + val context = LocalContext.current MifosScaffold( modifier = modifier, - topBarTitle = stringResource(Res.string.feature_merchants_merchant_transaction), + topBarTitle = R.string.feature_merchants_merchant_transaction, backPress = onBackPressed, - content = { paddingValues -> + scaffoldContent = { paddingValues -> Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center, + modifier = Modifier.padding(paddingValues), ) { when (uiState) { is MerchantTransferUiState.Loading -> { MfLoadingWheel( - contentDesc = stringResource(Res.string.feature_merchants_loading), + contentDesc = stringResource(R.string.feature_merchants_loading), backgroundColor = MaterialTheme.colorScheme.surface, ) } @@ -138,17 +135,18 @@ internal fun MerchantTransferScreen( is MerchantTransferUiState.Error -> { ErrorScreenContent( modifier = Modifier, - title = stringResource(Res.string.feature_merchants_error_oops), - subTitle = stringResource(Res.string.feature_merchants_unexpected_error_subtitle), + title = stringResource(id = R.string.feature_merchants_error_oops), + subTitle = stringResource(id = R.string.feature_merchants_unexpected_error_subtitle), ) } is MerchantTransferUiState.Empty -> { EmptyContentScreen( - title = stringResource(Res.string.feature_merchants_error_oops), - subTitle = stringResource(Res.string.feature_merchants_no_transactions_found), + title = stringResource(id = R.string.feature_merchants_error_oops), + subTitle = stringResource(id = R.string.feature_merchants_no_transactions_found), modifier = Modifier, iconTint = MaterialTheme.colorScheme.onSurface, + iconImageVector = MifosIcons.Info, ) } @@ -156,7 +154,14 @@ internal fun MerchantTransferScreen( TransactionList(uiState.transactionsList) } - is MerchantTransferUiState.InsufficientBalance -> {} + is MerchantTransferUiState.InsufficientBalance -> { + Toast + .makeText( + context, + stringResource(id = R.string.feature_merchants_insufficient_balance), + Toast.LENGTH_SHORT, + ).show() + } } if (showBottomSheet) { @@ -182,7 +187,7 @@ private fun TransactionList( LazyColumn(modifier) { items( items = transactions, - key = { it.transactionId }, + key = { it.transactionId ?: it.transferId }, ) { transaction -> SpecificTransactionItem(transaction) HorizontalDivider() @@ -210,7 +215,7 @@ private fun MerchantBottomSheet( horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = stringResource(Res.string.feature_merchants_transfer_money_to_this_merchant), + text = stringResource(R.string.feature_merchants_transfer_money_to_this_merchant), color = ElectricViolet, style = MaterialTheme.typography.bodyMedium, ) @@ -222,9 +227,9 @@ private fun MerchantBottomSheet( ) Spacer(modifier = Modifier.height(24.dp)) - MifosOutlinedTextField( + MfOutlinedTextField( value = amount, - label = stringResource(Res.string.feature_merchants_amount), + label = stringResource(id = R.string.feature_merchants_amount), onValueChange = onAmountChange, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), @@ -241,7 +246,7 @@ private fun MerchantBottomSheet( modifier = Modifier.width(155.dp), ) { Text( - stringResource(Res.string.feature_merchants_submit), + stringResource(id = R.string.feature_merchants_submit), color = Color.White, ) } @@ -312,14 +317,14 @@ private fun SpecificTransactionItem( Column(modifier = modifier.padding(horizontal = 12.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { SpecificTransactionAccountInfo( - amount = transaction.amount.toString(), - accountNo = transaction.accountNo, + account = transaction.transferDetail.fromAccount, + client = transaction.transferDetail.fromClient, modifier = Modifier.weight(1f), ) Icon(imageVector = MifosIcons.SendRightTilted, contentDescription = null) SpecificTransactionAccountInfo( - amount = transaction.amount.toString(), - accountNo = transaction.accountNo, + account = transaction.transferDetail.toAccount, + client = transaction.transferDetail.toClient, modifier = Modifier.weight(1f), ) } @@ -331,20 +336,20 @@ private fun SpecificTransactionItem( ) { Column { Text( - text = stringResource(Res.string.feature_merchants_transaction_id) + transaction.transactionId, + text = stringResource(id = R.string.feature_merchants_transaction_id) + transaction.transactionId, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.primary, ) Text( - text = stringResource(Res.string.feature_merchants_transaction_date) + transaction.date, + text = stringResource(id = R.string.feature_merchants_transaction_date) + transaction.date, style = MaterialTheme.typography.bodyLarge, ) Text( text = when (transaction.transactionType) { - TransactionType.DEBIT -> stringResource(Res.string.feature_merchants_debits) - TransactionType.CREDIT -> stringResource(Res.string.feature_merchants_credits) - TransactionType.OTHER -> stringResource(Res.string.feature_merchants_other) + TransactionType.DEBIT -> stringResource(id = R.string.feature_merchants_debits) + TransactionType.CREDIT -> stringResource(id = R.string.feature_merchants_credits) + TransactionType.OTHER -> stringResource(id = R.string.feature_merchants_other) }, style = MaterialTheme.typography.bodyLarge, ) @@ -366,25 +371,25 @@ private fun SpecificTransactionItem( @Composable private fun SpecificTransactionAccountInfo( - amount: String, - accountNo: String, + account: SavingAccount, + client: Client, modifier: Modifier = Modifier, accountClicked: (String) -> Unit = {}, ) { Column( modifier = modifier.clickable { - accountClicked(accountNo) + accountClicked(account.accountNo) }, horizontalAlignment = Alignment.CenterHorizontally, ) { Icon(imageVector = MifosIcons.AccountCircle, contentDescription = null) Text( - text = accountNo, + text = client.displayName, style = MaterialTheme.typography.titleSmall, ) Text( - text = amount, + text = account.accountNo, style = MaterialTheme.typography.bodyMedium, ) } @@ -394,7 +399,7 @@ internal class MerchantTransferUiStateProvider : PreviewParameterProvider get() = sequenceOf( - MerchantTransferUiState.Success(arrayListOf()), + MerchantTransferUiState.Success(arrayListOf(Transaction())), MerchantTransferUiState.Error, MerchantTransferUiState.Loading, MerchantTransferUiState.Empty, @@ -402,7 +407,7 @@ internal class MerchantTransferUiStateProvider : PreviewParameterProvider Unit, - onMerchantLongPressed: (String?) -> Unit, +internal fun AccountsItem( + bankAccountDetails: BankAccountDetails, + onAccountClicked: () -> Unit, modifier: Modifier = Modifier, ) { MifosCard( - modifier = modifier.combinedClickable( - onClick = onMerchantClicked, - onLongClick = { - onMerchantLongPressed(savingsWithAssociations.id.toString()) - }, - ), - colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surface), + modifier = modifier.padding(vertical = 15.dp), + onClick = { onAccountClicked.invoke() }, + colors = CardDefaults.cardColors(Color.Transparent), + elevation = 0.dp, ) { Column { Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), ) { - Icon( - painter = painterResource(Res.drawable.feature_merchants_ic_bank), - contentDescription = null, - modifier = Modifier - .align(Alignment.CenterVertically) - .padding(start = 16.dp, end = 16.dp) - .size(39.dp), + Text( + text = bankAccountDetails.accountholderName.toString(), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = bankAccountDetails.branch.toString(), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, ) - - Column { - Text( - text = savingsWithAssociations.clientName, - color = MaterialTheme.colorScheme.onSurface, - ) - Text( - text = savingsWithAssociations.accountNo, - modifier = Modifier.padding(top = 4.dp), - style = styleMedium16sp.copy(mifosText), - ) - } } + Text( + text = bankAccountDetails.bankName.toString(), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), + ) } - HorizontalDivider( - thickness = 1.dp, - modifier = Modifier.padding(8.dp), - ) } } -@Preview +@Preview(showBackground = true) @Composable private fun AccountsItemPreview() { - MerchantsItem( - savingsWithAssociations = sampleMerchantList.first(), - onMerchantClicked = {}, - onMerchantLongPressed = {}, + AccountsItem( + bankAccountDetails = BankAccountDetails("A", "B", "C"), + onAccountClicked = {}, ) } diff --git a/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantTransferViewModel.kt b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantTransferViewModel.kt new file mode 100644 index 000000000..8141528ca --- /dev/null +++ b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantTransferViewModel.kt @@ -0,0 +1,146 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.merchants + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.domain.Transaction +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.mifospay.common.Constants +import org.mifospay.core.data.base.TaskLooper +import org.mifospay.core.data.base.TaskLooper.TaskData +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCase.UseCaseCallback +import org.mifospay.core.data.base.UseCaseFactory +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.account.FetchAccount +import org.mifospay.core.data.domain.usecase.account.FetchAccountTransfer +import org.mifospay.core.data.domain.usecase.history.HistoryContract +import org.mifospay.core.data.domain.usecase.history.TransactionsHistory +import org.mifospay.core.data.repository.local.LocalRepository +import org.mifospay.core.data.util.Constants.FETCH_ACCOUNT_TRANSFER_USECASE +import org.mifospay.core.datastore.PreferencesHelper +import org.mifospay.feature.merchants.MerchantTransferUiState.Loading + +class MerchantTransferViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val localRepository: LocalRepository, + private val preferencesHelper: PreferencesHelper, + private val transactionsHistory: TransactionsHistory, + private val mUseCaseFactory: UseCaseFactory, + private val mFetchAccount: FetchAccount, + private var mTaskLooper: TaskLooper? = null, + private val savedStateHandle: SavedStateHandle, +) : ViewModel(), HistoryContract.TransactionsHistoryAsync { + + val merchantName = savedStateHandle.getStateFlow("merchantName", "") + val merchantVPA = savedStateHandle.getStateFlow("merchantVPA", "") + private val merchantAccountNumber = savedStateHandle.getStateFlow("merchantAccountNumber", "") + + private val state = MutableStateFlow(Loading) + val uiState: StateFlow = state.asStateFlow() + + init { + savedStateHandle.get("merchantAccountNumber")?.let { + transactionsHistory.fetchTransactionsHistory(preferencesHelper.accountId) + } + } + + fun checkBalanceAvailability( + proceedWithMakeTransferFlow: (String, Double) -> Unit, + externalId: String, + transferAmount: Double, + ) { + mUseCaseHandler.execute( + mFetchAccount, + FetchAccount.RequestValues(localRepository.clientDetails.clientId), + object : UseCaseCallback { + override fun onSuccess(response: FetchAccount.ResponseValue) { + if (transferAmount > response.account.balance) { + state.value = MerchantTransferUiState.InsufficientBalance + } else { + proceedWithMakeTransferFlow(externalId, transferAmount) + } + } + + override fun onError(message: String) { + state.value = MerchantTransferUiState.Error + } + }, + ) + } + + override fun onTransactionsFetchCompleted(transactions: List?) { + val specificTransactions = ArrayList() + val merchantAccountNumber = merchantAccountNumber.value + + if (!transactions.isNullOrEmpty()) { + for (i in transactions.indices) { + val transaction = transactions[i] + if (transaction.transferDetail == null && + transaction.transferId != 0L + ) { + val transferId = transaction.transferId + mTaskLooper?.addTask( + useCase = mUseCaseFactory.getUseCase(FETCH_ACCOUNT_TRANSFER_USECASE) + as UseCase, + values = transferId.let { FetchAccountTransfer.RequestValues(it) }, + taskData = TaskData(Constants.TRANSFER_DETAILS, i), + ) + } + } + mTaskLooper?.listen( + object : TaskLooper.Listener { + override fun onTaskSuccess( + taskData: TaskData, + response: R, + ) { + when (taskData.taskName) { + Constants.TRANSFER_DETAILS -> { + val responseValue = response as FetchAccountTransfer.ResponseValue + val index = taskData.taskId + transactions[index].transferDetail = responseValue.transferDetail + } + } + } + + override fun onComplete() { + for (transaction in transactions) { + if (transaction.transferDetail.toAccount + .accountNo == merchantAccountNumber + ) { + specificTransactions.add(transaction) + } + } + if (specificTransactions.size == 0) { + state.value = MerchantTransferUiState.Empty + } else { + state.value = MerchantTransferUiState.Success(specificTransactions) + } + } + + override fun onFailure(message: String?) { + state.value = MerchantTransferUiState.Error + } + }, + ) + } + } +} + +sealed class MerchantTransferUiState { + data object Loading : MerchantTransferUiState() + class Success(val transactionsList: ArrayList) : MerchantTransferUiState() + data object Empty : MerchantTransferUiState() + data object Error : MerchantTransferUiState() + data object InsufficientBalance : MerchantTransferUiState() +} diff --git a/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantViewModel.kt b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantViewModel.kt new file mode 100644 index 000000000..19180991c --- /dev/null +++ b/feature/merchants/src/main/kotlin/org/mifospay/feature/merchants/MerchantViewModel.kt @@ -0,0 +1,149 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.merchants + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifospay.core.model.entity.accounts.savings.SavingsWithAssociations +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.mifospay.core.data.base.TaskLooper +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseFactory +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.account.FetchMerchants +import org.mifospay.core.data.domain.usecase.client.FetchClientDetails +import org.mifospay.core.data.util.Constants + +class MerchantViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val mFetchMerchantsUseCase: FetchMerchants, + private val mUseCaseFactory: UseCaseFactory, + private val mTaskLooper: TaskLooper, + +) : ViewModel() { + + private val _searchQuery = MutableStateFlow("") + private val searchQuery: StateFlow = _searchQuery.asStateFlow() + + private val _merchantUiState = MutableStateFlow(MerchantUiState.Loading) + val merchantUiState: StateFlow = _merchantUiState + + init { + fetchMerchants() + } + + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow get() = _isRefreshing.asStateFlow() + + fun refresh() { + viewModelScope.launch { + _isRefreshing.emit(true) + fetchMerchants() + _isRefreshing.emit(false) + } + } + + val merchantsListUiState: StateFlow = searchQuery + .map { q -> + when (_merchantUiState.value) { + is MerchantUiState.ShowMerchants -> { + val merchantList = + (merchantUiState.value as MerchantUiState.ShowMerchants).merchants + val filterCards = merchantList.filter { + it.externalId.lowercase().contains(q.lowercase()) + it.savingsProductName?.lowercase()?.contains(q.lowercase()) + it.accountNo?.lowercase()?.contains(q.lowercase()) + it.clientName.lowercase().contains(q.lowercase()) + } + MerchantUiState.ShowMerchants(filterCards) + } + + else -> MerchantUiState.ShowMerchants(arrayListOf()) + } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = MerchantUiState.ShowMerchants(arrayListOf()), + ) + + fun updateSearchQuery(query: String) { + _searchQuery.update { query } + } + + private fun fetchMerchants() { + _merchantUiState.value = MerchantUiState.Loading + mUseCaseHandler.execute( + mFetchMerchantsUseCase, + FetchMerchants.RequestValues(), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchMerchants.ResponseValue) { + retrieveMerchantsData(response.savingsWithAssociationsList) + } + + override fun onError(message: String) { + _merchantUiState.value = MerchantUiState.Error(message) + } + }, + ) + } + + fun retrieveMerchantsData( + savingsWithAssociationsList: List, + ) { + for (i in savingsWithAssociationsList.indices) { + mTaskLooper.addTask( + useCase = mUseCaseFactory.getUseCase(Constants.FETCH_CLIENT_DETAILS_USE_CASE) + as UseCase, + values = FetchClientDetails.RequestValues( + savingsWithAssociationsList[i].clientId.toLong(), + ), + taskData = TaskLooper.TaskData("Client data", i), + ) + } + mTaskLooper.listen(object : TaskLooper.Listener { + override fun onTaskSuccess( + taskData: TaskLooper.TaskData, + response: R, + ) { + val responseValue = response as FetchClientDetails.ResponseValue + savingsWithAssociationsList[taskData.taskId].externalId = + responseValue.client.externalId + } + + override fun onComplete() { + if (savingsWithAssociationsList.isEmpty()) { + _merchantUiState.value = MerchantUiState.Empty + } else { + _merchantUiState.value = + MerchantUiState.ShowMerchants(savingsWithAssociationsList) + } + } + + override fun onFailure(message: String?) { + _merchantUiState.value = MerchantUiState.Error(message.toString()) + } + }) + } +} + +sealed class MerchantUiState { + data object Loading : MerchantUiState() + data object Empty : MerchantUiState() + data class Error(val message: String) : MerchantUiState() + data class ShowMerchants(val merchants: List) : MerchantUiState() +} diff --git a/feature/notification/build.gradle.kts b/feature/notification/build.gradle.kts index 7f3d89224..43e9782c5 100644 --- a/feature/notification/build.gradle.kts +++ b/feature/notification/build.gradle.kts @@ -8,22 +8,14 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { - namespace = "org.mifospay.feature.notification" + namespace = "org.mifospay.notification" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - } - } +dependencies { + implementation(projects.libs.pullrefresh) } \ No newline at end of file diff --git a/feature/notification/src/main/kotlin/org/mifospay/feature/notification/NotificationScreen.kt b/feature/notification/src/main/kotlin/org/mifospay/feature/notification/NotificationScreen.kt new file mode 100644 index 000000000..925a7aa28 --- /dev/null +++ b/feature/notification/src/main/kotlin/org/mifospay/feature/notification/NotificationScreen.kt @@ -0,0 +1,201 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.notification + +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +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.MaterialTheme +import androidx.compose.material3.Text +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.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifos.library.pullrefresh.PullRefreshIndicator +import com.mifos.library.pullrefresh.pullRefresh +import com.mifos.library.pullrefresh.rememberPullRefreshState +import com.mifospay.core.model.domain.NotificationPayload +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.component.MifosTopAppBar +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.core.ui.EmptyContentScreen +import org.mifospay.notification.R + +@Composable +fun NotificationScreen( + modifier: Modifier = Modifier, + viewmodel: NotificationViewModel = koinViewModel(), +) { + val uiState by viewmodel.notificationUiState.collectAsStateWithLifecycle() + val isRefreshing by viewmodel.isRefreshing.collectAsStateWithLifecycle() + NotificationScreen( + uiState = uiState, + isRefreshing = isRefreshing, + onRefresh = viewmodel::refresh, + modifier = modifier, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +@VisibleForTesting +internal fun NotificationScreen( + uiState: NotificationUiState, + isRefreshing: Boolean, + onRefresh: () -> Unit, + modifier: Modifier = Modifier, +) { + val pullRefreshState = rememberPullRefreshState(isRefreshing, onRefresh) + Box(modifier.pullRefresh(pullRefreshState)) { + Column( + modifier = Modifier.fillMaxSize(), + ) { + MifosTopAppBar(titleRes = R.string.feature_notification_notifications) + when (uiState) { + is NotificationUiState.Error -> { + EmptyContentScreen( + title = stringResource(id = R.string.feature_notification_error_oops), + subTitle = stringResource(id = R.string.feature_notification_unexpected_error_subtitle), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.primary, + iconImageVector = MifosIcons.RoundedInfo, + ) + } + + NotificationUiState.Loading -> { + MfLoadingWheel( + contentDesc = stringResource(R.string.feature_notification_loading), + backgroundColor = MaterialTheme.colorScheme.surface, + ) + } + + is NotificationUiState.Success -> { + if (uiState.notificationList.isEmpty()) { + EmptyContentScreen( + title = stringResource(R.string.feature_notification_nothing_to_notify), + subTitle = stringResource(R.string.feature_notification_there_is_nothing_to_show), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.onSurface, + iconImageVector = MifosIcons.RoundedInfo, + ) + } else { + LazyColumn { + items(uiState.notificationList) { notification -> + NotificationListItem( + title = notification.title.toString(), + body = notification.body.toString(), + timestamp = notification.timestamp.toString(), + ) + } + } + } + } + } + } + PullRefreshIndicator( + refreshing = isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) + } +} + +@Composable +private fun NotificationListItem( + title: String, + body: String, + timestamp: String, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(8.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(MaterialTheme.colorScheme.primaryContainer), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + ) + Text( + text = body, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + ) + Text( + text = timestamp, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + ) + } + } +} + +internal class NotificationUiStateProvider : + PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + NotificationUiState.Success(sampleNotificationList), + NotificationUiState.Error("Error Occurred"), + NotificationUiState.Loading, + ) +} + +@Preview(showBackground = true) +@Composable +private fun NotificationScreenPreview( + @PreviewParameter(NotificationUiStateProvider::class) + notificationUiState: NotificationUiState, +) { + MifosTheme { + NotificationScreen( + uiState = notificationUiState, + isRefreshing = false, + onRefresh = {}, + ) + } +} + +internal val sampleNotificationList = List(10) { + NotificationPayload("Title", "Body", "TimeStamp") +} diff --git a/feature/notification/src/main/kotlin/org/mifospay/feature/notification/NotificationViewModel.kt b/feature/notification/src/main/kotlin/org/mifospay/feature/notification/NotificationViewModel.kt new file mode 100644 index 000000000..4298f8dd6 --- /dev/null +++ b/feature/notification/src/main/kotlin/org/mifospay/feature/notification/NotificationViewModel.kt @@ -0,0 +1,70 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.notification + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifospay.core.model.domain.NotificationPayload +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.notification.FetchNotifications +import org.mifospay.core.data.repository.local.LocalRepository +import org.mifospay.feature.notification.NotificationUiState.Loading + +class NotificationViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val mLocalRepository: LocalRepository, + private val fetchNotificationsUseCase: FetchNotifications, +) : ViewModel() { + + private val mNotificationUiState: MutableStateFlow = + MutableStateFlow(Loading) + val notificationUiState = mNotificationUiState.asStateFlow() + + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing: StateFlow get() = _isRefreshing.asStateFlow() + + fun refresh() { + viewModelScope.launch { + _isRefreshing.emit(true) + fetchNotifications() + _isRefreshing.emit(false) + } + } + + private fun fetchNotifications() { + mUseCaseHandler.execute( + fetchNotificationsUseCase, + FetchNotifications.RequestValues( + mLocalRepository.clientDetails.clientId, + ), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchNotifications.ResponseValue) { + mNotificationUiState.value = + NotificationUiState.Success(response.notificationPayloadList.orEmpty()) + } + + override fun onError(message: String) { + mNotificationUiState.value = NotificationUiState.Error(message) + } + }, + ) + } +} + +sealed interface NotificationUiState { + data object Loading : NotificationUiState + data class Success(val notificationList: List) : NotificationUiState + data class Error(val message: String) : NotificationUiState +} diff --git a/feature/payments/build.gradle.kts b/feature/payments/build.gradle.kts index 73166ba8b..ae1b679f8 100644 --- a/feature/payments/build.gradle.kts +++ b/feature/payments/build.gradle.kts @@ -8,22 +8,14 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { namespace = "org.mifospay.feature.payments" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - } - } +dependencies { + implementation(libs.accompanist.pager) } \ No newline at end of file diff --git a/feature/payments/src/commonMain/composeResources/values/strings.xml b/feature/payments/src/commonMain/composeResources/values/strings.xml index 819d3ccf9..58b227c27 100644 --- a/feature/payments/src/commonMain/composeResources/values/strings.xml +++ b/feature/payments/src/commonMain/composeResources/values/strings.xml @@ -9,8 +9,8 @@ See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md --> - Virtual Payment Address (VPA) - Mobile Number + Virtual Payment Address (VPA) + Phone Number Receive Show code diff --git a/feature/payments/src/main/kotlin/org/mifospay/feature/payments/RequestScreen.kt b/feature/payments/src/main/kotlin/org/mifospay/feature/payments/RequestScreen.kt new file mode 100644 index 000000000..857adbaeb --- /dev/null +++ b/feature/payments/src/main/kotlin/org/mifospay/feature/payments/RequestScreen.kt @@ -0,0 +1,138 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.payments + +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.MifosTheme + +@Composable +fun RequestScreen( + showQr: (String) -> Unit, + modifier: Modifier = Modifier, + viewModel: TransferViewModel = koinViewModel(), +) { + val vpa by viewModel.vpa.collectAsState() + val mobile by viewModel.mobile.collectAsState() + + RequestScreenContent( + vpa = vpa, + mobile = mobile, + showQr = showQr, + modifier = modifier, + ) +} + +@Composable +@VisibleForTesting +internal fun RequestScreenContent( + vpa: String, + mobile: String, + showQr: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize(), + ) { + Text( + modifier = Modifier.padding(start = 20.dp, top = 30.dp), + text = stringResource(id = R.string.feature_payments_receive), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary, + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, top = 20.dp, end = 15.dp) + .weight(1f), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource( + id = R.string + .feature_payments_virtual_payment_address_vpa, + ), + color = MaterialTheme + .colorScheme.onSurface, + ) + Text( + text = vpa, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + IconButton( + onClick = { showQr(vpa) }, + ) { + Icon( + imageVector = MifosIcons.QrCode, + tint = MaterialTheme.colorScheme.onSurface, + contentDescription = stringResource(id = R.string.feature_payments_show_code), + ) + } + } + + HorizontalDivider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.onSurface.copy + (alpha = 0.13f), + ) + Column(modifier = Modifier.padding(top = 10.dp)) { + Text( + text = stringResource(id = R.string.feature_payments_mobile_number), + color = + MaterialTheme.colorScheme.onSurface, + ) + Text( + text = mobile, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } + } +} + +@Preview(showBackground = true, showSystemUi = true) +@Composable +private fun RequestScreenPreview() { + MifosTheme { + RequestScreenContent( + vpa = "1234567898", + mobile = "9078563421", + showQr = {}, + modifier = Modifier, + ) + } +} diff --git a/feature/payments/src/main/kotlin/org/mifospay/feature/payments/TransferViewModel.kt b/feature/payments/src/main/kotlin/org/mifospay/feature/payments/TransferViewModel.kt new file mode 100644 index 000000000..4fb2fb8e0 --- /dev/null +++ b/feature/payments/src/main/kotlin/org/mifospay/feature/payments/TransferViewModel.kt @@ -0,0 +1,90 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.payments + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import org.mifospay.core.data.base.UseCase.UseCaseCallback +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.account.FetchAccount +import org.mifospay.core.data.repository.local.LocalRepository + +class TransferViewModel( + val mUsecaseHandler: UseCaseHandler, + val localRepository: LocalRepository, + val mFetchAccount: FetchAccount, +) : ViewModel() { + + private val mVpa = MutableStateFlow("") + val vpa: StateFlow = mVpa + + private val mMobile = MutableStateFlow("") + val mobile: StateFlow = mMobile + + private var mTransferUiState = MutableStateFlow(TransferUiState.Loading) + var transferUiState: StateFlow = mTransferUiState + + private var mUpdateSuccess = MutableStateFlow(false) + var updateSuccess: StateFlow = mUpdateSuccess + + init { + fetchVpa() + fetchMobile() + } + + private fun fetchVpa() { + viewModelScope.launch { + mVpa.value = localRepository.clientDetails.externalId.toString() + } + } + + private fun fetchMobile() { + viewModelScope.launch { + mMobile.value = localRepository.preferencesHelper.mobile.toString() + } + } + + fun checkSelfTransfer(externalId: String?): Boolean { + return externalId == localRepository.clientDetails.externalId + } + + fun checkBalanceAvailability(externalId: String, transferAmount: Double) { + mUsecaseHandler.execute( + mFetchAccount, + FetchAccount.RequestValues(localRepository.clientDetails.clientId), + object : UseCaseCallback { + override fun onSuccess(response: FetchAccount.ResponseValue) { + mTransferUiState.value = TransferUiState.Loading + if (transferAmount > response.account.balance) { + mUpdateSuccess.value = true + } else { + mTransferUiState.value = + TransferUiState.ShowClientDetails(externalId, transferAmount) + } + } + + override fun onError(message: String) { + mUpdateSuccess.value = false + } + }, + ) + } +} + +sealed interface TransferUiState { + data object Loading : TransferUiState + data class ShowClientDetails( + val externalId: String, + val transferAmount: Double, + ) : TransferUiState +} diff --git a/feature/profile/build.gradle.kts b/feature/profile/build.gradle.kts index c8316537d..46179aadc 100644 --- a/feature/profile/build.gradle.kts +++ b/feature/profile/build.gradle.kts @@ -9,39 +9,23 @@ */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { namespace = "org.mifospay.feature.profile" - - defaultConfig { - consumerProguardFiles("consumer-rules.pro") + buildFeatures { + buildConfig = true } } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - implementation(libs.coil.kt.compose) - implementation(libs.filekit.core) - implementation(libs.filekit.compose) - } - } -} +dependencies { + implementation(projects.libs.countryCodePicker) -compose.desktop { - application { - nativeDistributions { - linux { - modules("jdk.security.auth") - } - } - } + implementation(libs.qrkit) + + implementation(libs.squareup.okhttp) + + implementation(libs.coil.kt.compose) } \ No newline at end of file diff --git a/feature/profile/src/commonMain/composeResources/values/strings.xml b/feature/profile/src/commonMain/composeResources/values/strings.xml index f89cd59c7..a9b0980f0 100644 --- a/feature/profile/src/commonMain/composeResources/values/strings.xml +++ b/feature/profile/src/commonMain/composeResources/values/strings.xml @@ -25,13 +25,11 @@ Proceed Dismiss Username - First Name - Last Name Phone Number Failed To Save Changes Save Click profile picture Pick profile picture from device Remove profile picture - Updated Successfully + Updated Successfully \ No newline at end of file diff --git a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileScreen.kt b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileScreen.kt new file mode 100644 index 000000000..a882264b7 --- /dev/null +++ b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileScreen.kt @@ -0,0 +1,137 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.profile + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.ui.ProfileImage + +@Composable +fun ProfileRoute( + onLinkAccount: () -> Unit, + modifier: Modifier = Modifier, + viewModel: ProfileViewModel = koinViewModel(), +) { + val profileState by viewModel.profileState.collectAsStateWithLifecycle() + + ProfileScreenContent( + profileState = profileState, + onLinkAccount = onLinkAccount, + modifier = modifier, + ) +} + +@Composable +fun ProfileScreenContent( + profileState: ProfileUiState, + onLinkAccount: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { + when (profileState) { + is ProfileUiState.Loading -> MfLoadingWheel() + + is ProfileUiState.Success -> { + ProfileImage(bitmap = profileState.bitmapImage) + + ProfileDetailsCard( + name = profileState.name ?: "", + email = profileState.email ?: "", + vpa = profileState.vpa ?: "", + mobile = profileState.mobile ?: "", + ) + + MifosButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .height(54.dp), + color = MaterialTheme.colorScheme.primary, + text = { Text(text = stringResource(id = R.string.feature_profile_personal_qr_code)) }, + onClick = { /*TODO*/ }, + leadingIcon = { + Icon( + imageVector = MifosIcons.QrCode, + contentDescription = "Personal QR Code", + ) + }, + ) + + Spacer(modifier = Modifier.height(20.dp)) + + MifosButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .height(55.dp), + color = MaterialTheme.colorScheme.primary, + text = { Text(text = stringResource(id = R.string.feature_profile_link_bank_account)) }, + onClick = onLinkAccount, + leadingIcon = { + Icon(imageVector = MifosIcons.AttachMoney, contentDescription = "") + }, + ) + + Spacer(modifier = Modifier.height(20.dp)) + } + } + } +} + +internal class ProfilePreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + ProfileUiState.Loading, + ProfileUiState.Success( + name = "John Doe", + email = "john.doe@example.com", + vpa = "john@vpa", + mobile = "+1234567890", + bitmapImage = null, + ), + ) +} + +@Preview(showSystemUi = true, showBackground = true) +@Composable +private fun ProfileScreenPreview( + @PreviewParameter(ProfilePreviewProvider::class) profileState: ProfileUiState, +) { + ProfileScreenContent( + profileState = profileState, + onLinkAccount = {}, + ) +} diff --git a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileViewModel.kt b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileViewModel.kt new file mode 100644 index 000000000..dd6d71855 --- /dev/null +++ b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/ProfileViewModel.kt @@ -0,0 +1,96 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.profile + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import okhttp3.ResponseBody +import org.mifospay.common.DebugUtil +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.client.FetchClientImage +import org.mifospay.core.data.repository.local.LocalRepository +import org.mifospay.core.datastore.PreferencesHelper + +class ProfileViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val fetchClientImageUseCase: FetchClientImage, + private val localRepository: LocalRepository, + private val mPreferencesHelper: PreferencesHelper, +) : ViewModel() { + + private val mProfileState = MutableStateFlow(ProfileUiState.Loading) + val profileState: StateFlow get() = mProfileState + + init { + fetchClientImage() + fetchProfileDetails() + } + + private fun fetchClientImage() { + viewModelScope.launch { + mUseCaseHandler.execute( + fetchClientImageUseCase, + FetchClientImage.RequestValues(localRepository.clientDetails.clientId), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchClientImage.ResponseValue) { + val bitmap = convertResponseToBitmap(response.responseBody) + val currentState = mProfileState.value as ProfileUiState.Success + mProfileState.value = currentState.copy(bitmapImage = bitmap) + } + + override fun onError(message: String) { + DebugUtil.log("image", message) + } + }, + ) + } + } + + private fun fetchProfileDetails() { + val name = mPreferencesHelper.fullName ?: "-" + val email = mPreferencesHelper.email ?: "-" + val vpa = mPreferencesHelper.clientVpa ?: "-" + val mobile = mPreferencesHelper.mobile ?: "-" + + mProfileState.value = ProfileUiState.Success( + name = name, + email = email, + vpa = vpa, + mobile = mobile, + ) + } + + private fun convertResponseToBitmap(responseBody: ResponseBody?): Bitmap? { + return try { + responseBody?.byteStream()?.use { inputStream -> + BitmapFactory.decodeStream(inputStream) + } + } catch (e: Exception) { + null + } + } +} + +sealed class ProfileUiState { + data object Loading : ProfileUiState() + data class Success( + val bitmapImage: Bitmap? = null, + val name: String?, + val email: String?, + val vpa: String?, + val mobile: String?, + ) : ProfileUiState() +} diff --git a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileScreen.kt b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileScreen.kt new file mode 100644 index 000000000..5371fa717 --- /dev/null +++ b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileScreen.kt @@ -0,0 +1,469 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.profile.edit + +import android.Manifest +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.component.MifosBottomSheet +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosDialogBox +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.component.MifosTextField +import org.mifospay.core.designsystem.component.PermissionBox +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.core.designsystem.theme.styleMedium16sp +import org.mifospay.feature.profile.R +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@Composable +fun EditProfileScreenRoute( + onBackClick: () -> Unit, + getUri: (context: Context, file: File) -> Uri, + modifier: Modifier = Modifier, + viewModel: EditProfileViewModel = koinViewModel(), +) { + val editProfileUiState by viewModel.editProfileUiState.collectAsStateWithLifecycle() + val updateSuccess by viewModel.updateSuccess.collectAsStateWithLifecycle() + + val context = LocalContext.current + val file = createImageFile(context) + val uri = getUri(context, file) + + EditProfileScreen( + editProfileUiState = editProfileUiState, + updateSuccess = updateSuccess, + onBackClick = onBackClick, + updateEmail = viewModel::updateEmail, + updateMobile = viewModel::updateMobile, + modifier = modifier, + uri = uri, + ) +} + +@Composable +private fun EditProfileScreen( + editProfileUiState: EditProfileUiState, + updateSuccess: Boolean, + onBackClick: () -> Unit, + updateEmail: (String) -> Unit, + updateMobile: (String) -> Unit, + modifier: Modifier = Modifier, + uri: Uri? = null, +) { + var showDiscardChangesDialog by rememberSaveable { mutableStateOf(false) } + val snackbarHostState = remember { SnackbarHostState() } + + Box( + modifier = modifier.fillMaxSize(), + ) { + MifosScaffold( + topBarTitle = R.string.feature_profile_edit_profile, + backPress = { showDiscardChangesDialog = true }, + snackbarHost = { SnackbarHost(snackbarHostState) }, + scaffoldContent = { + when (editProfileUiState) { + is EditProfileUiState.Loading -> { + MfLoadingWheel( + contentDesc = stringResource(R.string.feature_profile_loading), + backgroundColor = MaterialTheme.colorScheme.surface, + ) + } + + is EditProfileUiState.Success -> { + EditProfileScreenContent( + editProfileUiState = editProfileUiState, + updateSuccess = updateSuccess, + contentPadding = it, + updateEmail = updateEmail, + updateMobile = updateMobile, + onBackClick = onBackClick, + uri = uri, + ) + } + } + }, + ) + + MifosDialogBox( + showDialogState = showDiscardChangesDialog, + onDismiss = { showDiscardChangesDialog = false }, + title = R.string.feature_profile_discard_changes, + confirmButtonText = R.string.feature_profile_confirm_text, + onConfirm = { + showDiscardChangesDialog = false + onBackClick.invoke() + }, + dismissButtonText = R.string.feature_profile_dismiss_text, + ) + } +} + +@Suppress("LongMethod") +@Composable +private fun EditProfileScreenContent( + editProfileUiState: EditProfileUiState.Success, + updateSuccess: Boolean, + contentPadding: PaddingValues, + updateEmail: (String) -> Unit, + updateMobile: (String) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + uri: Uri? = null, +) { + var username by rememberSaveable { mutableStateOf(editProfileUiState.username) } + var mobile by rememberSaveable { mutableStateOf(editProfileUiState.mobile) } + var vpa by rememberSaveable { mutableStateOf(editProfileUiState.vpa) } + var email by rememberSaveable { mutableStateOf(editProfileUiState.email) } + var imageUri by rememberSaveable { mutableStateOf(null) } + var showBottomSheet by rememberSaveable { mutableStateOf(false) } + val context = LocalContext.current + val scope = rememberCoroutineScope() + + PermissionBox( + requiredPermissions = if (Build.VERSION.SDK_INT >= 33) { + listOf(Manifest.permission.CAMERA) + } else { + listOf( + Manifest.permission.CAMERA, + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + ) + }, + title = R.string.feature_profile_permission_required, + confirmButtonText = R.string.feature_profile_proceed, + dismissButtonText = R.string.feature_profile_dismiss, + description = R.string.feature_profile_approve_description, + onGranted = { + val cameraLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { + imageUri = uri + } + + val galleryLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent(), + ) { uri: Uri? -> + uri?.let { + imageUri = uri + } + } + + if (showBottomSheet) { + MifosBottomSheet( + content = { + EditProfileBottomSheetContent( + onClickProfilePicture = { + if (uri != null) { + cameraLauncher.launch(uri) + } + showBottomSheet = false + }, + onChangeProfilePicture = { + galleryLauncher.launch("image/*") + showBottomSheet = false + }, + onRemoveProfilePicture = { + imageUri = null + showBottomSheet = false + }, + ) + }, + onDismiss = { showBottomSheet = false }, + ) + } + }, + ) + + LazyColumn( + modifier = modifier + .padding(contentPadding) + .fillMaxSize(), + contentPadding = PaddingValues(top = 30.dp, bottom = 10.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item { + EditProfileScreenImage( + imageUri = imageUri, + onCameraIconClick = { showBottomSheet = true }, + modifier = Modifier.padding(bottom = 5.dp), + ) + } + + item { + MifosTextField( + value = username, + onValueChange = { username = it }, + label = stringResource(id = R.string.feature_profile_username), + textStyle = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurface, + ), + ) + } + + item { + MifosTextField( + value = email, + onValueChange = { email = it }, + label = stringResource(id = R.string.feature_profile_email), + textStyle = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurface, + ), + ) + } + + item { + MifosTextField( + value = vpa, + onValueChange = { vpa = it }, + label = stringResource(id = R.string.feature_profile_vpa), + textStyle = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurface, + ), + ) + } + + item { + MifosTextField( + value = mobile, + onValueChange = { mobile = it }, + label = stringResource(id = R.string.feature_profile_mobile), + textStyle = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurface, + ), + ) + } + + item { + MifosButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .height(54.dp), + color = MaterialTheme.colorScheme.primary, + text = { + Text( + text = stringResource(id = R.string.feature_profile_save), + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + }, + onClick = { + if (isDataSaveNecessary(email, editProfileUiState.email)) { + updateEmail(email) + } + if (isDataSaveNecessary(mobile, editProfileUiState.mobile)) { + updateMobile(mobile) + } + if (updateSuccess) { + // if user details is successfully saved then go back to Profile Activity + // same behaviour as onBackPress, hence reused the callback + Toast.makeText( + context, + context.getString(R.string.feature_profile_updated_sucessfully), + Toast.LENGTH_SHORT, + ).show() + onBackClick.invoke() + } else { + scope.launch { + Toast.makeText( + context, + context.getString(R.string.feature_profile_failed_to_save_changes), + Toast.LENGTH_SHORT, + ).show() + } + } + }, + ) + } + } +} + +private fun isDataSaveNecessary( + input: String, + initialInput: String, +): Boolean = input == initialInput + +@Composable +private fun EditProfileBottomSheetContent( + onClickProfilePicture: () -> Unit, + onChangeProfilePicture: () -> Unit, + onRemoveProfilePicture: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(MaterialTheme.colorScheme.surface) + .padding(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + ImagePickerOption( + label = stringResource(id = R.string.feature_profile_click_profile_picture), + icon = MifosIcons.Camera, + onClick = onClickProfilePicture, + ) + + ImagePickerOption( + label = stringResource(id = R.string.feature_profile_change_profile_picture), + icon = MifosIcons.PhotoLibrary, + onClick = onChangeProfilePicture, + ) + + ImagePickerOption( + label = stringResource(id = R.string.feature_profile_remove_profile_picture), + icon = MifosIcons.Delete, + onClick = onRemoveProfilePicture, + ) + } +} + +@Composable +private fun ImagePickerOption( + label: String, + icon: ImageVector, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Surface( + onClick = onClick, + modifier = modifier + .fillMaxWidth() + .height(48.dp), + shape = RoundedCornerShape(4.dp), + color = Color.Transparent, + ) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + Text(text = label, color = MaterialTheme.colorScheme.onSurface) + } + } +} + +@Composable +internal fun EditProfileSaveButton( + onClick: () -> Unit, + buttonText: Int, + modifier: Modifier = Modifier, +) { + MifosButton( + onClick = onClick, + colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.primary), + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(10.dp), + contentPadding = PaddingValues(12.dp), + ) { + Text( + text = stringResource(id = buttonText), + style = styleMedium16sp.copy(MaterialTheme.colorScheme.onPrimary), + ) + } +} + +private fun createImageFile(context: Context): File { + val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val storageDir: File? = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + return File.createTempFile( + "JPEG_${timeStamp}_", + ".jpg", + storageDir, + ) +} + +internal class EditProfilePreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + EditProfileUiState.Loading, + EditProfileUiState.Success(), + EditProfileUiState.Success( + name = "John Doe", + username = "John", + email = "john@mifos.org", + vpa = "vpa", + mobile = "+1 55557772901", + ), + ) +} + +@Preview(showBackground = true, showSystemUi = true) +@Composable +private fun EditProfileScreenPreview( + @PreviewParameter(EditProfilePreviewProvider::class) editProfileUiState: EditProfileUiState, +) { + MifosTheme { + EditProfileScreen( + editProfileUiState = editProfileUiState, + updateSuccess = false, + onBackClick = {}, + updateEmail = {}, + updateMobile = {}, + uri = null, + ) + } +} diff --git a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileScreenImage.kt b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileScreenImage.kt new file mode 100644 index 000000000..0668f199b --- /dev/null +++ b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileScreenImage.kt @@ -0,0 +1,101 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.profile.edit + +import android.net.Uri +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.MifosBlue +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.core.ui.DevicePreviews +import org.mifospay.feature.profile.R + +@Composable +fun EditProfileScreenImage( + modifier: Modifier = Modifier, + imageUri: Uri? = null, + onCameraIconClick: () -> Unit, +) { + Box( + modifier = modifier.size(150.dp), + ) { + if (imageUri != null) { + AsyncImage( + placeholder = painterResource(id = R.drawable.checker), + error = painterResource(id = R.drawable.checker), + model = imageUri, + modifier = Modifier + .size(150.dp) + .clip(CircleShape) + .border(4.dp, MifosBlue, CircleShape), + contentScale = ContentScale.Crop, + contentDescription = null, + ) + } else { + Image( + painter = painterResource(id = R.drawable.core_ui_ic_dp_placeholder), + contentDescription = "Empty UI", + modifier = Modifier + .size(150.dp) + .clip(CircleShape) + .border(4.dp, MifosBlue, CircleShape), + contentScale = ContentScale.Crop, + ) + } + + IconButton( + onClick = onCameraIconClick, + modifier = Modifier + .offset(y = 12.dp) + .size(36.dp) + .clip(CircleShape) + .align(Alignment.BottomCenter), + colors = IconButtonDefaults.iconButtonColors(MaterialTheme.colorScheme.onPrimaryContainer), + ) { + Icon( + painter = rememberVectorPainter(MifosIcons.Edit2), + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + } +} + +@DevicePreviews +@Composable +private fun EditProfileScreenImagePreview( + modifier: Modifier = Modifier, +) { + MifosTheme { + EditProfileScreenImage( + modifier = modifier, + onCameraIconClick = {}, + ) + } +} diff --git a/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileViewModel.kt b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileViewModel.kt new file mode 100644 index 000000000..e16cd4cad --- /dev/null +++ b/feature/profile/src/main/kotlin/org/mifospay/feature/profile/edit/EditProfileViewModel.kt @@ -0,0 +1,118 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.profile.edit + +import android.graphics.Bitmap +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.domain.user.UpdateUserEntityEmail +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.client.UpdateClient +import org.mifospay.core.data.domain.usecase.user.UpdateUser +import org.mifospay.core.datastore.PreferencesHelper +import org.mifospay.feature.profile.edit.EditProfileUiState.Loading + +class EditProfileViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val mPreferencesHelper: PreferencesHelper, + private val updateUserUseCase: UpdateUser, + private val updateClientUseCase: UpdateClient, +) : ViewModel() { + + private val mEditProfileUiState = MutableStateFlow(Loading) + val editProfileUiState: StateFlow = mEditProfileUiState + + private val mUpdateSuccess = MutableStateFlow(false) + val updateSuccess: StateFlow = mUpdateSuccess + + init { + fetchProfileDetails() + } + + private fun fetchProfileDetails() { + val name = mPreferencesHelper.fullName ?: "-" + val username = mPreferencesHelper.username + val email = mPreferencesHelper.email ?: "-" + val vpa = mPreferencesHelper.clientVpa ?: "-" + val mobile = mPreferencesHelper.mobile ?: "-" + + mEditProfileUiState.value = EditProfileUiState.Success( + name = name, + username = username, + email = email, + vpa = vpa, + mobile = mobile, + ) + } + + fun updateEmail(email: String?) { + mEditProfileUiState.value = Loading + mUseCaseHandler.execute( + updateUserUseCase, + UpdateUser.RequestValues( + UpdateUserEntityEmail( + email, + ), + mPreferencesHelper.userId.toInt(), + ), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: UpdateUser.ResponseValue?) { + mPreferencesHelper.saveEmail(email) + mEditProfileUiState.value = EditProfileUiState.Success(email = email!!) + mUpdateSuccess.value = true + } + + override fun onError(message: String) { + fetchProfileDetails() + mUpdateSuccess.value = false + } + }, + ) + } + + fun updateMobile(fullNumber: String?) { + mEditProfileUiState.value = Loading + mUseCaseHandler.execute( + updateClientUseCase, + UpdateClient.RequestValues( + com.mifospay.core.model.domain.client.UpdateClientEntityMobile( + fullNumber!!, + ), + mPreferencesHelper.clientId.toInt().toLong(), + ), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: UpdateClient.ResponseValue) { + mPreferencesHelper.saveMobile(fullNumber) + mEditProfileUiState.value = EditProfileUiState.Success(mobile = fullNumber) + mUpdateSuccess.value = true + } + + override fun onError(message: String) { + fetchProfileDetails() + mUpdateSuccess.value = false + } + }, + ) + } +} + +sealed interface EditProfileUiState { + data object Loading : EditProfileUiState + data class Success( + val bitmapImage: Bitmap? = null, + val name: String = "", + var username: String = "", + val email: String = "", + val vpa: String = "", + val mobile: String = "", + ) : EditProfileUiState +} diff --git a/feature/qr/build.gradle.kts b/feature/qr/build.gradle.kts index ec3069ca8..0741a395f 100644 --- a/feature/qr/build.gradle.kts +++ b/feature/qr/build.gradle.kts @@ -8,52 +8,18 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { namespace = "org.mifospay.feature.qr" - - defaultConfig { - consumerProguardFiles("consumer-rules.pro") - } -} - -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - implementation(libs.coil.kt.compose) - implementation(libs.filekit.core) - implementation(libs.filekit.compose) - } - - androidMain.dependencies { - implementation(libs.androidx.camera.view) - implementation(libs.androidx.camera.camera2) - implementation(libs.androidx.camera.lifecycle) - implementation(libs.accompanist.permissions) - implementation(libs.mlkit.barcode.scanning) - implementation(libs.guava) - } - - nativeMain.dependencies { - implementation(libs.moko.permission.compose) - } - } } -compose.desktop { - application { - nativeDistributions { - linux { - modules("jdk.security.auth") - } - } - } +dependencies { + implementation(libs.zxing) + implementation(libs.androidx.camera.view) + implementation(libs.androidx.camera.lifecycle) + // TODO:: this should be removed + implementation("com.google.guava:guava:27.0.1-android") } \ No newline at end of file diff --git a/feature/qr/src/main/kotlin/org/mifospay/feature/read/qr/ReadQrScreen.kt b/feature/qr/src/main/kotlin/org/mifospay/feature/read/qr/ReadQrScreen.kt new file mode 100644 index 000000000..4b463cd2c --- /dev/null +++ b/feature/qr/src/main/kotlin/org/mifospay/feature/read/qr/ReadQrScreen.kt @@ -0,0 +1,257 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.read.qr + +import android.Manifest +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.os.Build +import android.util.Log +import android.util.Size +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.VisibleForTesting +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.component.PermissionBox +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.core.ui.EmptyContentScreen +import org.mifospay.feature.qr.R +import org.mifospay.feature.read.qr.utils.QrCodeAnalyzer + +@Composable +internal fun ShowQrScreenRoute( + backPress: () -> Unit, + modifier: Modifier = Modifier, + viewModel: ReadQrViewModel = koinViewModel(), +) { + val uiState = viewModel.readQrUiState.collectAsStateWithLifecycle() + + ReadQrScreen( + uiState = uiState.value, + backPress = backPress, + scanQR = viewModel::scanQr, + modifier = modifier, + ) +} + +@Composable +@VisibleForTesting +internal fun ReadQrScreen( + uiState: ReadQrUiState, + backPress: () -> Unit, + scanQR: (Bitmap) -> Unit, + modifier: Modifier = Modifier, +) { + val lifecycleOwner = LocalLifecycleOwner.current + val context = LocalContext.current + + var isFlashOn by rememberSaveable { mutableStateOf(false) } + var scannedQrcode by rememberSaveable { mutableStateOf("") } + val cameraProviderFuture = rememberSaveable { ProcessCameraProvider.getInstance(context) } + + val galleryLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> + uri?.let { + val bitmap = loadBitmapFromUri(context, uri) + bitmap?.let { scanQR(it) } + } + } + + PermissionBox( + requiredPermissions = if (Build.VERSION.SDK_INT >= 33) { + listOf(Manifest.permission.CAMERA) + } else { + listOf( + Manifest.permission.CAMERA, + Manifest.permission.READ_EXTERNAL_STORAGE, + ) + }, + title = R.string.feature_qr_permission_required, + confirmButtonText = R.string.feature_qr_proceed, + dismissButtonText = R.string.feature_qr_dismiss, + description = R.string.feature_qr_approve_permission_description_camera, + onGranted = { + Box { + MifosScaffold( + topBarTitle = R.string.feature_qr_scan_code, + backPress = backPress, + scaffoldContent = { paddingValues -> + Box(modifier = Modifier.padding(paddingValues)) { + when (uiState) { + is ReadQrUiState.Loading -> { + MfLoadingWheel( + contentDesc = stringResource(R.string.feature_qr_loading), + backgroundColor = MaterialTheme.colorScheme.surface, + ) + } + + is ReadQrUiState.Success -> { + Text("QR Data: ${uiState.qrData}") + } + + is ReadQrUiState.Error -> { + EmptyContentScreen( + title = stringResource(R.string.feature_qr_oops), + subTitle = stringResource(id = R.string.feature_qr_unexpected_error_subtitle), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.onSurface, + iconImageVector = MifosIcons.Info, + ) + } + } + } + }, + modifier = modifier, + ) + + Column( + modifier = Modifier + .fillMaxSize(), + ) { + AndroidView( + factory = { context -> + val previewView = PreviewView(context) + val preview = Preview.Builder().build() + val selector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build() + preview.setSurfaceProvider(previewView.surfaceProvider) + val imageAnalysis = ImageAnalysis.Builder() + .setTargetResolution( + Size( + previewView.width, + previewView.height, + ), + ) + .setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST) + .build() + imageAnalysis.setAnalyzer( + ContextCompat.getMainExecutor(context), + QrCodeAnalyzer { result -> + scannedQrcode = result + }, + ) + try { + cameraProviderFuture.get().bindToLifecycle( + lifecycleOwner, + selector, + preview, + imageAnalysis, + ) + } catch (e: Exception) { + e.printStackTrace() + } + previewView + }, + modifier = Modifier.weight(1f), + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + IconButton(onClick = { isFlashOn = !isFlashOn }) { + Icon( + imageVector = if (isFlashOn) MifosIcons.FlashOff else MifosIcons.FlashOn, + contentDescription = null, + ) + } + + IconButton(onClick = { galleryLauncher.launch("image/*") }) { + Icon(imageVector = MifosIcons.Photo, contentDescription = null) + } + } + } + } + }, + ) +} + +private fun loadBitmapFromUri(context: Context, uri: Uri): Bitmap? { + return try { + val stream = context.contentResolver.openInputStream(uri) + BitmapFactory.decodeStream(stream) + } catch (e: Exception) { + Log.e("Error", e.message.toString()) + null + } +} + +internal class ReadQrUiStateProvider : + PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + ReadQrUiState.Success( + qrData = "This is QR data", + ), + ReadQrUiState.Error, + ReadQrUiState.Loading, + ) +} + +@androidx.compose.ui.tooling.preview.Preview(showSystemUi = true) +@Composable +private fun ShowQrScreenPreview( + @PreviewParameter(ReadQrUiStateProvider::class) + uiState: ReadQrUiState, +) { + MifosTheme { + ReadQrScreen( + uiState = uiState, + backPress = {}, + scanQR = { + Bitmap.createBitmap( + 100, + 100, + Bitmap.Config.ARGB_8888, + ) + }, + ) + } +} diff --git a/feature/qr/src/main/kotlin/org/mifospay/feature/read/qr/ReadQrViewModel.kt b/feature/qr/src/main/kotlin/org/mifospay/feature/read/qr/ReadQrViewModel.kt new file mode 100644 index 000000000..b213b9ffb --- /dev/null +++ b/feature/qr/src/main/kotlin/org/mifospay/feature/read/qr/ReadQrViewModel.kt @@ -0,0 +1,50 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.read.qr + +import android.graphics.Bitmap +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.feature.read.qr.utils.ScanQr + +class ReadQrViewModel( + private val useCaseHandler: UseCaseHandler, + private val scanQrUseCase: ScanQr, +) : ViewModel() { + + private val mReadQrUiState = MutableStateFlow(ReadQrUiState.Loading) + val readQrUiState = mReadQrUiState.asStateFlow() + + fun scanQr(bitmap: Bitmap) { + useCaseHandler.execute( + scanQrUseCase, + ScanQr.RequestValues(bitmap), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: ScanQr.ResponseValue?) { + mReadQrUiState.update { ReadQrUiState.Success(response?.result) } + } + + override fun onError(message: String) { + mReadQrUiState.update { ReadQrUiState.Error } + } + }, + ) + } +} + +sealed class ReadQrUiState { + data class Success(val qrData: String?) : ReadQrUiState() + data object Error : ReadQrUiState() + data object Loading : ReadQrUiState() +} diff --git a/feature/qr/src/main/kotlin/org/mifospay/feature/read/qr/utils/ScanQr.kt b/feature/qr/src/main/kotlin/org/mifospay/feature/read/qr/utils/ScanQr.kt new file mode 100644 index 000000000..82e849658 --- /dev/null +++ b/feature/qr/src/main/kotlin/org/mifospay/feature/read/qr/utils/ScanQr.kt @@ -0,0 +1,51 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.read.qr.utils + +import android.graphics.Bitmap +import com.google.zxing.BinaryBitmap +import com.google.zxing.RGBLuminanceSource +import com.google.zxing.Result +import com.google.zxing.common.HybridBinarizer +import com.google.zxing.qrcode.QRCodeReader +import org.mifospay.core.data.base.UseCase + +class ScanQr : UseCase() { + + override fun executeUseCase(requestValues: RequestValues) { + val bitmap = requestValues.bitmap + try { + val result = decodeQrCode(bitmap) + if (result != null) { + useCaseCallback.onSuccess(ResponseValue(result.text)) + } else { + useCaseCallback.onError("Failed to decode QR code") + } + } catch (e: Exception) { + useCaseCallback.onError("Error decoding QR code: ${e.message}") + } + } + + private fun decodeQrCode(bitmap: Bitmap): Result? { + val width = bitmap.width + val height = bitmap.height + val pixels = IntArray(width * height) + bitmap.getPixels(pixels, 0, width, 0, 0, width, height) + + val source = RGBLuminanceSource(width, height, pixels) + val binaryBitmap = BinaryBitmap(HybridBinarizer(source)) + val reader = QRCodeReader() + + return reader.decode(binaryBitmap) + } + + class RequestValues(val bitmap: Bitmap) : UseCase.RequestValues + class ResponseValue(val result: String) : UseCase.ResponseValue +} diff --git a/feature/receipt/build.gradle.kts b/feature/receipt/build.gradle.kts index b5b19fd01..943600377 100644 --- a/feature/receipt/build.gradle.kts +++ b/feature/receipt/build.gradle.kts @@ -8,23 +8,15 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { - namespace = "org.mifospay.feature.receipt" + namespace = "org.mifospay.receipt" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - implementation(libs.squareup.okio) - } - } +dependencies { + // TODO:: this should be removed + implementation(libs.squareup.okhttp) } \ No newline at end of file diff --git a/feature/receipt/src/commonMain/kotlin/org/mifospay/feature/receipt/di/ReceiptModule.kt b/feature/receipt/src/commonMain/kotlin/org/mifospay/feature/receipt/di/ReceiptModule.kt index 078dc6cd8..2576ecd05 100644 --- a/feature/receipt/src/commonMain/kotlin/org/mifospay/feature/receipt/di/ReceiptModule.kt +++ b/feature/receipt/src/commonMain/kotlin/org/mifospay/feature/receipt/di/ReceiptModule.kt @@ -7,12 +7,13 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.feature.receipt.di - -import org.koin.core.module.dsl.viewModelOf -import org.koin.dsl.module -import org.mifospay.feature.receipt.ReceiptViewModel +plugins { + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) +} -val ReceiptModule = module { - viewModelOf(::ReceiptViewModel) +android { + namespace = "org.mifospay.feature.search" } + +dependencies { } diff --git a/feature/receipt/src/main/kotlin/org/mifospay/feature/receipt/ReceiptScreen.kt b/feature/receipt/src/main/kotlin/org/mifospay/feature/receipt/ReceiptScreen.kt new file mode 100644 index 000000000..c4a10b1bc --- /dev/null +++ b/feature/receipt/src/main/kotlin/org/mifospay/feature/receipt/ReceiptScreen.kt @@ -0,0 +1,620 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.receipt + +import android.Manifest +import android.content.ActivityNotFoundException +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.os.Build +import android.widget.Toast +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.FileProvider +import com.mifospay.core.model.domain.Transaction +import com.mifospay.core.model.domain.TransactionType +import com.mifospay.core.model.entity.accounts.savings.TransferDetail +import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel +import org.mifospay.common.Constants +import org.mifospay.core.designsystem.component.FloatingActionButtonContent +import org.mifospay.core.designsystem.component.MifosOverlayLoadingWheel +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.component.PermissionBox +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.core.ui.DevicePreviews +import org.mifospay.receipt.R +import java.io.File + +@Composable +internal fun ReceiptScreenRoute( + openPassCodeActivity: () -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + viewModel: ReceiptViewModel = koinViewModel(), +) { + /** + * This function serves as the main entry point for the Receipt screen UI. + * It collects the receiptUiState and fileState from the ViewModel and + * calls the ReceiptScreen function, passing the collected states and + * other necessary parameters. + */ + val receiptUiState by viewModel.receiptUiState.collectAsState() + val fileState by viewModel.fileState.collectAsState() + + ReceiptScreen( + uiState = receiptUiState, + viewFileState = fileState, + downloadReceipt = viewModel::downloadReceipt, + openPassCodeActivity = openPassCodeActivity, + onBackClick = onBackClick, + modifier = modifier, + ) +} + +@Composable +@VisibleForTesting +internal fun ReceiptScreen( + uiState: ReceiptUiState, + viewFileState: PassFileState, + downloadReceipt: (String) -> Unit, + openPassCodeActivity: () -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + /** + * This function renders the UI based on the ReceiptUiState and PassFileState. + * The UI is rendered based on the ReceiptUiState using a when expression. + */ + val context = LocalContext.current + + Box( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + ) { + when (uiState) { + ReceiptUiState.Loading -> { + MifosOverlayLoadingWheel(contentDesc = stringResource(R.string.feature_receipt_loading)) + } + + ReceiptUiState.OpenPassCodeActivity -> { + openPassCodeActivity.invoke() + } + + is ReceiptUiState.Error -> { + val message = uiState.message + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + + is ReceiptUiState.Success -> { + ReceiptScreenContent( + transaction = uiState.transaction, + transferDetail = uiState.transferDetail, + receiptLink = uiState.receiptLink, + downloadData = downloadReceipt, + file = viewFileState.file, + onBackClick = onBackClick, + ) + } + } + } +} + +/** + * The following function renders the actual content of the Receipt screen. + * It includes components like MifosScaffold, SnackbarHost, PermissionBox, + * and various UI elements like Text, Image, and Icon. + */ +@Composable +private fun ReceiptScreenContent( + transaction: Transaction, + transferDetail: TransferDetail, + receiptLink: String, + file: File, + downloadData: (String) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val snackBarHostState = remember { SnackbarHostState() } + var needToHandlePermissions = rememberSaveable { false } + + val floatingActionButtonContent = FloatingActionButtonContent( + onClick = { + if (file.exists()) { + openReceiptFile(context, file) + } else { + needToHandlePermissions = true + } + }, + contentColor = MaterialTheme.colorScheme.onSurface, + content = { + Icon( + painter = painterResource(id = R.drawable.feature_receipt_ic_download), + contentDescription = stringResource(R.string.feature_receipt_downloading_receipt), + ) + }, + ) + + if (needToHandlePermissions) { + PermissionBox( + requiredPermissions = if (Build.VERSION.SDK_INT >= 33) { + listOf(Manifest.permission.READ_MEDIA_IMAGES) + } else { + listOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + ) + }, + title = R.string.feature_receipt_approve_permission_storage, + confirmButtonText = R.string.feature_receipt_proceed, + dismissButtonText = R.string.feature_receipt_dismiss, + description = R.string.feature_receipt_approve_permission_storage_receiptDescription, + onGranted = { + downloadData(transaction.transactionId.toString()) + LaunchedEffect(Unit) { + scope.launch { + val userAction = snackBarHostState.showSnackbar( + message = R.string.feature_receipt_download_complete.toString(), + actionLabel = R.string.feature_receipt_view_Receipt.toString(), + duration = SnackbarDuration.Indefinite, + withDismissAction = true, + ) + when (userAction) { + SnackbarResult.ActionPerformed -> { + openReceiptFile(context, file) + } + + SnackbarResult.Dismissed -> {} + } + } + } + }, + ) + } + + MifosScaffold( + topBarTitle = R.string.feature_receipt_receipt, + backPress = onBackClick, + floatingActionButtonContent = floatingActionButtonContent, + snackbarHost = { + SnackbarHost(hostState = snackBarHostState) + }, + scaffoldContent = { contentPadding -> + Box( + modifier = Modifier + .padding(contentPadding) + .fillMaxSize(), + ) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()), + ) { + Image( + painter = painterResource(id = R.drawable.feature_receipt_mifospay_round_logo), + contentDescription = stringResource(R.string.feature_receipt_pan_id), + modifier = Modifier + .size(120.dp) + .align(Alignment.CenterHorizontally), + ) + ReceiptHeaderBody(transaction, transferDetail) + Spacer(modifier = Modifier.size(height = 15.dp, width = 14.dp)) + ReceiptDetailsBody(transaction, transferDetail, receiptLink) + } + } + }, + modifier = modifier, + ) +} + +/** + * This function copies the given receiptLink to the system clipboard + * and displays a snackbar message to indicate successful copying. + * Used in ReceiptLinkActions. + */ +private fun copyToClipboard( + context: Context, + receiptLink: String, +) { + val clipboardManager = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText( + Constants.UNIQUE_RECEIPT_LINK, + receiptLink.trim { it <= ' ' }, + ) + clipboardManager.setPrimaryClip(clip) + onShowSnackbar(R.string.feature_receipt_unique_receipt_link_copied_to_clipboard, context) +} + +/** + * This function displays a toast message with the provided string resource. + * We will use onShowSnackbar(global Snackbar) when its added in navigation graph + */ +private fun onShowSnackbar(string: Int, context: Context) { + // onShowSnackbar(string,string) + Toast.makeText( + context, + string, + Toast.LENGTH_SHORT, + ).show() +} + +/** + * This function shares the given shareMessage using an Intent and is called in ReceiptLinkActions. + * It displays a chooser to select the app for sharing. + */ +private fun shareReceiptMessage( + shareMessage: String, + context: Context, +) { + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, shareMessage) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + val chooserIntent = + Intent.createChooser(intent, context.getString(R.string.feature_receipt_share_receipt)) + + try { + context.startActivity(chooserIntent) + } catch (e: ActivityNotFoundException) { + onShowSnackbar(R.string.feature_receipt_sharing_link_failed, context) + } +} + +/** + * This function opens the given receipt file pdf using an Intent and is called in ReceiptScreen. + * It displays a chooser to select the app for opening the file. + */ +private fun openReceiptFile( + context: Context, + file: File, +) { + val data = FileProvider.getUriForFile( + context, + "org.mifospay.provider", + file, + ) + var intent: Intent? = Intent(Intent.ACTION_VIEW) + .setDataAndType(data, "application/pdf") + intent = Intent.createChooser(intent, context.getString(R.string.feature_receipt_view_receipt)) + + try { + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + onShowSnackbar(R.string.feature_receipt_opening_pdf_failed, context) + } +} + +@Composable +private fun ReceiptHeaderBody( + transaction: Transaction, + transferDetail: TransferDetail, + modifier: Modifier = Modifier, +) { + Column(modifier.fillMaxWidth()) { + val centerWithPaddingModifier = Modifier + .padding(horizontal = 8.dp) + .align(Alignment.CenterHorizontally) + + Text( + text = transaction.amount.toString(), + style = TextStyle( + MaterialTheme.colorScheme.onSurface, + MaterialTheme.typography.headlineLarge.fontSize, + ), + modifier = centerWithPaddingModifier.padding(top = 10.dp), + ) + + Text( + text = when (transaction.transactionType) { + TransactionType.DEBIT -> stringResource(R.string.feature_receipt_paid_to) + TransactionType.CREDIT -> stringResource(R.string.feature_receipt_credited_by) + TransactionType.OTHER -> stringResource(R.string.feature_receipt_other) + }, + color = when (transaction.transactionType) { + TransactionType.DEBIT -> Color.Red + TransactionType.CREDIT -> Color.Cyan + TransactionType.OTHER -> Color.Black + }, + style = MaterialTheme.typography.bodyLarge, + modifier = centerWithPaddingModifier.padding(top = 8.dp), + ) + + Text( + text = + if (transaction.transactionType == TransactionType.DEBIT) { + transferDetail.toClient.displayName + } else { + transferDetail.fromClient.displayName + }, + fontSize = 20.sp, + modifier = centerWithPaddingModifier.padding(top = 8.dp), + ) + } +} + +@Composable +private fun ReceiptDetailsBody( + transaction: Transaction, + transferDetail: TransferDetail, + receiptLink: String, + modifier: Modifier = Modifier, +) { + /** + * This function renders the transaction details and receipt link section, + * displaying information such as transaction ID, date, account details, and the + * receipt link. + */ + Column( + modifier = modifier + .padding(horizontal = 30.dp) + .fillMaxWidth(), + ) { + Text( + text = stringResource(R.string.feature_receipt_transaction_id), + style = TextStyle( + Color.Gray, + MaterialTheme.typography.bodyLarge.fontSize, + ), + ) + Text( + text = transaction.transactionId.toString(), + style = TextStyle( + MaterialTheme.colorScheme.onSurface, + MaterialTheme.typography.bodyLarge.fontSize, + ), + ) + + Spacer(modifier = Modifier.size(height = 30.dp, width = 14.dp)) + + Text( + text = stringResource(R.string.feature_receipt_transaction_date), + style = TextStyle( + Color.Gray, + MaterialTheme.typography.bodyLarge.fontSize, + ), + ) + Text( + text = transaction.date.toString(), + style = TextStyle( + MaterialTheme.colorScheme.onSurface, + MaterialTheme.typography.bodyLarge.fontSize, + ), + ) + + Spacer(modifier = Modifier.size(height = 30.dp, width = 14.dp)) + + Text( + text = stringResource(R.string.feature_receipt_to), + style = TextStyle( + Color.Gray, + MaterialTheme.typography.bodyLarge.fontSize, + ), + ) + Text( + text = stringResource(R.string.feature_receipt_name) + transferDetail.toClient.displayName, + style = TextStyle( + MaterialTheme.colorScheme.onSurface, + MaterialTheme.typography.bodyLarge.fontSize, + ), + ) + Text( + text = stringResource(R.string.feature_receipt_account_no) + transferDetail.toAccount.accountNo, + style = TextStyle( + MaterialTheme.colorScheme.onSurface, + MaterialTheme.typography.bodyLarge.fontSize, + ), + ) + + Spacer(modifier = Modifier.size(height = 30.dp, width = 14.dp)) + + Text( + text = stringResource(R.string.feature_receipt_from), + style = TextStyle( + Color.Gray, + MaterialTheme.typography.bodyLarge.fontSize, + ), + ) + Text( + text = stringResource(R.string.feature_receipt_name) + transferDetail.fromClient.displayName, + style = TextStyle( + MaterialTheme.colorScheme.onSurface, + MaterialTheme.typography.bodyLarge.fontSize, + ), + ) + Text( + text = stringResource(R.string.feature_receipt_account_no) + transferDetail.fromAccount.accountNo, + style = TextStyle( + MaterialTheme.colorScheme.onSurface, + MaterialTheme.typography.bodyLarge.fontSize, + ), + ) + + Spacer(modifier = Modifier.size(height = 30.dp, width = 14.dp)) + + Text( + text = stringResource(R.string.feature_receipt_unique_receipt_link), + style = TextStyle( + Color.Gray, + MaterialTheme.typography.bodyLarge.fontSize, + ), + ) + + ReceiptLinkActions(transferDetail, receiptLink) + } +} + +@Composable +private fun ReceiptLinkActions( + transferDetail: TransferDetail, + receiptLink: String, + modifier: Modifier = Modifier, +) { + /** + * This function renders the copy and share icons at the bottom of the screen, + * allowing users to copy the receipt link or share the receipt message. + */ + val context = LocalContext.current + val prepareShareMessage = + Constants.RECEIPT_SHARING_MESSAGE + transferDetail.fromClient.displayName + + Constants.TO + + transferDetail.toClient.displayName + + Constants.COLON + + receiptLink.trim { it <= ' ' } + + Row( + modifier = modifier + .fillMaxWidth(), + ) { + Text( + text = receiptLink, + style = TextStyle( + MaterialTheme.colorScheme.primary, + MaterialTheme.typography.bodyMedium.fontSize, + ), + ) + + Spacer(modifier = Modifier.size(height = 0.dp, width = 5.dp)) + + Icon( + MifosIcons.Copy, + contentDescription = stringResource(R.string.feature_receipt_copy_link), + modifier = Modifier + .size(25.dp) + .clickable { + copyToClipboard(context, receiptLink) + }, + tint = MaterialTheme.colorScheme.onSurface, + ) + + Icon( + MifosIcons.Share, + contentDescription = stringResource(R.string.feature_receipt_share_receipt), + modifier = Modifier + .padding(horizontal = 10.dp) + .size(25.dp) + .clickable { + shareReceiptMessage(prepareShareMessage, context) + }, + tint = MaterialTheme.colorScheme.onSurface, + ) + } +} + +@DevicePreviews +@Composable +private fun MultiScreenReceiptPreviewWithDummyData() { + ReceiptScreenContent( + transaction = Transaction( + "12345", + 12345, + 12345, + 312.0, + "01/04/2024", + com.mifospay.core.model.domain.Currency(), + TransactionType.DEBIT, + 12345, + TransferDetail(), + "12345", + ), + transferDetail = TransferDetail(), + receiptLink = "https://receipt.mifospay.com/12345", + downloadData = {}, + file = File("/path/to/receipt.pdf"), + onBackClick = {}, + ) +} + +@Preview(showBackground = true) +@Composable +private fun ReceiptPreviewWithLoading() { + MifosTheme { + ReceiptScreen( + uiState = ReceiptUiState.Loading, + viewFileState = PassFileState(file = File(" ")), + downloadReceipt = {}, + openPassCodeActivity = {}, + onBackClick = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ReceiptPreviewWithErrorMessage() { + MifosTheme { + ReceiptScreen( + uiState = ReceiptUiState.Error(stringResource(R.string.feature_receipt_error_specific_transactions)), + viewFileState = PassFileState(file = File(" ")), + downloadReceipt = {}, + openPassCodeActivity = {}, + onBackClick = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ReceiptPreviewWithSuccess() { + MifosTheme { + ReceiptScreen( + uiState = ReceiptUiState.Success( + Transaction(), + TransferDetail(), + receiptLink = "https://receipt.mifospay.com/12345", + ), + viewFileState = PassFileState(file = File(" ")), + downloadReceipt = {}, + openPassCodeActivity = {}, + onBackClick = {}, + ) + } +} diff --git a/feature/receipt/src/main/kotlin/org/mifospay/feature/receipt/ReceiptViewModel.kt b/feature/receipt/src/main/kotlin/org/mifospay/feature/receipt/ReceiptViewModel.kt new file mode 100644 index 000000000..6769ac271 --- /dev/null +++ b/feature/receipt/src/main/kotlin/org/mifospay/feature/receipt/ReceiptViewModel.kt @@ -0,0 +1,167 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.receipt + +import android.net.Uri +import android.os.Environment +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.domain.Transaction +import com.mifospay.core.model.entity.accounts.savings.TransferDetail +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import okhttp3.ResponseBody +import org.mifospay.common.Constants +import org.mifospay.common.FileUtils +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.account.DownloadTransactionReceipt +import org.mifospay.core.data.domain.usecase.account.FetchAccountTransaction +import org.mifospay.core.data.domain.usecase.account.FetchAccountTransfer +import org.mifospay.core.datastore.PreferencesHelper +import java.io.File + +class ReceiptViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val preferencesHelper: PreferencesHelper, + private val downloadTransactionReceiptUseCase: DownloadTransactionReceipt, + private val fetchAccountTransactionUseCase: FetchAccountTransaction, + private val fetchAccountTransferUseCase: FetchAccountTransfer, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + private val mReceiptState = MutableStateFlow(ReceiptUiState.Loading) + val receiptUiState: StateFlow = mReceiptState.asStateFlow() + + private val mFileState = MutableStateFlow(PassFileState()) + val fileState: StateFlow = mFileState.asStateFlow() + + init { + savedStateHandle.get("uri")?.let { + getTransactionData(Uri.parse(it)) + } + } + + fun downloadReceipt(transactionId: String?) { + mUseCaseHandler.execute( + downloadTransactionReceiptUseCase, + DownloadTransactionReceipt.RequestValues(transactionId), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: DownloadTransactionReceipt.ResponseValue) { + val filename = Constants.RECEIPT + transactionId + Constants.PDF + writeReceiptToPDF(response.responseBody, filename) + } + + override fun onError(message: String) { + mReceiptState.value = ReceiptUiState.Error(message) + } + }, + ) + } + + fun writeReceiptToPDF(responseBody: ResponseBody?, filename: String) { + val mifosDirectory = File( + Environment.getExternalStorageDirectory(), + Constants.MIFOSPAY, + ) + if (!mifosDirectory.exists()) { + mifosDirectory.mkdirs() + } + val documentFile = File(mifosDirectory.path, filename) + if (FileUtils.writeInputStreamDataToFile(responseBody!!.byteStream(), documentFile)) { + mFileState.value = PassFileState(documentFile) + } + } + + private fun getTransactionData(data: Uri?) { + if (data != null) { + val params = data.pathSegments + val transactionId = params.getOrNull(0) + val receiptLink = data.toString() + fetchTransaction(transactionId, receiptLink) + } + } + + private fun fetchTransaction(transactionId: String?, receiptLink: String?) { + val accountId = preferencesHelper.accountId + + if (transactionId != null) { + mUseCaseHandler.execute( + fetchAccountTransactionUseCase, + FetchAccountTransaction.RequestValues(accountId, transactionId.toLong()), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchAccountTransaction.ResponseValue) { + if (receiptLink != null) { + fetchTransfer( + response.transaction, + response.transaction.transferId, + receiptLink, + ) + } + } + + override fun onError(message: String) { + if (message == Constants.UNAUTHORIZED_ERROR) { + mReceiptState.value = ReceiptUiState.OpenPassCodeActivity + } else { + mReceiptState.value = ReceiptUiState.Error(message) + } + } + }, + ) + } + } + + fun fetchTransfer( + transaction: Transaction, + transferId: Long, + receiptLink: String, + ) { + mUseCaseHandler.execute( + fetchAccountTransferUseCase, + FetchAccountTransfer.RequestValues(transferId), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchAccountTransfer.ResponseValue?) { + if (response != null) { + mReceiptState.value = + ReceiptUiState.Success( + transaction, + response.transferDetail, + receiptLink, + ) + } + } + + override fun onError(message: String) { + mReceiptState.value = ReceiptUiState.Error(message) + } + }, + ) + } +} + +data class PassFileState( + val file: File = File(""), +) + +sealed interface ReceiptUiState { + data class Success( + val transaction: Transaction, + val transferDetail: TransferDetail, + val receiptLink: String, + ) : ReceiptUiState + + data object OpenPassCodeActivity : ReceiptUiState + data class Error( + val message: String, + ) : ReceiptUiState + + data object Loading : ReceiptUiState +} diff --git a/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/GenerateQr.kt b/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/GenerateQr.kt new file mode 100644 index 000000000..bec299b65 --- /dev/null +++ b/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/GenerateQr.kt @@ -0,0 +1,284 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.request.money + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.RectF +import androidx.core.content.ContextCompat +import com.google.zxing.EncodeHintType +import com.google.zxing.WriterException +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import com.google.zxing.qrcode.encoder.ByteMatrix +import com.google.zxing.qrcode.encoder.Encoder +import com.google.zxing.qrcode.encoder.QRCode +import org.mifospay.core.data.base.UseCase +import java.util.Base64 +import java.util.EnumMap + +// Taken reference from this article +// https://ihareshvaghela.medium.com/generating-dotted-qr-codes-in-android-using-zxing-library-b02e824c895c + +class GenerateQr(private val context: Context) : UseCase< + GenerateQr.RequestValues, + GenerateQr + .ResponseValue?, + >() { + override fun executeUseCase(requestValues: RequestValues) { + try { + val bitmap = encodeAsBitmap(makeUpiString(requestValues.data), getLogoBitmap()) + useCaseCallback.onSuccess(ResponseValue(bitmap)) + } catch (e: WriterException) { + useCaseCallback.onError("Failed to write data to QR") + } + } + + private fun makeUpiString(requestQrData: RequestQrData): String { + val requestPaymentString = "upi://pay" + + "?pa=${requestQrData.vpaId}" + + "&am=${requestQrData.amount}" + + "&pn=${requestQrData.name}" + + "&cu=${requestQrData.currency}" + + "&mode=02" + + "&s=000000" + val sign = + Base64.getEncoder().encodeToString(requestPaymentString.toByteArray(Charsets.UTF_8)) + return "$requestPaymentString&sign=$sign" + } + + @Throws(WriterException::class) + private fun encodeAsBitmap(str: String, logo: Bitmap?): Bitmap { + val encodingHints: MutableMap = + EnumMap(com.google.zxing.EncodeHintType::class.java) + encodingHints[EncodeHintType.CHARACTER_SET] = "UTF-8" + val code: QRCode = Encoder.encode(str, ErrorCorrectionLevel.H, encodingHints) + return renderQRImage(code, logo) + } + + // Function to render QR code + private fun renderQRImage(code: QRCode, logo: Bitmap?): Bitmap { + val bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = BLUE + } + + val input = code.matrix + val inputWidth = input.width + val inputHeight = input.height + val qrWidth = inputWidth + (QUIET_ZONE * 2) + val qrHeight = inputHeight + (QUIET_ZONE * 2) + val outputWidth = WIDTH.coerceAtLeast(qrWidth) + val outputHeight = HEIGHT.coerceAtLeast(qrHeight) + val multiple = (outputWidth / qrWidth).coerceAtMost(outputHeight / qrHeight) + val leftPadding = (outputWidth - (inputWidth * multiple)) / 2 + val topPadding = (outputHeight - (inputHeight * multiple)) / 2 + + drawQrCodeDots( + input, + canvas, + paint, + leftPadding, + topPadding, + inputWidth, + inputHeight, + multiple, + ) + + val circleDiameter = multiple * FINDER_PATTERN_SIZE + drawFinderPatternSquareStyle(canvas, paint, leftPadding, topPadding, circleDiameter) + drawFinderPatternSquareStyle( + canvas, + paint, + leftPadding + (inputWidth - FINDER_PATTERN_SIZE) * multiple, + topPadding, + circleDiameter, + ) + drawFinderPatternSquareStyle( + canvas, + paint, + leftPadding, + topPadding + (inputHeight - FINDER_PATTERN_SIZE) * multiple, + circleDiameter, + ) + + logo?.let { + drawLogo(canvas, bitmap, it) + } + + return bitmap + } + + private fun drawQrCodeDots( + input: ByteMatrix, + canvas: Canvas, + paint: Paint, + leftPadding: Int, + topPadding: Int, + inputWidth: Int, + inputHeight: Int, + multiple: Int, + ) { + val circleSize = (multiple * CIRCLE_SCALE_DOWN_FACTOR).toInt() + for (inputY in 0 until inputHeight) { + var outputY = topPadding + outputY += multiple * inputY + for (inputX in 0 until inputWidth) { + var outputX = leftPadding + outputX += multiple * inputX + if (input.get(inputX, inputY).toInt() == 1 && + !isFinderPattern(inputX, inputY, inputWidth, inputHeight) + ) { + canvas.drawCircle( + (outputX + multiple / 2).toFloat(), + (outputY + multiple / 2).toFloat(), + circleSize.toFloat() / 2f, + paint, + ) + } + } + } + } + + private fun isFinderPattern( + inputX: Int, + inputY: Int, + inputWidth: Int, + inputHeight: Int, + ): Boolean { + return ( + inputX <= FINDER_PATTERN_SIZE && inputY <= FINDER_PATTERN_SIZE || + inputX >= inputWidth - FINDER_PATTERN_SIZE && inputY <= FINDER_PATTERN_SIZE || + inputX <= FINDER_PATTERN_SIZE && inputY >= inputHeight - FINDER_PATTERN_SIZE + ) + } + + private fun drawLogo(canvas: Canvas, bitmap: Bitmap, logo: Bitmap) { + val logoSize = (WIDTH * 0.11).toInt() + val centerX = (bitmap.width - logoSize) / 2 + val centerY = (bitmap.height - logoSize) / 2 + val backgroundRadius = (logoSize * 0.75).toFloat() + + val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = ELLIPSE_COLOR + } + + // Draw logo background + canvas.drawCircle( + (centerX + logoSize / 2).toFloat(), + (centerY + logoSize / 2).toFloat(), + backgroundRadius + 10f, + paint, + ) + + paint.color = Color.WHITE + canvas.drawCircle( + (centerX + logoSize / 2).toFloat(), + (centerY + logoSize / 2).toFloat(), + backgroundRadius + 6f, + paint, + ) + + // Draw the logo + val logoRect = Rect(0, 0, logo.width, logo.height) + val destRect = Rect(centerX, centerY, centerX + logoSize, centerY + logoSize) + canvas.drawBitmap(logo, logoRect, destRect, null) + } + + // Function to draw a finder pattern + private fun drawFinderPatternSquareStyle( + canvas: Canvas, + paint: Paint, + x: Int, + y: Int, + squareDiameter: Int, + ) { + val outerRadius = squareDiameter * 0.25f + val middleRadius = squareDiameter * 0.15f + val innerRadius = squareDiameter * 0.1f + val middleSquareScale = 0.7f + val innerSquareScale = 0.45f + + paint.color = PATTERN_COLOR + canvas.drawRoundRect( + RectF( + x.toFloat(), + y.toFloat(), + (x + squareDiameter).toFloat(), + (y + squareDiameter).toFloat(), + ), + outerRadius, + outerRadius, + paint, + ) + + val middleSquareSize = squareDiameter * middleSquareScale + val middleSquareOffset = (squareDiameter - middleSquareSize) / 2 + paint.color = Color.WHITE + canvas.drawRoundRect( + RectF( + (x + middleSquareOffset), + (y + middleSquareOffset), + (x + middleSquareOffset + middleSquareSize), + (y + middleSquareOffset + middleSquareSize), + ), + middleRadius, + middleRadius, + paint, + ) + + val innerSquareSize = squareDiameter * innerSquareScale + val innerSquareOffset = (squareDiameter - innerSquareSize) / 2 + paint.color = PATTERN_COLOR + canvas.drawRoundRect( + RectF( + (x + innerSquareOffset), + (y + innerSquareOffset), + (x + innerSquareOffset + innerSquareSize), + (y + innerSquareOffset + innerSquareSize), + ), + innerRadius, + innerRadius, + paint, + ) + } + + // Function to get logo in the center of QR code + private fun getLogoBitmap(): Bitmap? { + val drawable = ContextCompat.getDrawable(context, R.drawable.logo) + return drawable?.let { + val bitmap = + Bitmap.createBitmap(it.intrinsicWidth, it.intrinsicHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + it.setBounds(0, 0, canvas.width, canvas.height) + it.draw(canvas) + bitmap + } + } + + data class RequestValues(val data: RequestQrData) : UseCase.RequestValues + data class ResponseValue(val bitmap: Bitmap) : UseCase.ResponseValue + companion object { + private const val BLUE = 0xFF0673BA.toInt() // dots color + private const val PATTERN_COLOR = 0xFF6e6e6e.toInt() // corner square color + private const val ELLIPSE_COLOR = 0xFFe9e9e9.toInt() // logo background ellipse color + private const val WIDTH = 500 // width of QR code + private const val HEIGHT = 500 // height of QR code + private const val FINDER_PATTERN_SIZE = 13 // pattern size (in this case corner squares) + private const val CIRCLE_SCALE_DOWN_FACTOR = 1f // size of dots in qr code + private const val QUIET_ZONE = 5 // spacing from all sides for QR code + } +} diff --git a/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/ShowQrContent.kt b/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/ShowQrContent.kt new file mode 100644 index 000000000..3c65d24bd --- /dev/null +++ b/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/ShowQrContent.kt @@ -0,0 +1,121 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.request.money + +import android.graphics.Bitmap +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +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.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosOutlinedButton +import org.mifospay.core.designsystem.theme.MifosBlue + +@Composable +internal fun ShowQrContent( + qrDataBitmap: Bitmap, + showAmountDialog: () -> Unit, + modifier: Modifier = Modifier, + onShare: () -> Unit, +) { + Box( + modifier = modifier + .fillMaxSize() + .background( + MifosBlue, + ) + .padding(25.dp), + ) { + Column( + modifier = Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.weight(1f)) + Column( + modifier = Modifier + .clip(RoundedCornerShape(21.dp)) + .background(Color.White) + .border( + BorderStroke(2.dp, MaterialTheme.colorScheme.primary), + RoundedCornerShape(21.dp), + ) + .padding(vertical = 15.dp) + .aspectRatio(1f), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(id = R.string.feature_request_money_title), + color = MifosBlue, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + ) + AsyncImage( + model = qrDataBitmap, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + ) + } + Spacer(modifier = Modifier.weight(1f)) + Column( + verticalArrangement = Arrangement.spacedBy(15.dp), + ) { + MifosButton( + modifier = Modifier + .fillMaxWidth() + .height(55.dp), + onClick = { showAmountDialog() }, + color = Color.White, + ) { + Text( + text = stringResource(id = R.string.feature_request_money_set_amount), + color = MifosBlue, + ) + } + MifosOutlinedButton( + modifier = Modifier + .fillMaxWidth() + .height(55.dp), + onClick = { onShare() }, + border = BorderStroke( + 1.dp, + Color.White.copy(alpha = 0.3f), + ), + ) { + Text( + text = stringResource(id = R.string.feature_request_money_share), + color = Color.White, + ) + } + } + } + } +} diff --git a/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/ShowQrScreenRoute.kt b/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/ShowQrScreenRoute.kt new file mode 100644 index 000000000..f43a0902e --- /dev/null +++ b/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/ShowQrScreenRoute.kt @@ -0,0 +1,211 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.request.money + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.Intent.createChooser +import android.graphics.Bitmap +import android.net.Uri +import android.util.Log +import android.view.WindowManager +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.core.content.ContextCompat.startActivity +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.MifosBlue +import org.mifospay.core.ui.EmptyContentScreen +import org.mifospay.feature.request.money.util.ImageUtils + +@Composable +internal fun ShowQrScreenRoute( + backPress: () -> Unit, + modifier: Modifier = Modifier, + viewModel: ShowQrViewModel = koinViewModel(), +) { + val uiState by viewModel.showQrUiState.collectAsStateWithLifecycle() + + UpdateBrightness() + + ShowQrScreen( + uiState = uiState, + backPress = backPress, + generateQR = viewModel::generateQr, + modifier = modifier, + ) +} + +@Composable +@VisibleForTesting +internal fun ShowQrScreen( + uiState: ShowQrUiState, + backPress: () -> Unit, + generateQR: (RequestQrData) -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + var amountDialogState by rememberSaveable { mutableStateOf(false) } + var qrBitmap by rememberSaveable { mutableStateOf(null) } + var amount by rememberSaveable { mutableStateOf(null) } + var currency by rememberSaveable { mutableStateOf(context.getString(R.string.feature_request_money_usd)) } + + MifosScaffold( + topBarTitle = R.string.feature_request_money_request, + backPress = backPress, + titleColor = Color.White, + iconTint = Color.White, + scaffoldContent = { paddingValues -> + Box(modifier = Modifier.padding(paddingValues)) { + when (uiState) { + is ShowQrUiState.Loading -> { + MfLoadingWheel( + contentDesc = stringResource(R.string.feature_request_money_loading), + backgroundColor = MaterialTheme.colorScheme.surface, + ) + } + + is ShowQrUiState.Success -> { + if (uiState.qrDataBitmap == null) { + EmptyContentScreen( + title = stringResource(R.string.feature_request_money_nothing_to_notify), + subTitle = stringResource(R.string.feature_request_money_there_is_nothing_to_show), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.onSurface, + iconImageVector = MifosIcons.Info, + ) + } else { + qrBitmap = uiState.qrDataBitmap + ShowQrContent( + qrDataBitmap = uiState.qrDataBitmap, + showAmountDialog = { amountDialogState = true }, + onShare = { + qrBitmap?.let { + Log.d("yesyesyes", it.toString()) + val uri = ImageUtils.saveImage(context = context, bitmap = it) + shareQr(context, uri = uri) + } + }, + ) + } + } + + is ShowQrUiState.Error -> { + EmptyContentScreen( + title = stringResource(id = R.string.feature_request_money_error_oops), + subTitle = stringResource(id = R.string.feature_request_money_unexpected_error_subtitle), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.onSurface, + iconImageVector = MifosIcons.Info, + ) + } + } + } + }, + modifier = modifier.background(MifosBlue), + ) + + if (amountDialogState) { + SetAmountDialog( + dismissDialog = { amountDialogState = false }, + prefilledAmount = amount ?: "", + prefilledCurrency = currency, + confirmAmount = { confirmedAmount, confirmedCurrency -> + amount = if (confirmedAmount == "") { + null + } else { + confirmedAmount + } + currency = confirmedCurrency + generateQR(RequestQrData(amount = amount ?: "", currency = currency)) + amountDialogState = false + }, + ) + } +} + +@Composable +private fun UpdateBrightness() { + val context = LocalContext.current + DisposableEffect(Unit) { + setBrightness(context, isFull = true) + onDispose { + setBrightness(context, isFull = false) + } + } +} + +private fun setBrightness(context: Context, isFull: Boolean) { + val activity = context as? Activity ?: return + val layoutParams: WindowManager.LayoutParams = activity.window.attributes + layoutParams.screenBrightness = + if (isFull) 1f else WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE + activity.window.attributes = layoutParams +} + +private fun shareQr(context: Context, uri: Uri?) { + if (uri != null) { + var intent = Intent(Intent.ACTION_SEND) + intent.putExtra(Intent.EXTRA_STREAM, uri) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.type = "image/png" + intent = createChooser(intent, "Share Qr code") + startActivity(context, intent, null) + } +} + +internal class ShowQrUiStateProvider : + PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + ShowQrUiState.Success( + qrDataBitmap = Bitmap.createBitmap( + 100, + 100, + Bitmap.Config.ARGB_8888, + ), + ), + ShowQrUiState.Error, + ShowQrUiState.Loading, + ) +} + +@Preview(showSystemUi = true) +@Composable +private fun ShowQrScreenPreview( + @PreviewParameter(ShowQrUiStateProvider::class) + uiState: ShowQrUiState, +) { + ShowQrScreen( + uiState = uiState, + backPress = {}, + generateQR = {}, + ) +} diff --git a/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/ShowQrViewModel.kt b/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/ShowQrViewModel.kt new file mode 100644 index 000000000..484002812 --- /dev/null +++ b/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/ShowQrViewModel.kt @@ -0,0 +1,71 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.request.money + +import android.graphics.Bitmap +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.datastore.PreferencesHelper +import org.mifospay.feature.request.money.ShowQrUiState.Loading + +class ShowQrViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val generateQrUseCase: GenerateQr, + private val mPreferencesHelper: PreferencesHelper, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + val vpaId = savedStateHandle.getStateFlow("vpa", "") + + private val mShowQrUiState: MutableStateFlow = MutableStateFlow(Loading) + val showQrUiState get() = mShowQrUiState + + init { + savedStateHandle.get("vpa")?.let { + generateQr() + } + } + + fun generateQr(requestQrData: RequestQrData? = null) { + val requestQr = (requestQrData ?: RequestQrData()).copy( + name = mPreferencesHelper.fullName ?: "", + vpaId = vpaId.value, + ) + + mUseCaseHandler.execute( + generateQrUseCase, + GenerateQr.RequestValues(requestQr), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: GenerateQr.ResponseValue?) { + mShowQrUiState.value = ShowQrUiState.Success(response?.bitmap) + } + + override fun onError(message: String) { + mShowQrUiState.value = ShowQrUiState.Error + } + }, + ) + } +} + +data class RequestQrData( + val amount: String = "", + val vpaId: String = "", + val currency: String = "USD", + val name: String = "", +) + +sealed class ShowQrUiState { + data class Success(val qrDataBitmap: Bitmap?) : ShowQrUiState() + data object Error : ShowQrUiState() + data object Loading : ShowQrUiState() +} diff --git a/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/util/ImageUtils.kt b/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/util/ImageUtils.kt new file mode 100644 index 000000000..f014b0b6d --- /dev/null +++ b/feature/request-money/src/main/kotlin/org/mifospay/feature/request/money/util/ImageUtils.kt @@ -0,0 +1,41 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.request.money.util + +import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.util.Log +import androidx.core.content.FileProvider +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + +object ImageUtils { + fun saveImage(context: Context, bitmap: Bitmap): Uri? { + val imagesFolder = File(context.cacheDir, "codes") + var uri: Uri? = null + try { + imagesFolder.mkdirs() + val file = File(imagesFolder, "shared_code.png") + val stream = FileOutputStream(file) + bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream) + stream.flush() + stream.close() + uri = FileProvider.getUriForFile( + context, + context.packageName + ".provider", file, + ) + } catch (e: IOException) { + Log.d("Error", e.message.toString()) + } + return uri + } +} diff --git a/feature/savedcards/build.gradle.kts b/feature/savedcards/build.gradle.kts index 185e5ad9a..68361a5c3 100644 --- a/feature/savedcards/build.gradle.kts +++ b/feature/savedcards/build.gradle.kts @@ -8,23 +8,12 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { - namespace = "org.mifospay.feature.savedcards" + namespace = "org.mifospay.savedcards" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - implementation(libs.constraint.layout) - } - } -} \ No newline at end of file +dependencies {} \ No newline at end of file diff --git a/feature/savedcards/src/main/kotlin/org/mifospay/feature/savedcards/CardsScreen.kt b/feature/savedcards/src/main/kotlin/org/mifospay/feature/savedcards/CardsScreen.kt new file mode 100644 index 000000000..c50549905 --- /dev/null +++ b/feature/savedcards/src/main/kotlin/org/mifospay/feature/savedcards/CardsScreen.kt @@ -0,0 +1,495 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.savedcards + +import android.widget.Toast +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifospay.core.model.entity.savedcards.Card +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.component.MifosDialogBox +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.core.ui.EmptyContentScreen +import org.mifospay.core.ui.utility.AddCardChip +import org.mifospay.savedcards.R + +@Composable +fun CardsScreen( + onEditCard: (Card) -> Unit, + modifier: Modifier = Modifier, + viewModel: CardsScreenViewModel = koinViewModel(), +) { + val cardState by viewModel.cardState.collectAsStateWithLifecycle() + val cardListUiState by viewModel.cardListUiState.collectAsStateWithLifecycle() + + var showCardBottomSheet by rememberSaveable { mutableStateOf(false) } + var showConfirmDeleteDialog by rememberSaveable { mutableStateOf(false) } + + var deleteCardID by rememberSaveable { mutableStateOf(null) } + + if (showCardBottomSheet) { + AddCardDialogSheet( + cancelClicked = { + showCardBottomSheet = false + }, + addClicked = { + showCardBottomSheet = false + viewModel.addCard(it) + }, + onDismiss = { + showCardBottomSheet = false + }, + ) + } + + if (showConfirmDeleteDialog) { + MifosDialogBox( + showDialogState = true, + onDismiss = { showConfirmDeleteDialog = false }, + title = R.string.feature_savedcards_delete_card, + confirmButtonText = R.string.feature_savedcards_yes, + onConfirm = { + deleteCardID?.let { viewModel.deleteCard(it) } + showConfirmDeleteDialog = false + }, + dismissButtonText = R.string.feature_savedcards_no, + message = R.string.feature_savedcards_confirm_delete_card, + ) + } + + CardsScreen( + cardState = cardState, + cardListUiState = cardListUiState, + onEditCard = onEditCard, + onDeleteCard = { + showConfirmDeleteDialog = true + deleteCardID = it.id + }, + onAddBtn = { showCardBottomSheet = true }, + updateQuery = viewModel::updateSearchQuery, + modifier = modifier, + ) +} + +enum class CardMenuAction { + EDIT, + DELETE, + CANCEL, +} + +@Composable +@VisibleForTesting +internal fun CardsScreen( + cardState: CardsUiState, + cardListUiState: CardsUiState, + onEditCard: (Card) -> Unit, + onDeleteCard: (Card) -> Unit, + onAddBtn: () -> Unit, + updateQuery: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + when (cardState) { + CardsUiState.Loading -> { + MfLoadingWheel( + contentDesc = stringResource(R.string.feature_savedcards_loading), + backgroundColor = MaterialTheme.colorScheme.surface, + ) + } + + is CardsUiState.Empty -> { + NoCardAddCardsScreen(onAddBtn) + } + + is CardsUiState.Error -> { + EmptyContentScreen( + title = stringResource(id = R.string.feature_savedcards_error_oops), + subTitle = stringResource(id = R.string.feature_savedcards_unexpected_error_subtitle), + modifier = Modifier, + iconTint = Color.Unspecified, + iconDrawable = R.drawable.artwork, + ) + } + + is CardsUiState.CreditCardForm -> { + CardsScreenContent( + cardList = (cardListUiState as CardsUiState.CreditCardForm).cards, + onAddBtn = onAddBtn, + onDeleteCard = onDeleteCard, + onEditCard = onEditCard, + updateQuery = updateQuery, + ) + } + + is CardsUiState.Success -> { + when (cardState.cardsUiEvent) { + CardsUiEvent.CARD_ADDED_SUCCESSFULLY -> { + Toast.makeText( + LocalContext.current, + stringResource(id = R.string.feature_savedcards_card_added_successfully), + Toast.LENGTH_SHORT, + ).show() + } + + CardsUiEvent.CARD_UPDATED_SUCCESSFULLY -> { + Toast.makeText( + LocalContext.current, + stringResource(id = R.string.feature_savedcards_card_updated_successfully), + Toast.LENGTH_SHORT, + ).show() + } + + CardsUiEvent.CARD_DELETED_SUCCESSFULLY -> { + Toast.makeText( + LocalContext.current, + stringResource(id = R.string.feature_savedcards_card_deleted_successfully), + Toast.LENGTH_SHORT, + ).show() + } + } + } + } + } +} + +@Composable +private fun CardsScreenContent( + cardList: List, + onEditCard: (Card) -> Unit, + onDeleteCard: (Card) -> Unit, + onAddBtn: () -> Unit, + updateQuery: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val query by rememberSaveable { mutableStateOf("") } + + Box( + modifier = modifier, + ) { + Column { + SearchBarScreen( + query = query, + onQueryChange = { q -> + updateQuery(q) + }, + onSearch = {}, + onClearQuery = { updateQuery("") }, + ) + CardsList( + cards = cardList, + onMenuItemClick = { card, menuItem -> + when (menuItem) { + CardMenuAction.EDIT -> onEditCard(card) + CardMenuAction.DELETE -> onDeleteCard(card) + CardMenuAction.CANCEL -> Unit + } + }, + ) + } + Box( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .align(Alignment.BottomCenter) + .background(color = MaterialTheme.colorScheme.surface), + ) { + AddCardChip( + text = R.string.feature_savedcards_add_cards, + btnText = R.string.feature_savedcards_add_cards, + onAddBtn = onAddBtn, + modifier = Modifier.align(Alignment.Center), + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SearchBarScreen( + query: String, + onQueryChange: (String) -> Unit, + onSearch: (String) -> Unit, + onClearQuery: () -> Unit, + modifier: Modifier = Modifier, +) { + SearchBar( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 16.dp, horizontal = 16.dp), + query = query, + colors = SearchBarDefaults.colors(MaterialTheme.colorScheme.primaryContainer), + onQueryChange = onQueryChange, + onSearch = onSearch, + active = false, + onActiveChange = { }, + placeholder = { + Text( + text = stringResource(R.string.feature_savedcards_search), + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + }, + leadingIcon = { + Icon( + imageVector = MifosIcons.Search, + contentDescription = stringResource(R.string.feature_savedcards_search), + ) + }, + trailingIcon = { + IconButton( + onClick = onClearQuery, + ) { + Icon( + imageVector = MifosIcons.Close, + contentDescription = stringResource(R.string.feature_savedcards_close), + ) + } + }, + ) {} +} + +@Composable +private fun CardsList( + cards: List, + onMenuItemClick: (Card, CardMenuAction) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + items(cards) { card -> + CardItem( + card = card, + onMenuItemClick = onMenuItemClick, + ) + } + } +} + +@Composable +private fun CardItem( + card: Card, + onMenuItemClick: (Card, CardMenuAction) -> Unit, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + Card( + modifier = modifier + .clickable { expanded = true } + .background(color = MaterialTheme.colorScheme.surface) + .fillMaxWidth() + .padding(10.dp), + colors = CardDefaults.cardColors(MaterialTheme.colorScheme.primaryContainer), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + Row { + Column { + Row { + Text( + text = card.firstName, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = card.lastName, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = "${stringResource(R.string.feature_savedcards_card_number)} ${card.cardNumber}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = card.expiryDate, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + } + Spacer(modifier = Modifier.height(38.dp)) + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.background(MaterialTheme.colorScheme.background), + ) { + DropdownMenuItem( + text = { Text(text = stringResource(R.string.feature_savedcards_edit_card)) }, + onClick = { + onMenuItemClick(card, CardMenuAction.EDIT) + expanded = false + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.feature_savedcards_delete_card)) }, + onClick = { + onMenuItemClick(card, CardMenuAction.DELETE) + expanded = false + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.feature_savedcards_cancel)) }, + onClick = { + onMenuItemClick(card, CardMenuAction.CANCEL) + expanded = false + }, + ) + } + } + } + } +} + +@Composable +private fun NoCardAddCardsScreen( + onAddBtn: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.feature_savedcards_add_cards), + color = MaterialTheme.colorScheme.onSurface, + ) + AddCardChip( + text = R.string.feature_savedcards_add_cards, + btnText = R.string.feature_savedcards_add_cards, + onAddBtn = onAddBtn, + modifier = Modifier, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun CardsScreenWithSampleDataPreview() { + MifosTheme { + CardsScreen( + cardState = CardsUiState.CreditCardForm(sampleCards), + cardListUiState = CardsUiState.CreditCardForm(sampleCards), + onEditCard = {}, + onDeleteCard = {}, + updateQuery = {}, + onAddBtn = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun CardsScreenEmptyPreview() { + MifosTheme { + CardsScreen( + cardState = CardsUiState.Empty, + cardListUiState = CardsUiState.CreditCardForm(sampleCards), + onEditCard = {}, + onDeleteCard = {}, + updateQuery = {}, + onAddBtn = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun CardsScreenErrorPreview() { + MifosTheme { + CardsScreen( + cardState = CardsUiState.Error, + cardListUiState = CardsUiState.CreditCardForm(sampleCards), + onEditCard = {}, + onDeleteCard = {}, + updateQuery = {}, + onAddBtn = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun CardsScreenLoadingPreview() { + MifosTheme { + CardsScreen( + cardState = CardsUiState.Loading, + cardListUiState = CardsUiState.CreditCardForm(sampleCards), + onEditCard = {}, + onDeleteCard = {}, + updateQuery = {}, + onAddBtn = {}, + ) + } +} + +val sampleCards = List(7) { index -> + Card( + cardNumber = "**** **** **** ${index + 1000}", + cvv = "${index + 100}", + expiryDate = "$index /0$index/202$index", + firstName = "ABC", + lastName = " XYZ", + id = index, + ) +} diff --git a/feature/savedcards/src/main/kotlin/org/mifospay/feature/savedcards/CardsScreenViewModel.kt b/feature/savedcards/src/main/kotlin/org/mifospay/feature/savedcards/CardsScreenViewModel.kt new file mode 100644 index 000000000..be508e1d4 --- /dev/null +++ b/feature/savedcards/src/main/kotlin/org/mifospay/feature/savedcards/CardsScreenViewModel.kt @@ -0,0 +1,179 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.savedcards + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.mifospay.core.model.entity.savedcards.Card +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.savedcards.AddCard +import org.mifospay.core.data.domain.usecase.savedcards.DeleteCard +import org.mifospay.core.data.domain.usecase.savedcards.EditCard +import org.mifospay.core.data.domain.usecase.savedcards.FetchSavedCards +import org.mifospay.core.data.repository.local.LocalRepository + +class CardsScreenViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val mLocalRepository: LocalRepository, + private val addCardUseCase: AddCard, + private val fetchSavedCardsUseCase: FetchSavedCards, + private val editCardUseCase: EditCard, + private val deleteCardUseCase: DeleteCard, +) : ViewModel() { + + private val mSearchQuery = MutableStateFlow("") + private val searchQuery: StateFlow = mSearchQuery.asStateFlow() + + private val mCardState = MutableStateFlow(CardsUiState.Loading) + val cardState: StateFlow = mCardState.asStateFlow() + + init { + fetchSavedCards() + } + + val cardListUiState: StateFlow = searchQuery + .map { q -> + when (mCardState.value) { + is CardsUiState.CreditCardForm -> { + val cardList = (cardState.value as CardsUiState.CreditCardForm).cards + val filterCards = cardList.filter { + it.cardNumber.lowercase().contains(q.lowercase()) + it.firstName.lowercase().contains(q.lowercase()) + it.lastName.lowercase().contains(q.lowercase()) + it.cvv.lowercase().contains(q.lowercase()) + it.expiryDate.lowercase().contains(q.lowercase()) + } + CardsUiState.CreditCardForm(filterCards) + } + + else -> CardsUiState.CreditCardForm(arrayListOf()) + } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = CardsUiState.CreditCardForm(arrayListOf()), + ) + + fun updateSearchQuery(query: String) { + mSearchQuery.update { query } + } + + fun fetchSavedCards() { + fetchSavedCardsUseCase.walletRequestValues = FetchSavedCards.RequestValues( + mLocalRepository.clientDetails.clientId, + ) + val requestValues = fetchSavedCardsUseCase.walletRequestValues + mUseCaseHandler.execute( + fetchSavedCardsUseCase, + requestValues, + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchSavedCards.ResponseValue) { + response.cardList.let { mCardState.value = CardsUiState.CreditCardForm(it) } + } + + override fun onError(message: String) { + mCardState.value = CardsUiState.Error + } + }, + ) + } + + fun addCard(card: Card) { + addCardUseCase.walletRequestValues = AddCard.RequestValues( + mLocalRepository.clientDetails.clientId, + card, + ) + val requestValues = addCardUseCase.walletRequestValues + mUseCaseHandler.execute( + addCardUseCase, + requestValues, + object : UseCase.UseCaseCallback { + override fun onSuccess(response: AddCard.ResponseValue) { + mCardState.value = CardsUiState.Success(CardsUiEvent.CARD_ADDED_SUCCESSFULLY) + fetchSavedCards() + } + + override fun onError(message: String) { + mCardState.value = CardsUiState.Error + } + }, + ) + } + + fun editCard(card: Card) { + editCardUseCase.walletRequestValues = EditCard.RequestValues( + mLocalRepository.clientDetails.clientId.toInt(), + card, + ) + val requestValues = editCardUseCase.walletRequestValues + mUseCaseHandler.execute( + editCardUseCase, + requestValues, + object : UseCase.UseCaseCallback { + override fun onSuccess(response: EditCard.ResponseValue) { + mCardState.value = CardsUiState.Success(CardsUiEvent.CARD_UPDATED_SUCCESSFULLY) + fetchSavedCards() + } + + override fun onError(message: String) { + mCardState.value = CardsUiState.Error + } + }, + ) + } + + fun deleteCard(cardId: Int) { + deleteCardUseCase.walletRequestValues = DeleteCard.RequestValues( + mLocalRepository.clientDetails.clientId.toInt(), + cardId, + ) + val requestValues = deleteCardUseCase.walletRequestValues + mUseCaseHandler.execute( + deleteCardUseCase, + requestValues, + object : UseCase.UseCaseCallback { + override fun onSuccess(response: DeleteCard.ResponseValue) { + mCardState.value = CardsUiState.Success(CardsUiEvent.CARD_DELETED_SUCCESSFULLY) + fetchSavedCards() + } + + override fun onError(message: String) { + mCardState.value = CardsUiState.Error + } + }, + ) + } +} + +sealed interface CardsUiState { + data class CreditCardForm( + val cards: List, + ) : CardsUiState + + data object Empty : CardsUiState + data object Error : CardsUiState + data object Loading : CardsUiState + data class Success(val cardsUiEvent: CardsUiEvent) : CardsUiState +} + +enum class CardsUiEvent { + CARD_ADDED_SUCCESSFULLY, + CARD_UPDATED_SUCCESSFULLY, + CARD_DELETED_SUCCESSFULLY, +} diff --git a/feature/search/src/main/kotlin/org/mifospay/feature/search/SearchScreen.kt b/feature/search/src/main/kotlin/org/mifospay/feature/search/SearchScreen.kt new file mode 100644 index 000000000..1263fbac3 --- /dev/null +++ b/feature/search/src/main/kotlin/org/mifospay/feature/search/SearchScreen.kt @@ -0,0 +1,290 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.search + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +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.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.mifospay.core.model.domain.SearchResult +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfLoadingWheel +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.MifosTheme + +@Composable +fun SearchScreenRoute( + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + viewModel: SearchViewModel = koinViewModel(), +) { + val searchQuery by viewModel.searchQuery.collectAsState() + val searchResultState by viewModel.searchResults.collectAsState() + + SearchScreen( + onBackClick = onBackClick, + searchResultState, + searchQueryChanged = viewModel::onSearchQueryChanged, + searchQuery = searchQuery, + modifier = modifier, + ) +} + +@Composable +fun SearchScreen( + onBackClick: () -> Unit, + searchResultState: SearchResultState, + modifier: Modifier = Modifier, + searchQuery: String = "", + searchQueryChanged: (String) -> Unit = {}, +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + ) { + SearchToolbar( + onBackClick = onBackClick, + onSearchQueryChanged = searchQueryChanged, + searchQuery = searchQuery, + ) + + when (searchResultState) { + SearchResultState.Idle -> {} + SearchResultState.Loading -> { + MfLoadingWheel( + contentDesc = stringResource(R.string.feature_search_loading), + backgroundColor = MaterialTheme.colorScheme.surface, + ) + } + + is SearchResultState.Success -> { + SearchResultList( + searchResults = searchResultState.results, + ) + } + + is SearchResultState.Error -> { + Text( + text = searchResultState.message, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } + } +} + +@Composable +private fun SearchToolbar( + onBackClick: () -> Unit, + onSearchQueryChanged: (String) -> Unit, + modifier: Modifier = Modifier, + searchQuery: String = "", +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.fillMaxWidth(), + ) { + IconButton(onClick = onBackClick) { + Icon( + imageVector = MifosIcons.ArrowBack, + contentDescription = "Back", + ) + } + SearchTextField( + onSearchQueryChanged = onSearchQueryChanged, + searchQuery = searchQuery, + ) + } +} + +@Composable +fun SearchTextField( + onSearchQueryChanged: (String) -> Unit, + searchQuery: String, + modifier: Modifier = Modifier, +) { + val keyboardController = LocalSoftwareKeyboardController.current + val onSearchExplicitlyTriggered = { + keyboardController?.hide() + } + + TextField( + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + ), + leadingIcon = { + Icon( + imageVector = MifosIcons.Search, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + }, + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton( + onClick = { + onSearchQueryChanged("") + }, + ) { + Icon( + imageVector = MifosIcons.Cancel, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + } + } + }, + onValueChange = { + if ("\n" !in it) onSearchQueryChanged(it) + }, + modifier = modifier + .fillMaxWidth() + .padding(16.dp) + .onKeyEvent { + if (it.key == Key.Enter) { + onSearchExplicitlyTriggered() + true + } else { + false + } + }, + shape = RoundedCornerShape(32.dp), + value = searchQuery, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Search, + ), + keyboardActions = KeyboardActions( + onSearch = { + onSearchExplicitlyTriggered() + }, + ), + maxLines = 1, + singleLine = true, + ) +} + +@Composable +fun SearchResultList( + searchResults: List, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + ) { + items(searchResults) { searchResult -> + SearchResultItem(searchResult = searchResult) + } + } +} + +@Composable +fun SearchResultItem( + searchResult: SearchResult, + modifier: Modifier = Modifier, +) { + Text( + text = searchResult.resultName, + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + color = MaterialTheme.colorScheme.onSurface, + ) +} + +@Preview +@Composable +private fun SearchToolbarPreview() { + MifosTheme { + SearchToolbar( + onBackClick = {}, + onSearchQueryChanged = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +fun SearchScreenSuccessPreview() { + MifosTheme { + SearchScreen( + onBackClick = {}, + searchResultState = SearchResultState.Success( + mutableListOf( + SearchResult(1, "John Doe", "Client"), + SearchResult(2, "Jane Smith", "Client"), + SearchResult(3, "example@email.com", "Email"), + SearchResult(4, "555-1234", "Phone Number"), + ), + ), + ) + } +} + +@Preview(showBackground = true) +@Composable +fun SearchScreenLoadingPreview() { + MifosTheme { + SearchScreen( + onBackClick = {}, + searchResultState = SearchResultState.Loading, + ) + } +} + +@Preview(showBackground = true) +@Composable +fun SearchScreenIdlePreview() { + MifosTheme { + SearchScreen( + onBackClick = {}, + searchResultState = SearchResultState.Idle, + ) + } +} + +@Preview(showBackground = true) +@Composable +fun SearchScreenErrorPreview() { + MifosTheme { + SearchScreen( + onBackClick = {}, + searchResultState = SearchResultState.Error("Error"), + ) + } +} diff --git a/feature/search/src/main/kotlin/org/mifospay/feature/search/SearchViewModel.kt b/feature/search/src/main/kotlin/org/mifospay/feature/search/SearchViewModel.kt new file mode 100644 index 000000000..3f50176ef --- /dev/null +++ b/feature/search/src/main/kotlin/org/mifospay/feature/search/SearchViewModel.kt @@ -0,0 +1,65 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.search + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.domain.SearchResult +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.client.SearchClient + +class SearchViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val searchClient: SearchClient, + private val savedStateHandle: SavedStateHandle, +) : ViewModel() { + + val searchQuery = savedStateHandle.getStateFlow(SEARCH_QUERY, "") + + private val _searchResults = MutableStateFlow(SearchResultState.Loading) + val searchResults: StateFlow = _searchResults.asStateFlow() + + fun onSearchQueryChanged(query: String) { + savedStateHandle[SEARCH_QUERY] = query + if (query.length > 3) { + performSearch(query) + } else { + _searchResults.value = SearchResultState.Idle + } + } + + private fun performSearch(query: String) { + mUseCaseHandler.execute( + searchClient, + SearchClient.RequestValues(query), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: SearchClient.ResponseValue) { + _searchResults.value = + SearchResultState.Success(response.results.toMutableList()) + } + + override fun onError(message: String) {} + }, + ) + } +} + +sealed class SearchResultState { + data object Idle : SearchResultState() + data object Loading : SearchResultState() + data class Success(val results: MutableList) : SearchResultState() + data class Error(val message: String) : SearchResultState() +} + +private const val SEARCH_QUERY = "searchQuery" diff --git a/feature/send-money/build.gradle.kts b/feature/send-money/build.gradle.kts index 4d5707f3f..4e5e691af 100644 --- a/feature/send-money/build.gradle.kts +++ b/feature/send-money/build.gradle.kts @@ -8,26 +8,18 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { namespace = "org.mifospay.feature.send.money" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - } +dependencies { + // we need it for country picker library + implementation(projects.libs.countryCodePicker) - androidMain.dependencies { - implementation(libs.google.play.services.code.scanner) - } - } -} \ No newline at end of file + // Google Bar code scanner + implementation(libs.google.play.services.code.scanner) +} diff --git a/feature/send-money/src/commonMain/composeResources/values/strings.xml b/feature/send-money/src/commonMain/composeResources/values/strings.xml index b58e58e58..9d0e56b0f 100644 --- a/feature/send-money/src/commonMain/composeResources/values/strings.xml +++ b/feature/send-money/src/commonMain/composeResources/values/strings.xml @@ -12,13 +12,13 @@ Insufficient balance Error fetching balance Self Account transfer is not allowed - Send Money - Select transfer method + Send + Select Method VPA Mobile number - Amount + Enter your Amount Virtual Payment Address - Submit + Proceed Please wait… - Phone Number + Enter Mobile Number \ No newline at end of file diff --git a/feature/send-money/src/main/kotlin/org/mifospay/feature/send/money/SendPaymentViewModel.kt b/feature/send-money/src/main/kotlin/org/mifospay/feature/send/money/SendPaymentViewModel.kt new file mode 100644 index 000000000..4e8cd7e62 --- /dev/null +++ b/feature/send-money/src/main/kotlin/org/mifospay/feature/send/money/SendPaymentViewModel.kt @@ -0,0 +1,105 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.send.money + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.account.FetchAccount +import org.mifospay.core.data.repository.local.LocalRepository + +class SendPaymentViewModel( + private val useCaseHandler: UseCaseHandler, + private val localRepository: LocalRepository, + private val fetchAccount: FetchAccount, +) : ViewModel() { + + private val mShowProgress = MutableStateFlow(false) + val showProgress: StateFlow = mShowProgress + + private val mVpa = MutableStateFlow("") + val vpa: StateFlow = mVpa + + private val mMobile = MutableStateFlow("") + val mobile: StateFlow = mMobile + + init { + fetchVpa() + fetchMobile() + } + + fun updateProgressState(isVisible: Boolean) { + mShowProgress.update { isVisible } + } + + private fun fetchVpa() { + viewModelScope.launch { + mVpa.value = localRepository.clientDetails.externalId.toString() + } + } + + private fun fetchMobile() { + viewModelScope.launch { + mMobile.value = localRepository.preferencesHelper.mobile.toString() + } + } + + fun checkSelfTransfer( + selfVpa: String?, + selfMobile: String?, + externalIdOrMobile: String?, + sendMethodType: SendMethodType, + ): Boolean { + return when (sendMethodType) { + SendMethodType.VPA -> { + selfVpa.takeIf { !it.isNullOrEmpty() }?.let { it == externalIdOrMobile } ?: false + } + + SendMethodType.MOBILE -> { + selfMobile.takeIf { !it.isNullOrEmpty() }?.let { it == externalIdOrMobile } ?: false + } + } + } + + fun checkBalanceAvailabilityAndTransfer( + externalId: String?, + transferAmount: Double, + onAnyError: (Int) -> Unit, + proceedWithTransferFlow: (String, Double) -> Unit, + ) { + updateProgressState(true) + useCaseHandler.execute( + fetchAccount, + FetchAccount.RequestValues(localRepository.clientDetails.clientId), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: FetchAccount.ResponseValue) { + updateProgressState(false) + if (transferAmount > response.account.balance) { + onAnyError(R.string.feature_send_money_insufficient_balance) + } else { + if (externalId != null) { + proceedWithTransferFlow(externalId, transferAmount) + } + } + } + + override fun onError(message: String) { + updateProgressState(false) + onAnyError.invoke(R.string.feature_send_money_error_fetching_balance) + } + }, + ) + } +} diff --git a/feature/send-money/src/main/kotlin/org/mifospay/feature/send/money/SendScreenRoute.kt b/feature/send-money/src/main/kotlin/org/mifospay/feature/send/money/SendScreenRoute.kt new file mode 100644 index 000000000..d3bc1ea40 --- /dev/null +++ b/feature/send-money/src/main/kotlin/org/mifospay/feature/send/money/SendScreenRoute.kt @@ -0,0 +1,438 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.send.money + +import android.content.Context +import android.net.Uri +import android.provider.ContactsContract +import android.util.Log +import android.widget.Toast +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.border +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.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AttachMoney +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions +import com.google.mlkit.vision.codescanner.GmsBarcodeScanning +import com.mifos.library.countrycodepicker.CountryCodePickerPayment +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MfOverlayLoadingWheel +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosNavigationTopAppBar +import org.mifospay.core.designsystem.component.MifosTextField +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.styleMedium16sp +import org.mifospay.core.designsystem.theme.styleNormal18sp + +@Composable +fun SendScreenRoute( + showToolBar: Boolean, + onBackClick: () -> Unit, + proceedWithMakeTransferFlow: (String, String) -> Unit, + modifier: Modifier = Modifier, + viewModel: SendPaymentViewModel = koinViewModel(), +) { + val context = LocalContext.current + val selfVpa by viewModel.vpa.collectAsStateWithLifecycle() + val selfMobile by viewModel.mobile.collectAsStateWithLifecycle() + val showProgress by viewModel.showProgress.collectAsStateWithLifecycle() + + fun showToast(message: String) { + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + + SendMoneyScreen( + showToolBar = showToolBar, + onBackClick = onBackClick, + showProgress = showProgress, + onSubmit = { amount, externalIdOrMobile, sendMethodType -> + if (!viewModel.checkSelfTransfer( + selfVpa = selfVpa, + selfMobile = selfMobile, + sendMethodType = sendMethodType, + externalIdOrMobile = externalIdOrMobile, + ) + ) { + viewModel.checkBalanceAvailabilityAndTransfer( + externalId = selfVpa, + transferAmount = amount.toDouble(), + onAnyError = { + showToast(context.getString(it)) + }, + proceedWithTransferFlow = { externalId, transferAmount -> + proceedWithMakeTransferFlow.invoke( + externalId, + transferAmount.toString(), + ) + }, + ) + } else { + showToast(context.getString(R.string.feature_send_money_not_allowed)) + } + }, + modifier = modifier, + ) +} + +enum class SendMethodType { + VPA, + MOBILE, +} + +@Composable +@Suppress("LongMethod", "CyclomaticComplexMethod") +@VisibleForTesting +internal fun SendMoneyScreen( + showToolBar: Boolean, + showProgress: Boolean, + onSubmit: (String, String, SendMethodType) -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + var amount by rememberSaveable { mutableStateOf("") } + var vpa by rememberSaveable { mutableStateOf("") } + var mobileNumber by rememberSaveable { mutableStateOf("") } + var isValidMobileNumber by rememberSaveable { mutableStateOf(false) } + var sendMethodType by rememberSaveable { mutableStateOf(SendMethodType.VPA) } + var isValidInfo by rememberSaveable { mutableStateOf(false) } + + val contactUri by rememberSaveable { mutableStateOf(null) } + + fun validateInfo() { + isValidInfo = when (sendMethodType) { + SendMethodType.VPA -> amount.isNotEmpty() && vpa.isNotEmpty() + SendMethodType.MOBILE -> { + isValidMobileNumber && mobileNumber.isNotEmpty() && amount.isNotEmpty() + } + } + } + + LaunchedEffect(key1 = contactUri) { + contactUri?.let { + mobileNumber = getContactPhoneNumber(it, context) + } + } + + val options = GmsBarcodeScannerOptions.Builder().setBarcodeFormats( + Barcode.FORMAT_QR_CODE, + Barcode.FORMAT_AZTEC, + ).build() + + val scanner = GmsBarcodeScanning.getClient(context, options) + + fun startScan() { + scanner.startScan().addOnSuccessListener { barcode -> + barcode.rawValue?.let { + vpa = it + } + }.addOnCanceledListener { + // Task canceled + }.addOnFailureListener { e -> + // Task failed with an exception + e.localizedMessage?.let { Log.d("SendMoney: Barcode scan failed", it) } + } + } + + Box( + modifier + .padding(top = 5.dp) + .imePadding(), + ) { + Column(Modifier.fillMaxSize()) { + if (showToolBar) { + MifosNavigationTopAppBar( + titleRes = R.string.feature_send_money_send, + onNavigationClick = onBackClick, + ) + } + + MifosTextField( + value = amount, + label = stringResource(id = R.string.feature_send_money_amount), + onValueChange = { + amount = it + validateInfo() + }, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + singleLine = true, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number), + leadingIcon = { + Icon( + imageVector = Icons.Default.AttachMoney, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(end = 8.dp), + ) + }, + indicatorColor = MaterialTheme.colorScheme.primary, + textStyle = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurface, + ), + ) + + when (sendMethodType) { + SendMethodType.VPA -> { + MifosTextField( + value = vpa, + label = stringResource(id = R.string.feature_send_money_virtual_payment_address), + onValueChange = { + vpa = it + validateInfo() + }, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + trailingIcon = { + IconButton( + onClick = { startScan() }, + ) { + Icon( + imageVector = MifosIcons.QrCode2, + contentDescription = "Scan QR", + tint = MaterialTheme.colorScheme.onSurface, + ) + } + }, + indicatorColor = MaterialTheme.colorScheme.primary, + textStyle = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurface, + ), + ) + } + + SendMethodType.MOBILE -> { + EnterPhoneScreen( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + initialPhoneNumber = mobileNumber, + onNumberUpdated = { _, fullPhone, valid -> + if (valid) { + mobileNumber = fullPhone + } + isValidMobileNumber = valid + validateInfo() + }, + + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + Text( + modifier = Modifier.padding(start = 20.dp, top = 20.dp), + text = stringResource(id = R.string.feature_send_money_select_transfer_method), + style = styleNormal18sp, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + VpaMobileChip( + label = stringResource(id = R.string.feature_send_money_vpa), + selected = sendMethodType == SendMethodType.VPA, + onClick = { sendMethodType = SendMethodType.VPA }, + ) + Spacer(modifier = Modifier.width(8.dp)) + VpaMobileChip( + label = stringResource(id = R.string.feature_send_money_mobile), + selected = sendMethodType == SendMethodType.MOBILE, + onClick = { sendMethodType = SendMethodType.MOBILE }, + ) + } + + MifosButton( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + color = MaterialTheme.colorScheme.primary, + enabled = isValidInfo, + onClick = { + if (!isValidInfo) return@MifosButton + onSubmit( + amount, + when (sendMethodType) { + SendMethodType.VPA -> vpa + SendMethodType.MOBILE -> mobileNumber + }, + sendMethodType, + ) + // TODO: Navigate to MakeTransferScreenRoute + }, + contentPadding = PaddingValues(18.dp), + ) { + Text( + stringResource(id = R.string.feature_send_money_submit), + style = styleMedium16sp.copy(color = MaterialTheme.colorScheme.surface), + ) + } + } + + if (showProgress) { + MfOverlayLoadingWheel( + contentDesc = stringResource(id = R.string.feature_send_money_please_wait), + ) + } + } +} + +@Composable +private fun EnterPhoneScreen( + onNumberUpdated: (String, String, Boolean) -> Unit, + modifier: Modifier = Modifier, + initialPhoneNumber: String? = null, +) { + val keyboardController = LocalSoftwareKeyboardController.current + CountryCodePickerPayment( + modifier = modifier, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + ), + initialPhoneNumber = initialPhoneNumber, + onValueChange = { (code, phone), isValid -> + onNumberUpdated(phone, code + phone, isValid) + }, + label = { + Text( + stringResource(id = R.string.feature_send_money_phone_number), + color = MaterialTheme.colorScheme.primary, + ) + }, + keyboardActions = KeyboardActions { keyboardController?.hide() }, + indicatorColor = MaterialTheme.colorScheme.primary, + errorIndicatorColor = MaterialTheme.colorScheme.error, + textStyle = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurface, + ), + ) +} + +@Composable +private fun VpaMobileChip( + label: String, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + MifosButton( + onClick = onClick, + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = modifier + .wrapContentSize() + .padding(4.dp) + .then( + if (selected) { + Modifier.border( + width = 1.dp, + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(25.dp), + ) + } else { + Modifier + }, + ), + ) { + Text( + modifier = Modifier.padding(top = 4.dp, bottom = 4.dp), + color = MaterialTheme.colorScheme.onSurface, + text = label, + ) + } +} + +private suspend fun getContactPhoneNumber( + uri: Uri, + context: Context, +): String { + val contactId: String = uri.lastPathSegment ?: return "" + return withContext(Dispatchers.IO) { + val phoneCursor = context.contentResolver.query( + ContactsContract.CommonDataKinds.Phone.CONTENT_URI, + arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER), + "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?", + arrayOf(contactId), + null, + ) + phoneCursor?.use { cursor -> + if (cursor.moveToFirst()) { + val phoneNumberIndex = + cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER) + cursor.getString(phoneNumberIndex) + } else { + "" + } + } ?: "" + } +} + +@Preview(showSystemUi = true, showBackground = true) +@Composable +private fun SendMoneyScreenWithToolBarPreview() { + SendMoneyScreen( + onSubmit = { _, _, _ -> }, + onBackClick = {}, + showProgress = false, + showToolBar = true, + ) +} + +@Preview(showSystemUi = true, showBackground = true) +@Composable +private fun SendMoneyScreenWithoutToolBarPreview() { + SendMoneyScreen( + onSubmit = { _, _, _ -> }, + onBackClick = {}, + showProgress = false, + showToolBar = false, + ) +} diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index d37f3abd5..0de327314 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -8,25 +8,12 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { namespace = "org.mifospay.feature.settings" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - - implementation(libs.koin.compose.viewmodel) - implementation(libs.koin.compose) - } - } -} \ No newline at end of file +dependencies {} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/navigation/SettingsNavigation.kt b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/navigation/SettingsNavigation.kt index 281cf082a..9c2b24a56 100644 --- a/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/navigation/SettingsNavigation.kt +++ b/feature/settings/src/commonMain/kotlin/org/mifospay/feature/settings/navigation/SettingsNavigation.kt @@ -12,7 +12,7 @@ package org.mifospay.feature.settings.navigation import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions -import org.mifospay.core.ui.composableWithSlideTransitions +import androidx.navigation.compose.composable import org.mifospay.feature.settings.SettingsScreenRoute const val SETTINGS_ROUTE = "settings_route" @@ -23,20 +23,18 @@ fun NavController.navigateToSettings(navOptions: NavOptions? = null) { fun NavGraphBuilder.settingsScreen( onBackPress: () -> Unit, + navigateToEditPasswordScreen: () -> Unit, onLogout: () -> Unit, onChangePasscode: () -> Unit, - navigateToEditPasswordScreen: () -> Unit, navigateToFaqScreen: () -> Unit, - navigateToNotificationScreen: () -> Unit, ) { - composableWithSlideTransitions(route = SETTINGS_ROUTE) { + composable(route = SETTINGS_ROUTE) { SettingsScreenRoute( backPress = onBackPress, - onEditPassword = navigateToEditPasswordScreen, + navigateToEditPasswordScreen = navigateToEditPasswordScreen, onLogout = onLogout, onChangePasscode = onChangePasscode, navigateToFaqScreen = navigateToFaqScreen, - navigateToNotificationScreen = navigateToNotificationScreen, ) } } diff --git a/feature/settings/src/main/kotlin/org/mifospay/feature/settings/SettingsScreen.kt b/feature/settings/src/main/kotlin/org/mifospay/feature/settings/SettingsScreen.kt new file mode 100644 index 000000000..be413be00 --- /dev/null +++ b/feature/settings/src/main/kotlin/org/mifospay/feature/settings/SettingsScreen.kt @@ -0,0 +1,170 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.settings + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +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 org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MifosTopBar +import org.mifospay.core.ui.utility.DialogState +import org.mifospay.core.ui.utility.DialogType + +@Composable +fun SettingsScreenRoute( + backPress: () -> Unit, + navigateToEditPasswordScreen: () -> Unit, + onLogout: () -> Unit, + onChangePasscode: () -> Unit, + navigateToFaqScreen: () -> Unit, + modifier: Modifier = Modifier, + viewmodel: SettingsViewModel = koinViewModel(), +) { + var dialogState by remember { mutableStateOf(DialogState()) } + + DialogManager( + dialogState = dialogState, + onDismiss = { dialogState = DialogState(type = DialogType.NONE) }, + ) + + Scaffold( + topBar = { + MifosTopBar( + topBarTitle = R.string.feature_settings_settings, + backPress = { backPress.invoke() }, + ) + }, + modifier = modifier, + ) { contentPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + .padding(contentPadding) + .verticalScroll(rememberScrollState()), + ) { + SettingsCardItem( + title = stringResource(id = R.string.feature_settings_notification_settings), + ) + + SettingsCardItem( + title = stringResource(id = R.string.feature_settings_faq), + onClick = navigateToFaqScreen, + ) + + SettingsCardItem( + title = stringResource(id = R.string.feature_settings_change_password), + onClick = navigateToEditPasswordScreen, + ) + + SettingsCardItem( + title = stringResource(id = R.string.feature_settings_change_passcode), + onClick = onChangePasscode, + ) + + SettingsCardItem( + title = stringResource(id = R.string.feature_settings_log_out), + onClick = { + dialogState = DialogState( + type = DialogType.LOGOUT, + onConfirm = { + viewmodel.logout() + onLogout() + }, + ) + }, + ) + + SettingsCardItem( + title = stringResource(id = R.string.feature_settings_disable_account), + color = Color.Red, + onClick = { + dialogState = DialogState( + type = DialogType.DISABLE_ACCOUNT, + onConfirm = { viewmodel.disableAccount() }, + ) + }, + hasHorizontalDivider = false, + ) + } + } +} + +@Composable +fun SettingsCardItem( + title: String, + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.onSurface, + onClick: (() -> Unit)? = null, + hasHorizontalDivider: Boolean = true, +) { + Card( + modifier = modifier + .fillMaxWidth() + .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier), + shape = RoundedCornerShape(0.dp), + colors = CardDefaults.cardColors( + containerColor = Color.Transparent, + ), + ) { + Text( + text = title, + fontWeight = FontWeight(400), + modifier = Modifier.padding(20.dp), + color = color, + ) + + if (hasHorizontalDivider) { + HorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + thickness = 1.dp, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f), + ) + } + } +} + +@Preview(showSystemUi = true) +@Composable +private fun SettingsScreenPreview() { + SettingsScreenRoute( + backPress = {}, + navigateToEditPasswordScreen = {}, + onLogout = {}, + onChangePasscode = {}, + viewmodel = koinViewModel(), + navigateToFaqScreen = { }, + ) +} diff --git a/feature/settings/src/main/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt new file mode 100644 index 000000000..2ab18b0bd --- /dev/null +++ b/feature/settings/src/main/kotlin/org/mifospay/feature/settings/SettingsViewModel.kt @@ -0,0 +1,45 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.settings + +import androidx.lifecycle.ViewModel +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.account.BlockUnblockCommand +import org.mifospay.core.data.repository.local.LocalRepository + +class SettingsViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val mLocalRepository: LocalRepository, + private val blockUnblockCommandUseCase: BlockUnblockCommand, +) : ViewModel() { + + fun logout() { + mLocalRepository.preferencesHelper.clear() + } + + fun disableAccount() { + // keep it disabled for now + if (0 * 67 == 0) { + return + } + mUseCaseHandler.execute( + blockUnblockCommandUseCase, + BlockUnblockCommand.RequestValues( + mLocalRepository.clientDetails.clientId, + "block", + ), + object : UseCase.UseCaseCallback { + override fun onSuccess(response: BlockUnblockCommand.ResponseValue) {} + override fun onError(message: String) {} + }, + ) + } +} diff --git a/feature/standing-instruction/build.gradle.kts b/feature/standing-instruction/build.gradle.kts index 85006106e..8fac8140e 100644 --- a/feature/standing-instruction/build.gradle.kts +++ b/feature/standing-instruction/build.gradle.kts @@ -8,22 +8,15 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { namespace = "org.mifospay.feature.standing.instruction" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - } - } +dependencies { + // Google Bar code scanner + implementation(libs.google.play.services.code.scanner) } \ No newline at end of file diff --git a/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/NewSIScreenRoute.kt b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/NewSIScreenRoute.kt new file mode 100644 index 000000000..01322a4ec --- /dev/null +++ b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/NewSIScreenRoute.kt @@ -0,0 +1,485 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.standing.instruction + +import android.app.DatePickerDialog +import android.util.Log +import android.widget.Toast +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.border +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions +import com.google.mlkit.vision.codescanner.GmsBarcodeScanning +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.MifosButton +import org.mifospay.core.designsystem.component.MifosOutlinedTextField +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.designsystem.theme.MifosTheme +import java.util.Calendar + +@Composable +internal fun NewSIScreenRoute( + onBackPress: () -> Unit, + modifier: Modifier = Modifier, + viewModel: NewSIViewModel = koinViewModel(), +) { + val uiState by viewModel.newSIUiState.collectAsStateWithLifecycle() + val updateSuccess by viewModel.updateSuccess.collectAsStateWithLifecycle() + + var cancelClicked by rememberSaveable { mutableStateOf(false) } + + NewSIScreen( + uiState = uiState, + cancelClicked = cancelClicked, + updateSuccess = updateSuccess, + onBackPress = onBackPress, + fetchClient = viewModel::fetchClient, + setCancelClicked = { cancelClicked = it }, + confirm = viewModel::createNewSI, + modifier = modifier, + ) +} + +@Composable +@VisibleForTesting +internal fun NewSIScreen( + uiState: NewSIUiState, + cancelClicked: Boolean, + updateSuccess: Boolean, + onBackPress: () -> Unit, + fetchClient: (String) -> Unit, + setCancelClicked: (Boolean) -> Unit, + confirm: (Long, Double, Int, String) -> Unit, + modifier: Modifier = Modifier, +) { + MifosScaffold( + topBarTitle = R.string.feature_standing_instruction_tile_si_activity, + backPress = onBackPress, + modifier = modifier, + scaffoldContent = { + when (uiState) { + NewSIUiState.Loading -> NewSIBody( + paddingValues = it, + fetchClient = fetchClient, + cancelClicked = cancelClicked, + setCancelClicked = setCancelClicked, + confirm = confirm, + clientId = 0, + updateSuccess = updateSuccess, + ) + + is NewSIUiState.ShowClientDetails -> { + NewSIBody( + paddingValues = it, + fetchClient = fetchClient, + cancelClicked = cancelClicked, + setCancelClicked = setCancelClicked, + confirm = confirm, + clientId = uiState.clientId, + updateSuccess = updateSuccess, + ) + } + } + }, + ) +} + +@Composable +private fun NewSIBody( + paddingValues: PaddingValues, + fetchClient: (String) -> Unit, + cancelClicked: Boolean, + setCancelClicked: (Boolean) -> Unit, + confirm: (Long, Double, Int, String) -> Unit, + clientId: Long, + updateSuccess: Boolean, + modifier: Modifier = Modifier, +) { + var amount by rememberSaveable { mutableStateOf("") } + var vpa by rememberSaveable { mutableStateOf("") } + var siInterval by rememberSaveable { mutableStateOf("") } + var selectedDate by rememberSaveable { mutableStateOf("") } + + val context = LocalContext.current + + if (!cancelClicked) { + ConfirmTransfer( + paddingValues = paddingValues, + clientName = "", + clientVpa = vpa, + amount = amount, + cancel = { setCancelClicked(true) }, + confirm = confirm, + clientId = clientId, + recurrenceInterval = siInterval, + validTill = selectedDate, + updateSuccess = updateSuccess, + modifier = modifier, + ) + } else { + val options = GmsBarcodeScannerOptions.Builder() + .setBarcodeFormats( + Barcode.FORMAT_QR_CODE, + Barcode.FORMAT_AZTEC, + ) + .build() + val scanner = GmsBarcodeScanning.getClient(context, options) + + fun startScan() { + scanner.startScan() + .addOnSuccessListener { barcode -> + barcode.rawValue?.let { + vpa = it + } + } + .addOnCanceledListener { + // Task canceled + } + .addOnFailureListener { e -> + // Task failed with an exception + e.localizedMessage?.let { Log.d("SendMoney: Barcode scan failed", it) } + } + } + + fun showDatePickerDialog() { + val calendar = Calendar.getInstance() + val datePickerDialog = DatePickerDialog( + context, + { _, year, month, dayOfMonth -> + selectedDate = "$dayOfMonth/${month + 1}/$year" + }, + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH), + ) + datePickerDialog.show() + } + + Column( + modifier = modifier + .fillMaxSize() + .padding(paddingValues), + ) { + MifosOutlinedTextField( + label = R.string.feature_standing_instruction_amount, + value = amount, + onValueChange = { + amount = it + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number), + ) + Spacer(modifier = Modifier.padding(top = 16.dp)) + MifosOutlinedTextField( + label = R.string.feature_standing_instruction_vpa, + value = vpa, + onValueChange = { + vpa = it + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + trailingIcon = { + IconButton(onClick = { startScan() }) { + Icon( + imageVector = MifosIcons.QrCode2, + contentDescription = "Scan QR", + tint = MaterialTheme.colorScheme.surface, + ) + } + }, + ) + Spacer(modifier = Modifier.padding(top = 16.dp)) + MifosOutlinedTextField( + label = R.string.feature_standing_instruction_interval, + value = siInterval, + onValueChange = { + siInterval = it + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 10.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = stringResource(id = R.string.feature_standing_instruction_valid_till)) + } + MifosButton( + modifier = Modifier + .width(150.dp) + .padding(top = 16.dp) + .align(Alignment.CenterHorizontally), + color = MaterialTheme.colorScheme.primary, + onClick = { showDatePickerDialog() }, + ) { + val text = + selectedDate.ifEmpty { stringResource(R.string.feature_standing_instruction_select_date) } + Text(text = text, color = MaterialTheme.colorScheme.onPrimary) + } + + MifosButton( + modifier = Modifier + .width(150.dp) + .padding(top = 16.dp) + .align(Alignment.CenterHorizontally), + color = MaterialTheme.colorScheme.primary, + onClick = { + fetchClient(vpa) + if (updateSuccess) { + Toast.makeText( + context, + context.getString(R.string.feature_standing_instruction_creates), + Toast.LENGTH_SHORT, + ) + .show() + } else { + Toast.makeText( + context, + R.string.feature_standing_instruction_failed_to_save_changes, + Toast.LENGTH_SHORT, + ) + .show() + } + }, + enabled = selectedDate.isNotEmpty() && + vpa.isNotEmpty() && + amount.isNotEmpty() && siInterval.isNotEmpty(), + ) { + Text( + text = stringResource(id = R.string.feature_standing_instruction_submit), + color = MaterialTheme.colorScheme.onPrimary, + ) + } + } + } +} + +internal class NewSiUiStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + NewSIUiState.Loading, + NewSIUiState.ShowClientDetails( + 0L, + name = "Pratyush", + externalId = "External Id", + ), + ) +} + +@Composable +private fun ConfirmTransfer( + paddingValues: PaddingValues, + clientName: String, + clientVpa: String, + amount: String, + cancel: () -> Unit, + confirm: (Long, Double, Int, String) -> Unit, + clientId: Long, + recurrenceInterval: String, + validTill: String, + updateSuccess: Boolean, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + Column( + modifier = modifier + .fillMaxSize() + .padding(paddingValues), + ) { + Column( + modifier = Modifier + .wrapContentSize() + .padding(12.dp) + .border(2.dp, Color.Black, RoundedCornerShape(12.dp)) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.feature_standing_instruction_sending_to), + style = TextStyle(color = MaterialTheme.colorScheme.primary, fontSize = 15.sp), + ) + Text( + text = clientName, + style = TextStyle(color = MaterialTheme.colorScheme.primary, fontSize = 20.sp), + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.feature_standing_instruction_vpa), + style = TextStyle(color = MaterialTheme.colorScheme.primary, fontSize = 15.sp), + ) + Text( + text = clientVpa, + style = TextStyle(color = MaterialTheme.colorScheme.primary, fontSize = 20.sp), + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.feature_standing_instruction_amount), + style = TextStyle(color = MaterialTheme.colorScheme.primary, fontSize = 15.sp), + ) + Text( + text = amount, + style = TextStyle(color = MaterialTheme.colorScheme.primary, fontSize = 20.sp), + ) + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + MifosButton( + modifier = Modifier + .width(150.dp) + .padding(top = 16.dp), + color = MaterialTheme.colorScheme.primary, + onClick = { cancel.invoke() }, + ) { + Text( + text = stringResource(id = R.string.feature_standing_instruction_cancel), + color = MaterialTheme.colorScheme.onPrimary, + ) + } + + MifosButton( + modifier = Modifier + .width(150.dp) + .padding(top = 16.dp), + color = MaterialTheme.colorScheme.primary, + onClick = { + if (amount.isNotEmpty()) { + confirm.invoke( + clientId, + amount.toDouble(), + recurrenceInterval.toInt(), + validTill, + ) + } else { + // Handle the case when the amount is empty + Toast.makeText( + context, + R.string.feature_standing_instruction_failed_to_save_changes, + Toast.LENGTH_SHORT, + ) + .show() + } + if (updateSuccess) { + Toast.makeText( + context, + context.getString(R.string.feature_standing_instruction_creates), + Toast.LENGTH_SHORT, + ).show() + } else { + Toast.makeText( + context, + R.string.feature_standing_instruction_failed_to_save_changes, + Toast.LENGTH_SHORT, + ) + .show() + } + }, + + ) { + Text( + text = stringResource(id = R.string.feature_standing_instruction_confirm), + color = MaterialTheme.colorScheme.onPrimary, + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ConfirmTransferPreview() { + ConfirmTransfer( + PaddingValues(12.dp), + "Pratyush", + "999999999@axl", + "100", + {}, { _, _, _, _ -> }, + 0L, "", "", true, + ) +} + +@Preview(showBackground = true) +@Composable +private fun NewSIScreenPreview( + @PreviewParameter(NewSiUiStateProvider::class) newSIUiState: NewSIUiState, +) { + MifosTheme { + NewSIScreen( + newSIUiState, + cancelClicked = false, + updateSuccess = true, + onBackPress = {}, + fetchClient = {}, + setCancelClicked = {}, + confirm = { _, _, _, _ -> }, + ) + } +} diff --git a/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/NewSIViewModel.kt b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/NewSIViewModel.kt new file mode 100644 index 000000000..64afb8992 --- /dev/null +++ b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/NewSIViewModel.kt @@ -0,0 +1,107 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.standing.instruction + +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.domain.SearchResult +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.client.SearchClient +import org.mifospay.core.data.domain.usecase.standinginstruction.CreateStandingTransaction +import org.mifospay.core.datastore.PreferencesHelper +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class NewSIViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val preferencesHelper: PreferencesHelper, + private val searchClient: SearchClient, + private var createStandingTransaction: CreateStandingTransaction, +) : ViewModel() { + + private val mNewSIUiState = MutableStateFlow(NewSIUiState.Loading) + val newSIUiState: StateFlow = mNewSIUiState + + private val mUpdateSuccess = MutableStateFlow(false) + val updateSuccess: StateFlow = mUpdateSuccess + + fun fetchClient(externalId: String) { + mNewSIUiState.value = NewSIUiState.Loading + mUseCaseHandler.execute( + searchClient, + SearchClient.RequestValues(externalId), + object : UseCase.UseCaseCallback { + + override fun onSuccess(response: SearchClient.ResponseValue) { + val searchResult: SearchResult = response.results[0] + mNewSIUiState.value = NewSIUiState.ShowClientDetails( + searchResult.resultId.toLong(), + searchResult.resultName, externalId, + ) + } + + override fun onError(message: String) { + mUpdateSuccess.value = false + } + }, + ) + } + + fun createNewSI( + toClientId: Long, + amount: Double, + recurrenceInterval: Int, + validTill: String, + ) { + mNewSIUiState.value = NewSIUiState.Loading + val validTillDateArray = validTill.split("-") + val validTillString = + "${validTillDateArray[0]} ${validTillDateArray[1]} ${validTillDateArray[2]}" + + var validFrom: String = + SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(Date()) + val validFromDateArray = validFrom.split("-") + validFrom = "${validFromDateArray[0]} ${validFromDateArray[1]} ${validFromDateArray[2]}" + val recurrenceOnDateMonth = "${validFromDateArray[0]} ${validFromDateArray[1]}" + + mUseCaseHandler.execute( + createStandingTransaction, + CreateStandingTransaction.RequestValues( + validTillString, + validFrom, + recurrenceInterval, + recurrenceOnDateMonth, + preferencesHelper.clientId, + toClientId, + amount, + ), + object : + UseCase.UseCaseCallback { + + override fun onSuccess(response: CreateStandingTransaction.ResponseValue) { + mUpdateSuccess.value = true + } + + override fun onError(message: String) { + mUpdateSuccess.value = false + } + }, + ) + } +} + +sealed interface NewSIUiState { + data object Loading : NewSIUiState + data class ShowClientDetails(val clientId: Long, val name: String, val externalId: String) : + NewSIUiState +} diff --git a/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/SIDetailsScreen.kt b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/SIDetailsScreen.kt new file mode 100644 index 000000000..6ae9df3b6 --- /dev/null +++ b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/SIDetailsScreen.kt @@ -0,0 +1,294 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.standing.instruction + +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.layout.Column +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.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mifospay.core.model.entity.accounts.savings.SavingAccount +import com.mifospay.core.model.entity.client.Client +import com.mifospay.core.model.entity.client.Status +import com.mifospay.core.model.entity.standinginstruction.StandingInstruction +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.FloatingActionButtonContent +import org.mifospay.core.designsystem.component.MifosLoadingWheel +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.icon.MifosIcons + +@Composable +internal fun SIDetailsScreen( + onClickCreateNew: () -> Unit, + onBackPress: () -> Unit, + modifier: Modifier = Modifier, + viewModel: StandingInstructionDetailsViewModel = koinViewModel(), +) { + val siDetailUiState by viewModel.siDetailsUiState.collectAsStateWithLifecycle() + + SIDetailsScreen( + siDetailsUiState = siDetailUiState, + onClickCreateNew = onClickCreateNew, + onBackPress = onBackPress, + modifier = modifier, + ) +} + +@Composable +@VisibleForTesting +internal fun SIDetailsScreen( + siDetailsUiState: SiDetailsUiState, + onClickCreateNew: () -> Unit, + onBackPress: () -> Unit, + modifier: Modifier = Modifier, +) { + val floatingActionButtonContent = FloatingActionButtonContent( + onClick = onClickCreateNew, + contentColor = Color.Black, + content = { + androidx.compose.material3.Icon( + imageVector = MifosIcons.Edit, + contentDescription = stringResource(R.string.feature_standing_instruction_downloading_receipt), + ) + }, + ) + + MifosScaffold( + topBarTitle = R.string.feature_standing_instruction_details, + floatingActionButtonContent = floatingActionButtonContent, + backPress = onBackPress, + modifier = modifier, + scaffoldContent = { + when (siDetailsUiState) { + SiDetailsUiState.Loading -> MifosLoadingWheel( + modifier = Modifier.fillMaxWidth(), + contentDesc = stringResource(R.string.feature_standing_instruction_loading), + ) + + is SiDetailsUiState.ShowSiDetails -> { + Column( + modifier = Modifier.fillMaxSize(), + ) { + SIDetailsContent( + paddingValues = it, + siName = siDetailsUiState.standingInstruction.name, + siId = siDetailsUiState.standingInstruction.id, + amount = siDetailsUiState.standingInstruction.amount, + validFrom = siDetailsUiState.standingInstruction.validFrom, + validTill = siDetailsUiState.standingInstruction.validTill + ?: emptyList(), + siToName = siDetailsUiState.standingInstruction.toClient.displayName + ?: "", + siFromNumber = siDetailsUiState.standingInstruction.fromClient.mobileNo, + siToNumber = siDetailsUiState.standingInstruction.toClient.mobileNo, + recurrenceInterval = siDetailsUiState.standingInstruction.recurrenceInterval, + status = siDetailsUiState.standingInstruction.status.value ?: "", + siFromName = siDetailsUiState.standingInstruction.fromAccount.clientName + ?: "", + ) + } + } + } + }, + ) +} + +@Composable +private fun SIDetailsContent( + paddingValues: PaddingValues, + siName: String, + siId: Long, + amount: Double, + validFrom: List, + validTill: List, + siToName: String, + siToNumber: String, + siFromName: String, + siFromNumber: String, + recurrenceInterval: Int, + status: String, +) { + Column( + modifier = Modifier + .padding(paddingValues) + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) { + Text( + text = siName, + fontSize = 30.sp, + color = Color.Black, + modifier = Modifier.padding(vertical = 8.dp), + ) + + Text( + text = stringResource(R.string.feature_standing_instruction_standing_instruction_id), + fontSize = 16.sp, + color = Color.DarkGray, + modifier = Modifier.padding(vertical = 8.dp), + ) + Text( + text = siId.toString(), + fontSize = 16.sp, + color = Color.Blue, + modifier = Modifier.padding(bottom = 16.dp), + ) + + Text( + text = stringResource(id = R.string.feature_standing_instruction_amount), + fontSize = 16.sp, + color = Color.DarkGray, + modifier = Modifier.padding(vertical = 8.dp), + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = amount.toString(), + fontSize = 16.sp, + color = Color.Black, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(end = 16.dp), + ) + } + + Text( + text = stringResource(R.string.feature_standing_instruction_valid_from), + fontSize = 16.sp, + color = Color.DarkGray, + modifier = Modifier.padding(vertical = 8.dp), + ) + + Text( + text = validFrom.toString(), + fontSize = 16.sp, + color = Color.Black, + modifier = Modifier.padding(bottom = 16.dp), + ) + + Text( + text = stringResource(id = R.string.feature_standing_instruction_valid_till), + fontSize = 16.sp, + color = Color.DarkGray, + modifier = Modifier.padding(vertical = 8.dp), + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = validTill.toString(), + fontSize = 16.sp, + color = Color.Black, + modifier = Modifier.padding(end = 16.dp), + ) + } + + Text( + text = stringResource(R.string.feature_standing_instruction_to), + fontSize = 16.sp, + color = Color.DarkGray, + modifier = Modifier.padding(vertical = 8.dp), + ) + Text( + text = siToName, + fontSize = 16.sp, + color = Color.Black, + modifier = Modifier.padding(bottom = 8.dp), + ) + Text( + text = siToNumber, + fontSize = 16.sp, + color = Color.Black, + modifier = Modifier.padding(bottom = 16.dp), + ) + + Text( + text = stringResource(R.string.feature_standing_instruction_from), + fontSize = 16.sp, + color = Color.DarkGray, + modifier = Modifier.padding(vertical = 8.dp), + ) + Text( + text = siFromName, + fontSize = 16.sp, + color = Color.Black, + modifier = Modifier.padding(bottom = 8.dp), + ) + Text( + text = siFromNumber, + fontSize = 16.sp, + color = Color.Black, + modifier = Modifier.padding(bottom = 16.dp), + ) + + Text( + text = stringResource(id = R.string.feature_standing_instruction_interval), + fontSize = 16.sp, + color = Color.DarkGray, + modifier = Modifier.padding(vertical = 8.dp), + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = recurrenceInterval.toString(), + fontSize = 16.sp, + color = Color.Black, + modifier = Modifier.padding(end = 16.dp), + ) + } + + Text( + text = stringResource(R.string.feature_standing_instruction_status), + fontSize = 16.sp, + color = Color.DarkGray, + modifier = Modifier.padding(vertical = 8.dp), + ) + Text( + text = status, + fontSize = 16.sp, + color = Color.Blue, + modifier = Modifier.padding(bottom = 16.dp), + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewSIDetailsScreen() { + SIDetailsScreen( + siDetailsUiState = SiDetailsUiState.ShowSiDetails( + StandingInstruction( + id = 1, + name = "Dummy Standing Instruction", + fromClient = Client(), + fromAccount = SavingAccount(), + toClient = Client(), + toAccount = SavingAccount(), + status = Status(), + amount = 1000.0, + validFrom = listOf(2024, 1, 1), + validTill = listOf(2025, 12, 31), + recurrenceInterval = 1, + recurrenceOnMonthDay = listOf(1), + ), + ), + onBackPress = {}, + onClickCreateNew = {}, + ) +} diff --git a/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/StandingInstructionDetailsViewModel.kt b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/StandingInstructionDetailsViewModel.kt new file mode 100644 index 000000000..ec6b5adb2 --- /dev/null +++ b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/StandingInstructionDetailsViewModel.kt @@ -0,0 +1,106 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.standing.instruction + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.entity.standinginstruction.StandingInstruction +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.standinginstruction.DeleteStandingInstruction +import org.mifospay.core.data.domain.usecase.standinginstruction.FetchStandingInstruction +import org.mifospay.core.data.domain.usecase.standinginstruction.UpdateStandingInstruction + +class StandingInstructionDetailsViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val fetchStandingInstruction: FetchStandingInstruction, + private val updateStandingInstruction: UpdateStandingInstruction, + private val deleteStandingInstruction: DeleteStandingInstruction, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + private var _siDetailsUiState = MutableStateFlow(SiDetailsUiState.Loading) + var siDetailsUiState: StateFlow = _siDetailsUiState + + private val _updateSuccess = MutableStateFlow(false) + val updateSuccess: StateFlow = _updateSuccess + + private val _deleteSuccess = MutableStateFlow(false) + val deleteSuccess: StateFlow = _deleteSuccess + + init { + savedStateHandle.get("standingInstructionId")?.let { + fetchStandingInstructionDetails(it) + } + } + + private fun fetchStandingInstructionDetails(standingInstructionId: Long) { + _siDetailsUiState.value = SiDetailsUiState.Loading + mUseCaseHandler.execute( + fetchStandingInstruction, + FetchStandingInstruction.RequestValues(standingInstructionId), + object : + UseCase.UseCaseCallback { + + override fun onSuccess(response: FetchStandingInstruction.ResponseValue) { + _siDetailsUiState.value = + SiDetailsUiState.ShowSiDetails(response.standingInstruction) + } + + override fun onError(message: String) { + _updateSuccess.value = false + } + }, + ) + } + + fun deleteStandingInstruction(standingInstructionId: Long) { + _siDetailsUiState.value = SiDetailsUiState.Loading + mUseCaseHandler.execute( + deleteStandingInstruction, + DeleteStandingInstruction.RequestValues(standingInstructionId), + object : + UseCase.UseCaseCallback { + + override fun onSuccess(response: DeleteStandingInstruction.ResponseValue) { + _deleteSuccess.value = true + } + + override fun onError(message: String) { + _updateSuccess.value = false + } + }, + ) + } + + fun updateStandingInstruction(standingInstruction: StandingInstruction) { + _siDetailsUiState.value = SiDetailsUiState.Loading + mUseCaseHandler.execute( + updateStandingInstruction, + UpdateStandingInstruction.RequestValues(standingInstruction.id, standingInstruction), + object : UseCase.UseCaseCallback { + + override fun onSuccess(response: UpdateStandingInstruction.ResponseValue) { + _siDetailsUiState.value = SiDetailsUiState.ShowSiDetails(standingInstruction) + } + + override fun onError(message: String) { + _updateSuccess.value = false + } + }, + ) + } +} + +sealed interface SiDetailsUiState { + data object Loading : SiDetailsUiState + data class ShowSiDetails(val standingInstruction: StandingInstruction) : SiDetailsUiState +} diff --git a/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/StandingInstructionScreen.kt b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/StandingInstructionScreen.kt new file mode 100644 index 000000000..c823e32e8 --- /dev/null +++ b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/StandingInstructionScreen.kt @@ -0,0 +1,152 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.standing.instruction + +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.koin.androidx.compose.koinViewModel +import org.mifospay.core.designsystem.component.FloatingActionButtonContent +import org.mifospay.core.designsystem.component.MifosLoadingWheel +import org.mifospay.core.designsystem.component.MifosScaffold +import org.mifospay.core.designsystem.icon.MifosIcons +import org.mifospay.core.ui.EmptyContentScreen + +@Composable +fun StandingInstructionsScreenRoute( + onNewSI: () -> Unit, + onBackPress: () -> Unit, + modifier: Modifier = Modifier, + viewModel: StandingInstructionViewModel = koinViewModel(), +) { + val standingInstructionsUiState by viewModel.standingInstructionsUiState.collectAsStateWithLifecycle() + + StandingInstructionScreen( + standingInstructionsUiState = standingInstructionsUiState, + onNewSI = onNewSI, + onBackPress = onBackPress, + modifier = modifier, + ) +} + +@Composable +@VisibleForTesting +internal fun StandingInstructionScreen( + standingInstructionsUiState: StandingInstructionsUiState, + onNewSI: () -> Unit, + onBackPress: () -> Unit, + modifier: Modifier = Modifier, +) { + val floatingActionButtonContent = FloatingActionButtonContent( + onClick = onNewSI, + contentColor = MaterialTheme.colorScheme.onPrimary, + content = { + Icon( + imageVector = MifosIcons.Add, + contentDescription = stringResource(R.string.feature_standing_instruction_downloading_receipt), + ) + }, + ) + + MifosScaffold( + backPress = onBackPress, + floatingActionButtonContent = floatingActionButtonContent, + modifier = modifier, + scaffoldContent = {}, + ) { + when (standingInstructionsUiState) { + StandingInstructionsUiState.Empty -> { + EmptyContentScreen( + title = stringResource(id = R.string.feature_standing_instruction_error_oops), + subTitle = stringResource(id = R.string.feature_standing_instruction_empty_standing_instructions), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.primary, + iconImageVector = MifosIcons.Info, + ) + } + + is StandingInstructionsUiState.Error -> { + EmptyContentScreen( + title = stringResource(id = R.string.feature_standing_instruction_error_oops), + subTitle = stringResource(id = R.string.feature_standing_instruction_error_fetching_si_list), + modifier = Modifier, + iconTint = MaterialTheme.colorScheme.primary, + iconImageVector = MifosIcons.RoundedInfo, + ) + } + + StandingInstructionsUiState.Loading -> { + MifosLoadingWheel( + modifier = Modifier.fillMaxWidth(), + contentDesc = stringResource(R.string.feature_standing_instruction_loading), + ) + } + + is StandingInstructionsUiState.StandingInstructionList -> { + Column( + modifier = Modifier + .fillMaxSize(), + ) { + LazyColumn(modifier = Modifier.fillMaxSize()) { + items(standingInstructionsUiState.standingInstructionList) { items -> + SIContent( + fromClientName = items.fromClient.displayName.toString(), + toClientName = items.toClient.displayName.toString(), + validTill = items.validTill.toString(), + amount = items.amount.toString(), + ) + } + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun StandingInstructionsScreenLoadingPreview() { + StandingInstructionScreen( + standingInstructionsUiState = StandingInstructionsUiState.Loading, + onNewSI = {}, + onBackPress = {}, + ) +} + +@Preview(showBackground = true) +@Composable +private fun StandingInstructionsEmptyPreview() { + StandingInstructionScreen( + standingInstructionsUiState = StandingInstructionsUiState.Empty, + onNewSI = {}, + onBackPress = {}, + ) +} + +@Preview(showBackground = true) +@Composable +private fun StandingInstructionsErrorPreview() { + StandingInstructionScreen( + standingInstructionsUiState = StandingInstructionsUiState.Error("Error Screen"), + onNewSI = {}, + onBackPress = {}, + ) +} diff --git a/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/StandingInstructionViewModel.kt b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/StandingInstructionViewModel.kt new file mode 100644 index 000000000..7d1c6b798 --- /dev/null +++ b/feature/standing-instruction/src/main/kotlin/org/mifospay/feature/standing/instruction/StandingInstructionViewModel.kt @@ -0,0 +1,67 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.feature.standing.instruction + +import androidx.lifecycle.ViewModel +import com.mifospay.core.model.entity.standinginstruction.StandingInstruction +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.mifospay.core.data.base.UseCase +import org.mifospay.core.data.base.UseCaseHandler +import org.mifospay.core.data.domain.usecase.standinginstruction.GetAllStandingInstructions +import org.mifospay.core.data.repository.local.LocalRepository +import org.mifospay.feature.standing.instruction.StandingInstructionsUiState.Loading + +class StandingInstructionViewModel( + private val mUseCaseHandler: UseCaseHandler, + private val localRepository: LocalRepository, + private val getAllStandingInstructions: GetAllStandingInstructions, +) : ViewModel() { + + private val mInstructionState = MutableStateFlow(Loading) + val standingInstructionsUiState: StateFlow = mInstructionState + + init { + getAllSI() + } + + private fun getAllSI() { + val client = localRepository.clientDetails + mInstructionState.value = Loading + mUseCaseHandler.execute( + getAllStandingInstructions, + GetAllStandingInstructions.RequestValues(client.clientId), + object : + UseCase.UseCaseCallback { + + override fun onSuccess(response: GetAllStandingInstructions.ResponseValue) { + if (response.standingInstructionsList.isEmpty()) { + mInstructionState.value = StandingInstructionsUiState.Empty + } else { + mInstructionState.value = + StandingInstructionsUiState.StandingInstructionList(response.standingInstructionsList) + } + } + + override fun onError(message: String) { + mInstructionState.value = StandingInstructionsUiState.Error(message) + } + }, + ) + } +} + +sealed class StandingInstructionsUiState { + data object Loading : StandingInstructionsUiState() + data object Empty : StandingInstructionsUiState() + data class Error(val message: String) : StandingInstructionsUiState() + data class StandingInstructionList(val standingInstructionList: List) : + StandingInstructionsUiState() +} diff --git a/feature/upi-setup/build.gradle.kts b/feature/upi-setup/build.gradle.kts index aab0f4051..a40950877 100644 --- a/feature/upi-setup/build.gradle.kts +++ b/feature/upi-setup/build.gradle.kts @@ -8,22 +8,12 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.mifospay.android.feature) + alias(libs.plugins.mifospay.android.library.compose) } android { - namespace = "org.mifospay.feature.upi.setup" + namespace = "org.mifospay.feature.upi_setup" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - } - } -} \ No newline at end of file +dependencies { } \ No newline at end of file diff --git a/feature/upi-setup/src/commonMain/kotlin/org/mifospay/feature/upi/setup/screens/DebitCardScreen.kt b/feature/upi-setup/src/commonMain/kotlin/org/mifospay/feature/upi/setup/screens/DebitCardScreen.kt index 01652616e..06c7de46a 100644 --- a/feature/upi-setup/src/commonMain/kotlin/org/mifospay/feature/upi/setup/screens/DebitCardScreen.kt +++ b/feature/upi-setup/src/commonMain/kotlin/org/mifospay/feature/upi/setup/screens/DebitCardScreen.kt @@ -7,7 +7,7 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.feature.upi.setup.screens +package org.mifospay.feature.upiSetup.screens import androidx.annotation.VisibleForTesting import androidx.compose.foundation.layout.Box @@ -21,15 +21,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.jetbrains.compose.ui.tooling.preview.Preview -import org.koin.compose.viewmodel.koinViewModel +import org.koin.androidx.compose.koinViewModel import org.mifospay.core.designsystem.component.MifosLoadingWheel import org.mifospay.core.designsystem.theme.MifosTheme import org.mifospay.core.ui.VerifyStepHeader -import org.mifospay.feature.upi.setup.viewmodel.DebitCardUiState -import org.mifospay.feature.upi.setup.viewmodel.DebitCardViewModel +import org.mifospay.feature.upiSetup.viewmodel.DebitCardUiState +import org.mifospay.feature.upiSetup.viewmodel.DebitCardViewModel @Composable internal fun DebitCardScreen( diff --git a/feature/upi-setup/src/commonMain/kotlin/org/mifospay/feature/upi/setup/screens/SetUpUPiPinScreen.kt b/feature/upi-setup/src/commonMain/kotlin/org/mifospay/feature/upi/setup/screens/SetUpUPiPinScreen.kt index 6ae2c934d..02e57efc5 100644 --- a/feature/upi-setup/src/commonMain/kotlin/org/mifospay/feature/upi/setup/screens/SetUpUPiPinScreen.kt +++ b/feature/upi-setup/src/commonMain/kotlin/org/mifospay/feature/upi/setup/screens/SetUpUPiPinScreen.kt @@ -7,8 +7,11 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.feature.upi.setup.screens +package org.mifospay.feature.upiSetup.screens +import android.app.Activity +import android.content.Intent +import android.widget.Toast import androidx.annotation.VisibleForTesting import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -22,15 +25,15 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import mobile_wallet.feature.upi_setup.generated.resources.Res -import mobile_wallet.feature.upi_setup.generated.resources.feature_upi_setup_back -import org.jetbrains.compose.resources.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview -import org.koin.compose.viewmodel.koinViewModel -import org.mifospay.core.common.Constants +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.mifospay.core.model.domain.BankAccountDetails +import org.koin.androidx.compose.koinViewModel +import org.mifospay.common.Constants import org.mifospay.core.designsystem.icon.MifosIcons -import org.mifospay.core.model.bank.BankAccountDetails -import org.mifospay.feature.upi.setup.viewmodel.SetUpUpiViewModal +import org.mifospay.feature.upiSetup.viewmodel.SetUpUpiViewModal +import org.mifospay.feature.upi_setup.R @Composable internal fun SetupUpiPinScreenRoute( @@ -66,6 +69,7 @@ internal fun SetupUpiPinScreen( onBackPress: () -> Unit, modifier: Modifier = Modifier, ) { + val context = LocalContext.current Scaffold( modifier = modifier, topBar = { @@ -86,7 +90,7 @@ internal fun SetupUpiPinScreen( ) { Icon( MifosIcons.ArrowBack, - contentDescription = stringResource(Res.string.feature_upi_setup_back), + contentDescription = stringResource(id = R.string.feature_upi_setup_back), ) } }, @@ -104,6 +108,19 @@ internal fun SetupUpiPinScreen( otpText = otpText, correctlySettingUpi = { setupUpiPin(it) + bankAccountDetails.isUpiEnabled = true + bankAccountDetails.upiPin = it + Toast.makeText( + context, + Constants.UPI_PIN_SETUP_COMPLETED_SUCCESSFULLY, + Toast.LENGTH_SHORT, + ).show() + val intent = Intent().apply { + putExtra(Constants.UPDATED_BANK_ACCOUNT, bankAccountDetails) + putExtra(Constants.INDEX, index) + } + (context as? Activity)?.setResult(Activity.RESULT_OK, intent) + (context as? Activity)?.finish() }, ) } @@ -152,13 +169,10 @@ fun PreviewForgetUpi() { fun getBankAccountDetails(): BankAccountDetails { return BankAccountDetails( - accountNo = "SBI", - bankName = "Ankur Sharma", - accountHolderName = "New Delhi", - branch = "XXXXXXXX9990XXX " + " ", - ifsc = "Savings", - type = "Debit", - isUpiEnabled = false, - upiPin = "0000", + "SBI", + "Ankur Sharma", + "New Delhi", + "XXXXXXXX9990XXX " + " ", + "Savings", ) } diff --git a/feature/upi-setup/src/commonMain/kotlin/org/mifospay/feature/upi/setup/viewmodel/SetUpUpiViewModal.kt b/feature/upi-setup/src/commonMain/kotlin/org/mifospay/feature/upi/setup/viewmodel/SetUpUpiViewModal.kt index bfd53a7aa..1f402a00c 100644 --- a/feature/upi-setup/src/commonMain/kotlin/org/mifospay/feature/upi/setup/viewmodel/SetUpUpiViewModal.kt +++ b/feature/upi-setup/src/commonMain/kotlin/org/mifospay/feature/upi/setup/viewmodel/SetUpUpiViewModal.kt @@ -7,10 +7,10 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.feature.upi.setup.viewmodel +package org.mifospay.feature.upiSetup.viewmodel import androidx.lifecycle.ViewModel -import org.mifospay.core.model.bank.BankAccountDetails +import com.mifospay.core.model.domain.BankAccountDetails @Suppress("UnusedParameter") class SetUpUpiViewModal : ViewModel() { diff --git a/gradle.properties b/gradle.properties index 577ab461e..79d506e00 100644 --- a/gradle.properties +++ b/gradle.properties @@ -47,7 +47,6 @@ kotlin.code.style=official android.defaults.buildfeatures.resvalues=false android.defaults.buildfeatures.shaders=false android.testOptions.unitTests.isIncludeAndroidResources = true -org.jetbrains.compose.experimental.jscanvas.enabled=true RblClientIdProp=a RblClientSecretProp=b diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 17bed6764..4c289848f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,186 +1,142 @@ [versions] -accompanist = "0.34.0" - -# Android -androidDesugarJdkLibs = "2.1.2" +accompanistPagerVersion = "0.34.0" +activityVersion = "1.9.1" +androidDesugarJdkLibs = "2.0.4" androidGradlePlugin = "8.5.2" androidTools = "31.5.2" - -# AndroidX Dependencies androidx-test-ext-junit = "1.2.1" -androidxActivity = "1.9.3" +androidxActivity = "1.9.1" androidxBrowser = "1.8.0" -androidxComposeBom = "2024.10.01" +androidxComposeBom = "2024.08.00" androidxComposeCompiler = "1.5.15" -androidxComposeMaterial3Adaptive = "1.0.0" -androidxComposeRuntimeTracing = "1.7.5" +androidxComposeMaterial3Adaptive = "1.0.0-rc01" +androidxComposeRuntime = "1.6.8" +androidxComposeRuntimeTracing = "1.0.0-beta01" +androidxComposeUi = "1.6.8" +androidxComposeUiTest = "1.7.0-beta01" androidxCoreSplashscreen = "1.0.1" -androidxLifecycle = "2.8.7" +androidxDataStore = "1.1.1" +androidxLifecycle = "2.8.4" androidxMetrics = "1.0.0-beta01" -androidxNavigation = "2.8.3" -androidxProfileinstaller = "1.4.1" +androidxNavigation = "2.8.0-rc01" +androidxProfileinstaller = "1.3.1" androidxTracing = "1.3.0-alpha02" appcompatVersion = "1.7.0" +cameraLifecycleVersion = "1.3.4" +cameraViewVersion = "1.3.4" +coil = "2.6.0" +compileSdk = "34" +compose-plugin = "1.6.11" coreKtxVersion = "1.13.1" - -# KotlinX Dependencies -lifecycleExtensionsVersion = "2.2.0" -lifecycleVersion = "2.8.7" - -# Android Camera & Play Services -cameraLifecycleVersion = "1.4.0" -cameraViewVersion = "1.4.0" -playServicesAuthVersion = "21.2.0" -playServicesCodeScanner = "16.1.0" -mlkit="17.3.0" - -# Testing Dependencies -espresso-core = "3.6.1" -junitVersion = "4.13.2" -truth = "1.4.4" -roborazzi = "1.26.0" -zxingVersion = "3.5.3" - -# Utility Dependencies +credentialsVersion = "1.2.2" +datastore = "1.1.1" dependencyGuard = "0.5.0" -moduleGraph = "2.5.0" -secrets = "2.0.1" -protobuf = "4.26.0" -protobufPlugin = "0.9.4" +detekt = "1.23.5" +espresso-core = "3.6.1" +firebaseBom = "33.1.2" +firebaseCrashlyticsPlugin = "3.0.2" +firebasePerfPlugin = "1.4.2" gmsPlugin = "4.4.2" googleOss = "17.1.0" googleOssPlugin = "0.10.6" googleidVersion = "1.1.1" -guavaVersion = "33.3.1-android" -credentialsVersion = "1.3.0" - -# Static Analysis & Code Formatting -ktlint = "12.1.1" -detekt = "1.23.7" -spotlessVersion = "6.25.0" -twitter-detekt-compose = "0.0.26" -versionCatalogLinterVersion = "1.0.3" - -# Fineract KMP Library -fineractSdk = "1.0.3" - -# Firebase -firebaseBom = "33.5.1" -firebaseCrashlyticsPlugin = "3.0.2" -firebasePerfPlugin = "1.4.2" - -# Kotlin KMP Dependencies -kotlin = "2.0.20" -kotlinInject = "0.7.2" -kotlinxCoroutines = "1.9.0" -kotlinxDatetime = "0.6.1" -kotlinxImmutable = "0.3.8" -kotlinxSerializationJson = "1.7.2" -ksp = "2.0.20-1.0.25" - -# Ktor & Ktorfit -ktorVersion = "3.0.0-rc-1" -ktorfit = "2.1.0" -ktorfitKsp = "2.1.0-1.0.25" - -# Koin CMP Dependencies +junitVersion = "4.13.2" koin = "4.0.0-RC2" koinAnnotationsVersion = "1.4.0-RC4" koinComposeMultiplatform = "1.2.0-Beta4" - -# CMP Libraries -compose-plugin = "1.7.0-rc01" -coil = "3.0.0-alpha10" -backHandlerVersion = "2.1.0" -constraintLayout = "0.4.0" -multiplatformSettings = "1.2.0" -mokoPermission = "0.18.0" -qroseVersion = "1.0.1" -okioVersion = "3.9.1" -kermit = "2.0.4" -fileKit = "0.8.7" -wire = "5.0.0" - -# Jetbrains CMP -windowsSizeClass = "0.5.0" +kotlin = "2.0.20" +kotlinStdlibVersion = "1.9.0" +kotlinxCoroutines = "1.8.1" +kotlinxDatetime = "0.6.0" +kotlinxImmutable = "0.3.7" +kotlinxSerializationJson = "1.7.1" +kotlinxSerializationJsonVersion = "1.3.0" +ksp = "2.0.20-1.0.24" +ktlint = "12.1.1" +ktorVersion = "2.3.4" +libphonenumberAndroidVersion = "8.13.35" +lifecycleExtensionsVersion = "2.2.0" +lifecycleVersion = "2.8.4" +logbackClassicVersion = "1.2.3" +minSdk = "24" +moduleGraph = "2.5.0" +okHttp3Version = "4.12.0" +playServicesAuthVersion = "21.2.0" +playServicesCodeScanner = "16.1.0" +protobuf = "4.26.0" +protobufPlugin = "0.9.4" +qrkitVersion = "2.0.0" +retrofitKotlinxSerializationJson = "1.0.0" +retrofitVersion = "2.11.0" +roborazzi = "1.26.0" +room = "2.6.1" +rxandroidVersion = "1.1.0" +rxjavaVersion = "1.3.8" +secrets = "2.0.1" +sheets_compose_dialogs_core = "1.3.0" +spotlessVersion = "6.23.3" +targetSdk = "34" +truth = "1.4.2" +twitter-detekt-compose = "0.0.26" uiDesktopVersion = "1.7.0" -composeJB = "1.7.0" -composeLifecycle = "2.8.2" -composeNavigation = "2.8.0-alpha10" -jbCoreBundle = "1.0.1" -jbSavedState = "1.2.2" +versionCatalogLinterVersion = "1.0.3" +wire = "5.0.0" +zxingVersion = "3.5.3" [libraries] -accompanist-pager = { group = "com.google.accompanist", name = "accompanist-pager", version.ref = "accompanist" } -accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } - +accompanist-pager = { group = "com.google.accompanist", name = "accompanist-pager", version.ref = "accompanistPagerVersion" } android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" } android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } android-tools-common = { group = "com.android.tools", name = "common", version.ref = "androidTools" } - androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } -androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "androidxActivity" } - +androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityVersion" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompatVersion" } androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "androidxBrowser" } - -androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraViewVersion" } androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "cameraLifecycleVersion" } androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "cameraViewVersion" } - androidx-compose-animation = { group = "androidx.compose.animation", name = "animation" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" } androidx-compose-compiler = { group = "androidx.compose.compiler", name = "compiler", version.ref = "androidxComposeCompiler" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" } - androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "androidxComposeMaterial3Adaptive" } androidx-compose-material3-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout", version.ref = "androidxComposeMaterial3Adaptive" } androidx-compose-material3-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "androidxComposeMaterial3Adaptive" } androidx-compose-material3-windowSizeClass = { group = "androidx.compose.material3", name = "material3-window-size-class" } - -androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } +androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "androidxComposeRuntime" } androidx-compose-runtime-tracing = { group = "androidx.compose.runtime", name = "runtime-tracing", version.ref = "androidxComposeRuntimeTracing" } -androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } -androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidxComposeUi" } +androidx-compose-ui-test = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "androidxComposeUiTest" } androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } -androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" } - +androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util", version.ref = "androidxComposeUi" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtxVersion" } - androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidxCoreSplashscreen" } - androidx-credentials = { group = "androidx.credentials", name = "credentials", version.ref = "credentialsVersion" } androidx-credentials-play-services-auth = { group = "androidx.credentials", name = "credentials-play-services-auth", version.ref = "credentialsVersion" } - +androidx-dataStore-core = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDataStore" } androidx-lifecycle-extensions = { group = "androidx.lifecycle", name = "lifecycle-extensions", version.ref = "lifecycleExtensionsVersion" } androidx-lifecycle-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleVersion" } androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } -androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "androidxLifecycle" } androidx-lifecycle-runtimeTesting = { group = "androidx.lifecycle", name = "lifecycle-runtime-testing", version.ref = "androidxLifecycle" } androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } - androidx-metrics = { group = "androidx.metrics", name = "metrics-performance", version.ref = "androidxMetrics" } - androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxNavigation" } - androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfileinstaller" } - androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } androidx-tracing-ktx = { group = "androidx.tracing", name = "tracing-ktx", version.ref = "androidxTracing" } - -ktlint-gradlePlugin = { group = "org.jlleitschuh.gradle", name = "ktlint-gradle", version.ref = "ktlint" } +androidx-ui-desktop = { group = "androidx.compose.ui", name = "ui-desktop", version.ref = "uiDesktopVersion" } +coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil" } +coil-kt-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } +compose-gradlePlugin = { group = "org.jetbrains.kotlin", name = "compose-compiler-gradle-plugin", version.ref = "kotlin" } +datastore = { group = "androidx.datastore", name = "datastore-core-okio", version.ref = "datastore" } detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" } -spotless-gradle = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotlessVersion" } detekt-gradlePlugin = { group = "io.gitlab.arturbosch.detekt", name = "detekt-gradle-plugin", version.ref = "detekt" } -guava = { module = "com.google.guava:guava", version.ref = "guavaVersion" } -twitter-detekt-compose = { group = "com.twitter.compose.rules", name = "detekt", version.ref = "twitter-detekt-compose" } - +espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" } firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } firebase-cloud-messaging = { group = "com.google.firebase", name = "firebase-messaging-ktx" } @@ -188,45 +144,12 @@ firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashly firebase-crashlytics-gradlePlugin = { group = "com.google.firebase", name = "firebase-crashlytics-gradle", version.ref = "firebaseCrashlyticsPlugin" } firebase-performance = { group = "com.google.firebase", name = "firebase-perf-ktx" } firebase-performance-gradlePlugin = { group = "com.google.firebase", name = "perf-plugin", version.ref = "firebasePerfPlugin" } - -play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "playServicesAuthVersion" } google-oss-licenses = { group = "com.google.android.gms", name = "play-services-oss-licenses", version.ref = "googleOss" } google-oss-licenses-plugin = { group = "com.google.android.gms", name = "oss-licenses-plugin", version.ref = "googleOssPlugin" } google-play-services-code-scanner = { group = "com.google.android.gms", name = "play-services-code-scanner", version.ref = "playServicesCodeScanner" } googleid = { group = "com.google.android.libraries.identity.googleid", name = "googleid", version.ref = "googleidVersion" } -mlkit-barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "mlkit" } - +jetbrains-kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlinStdlibVersion" } junit = { group = "junit", name = "junit", version.ref = "junitVersion" } - -lint-api = { group = "com.android.tools.lint", name = "lint-api", version.ref = "androidTools" } -lint-checks = { group = "com.android.tools.lint", name = "lint-checks", version.ref = "androidTools" } -lint-tests = { group = "com.android.tools.lint", name = "lint-tests", version.ref = "androidTools" } - -protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" } -protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" } - -truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } -espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" } - -zxing = { group = "com.google.zxing", name = "core", version.ref = "zxingVersion" } - -fineract-api = { group = "io.github.niyajali", name = "fineract-client-kmp", version.ref = "fineractSdk" } -fineract-sdk = { group = "com.github.openMF", name = "mifos-android-sdk-arch", version.ref = "fineractSdk" } - - -# jb Compose -jb-kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } -jb-kotlin-stdlib-js = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-js", version.ref = "kotlin" } -jb-kotlin-dom = { group = "org.jetbrains.kotlin", name = "kotlin-dom-api-compat", version.ref = "kotlin" } -jb-composeRuntime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeJB" } -jb-composeViewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "composeLifecycle" } -jb-lifecycleViewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel", version.ref = "composeLifecycle" } -jb-lifecycleViewmodelSavedState = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "composeLifecycle" } -jb-bundle = { module = "org.jetbrains.androidx.core:core-bundle", version.ref = "jbCoreBundle" } -jb-savedstate = { module = "org.jetbrains.androidx.savedstate:savedstate", version.ref = "jbSavedState" } -jb-composeNavigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "composeNavigation" } -jb-navigation = { module = "org.jetbrains.androidx.navigation:navigation-common", version.ref = "composeNavigation" } - koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" } koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" } koin-androidx-navigation = { group = "io.insert-koin", name = "koin-androidx-navigation", version.ref = "koin" } @@ -234,80 +157,54 @@ koin-annotations = { group = "io.insert-koin", name = "koin-annotations", versio koin-bom = { group = "io.insert-koin", name = "koin-bom", version.ref = "koin" } koin-compose = { group = "io.insert-koin", name = "koin-compose", version.ref = "koinComposeMultiplatform" } koin-compose-viewmodel = { group = "io.insert-koin", name = "koin-compose-viewmodel", version.ref = "koinComposeMultiplatform" } -koin-compose-navigation = { group = "io.insert-koin", name = "koin-compose-viewmodel-navigation", version.ref = "koinComposeMultiplatform" } koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" } koin-core-viewmodel = { group = "io.insert-koin", name = "koin-core-viewmodel", version.ref = "koin" } koin-ksp-compiler = { group = "io.insert-koin", name = "koin-ksp-compiler", version.ref = "koinAnnotationsVersion" } koin-test = { group = "io.insert-koin", name = "koin-test", version.ref = "koin" } koin-test-junit4 = { group = "io.insert-koin", name = "koin-test-junit4", version.ref = "koin" } koin-test-junit5 = { group = "io.insert-koin", name = "koin-test-junit5", version.ref = "koin" } - kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } -kotlin-inject-compiler-ksp = { group = "me.tatarka.inject", name = "kotlin-inject-compiler-ksp", version.ref = "kotlinInject" } -kotlin-inject-runtime = { group = "me.tatarka.inject", name = "kotlin-inject-runtime", version.ref = "kotlinInject" } -kotlin-inject-runtime-kmp = { group = "me.tatarka.inject", name = "kotlin-inject-runtime-kmp", version.ref = "kotlinInject" } kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" } -kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" } -kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" } - kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxImmutable" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } -kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDatetime" } -kotlinx-serialization-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "kotlinxSerializationJson" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } - ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } - +ktlint-gradlePlugin = { group = "org.jlleitschuh.gradle", name = "ktlint-gradle", version.ref = "ktlint" } ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktorVersion" } -ktor-client-auth = { group = "io.ktor", name = "ktor-client-auth", version.ref = "ktorVersion" } -ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktorVersion" } ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktorVersion" } ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktorVersion" } -ktor-client-darwin = { group = "io.ktor", name = "ktor-client-darwin", version.ref = "ktorVersion" } -ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktorVersion" } -ktor-client-java = { group = "io.ktor", name = "ktor-client-java", version.ref = "ktorVersion" } -ktor-client-js = { group = "io.ktor", name = "ktor-client-js", version.ref = "ktorVersion" } ktor-client-json = { group = "io.ktor", name = "ktor-client-json", version.ref = "ktorVersion" } ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktorVersion" } ktor-client-serialization = { group = "io.ktor", name = "ktor-client-serialization", version.ref = "ktorVersion" } ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktorVersion" } -ktor-client-winhttp = { group = "io.ktor", name = "ktor-client-winhttp", version.ref = "ktorVersion" } ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktorVersion" } -ktor-server-auth = { group = "io.ktor", name = "ktor-server-auth", version.ref = "ktorVersion" } - -ktorfit-converters-flow = { group = "de.jensklingenberg.ktorfit", name = "ktorfit-converters-flow", version.ref = "ktorfit" } -ktorfit-ksp = { group = "de.jensklingenberg.ktorfit", name = "ktorfit-ksp", version.ref = "ktorfitKsp" } -ktorfit-lib = { group = "de.jensklingenberg.ktorfit", name = "ktorfit-lib-ktor-3.0.0-beta-2", version.ref = "ktorfit" } - -coil-core = { group = "io.coil-kt.coil3", name = "coil-core", version.ref = "coil" } -coil-kt = { group = "io.coil-kt.coil3", name = "coil", version.ref = "coil" } -coil-kt-compose = { group = "io.coil-kt.coil3", name = "coil-compose-core", version.ref = "coil" } -coil-network-ktor = { group = "io.coil-kt.coil3", name = "coil-network-ktor3", version.ref = "coil" } -coil-svg = { group = "io.coil-kt.coil3", name = "coil-svg", version.ref = "coil" } - -compose-gradlePlugin = { group = "org.jetbrains.kotlin", name = "compose-compiler-gradle-plugin", version.ref = "kotlin" } -squareup-okio = { group = "com.squareup.okio", name = "okio", version.ref = "okioVersion" } -back-handler = { group = "com.arkivanov.essenty", name = "back-handler", version.ref = "backHandlerVersion" } -constraint-layout = { group = "tech.annexflow.compose", name="constraintlayout-compose-multiplatform", version.ref = "constraintLayout" } -filekit-core = { group = "io.github.vinceglb", name = "filekit-core", version.ref = "fileKit" } -filekit-compose = { group = "io.github.vinceglb", name = "filekit-compose", version.ref = "fileKit" } -qrose = { group = "io.github.alexzhirkevich", name="qrose", version.ref = "qroseVersion" } - -kermit-logging = { group = "co.touchlab", name = "kermit", version.ref = "kermit" } -kermit-simple = { group = "co.touchlab", name = "kermit-simple", version.ref = "kermit" } - -multiplatform-settings = { group = "com.russhwolf", name = "multiplatform-settings-no-arg", version.ref = "multiplatformSettings" } -multiplatform-settings-coroutines = { group = "com.russhwolf", name = "multiplatform-settings-coroutines", version.ref = "multiplatformSettings" } -multiplatform-settings-serialization = { group = "com.russhwolf", name = "multiplatform-settings-serialization", version.ref = "multiplatformSettings" } -multiplatform-settings-test = { group = "com.russhwolf", name = "multiplatform-settings-test", version.ref = "multiplatformSettings" } - -moko-permission = { group = "dev.icerock.moko", name = "permissions", version.ref = "mokoPermission" } -moko-permission-compose = { group = "dev.icerock.moko", name = "permissions-compose", version.ref = "mokoPermission" } - -window-size = { group = "dev.chrisbanes.material3", name = "material3-window-size-class-multiplatform", version.ref = "windowsSizeClass" } +libphonenumber-android = { group = "io.michaelrocks", name = "libphonenumber-android", version.ref = "libphonenumberAndroidVersion" } +lint-api = { group = "com.android.tools.lint", name = "lint-api", version.ref = "androidTools" } +lint-checks = { group = "com.android.tools.lint", name = "lint-checks", version.ref = "androidTools" } +lint-tests = { group = "com.android.tools.lint", name = "lint-tests", version.ref = "androidTools" } +logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logbackClassicVersion" } +play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "playServicesAuthVersion" } +protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" } +protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" } +qrkit = { group = "network.chaintech", name = "qr-kit", version.ref = "qrkitVersion" } +reactivex-rxjava = { group = "io.reactivex", name = "rxjava", version.ref = "rxjavaVersion" } +reactivex-rxjava-android = { group = "io.reactivex", name = "rxandroid", version.ref = "rxandroidVersion" } +retrofit-kotlin-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinxSerializationJson" } +room-gradlePlugin = { group = "androidx.room", name = "room-gradle-plugin", version.ref = "room" } +sheets-compose-dialogs-calender = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "calendar", version.ref = "sheets_compose_dialogs_core" } +sheets-compose-dialogs-core = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "core", version.ref = "sheets_compose_dialogs_core" } +spotless-gradle = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotlessVersion" } +squareup-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okHttp3Version" } +squareup-okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okHttp3Version" } +squareup-retrofit-adapter-rxjava = { group = "com.squareup.retrofit2", name = "adapter-rxjava", version.ref = "retrofitVersion" } +squareup-retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofitVersion" } +squareup-retrofit2 = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofitVersion" } +truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } +twitter-detekt-compose = { group = "com.twitter.compose.rules", name = "detekt", version.ref = "twitter-detekt-compose" } +zxing = { group = "com.google.zxing", name = "core", version.ref = "zxingVersion" } [bundles] androidx-compose-ui-test = [ @@ -316,49 +213,44 @@ androidx-compose-ui-test = [ ] [plugins] -# Android & Kotlin Plugins android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } - -mifospay-android-application = { id = "mifospay.android.application", version = "unspecified" } -mifospay-android-application-compose = { id = "mifospay.android.application.compose", version = "unspecified" } -mifospay-android-application-flavors = { id = "mifospay.android.application.flavors", version = "unspecified" } - -# KMP & CMP Plugins compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +dependencyGuard = { id = "com.dropbox.dependency-guard", version.ref = "dependencyGuard" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" } +firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin" } +gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin" } jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } -ktrofit = { id = "de.jensklingenberg.ktorfit", version.ref = "ktorfit" } -wire = { id = "com.squareup.wire", version.ref = "wire" } - -mifospay-cmp-feature = { id = "mifospay.cmp.feature", version = "unspecified" } -mifospay-kmp-koin = { id = "mifospay.kmp.koin", version = "unspecified" } -mifospay-kmp-library = { id = "mifospay.kmp.library", version = "unspecified" } - -# Utility Plugins +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } mifos-detekt-plugin = { id = "mifos.detekt.plugin", version = "unspecified" } mifos-git-hooks = { id = "mifos.git.hooks", version = "unspecified" } mifos-ktlint-plugin = { id = "mifos.ktlint.plugin", version = "unspecified" } mifos-spotless-plugin = { id = "mifos.spotless.plugin", version = "unspecified" } - -firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" } -firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin" } - +mifospay-android-application = { id = "mifospay.android.application", version = "unspecified" } +mifospay-android-application-compose = { id = "mifospay.android.application.compose", version = "unspecified" } +mifospay-android-application-firebase = { id = "mifospay.android.application.firebase", version = "unspecified" } +mifospay-android-application-flavors = { id = "mifospay.android.application.flavors", version = "unspecified" } +mifospay-android-feature = { id = "mifospay.android.feature", version = "unspecified" } +mifospay-android-koin = { id = "mifospay.android.koin", version = "unspecified" } +mifospay-android-library = { id = "mifospay.android.library", version = "unspecified" } +mifospay-android-library-compose = { id = "mifospay.android.library.compose", version = "unspecified" } +mifospay-android-lint = { id = "mifospay.android.lint", version = "unspecified" } +mifospay-android-room = { id = "mifospay.android.room", version = "unspecified" } +mifospay-android-test = { id = "mifospay.android.test", version = "unspecified" } +mifospay-jvm-library = { id = "mifospay.jvm.library", version = "unspecified" } +module-graph = { id = "com.jraska.module.graph.assertion", version.ref = "moduleGraph" } protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } -gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin" } -module-graph = { id = "com.jraska.module.graph.assertion", version.ref = "moduleGraph" } -dependencyGuard = { id = "com.dropbox.dependency-guard", version.ref = "dependencyGuard" } +room = { id = "androidx.room", version.ref = "room" } secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secrets" } - -detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } -ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } spotless = { id = "com.diffplug.spotless", version.ref = "spotlessVersion" } version-catalog-linter = { id = "io.github.pemistahl.version-catalog-linter", version.ref = "versionCatalogLinterVersion" } - +wire = { id = "com.squareup.wire", version.ref = "wire" } diff --git a/libs/country-code-picker/src/main/kotlin/com/mifos/library/countrycodepicker/CountryCodePicker.kt b/libs/country-code-picker/src/main/kotlin/com/mifos/library/countrycodepicker/CountryCodePicker.kt new file mode 100644 index 000000000..e5072349c --- /dev/null +++ b/libs/country-code-picker/src/main/kotlin/com/mifos/library/countrycodepicker/CountryCodePicker.kt @@ -0,0 +1,616 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package com.mifos.library.countrycodepicker + +import android.content.Context +import android.util.Log +import androidx.compose.foundation.focusable +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.Box +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.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldColors +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.mifos.library.countrycodepicker.component.CountryCodeDialog +import com.mifos.library.countrycodepicker.component.autofill +import com.mifos.library.countrycodepicker.data.CountryData +import com.mifos.library.countrycodepicker.data.Iso31661alpha2 +import com.mifos.library.countrycodepicker.data.PhoneCode +import com.mifos.library.countrycodepicker.data.utils.ValidatePhoneNumber +import com.mifos.library.countrycodepicker.data.utils.extractCountryCode +import com.mifos.library.countrycodepicker.data.utils.getCountryFromPhoneCode +import com.mifos.library.countrycodepicker.data.utils.getUserIsoCode +import com.mifos.library.countrycodepicker.data.utils.numberHint +import com.mifos.library.countrycodepicker.transformation.PhoneNumberTransformation +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.coroutines.launch + +private val DEFAULT_TEXT_FIELD_SHAPE = RoundedCornerShape(24.dp) +private const val TAG = "CountryCodePicker" + +/** + * @param onValueChange Called when the text in the text field changes. + * The first parameter is string pair of (country phone code, phone number) and the second parameter is + * a boolean indicating whether the phone number is valid. + * @param modifier Modifier to be applied to the inner OutlinedTextField. + * @param autoDetectCode Boolean indicating if will auto detect the code from initial phone number + * @param enabled Boolean indicating whether the field is enabled. + * @param shape Shape of the text field. + * @param showCountryCode Whether to show the country code in the text field. + * @param showCountryFlag Whether to show the country flag in the text field. + * @param colors TextFieldColors to be used for the text field. + * @param fallbackCountry The country to be used as a fallback if the user's country cannot be determined. + * Defaults to the United States. + * @param showPlaceholder Whether to show the placeholder number hint in the text field. + * @param includeOnly A set of 2 digit country codes to be included in the list of countries. + * Set to null to include all supported countries. + * @param clearIcon ImageVector to be used for the clear button. Set to null to disable the clear button. + * Defaults to Icons.Filled.Clear + * @param initialPhoneNumber an optional phone number to be initial value of the input field + * @param initialCountryIsoCode Optional ISO-3166-1 alpha-2 country code to set the initially selected country. + * Note that if a valid initialCountryPhoneCode is provided, this will be ignored. + * @param initialCountryPhoneCode Optional country phone code to set the initially selected country. + * This takes precedence over [initialCountryIsoCode]. + * @param label An optional composable to be used as a label for input field + * @param textStyle An optional [TextStyle] for customizing text style of phone number input field. + * Defaults to MaterialTheme.typography.body1 + * @param [keyboardOptions] An optional [KeyboardOptions] to customize keyboard options. + * @param [keyboardActions] An optional [KeyboardActions] to customize keyboard actions. + * @param [showError] Whether to show error on field when number is invalid, default true. + */ +@OptIn(ExperimentalComposeUiApi::class) +@Suppress("LongMethod", "LongParameterList") +@Composable +fun CountryCodePicker( + onValueChange: (Pair, Boolean) -> Unit, + modifier: Modifier = Modifier, + autoDetectCode: Boolean = false, + enabled: Boolean = true, + shape: Shape = DEFAULT_TEXT_FIELD_SHAPE, + showCountryCode: Boolean = true, + showCountryFlag: Boolean = true, + colors: TextFieldColors = OutlinedTextFieldDefaults.colors(), + fallbackCountry: CountryData = CountryData.UnitedStates, + showPlaceholder: Boolean = true, + includeOnly: ImmutableSet? = null, + clearIcon: ImageVector? = Icons.Filled.Clear, + initialPhoneNumber: String? = null, + initialCountryIsoCode: Iso31661alpha2? = null, + initialCountryPhoneCode: PhoneCode? = null, + label: @Composable (() -> Unit)? = null, + textStyle: TextStyle = LocalTextStyle.current, + keyboardOptions: KeyboardOptions? = null, + keyboardActions: KeyboardActions? = null, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + showError: Boolean = true, +) { + val context = LocalContext.current + val focusRequester = remember { FocusRequester() } + + val countryCode = autoDetectedCountryCode( + autoDetectCode = autoDetectCode, + initialPhoneNumber = initialPhoneNumber, + ) + + val phoneNumberWithoutCode = if (countryCode != null) { + initialPhoneNumber?.replace(countryCode, "") + } else { + initialPhoneNumber + } + + var phoneNumber by remember { + mutableStateOf( + TextFieldValue( + text = phoneNumberWithoutCode.orEmpty(), + selection = TextRange(phoneNumberWithoutCode?.length ?: 0), + ), + ) + } + val keyboardController = LocalSoftwareKeyboardController.current + + var country: CountryData by rememberSaveable( + context, + countryCode, + initialCountryPhoneCode, + initialCountryIsoCode, + ) { + mutableStateOf( + configureInitialCountry( + initialCountryPhoneCode = countryCode ?: initialCountryPhoneCode, + context = context, + initialCountryIsoCode = initialCountryIsoCode, + fallbackCountry = fallbackCountry, + ), + ) + } + + val phoneNumberTransformation = remember(country) { + PhoneNumberTransformation(country.countryIso, context) + } + val validatePhoneNumber = remember(context) { ValidatePhoneNumber(context) } + + var isNumberValid: Boolean by rememberSaveable(country, phoneNumber) { + mutableStateOf( + validatePhoneNumber( + fullPhoneNumber = country.countryPhoneCode + phoneNumber.text, + ), + ) + } + + val coroutineScope = rememberCoroutineScope() + + OutlinedTextField( + value = phoneNumber, + onValueChange = { enteredPhoneNumber -> + val preFilteredPhoneNumber = phoneNumberTransformation.preFilter(enteredPhoneNumber) + phoneNumber = TextFieldValue( + text = preFilteredPhoneNumber, + selection = TextRange(preFilteredPhoneNumber.length), + ) + isNumberValid = validatePhoneNumber( + fullPhoneNumber = country.countryPhoneCode + phoneNumber.text, + ) + onValueChange(country.countryPhoneCode to phoneNumber.text, isNumberValid) + }, + modifier = modifier + .fillMaxWidth() + .focusable() + .autofill( + autofillTypes = listOf(AutofillType.PhoneNumberNational), + onFill = { filledPhoneNumber -> + val preFilteredPhoneNumber = + phoneNumberTransformation.preFilter(filledPhoneNumber) + phoneNumber = TextFieldValue( + text = preFilteredPhoneNumber, + selection = TextRange(preFilteredPhoneNumber.length), + ) + isNumberValid = validatePhoneNumber( + fullPhoneNumber = country.countryPhoneCode + phoneNumber.text, + ) + onValueChange(country.countryPhoneCode to phoneNumber.text, isNumberValid) + keyboardController?.hide() + coroutineScope.launch { + focusRequester.safeFreeFocus() + } + }, + focusRequester = focusRequester, + ) + .focusRequester(focusRequester = focusRequester), + enabled = enabled, + textStyle = textStyle, + label = label, + placeholder = { + if (showPlaceholder) { + PlaceholderNumberHint(country.countryIso) + } + }, + leadingIcon = { + CountryCodeDialog( + selectedCountry = country, + includeOnly = includeOnly, + onCountryChange = { countryData -> + country = countryData + isNumberValid = validatePhoneNumber( + fullPhoneNumber = country.countryPhoneCode + phoneNumber.text, + ) + onValueChange(country.countryPhoneCode to phoneNumber.text, isNumberValid) + }, + showCountryCode = showCountryCode, + showFlag = showCountryFlag, + textStyle = textStyle, + ) + }, + trailingIcon = { + if (clearIcon != null) { + ClearIconButton( + imageVector = clearIcon, + colors = colors, + isNumberValid = !showError || isNumberValid, + ) { + phoneNumber = TextFieldValue("") + isNumberValid = false + onValueChange(country.countryPhoneCode to phoneNumber.text, isNumberValid) + } + } + }, + isError = showError && !isNumberValid, + interactionSource = interactionSource, + visualTransformation = phoneNumberTransformation, + keyboardOptions = keyboardOptions ?: KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Phone, + autoCorrect = true, + imeAction = ImeAction.Done, + ), + keyboardActions = keyboardActions ?: KeyboardActions( + onDone = { + keyboardController?.hide() + coroutineScope.launch { + focusRequester.safeFreeFocus() + } + }, + ), + singleLine = true, + shape = shape, + colors = colors, + ) +} + +@OptIn(ExperimentalComposeUiApi::class) +@Suppress("LongMethod", "LongParameterList", "ComplexMethod") +@Composable +fun CountryCodePickerPayment( + onValueChange: (Pair, Boolean) -> Unit, + modifier: Modifier = Modifier, + autoDetectCode: Boolean = false, + enabled: Boolean = true, + showCountryCode: Boolean = true, + showCountryFlag: Boolean = true, + colors: TextFieldColors = OutlinedTextFieldDefaults.colors(), + fallbackCountry: CountryData = CountryData.UnitedStates, + showPlaceholder: Boolean = true, + includeOnly: ImmutableSet? = null, + clearIcon: ImageVector? = Icons.Filled.Clear, + initialPhoneNumber: String? = null, + initialCountryIsoCode: Iso31661alpha2? = null, + initialCountryPhoneCode: PhoneCode? = null, + label: @Composable (() -> Unit)? = null, + textStyle: TextStyle = LocalTextStyle.current, + keyboardOptions: KeyboardOptions? = null, + keyboardActions: KeyboardActions? = null, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + showError: Boolean = true, + indicatorColor: Color? = null, + errorIndicatorColor: Color? = null, +) { + val context = LocalContext.current + val focusRequester = remember { FocusRequester() } + + val countryCode = autoDetectedCountryCode( + autoDetectCode = autoDetectCode, + initialPhoneNumber = initialPhoneNumber, + ) + + val phoneNumberWithoutCode = if (countryCode != null) { + initialPhoneNumber?.replace(countryCode, "") + } else { + initialPhoneNumber + } + + var phoneNumber by remember { + mutableStateOf( + TextFieldValue( + text = phoneNumberWithoutCode.orEmpty(), + selection = TextRange(phoneNumberWithoutCode?.length ?: 0), + ), + ) + } + val keyboardController = LocalSoftwareKeyboardController.current + + var country: CountryData by rememberSaveable( + context, + countryCode, + initialCountryPhoneCode, + initialCountryIsoCode, + ) { + mutableStateOf( + configureInitialCountry( + initialCountryPhoneCode = countryCode ?: initialCountryPhoneCode, + context = context, + initialCountryIsoCode = initialCountryIsoCode, + fallbackCountry = fallbackCountry, + ), + ) + } + + val phoneNumberTransformation = remember(country) { + PhoneNumberTransformation(country.countryIso, context) + } + val validatePhoneNumber = remember(context) { ValidatePhoneNumber(context) } + + var isNumberValid: Boolean by rememberSaveable(country, phoneNumber) { + mutableStateOf( + validatePhoneNumber( + fullPhoneNumber = country.countryPhoneCode + phoneNumber.text, + ), + ) + } + + val coroutineScope = rememberCoroutineScope() + BasicTextField( + value = phoneNumber, + onValueChange = { enteredPhoneNumber -> + val preFilteredPhoneNumber = phoneNumberTransformation.preFilter(enteredPhoneNumber) + phoneNumber = TextFieldValue( + text = preFilteredPhoneNumber, + selection = TextRange(preFilteredPhoneNumber.length), + ) + isNumberValid = validatePhoneNumber( + fullPhoneNumber = country.countryPhoneCode + phoneNumber.text, + ) + onValueChange(country.countryPhoneCode to phoneNumber.text, isNumberValid) + }, + modifier = modifier + .fillMaxWidth() + .focusable() + .autofill( + autofillTypes = listOf(AutofillType.PhoneNumberNational), + onFill = { filledPhoneNumber -> + val preFilteredPhoneNumber = + phoneNumberTransformation.preFilter(filledPhoneNumber) + phoneNumber = TextFieldValue( + text = preFilteredPhoneNumber, + selection = TextRange(preFilteredPhoneNumber.length), + ) + isNumberValid = validatePhoneNumber( + fullPhoneNumber = country.countryPhoneCode + phoneNumber.text, + ) + onValueChange(country.countryPhoneCode to phoneNumber.text, isNumberValid) + keyboardController?.hide() + coroutineScope.launch { + focusRequester.safeFreeFocus() + } + }, + focusRequester = focusRequester, + ) + .focusRequester(focusRequester = focusRequester), + enabled = enabled, + textStyle = textStyle, + decorationBox = { innerTextField -> + Column { + if (label != null) { + label() + } + Spacer(modifier = Modifier.height(5.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + CountryCodeDialogWrapper( + country = country, + onCountryChange = { countryData -> + country = countryData + isNumberValid = validatePhoneNumber( + fullPhoneNumber = country.countryPhoneCode + phoneNumber.text, + ) + onValueChange( + country.countryPhoneCode to phoneNumber.text, + isNumberValid, + ) + }, + includeOnly = includeOnly, + showCountryCode = showCountryCode, + showFlag = showCountryFlag, + textStyle = textStyle, + ) + + Box(modifier = Modifier.weight(1f)) { + innerTextField() + if (showPlaceholder && phoneNumber.text.isEmpty()) { + PlaceholderNumberHint(country.countryIso) + } + } + if (clearIcon != null) { + ClearIconButtonWrapper( + clearIcon = clearIcon, + colors = colors, + isNumberValid = !showError || isNumberValid, + onClearClick = { + phoneNumber = TextFieldValue("") + isNumberValid = false + onValueChange( + country.countryPhoneCode to phoneNumber.text, + isNumberValid, + ) + }, + ) + } + } + if (showError && !isNumberValid) { + errorIndicatorColor?.let { + HorizontalDivider( + thickness = 1.dp, + color = it, + ) + } + } else { + indicatorColor?.let { + HorizontalDivider( + thickness = 1.dp, + color = it, + ) + } + } + } + }, + interactionSource = interactionSource, + visualTransformation = phoneNumberTransformation, + keyboardOptions = keyboardOptions ?: KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Phone, + autoCorrect = true, + imeAction = ImeAction.Done, + ), + keyboardActions = keyboardActions ?: KeyboardActions( + onDone = { + keyboardController?.hide() + coroutineScope.launch { + focusRequester.safeFreeFocus() + } + }, + ), + singleLine = true, + ) +} + +@Composable +fun CountryCodeDialogWrapper( + country: CountryData, + onCountryChange: (CountryData) -> Unit, + includeOnly: ImmutableSet?, + showCountryCode: Boolean, + showFlag: Boolean, + textStyle: TextStyle, +) { + CountryCodeDialog( + selectedCountry = country, + includeOnly = includeOnly, + onCountryChange = onCountryChange, + showCountryCode = showCountryCode, + showFlag = showFlag, + textStyle = textStyle, + ) +} + +@Composable +fun ClearIconButtonWrapper( + clearIcon: ImageVector, + colors: TextFieldColors, + isNumberValid: Boolean, + onClearClick: () -> Unit, +) { + ClearIconButton( + imageVector = clearIcon, + colors = colors, + isNumberValid = isNumberValid, + onClick = onClearClick, + ) +} + +private fun configureInitialCountry( + initialCountryPhoneCode: PhoneCode?, + context: Context, + initialCountryIsoCode: Iso31661alpha2?, + fallbackCountry: CountryData, +): CountryData { + if (initialCountryPhoneCode?.run { !startsWith("+") } == true) { + Log.e(TAG, "initialCountryPhoneCode must start with +") + } + return initialCountryPhoneCode?.let { getCountryFromPhoneCode(it, context) } + ?: CountryData.entries.firstOrNull { it.countryIso == initialCountryIsoCode } + ?: CountryData.isoMap[getUserIsoCode(context)] + ?: fallbackCountry +} + +private fun FocusRequester.safeFreeFocus() { + try { + this.freeFocus() + } catch (exception: IllegalStateException) { + Log.e(TAG, "Unable to free focus", exception) + } +} + +@Composable +private fun PlaceholderNumberHint(countryIso: Iso31661alpha2) { + Text( + text = stringResource( + id = numberHint.getOrDefault(countryIso, R.string.unknown), + ), + ) +} + +@Composable +private fun ClearIconButton( + imageVector: ImageVector, + colors: TextFieldColors, + isNumberValid: Boolean, + onClick: () -> Unit, +) = IconButton(onClick = onClick) { + Icon( + imageVector = imageVector, + contentDescription = stringResource(id = R.string.clear), + tint = colors.trailingIconColor( + enabled = true, + isError = !isNumberValid, + interactionSource = remember { MutableInteractionSource() }, + ).value, + ) +} + +@Composable +private fun autoDetectedCountryCode(autoDetectCode: Boolean, initialPhoneNumber: String?): String? = + if (initialPhoneNumber?.startsWith("+") == true && autoDetectCode) { + extractCountryCode(initialPhoneNumber) + } else { + null + } + +@Composable +private fun TextFieldColors.trailingIconColor( + enabled: Boolean, + isError: Boolean, + interactionSource: InteractionSource, +): State { + val focused by interactionSource.collectIsFocusedAsState() + + return rememberUpdatedState( + when { + !enabled -> disabledTrailingIconColor + isError -> errorTrailingIconColor + focused -> focusedTrailingIconColor + else -> unfocusedTrailingIconColor + }, + ) +} + +@Preview +@Composable +private fun CountryCodePickerPreview() { + CountryCodePicker( + onValueChange = { _, _ -> }, + showCountryCode = true, + showCountryFlag = true, + showPlaceholder = true, + includeOnly = null, + ) +} diff --git a/libs/country-code-picker/src/main/kotlin/com/mifos/library/countrycodepicker/transformation/PhoneNumberTransformation.kt b/libs/country-code-picker/src/main/kotlin/com/mifos/library/countrycodepicker/transformation/PhoneNumberTransformation.kt new file mode 100644 index 000000000..10bed7f68 --- /dev/null +++ b/libs/country-code-picker/src/main/kotlin/com/mifos/library/countrycodepicker/transformation/PhoneNumberTransformation.kt @@ -0,0 +1,119 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package com.mifos.library.countrycodepicker.transformation + +import android.content.Context +import android.telephony.PhoneNumberUtils +import android.text.Selection +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import io.michaelrocks.libphonenumber.android.PhoneNumberUtil + +// https://medium.com/google-developer-experts/hands-on-jetpack-compose-visualtransformation-to-create-a-phone-number-formatter-99b0347fc4f6 + +class PhoneNumberTransformation(countryCode: String, context: Context) : VisualTransformation { + private val phoneNumberFormatter by lazy { + PhoneNumberUtil.createInstance(context).getAsYouTypeFormatter(countryCode) + } + + fun preFilter(text: String): String = text.filter { PhoneNumberUtils.isReallyDialable(it) } + + fun preFilter(textValue: TextFieldValue): String = preFilter(textValue.text) + + override fun filter(text: AnnotatedString): TransformedText { + val transformation = reformat(text, Selection.getSelectionEnd(text)) + + return TransformedText( + AnnotatedString(transformation.formatted.orEmpty()), + object : OffsetMapping { + @Suppress("TooGenericExceptionCaught", "SwallowedException") + override fun originalToTransformed(offset: Int): Int { + return try { + transformation.originalToTransformed[offset] + } catch (ex: IndexOutOfBoundsException) { + transformation.transformedToOriginal.lastIndex + } + } + + override fun transformedToOriginal(offset: Int): Int = + transformation.transformedToOriginal[offset] + }, + ) + } + + @Suppress("AvoidMutableCollections", "AvoidVarsExceptWithDelegate") + private fun reformat(s: CharSequence, cursor: Int): Transformation { + if (s.isEmpty()) { + return Transformation("", listOf(0), listOf(0)) + } + phoneNumberFormatter.clear() + + val curIndex = cursor - 1 + var formatted: String? = null + var lastNonSeparator = 0.toChar() + var hasCursor = false + + s.forEachIndexed { index, char -> + if (PhoneNumberUtils.isNonSeparator(char)) { + if (lastNonSeparator.code != 0) { + formatted = getFormattedNumber(lastNonSeparator, hasCursor) + hasCursor = false + } + lastNonSeparator = char + } + if (index == curIndex) { + hasCursor = true + } + } + + if (lastNonSeparator.code != 0) { + formatted = getFormattedNumber(lastNonSeparator, hasCursor) + } + val originalToTransformed = mutableListOf() + val transformedToOriginal = mutableListOf() + var specialCharsCount = 0 + if (formatted != null) { + formatted?.forEachIndexed { index, char -> + if (!PhoneNumberUtils.isNonSeparator(char)) { + specialCharsCount++ + } else { + originalToTransformed.add(index) + } + transformedToOriginal.add(index - specialCharsCount) + } + originalToTransformed.add(originalToTransformed.maxOrNull()?.plus(1) ?: 0) + transformedToOriginal.add(transformedToOriginal.maxOrNull()?.plus(1) ?: 0) + } else { + originalToTransformed.add(0) + transformedToOriginal.add(0) + } + if (transformedToOriginal.any { it < 0 }) { + transformedToOriginal.replaceAll { if (it < 0) 0 else it } + } + return Transformation(formatted, originalToTransformed, transformedToOriginal) + } + + private fun getFormattedNumber(lastNonSeparator: Char, hasCursor: Boolean): String? { + return if (hasCursor) { + phoneNumberFormatter.inputDigitAndRememberPosition(lastNonSeparator) + } else { + phoneNumberFormatter.inputDigit(lastNonSeparator) + } + } + + private data class Transformation( + val formatted: String?, + val originalToTransformed: List, + val transformedToOriginal: List, + ) +} diff --git a/libs/mifos-passcode/build.gradle.kts b/libs/mifos-passcode/build.gradle.kts index 64cb920fc..93d85ab8f 100644 --- a/libs/mifos-passcode/build.gradle.kts +++ b/libs/mifos-passcode/build.gradle.kts @@ -8,61 +8,39 @@ * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifospay.cmp.feature) - alias(libs.plugins.kotlin.parcelize) - alias(libs.plugins.kotlin.serialization) - alias(libs.plugins.protobuf) + alias(libs.plugins.mifospay.android.library) + alias(libs.plugins.mifospay.android.library.compose) + id("com.google.devtools.ksp") } android { namespace = "com.mifos.library.passcode" } -kotlin { - sourceSets { - commonMain.dependencies { - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.materialIconsExtended) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) +dependencies { + implementation(libs.androidx.core.ktx) - implementation(libs.koin.compose.viewmodel) - implementation(libs.koin.compose) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.foundation.layout) + implementation(libs.androidx.compose.material.iconsExtended) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.ui.util) - implementation(libs.jb.kotlin.stdlib) - implementation(libs.kotlin.reflect) + implementation(libs.androidx.lifecycle.runtimeCompose) + implementation(libs.androidx.lifecycle.viewModelCompose) + implementation(libs.androidx.navigation.compose) - api(libs.protobuf.kotlin.lite) - implementation(libs.kotlinx.serialization.core) + implementation(platform(libs.koin.bom)) + implementation(libs.koin.core) + implementation(libs.koin.android) + implementation(libs.koin.androidx.navigation) + implementation(libs.koin.androidx.compose) + implementation(libs.koin.core.viewmodel) - implementation(libs.multiplatform.settings) - implementation(libs.multiplatform.settings.serialization) - implementation(libs.multiplatform.settings.coroutines) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.serialization.core) - } + testImplementation(libs.koin.test) + testImplementation(libs.koin.test.junit4) + testImplementation(libs.koin.test.junit5) - desktopMain.dependencies { - implementation(libs.kotlinx.coroutines.swing) - } - } } - -// Setup protobuf configuration, generating lite Java and Kotlin classes -protobuf { - protoc { - artifact = libs.protobuf.protoc.get().toString() - } - generateProtoTasks { - all().forEach { task -> - task.builtins { - register("kotlin") { - option("lite") - } - } - } - } -} \ No newline at end of file diff --git a/libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt b/libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt index ba9cf7bc1..e7225a959 100644 --- a/libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt +++ b/libs/mifos-passcode/src/commonMain/kotlin/org/mifos/library/passcode/PassCodeScreen.kt @@ -32,20 +32,20 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import kotlinx.coroutines.launch -import org.jetbrains.compose.ui.tooling.preview.Preview -import org.koin.compose.viewmodel.koinViewModel +import org.koin.androidx.compose.koinViewModel import org.mifos.library.passcode.component.MifosIcon import org.mifos.library.passcode.component.PasscodeForgotButton import org.mifos.library.passcode.component.PasscodeHeader @@ -55,11 +55,10 @@ 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.viewmodels.PasscodeAction -import org.mifos.library.passcode.viewmodels.PasscodeEvent +import org.mifos.library.passcode.utility.VibrationFeedback.vibrateFeedback import org.mifos.library.passcode.viewmodels.PasscodeViewModel -import org.mifospay.core.ui.utils.EventsEffect @Composable internal fun PasscodeScreen( @@ -70,25 +69,29 @@ internal fun PasscodeScreen( modifier: Modifier = Modifier, viewModel: PasscodeViewModel = koinViewModel(), ) { - val scope = rememberCoroutineScope() - val state by viewModel.stateFlow.collectAsStateWithLifecycle() + val context = LocalContext.current + val preferenceManager = remember { PreferenceManager(context) } + + val activeStep by viewModel.activeStep.collectAsStateWithLifecycle() + val filledDots by viewModel.filledDots.collectAsStateWithLifecycle() + val passcodeVisible by viewModel.passcodeVisible.collectAsStateWithLifecycle() + val currentPasscode by viewModel.currentPasscodeInput.collectAsStateWithLifecycle() val xShake = remember { Animatable(initialValue = 0.0F) } var passcodeRejectedDialogVisible by remember { mutableStateOf(false) } - EventsEffect(viewModel) { event -> - when (event) { - is PasscodeEvent.PasscodeConfirmed -> { - onPasscodeConfirm(event.passcode) - } + LaunchedEffect(key1 = viewModel.onPasscodeConfirmed) { + viewModel.onPasscodeConfirmed.collect { + onPasscodeConfirm(it) + } + } - is PasscodeEvent.PasscodeRejected -> { - passcodeRejectedDialogVisible = true - scope.launch { - performShakeAnimation(xShake) - } - onPasscodeRejected() - } + LaunchedEffect(key1 = viewModel.onPasscodeRejected) { + viewModel.onPasscodeRejected.collect { + passcodeRejectedDialogVisible = true + vibrateFeedback(context) + performShakeAnimation(xShake) + onPasscodeRejected() } } @@ -103,10 +106,10 @@ internal fun PasscodeScreen( .padding(paddingValues), horizontalAlignment = Alignment.CenterHorizontally, ) { - PasscodeToolbar(activeStep = state.activeStep, state.hasPasscode) + PasscodeToolbar(activeStep = activeStep, preferenceManager.hasPasscode) PasscodeSkipButton( - hasPassCode = state.hasPasscode, + hasPassCode = preferenceManager.hasPasscode, onSkipButton = onSkipButton, ) @@ -119,19 +122,15 @@ internal fun PasscodeScreen( horizontalAlignment = Alignment.CenterHorizontally, ) { PasscodeHeader( - activeStep = state.activeStep, - isPasscodeAlreadySet = state.hasPasscode, + activeStep = activeStep, + isPasscodeAlreadySet = preferenceManager.hasPasscode, ) PasscodeView( - restart = remember(viewModel) { - { viewModel.trySendAction(PasscodeAction.Restart) } - }, - togglePasscodeVisibility = remember(viewModel) { - { viewModel.trySendAction(PasscodeAction.TogglePasscodeVisibility) } - }, - filledDots = state.filledDots, - passcodeVisible = state.passcodeVisible, - currentPasscode = state.currentPasscodeInput, + restart = { viewModel.restart() }, + togglePasscodeVisibility = { viewModel.togglePasscodeVisibility() }, + filledDots = filledDots, + passcodeVisible = passcodeVisible, + currentPasscode = currentPasscode, passcodeRejectedDialogVisible = passcodeRejectedDialogVisible, onDismissDialog = { passcodeRejectedDialogVisible = false }, xShake = xShake, @@ -141,21 +140,16 @@ internal fun PasscodeScreen( Spacer(modifier = Modifier.height(6.dp)) PasscodeKeys( - enterKey = remember(viewModel) { - { viewModel.trySendAction(PasscodeAction.EnterKey(it)) } - }, - deleteKey = remember(viewModel) { - { viewModel.trySendAction(PasscodeAction.DeleteKey) } - }, - deleteAllKeys = remember(viewModel) { - { viewModel.trySendAction(PasscodeAction.DeleteAllKeys) } - }, + enterKey = viewModel::enterKey, + deleteKey = viewModel::deleteKey, + deleteAllKeys = viewModel::deleteAllKeys, + modifier = Modifier.padding(horizontal = 12.dp), ) Spacer(modifier = Modifier.height(8.dp)) PasscodeForgotButton( - hasPassCode = state.hasPasscode, + hasPassCode = preferenceManager.hasPasscode, onForgotButton = onForgotButton, ) } @@ -236,7 +230,7 @@ private fun PasscodeView( } } -@Preview +@Preview(showBackground = true) @Composable private fun PasscodeScreenPreview() { PasscodeScreen( 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 new file mode 100644 index 000000000..15899edd1 --- /dev/null +++ b/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/data/PasscodeManager.kt @@ -0,0 +1,29 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.data + +import org.mifos.library.passcode.utility.PreferenceManager + +class PasscodeManager( + private val passcodePreferencesHelper: PreferenceManager, +) { + + val getPasscode = passcodePreferencesHelper.getSavedPasscode() + + val hasPasscode = passcodePreferencesHelper.hasPasscode + + fun savePasscode(passcode: String) { + passcodePreferencesHelper.savePasscode(passcode) + } + + fun clearPasscode() { + passcodePreferencesHelper.clearPasscode() + } +} 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 new file mode 100644 index 000000000..3346a2670 --- /dev/null +++ b/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/data/PasscodeRepositoryImpl.kt @@ -0,0 +1,28 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.data + +import org.mifos.library.passcode.utility.PreferenceManager + +class PasscodeRepositoryImpl( + private val preferenceManager: PreferenceManager, +) : PasscodeRepository { + + override val hasPasscode: Boolean + get() = preferenceManager.hasPasscode + + override fun getSavedPasscode(): String { + return preferenceManager.getSavedPasscode() + } + + override fun savePasscode(passcode: String) { + preferenceManager.savePasscode(passcode) + } +} 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 new file mode 100644 index 000000000..ad1c93c66 --- /dev/null +++ b/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/di/ApplicationModule.kt @@ -0,0 +1,35 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.di + +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module +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 org.mifos.library.passcode.viewmodels.PasscodeViewModel + +val ApplicationModule = module { + + factory { + PreferenceManager(context = androidContext()) + } + + single { PasscodeRepositoryImpl(preferenceManager = get()) } + + viewModel { + PasscodeViewModel(passcodeRepository = get()) + } + factory { + PasscodeManager(passcodePreferencesHelper = get()) + } +} 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 new file mode 100644 index 000000000..c0d2631a6 --- /dev/null +++ b/libs/mifos-passcode/src/main/kotlin/org/mifos/library/passcode/viewmodels/PasscodeViewModel.kt @@ -0,0 +1,148 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifos.library.passcode.viewmodels + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.mifos.library.passcode.data.PasscodeRepository +import org.mifos.library.passcode.utility.Constants.PASSCODE_LENGTH +import org.mifos.library.passcode.utility.Step + +internal class PasscodeViewModel( + private val passcodeRepository: PasscodeRepository, +) : ViewModel() { + + private val mOnPasscodeConfirmed = MutableSharedFlow() + val onPasscodeConfirmed = mOnPasscodeConfirmed.asSharedFlow() + + private val mOnPasscodeRejected = MutableSharedFlow() + val onPasscodeRejected = mOnPasscodeRejected.asSharedFlow() + + private val mActiveStep = MutableStateFlow(Step.Create) + val activeStep = mActiveStep.asStateFlow() + + private val mFilledDots = MutableStateFlow(0) + val filledDots = mFilledDots.asStateFlow() + + private var createPasscode: StringBuilder = StringBuilder() + private var confirmPasscode: StringBuilder = StringBuilder() + + private val mPasscodeVisible = MutableStateFlow(false) + val passcodeVisible = mPasscodeVisible.asStateFlow() + + private val mCurrentPasscodeInput = MutableStateFlow("") + val currentPasscodeInput = mCurrentPasscodeInput.asStateFlow() + + private var mIsPasscodeAlreadySet = mutableStateOf(passcodeRepository.hasPasscode) + + init { + resetData() + } + + private fun emitActiveStep(activeStep: Step) = viewModelScope.launch { + mActiveStep.emit(activeStep) + } + + private fun emitFilledDots(filledDots: Int) = viewModelScope.launch { + mFilledDots.emit(filledDots) + } + + private fun emitOnPasscodeConfirmed(confirmPassword: String) = viewModelScope.launch { + mOnPasscodeConfirmed.emit(confirmPassword) + } + + private fun emitOnPasscodeRejected() = viewModelScope.launch { + mOnPasscodeRejected.emit(Unit) + } + + fun togglePasscodeVisibility() { + mPasscodeVisible.value = !mPasscodeVisible.value + } + + private fun resetData() { + emitActiveStep(Step.Create) + emitFilledDots(0) + + createPasscode.clear() + confirmPasscode.clear() + } + + fun enterKey(key: String) { + if (mFilledDots.value >= PASSCODE_LENGTH) { + return + } + + val currentPasscode = + if (mActiveStep.value == Step.Create) createPasscode else confirmPasscode + currentPasscode.append(key) + mCurrentPasscodeInput.value = currentPasscode.toString() + emitFilledDots(currentPasscode.length) + + if (mFilledDots.value == PASSCODE_LENGTH) { + if (mIsPasscodeAlreadySet.value) { + if (passcodeRepository.getSavedPasscode() == createPasscode.toString()) { + emitOnPasscodeConfirmed(createPasscode.toString()) + createPasscode.clear() + } else { + emitOnPasscodeRejected() + // logic for retires can be written here + } + mCurrentPasscodeInput.value = "" + } else if (mActiveStep.value == Step.Create) { + emitActiveStep(Step.Confirm) + emitFilledDots(0) + mCurrentPasscodeInput.value = "" + } else { + if (createPasscode.toString() == confirmPasscode.toString()) { + emitOnPasscodeConfirmed(confirmPasscode.toString()) + passcodeRepository.savePasscode(confirmPasscode.toString()) + mIsPasscodeAlreadySet.value = true + resetData() + } else { + emitOnPasscodeRejected() + resetData() + } + mCurrentPasscodeInput.value = "" + } + } + } + + fun deleteKey() { + val currentPasscode = + if (mActiveStep.value == Step.Create) createPasscode else confirmPasscode + + if (currentPasscode.isNotEmpty()) { + currentPasscode.deleteAt(currentPasscode.length - 1) + mCurrentPasscodeInput.value = currentPasscode.toString() + emitFilledDots(currentPasscode.length) + } + } + + fun deleteAllKeys() { + if (mActiveStep.value == Step.Create) { + createPasscode.clear() + } else { + confirmPasscode.clear() + } + mCurrentPasscodeInput.value = "" + emitFilledDots(0) + } + + fun restart() { + resetData() + mPasscodeVisible.value = false + } +} diff --git a/mifospay-android/build.gradle.kts b/mifospay-android/build.gradle.kts index 106ff7daf..c6f5e483e 100644 --- a/mifospay-android/build.gradle.kts +++ b/mifospay-android/build.gradle.kts @@ -24,6 +24,7 @@ plugins { alias(libs.plugins.mifospay.android.application) alias(libs.plugins.mifospay.android.application.compose) alias(libs.plugins.mifospay.android.application.flavors) + alias(libs.plugins.mifospay.android.application.firebase) alias(libs.plugins.roborazzi) id("com.google.android.gms.oss-licenses-plugin") id("com.google.devtools.ksp") @@ -86,9 +87,38 @@ android { } dependencies { - implementation(projects.mifospayShared) + implementation(projects.shared) + implementation(projects.core.data) implementation(projects.core.ui) + implementation(projects.core.designsystem) + + implementation(projects.feature.receipt) + implementation(projects.feature.profile) + implementation(projects.feature.auth) + implementation(projects.feature.makeTransfer) + implementation(projects.feature.faq) + implementation(projects.feature.editpassword) + implementation(projects.feature.notification) + implementation(projects.feature.requestMoney) + implementation(projects.feature.upiSetup) + implementation(projects.feature.settings) + implementation(projects.feature.savedcards) + implementation(projects.feature.qr) + implementation(projects.feature.invoices) + implementation(projects.feature.merchants) + implementation(projects.feature.history) + implementation(projects.feature.kyc) + implementation(projects.feature.home) + implementation(projects.feature.accounts) + implementation(projects.feature.finance) + implementation(projects.feature.payments) + implementation(projects.feature.sendMoney) + implementation(projects.feature.standingInstruction) + implementation(projects.feature.search) + + implementation(projects.libs.mifosPasscode) + implementation(projects.libs.material3Navigation) // Compose implementation(libs.androidx.core.ktx) @@ -96,12 +126,14 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.ktx) implementation(libs.androidx.core.splashscreen) - implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material3.adaptive) implementation(libs.androidx.compose.material3.adaptive.layout) implementation(libs.androidx.compose.material3.adaptive.navigation) + implementation(libs.androidx.compose.material3.windowSizeClass) implementation(libs.androidx.compose.runtime.tracing) + implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.android) @@ -114,14 +146,10 @@ dependencies { implementation(libs.androidx.profileinstaller) implementation(libs.androidx.tracing.ktx) - implementation(libs.koin.core) - implementation(libs.koin.android) - implementation(libs.koin.compose) - implementation(libs.koin.compose.viewmodel) - runtimeOnly(libs.androidx.compose.runtime) debugImplementation(libs.androidx.compose.ui.tooling) + testImplementation(libs.junit) testImplementation(libs.androidx.compose.ui.test) @@ -129,6 +157,9 @@ dependencies { androidTestImplementation(libs.espresso.core) androidTestImplementation(libs.androidx.test.ext.junit) + implementation(libs.koin.android) + implementation(libs.ktor.client.core) + testImplementation(kotlin("test")) testImplementation(libs.koin.test) testImplementation(libs.koin.test.junit4) diff --git a/mifospay-android/prodRelease-badging.txt b/mifospay-android/prodRelease-badging.txt index 7d11a2720..b46890558 100644 --- a/mifospay-android/prodRelease-badging.txt +++ b/mifospay-android/prodRelease-badging.txt @@ -1,4 +1,4 @@ -package: name='org.mifospay' versionCode='1' versionName='0.0.4-beta.0.7' platformBuildVersionName='14' platformBuildVersionCode='34' compileSdkVersion='34' compileSdkVersionCodename='14' +package: name='org.mifospay' versionCode='1' versionName='0.0.1-beta.0.836' platformBuildVersionName='14' platformBuildVersionCode='34' compileSdkVersion='34' compileSdkVersionCodename='14' sdkVersion:'26' targetSdkVersion:'34' uses-permission: name='android.permission.INTERNET' @@ -7,6 +7,7 @@ uses-permission: name='android.permission.READ_EXTERNAL_STORAGE' uses-permission: name='android.permission.WRITE_EXTERNAL_STORAGE' uses-permission: name='android.permission.READ_CONTACTS' uses-permission: name='android.permission.ACCESS_NETWORK_STATE' +uses-permission: name='android.permission.VIBRATE' uses-permission: name='android.permission.POST_NOTIFICATIONS' uses-permission: name='android.permission.WAKE_LOCK' uses-permission: name='com.google.android.c2dm.permission.RECEIVE' @@ -14,6 +15,7 @@ uses-permission: name='android.permission.ACCESS_ADSERVICES_ATTRIBUTION' uses-permission: name='android.permission.ACCESS_ADSERVICES_AD_ID' uses-permission: name='com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE' uses-permission: name='org.mifospay.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION' +uses-permission: name='android.permission.REORDER_TASKS' application-label:'Mifos Pay' application-label-af:'Mifos Pay' application-label-am:'Mifos Pay' @@ -28,12 +30,14 @@ application-label-ca:'Mifos Pay' application-label-cs:'Mifos Pay' application-label-da:'Mifos Pay' application-label-de:'Mifos Pay' +application-label-de-DE:'Mifos Pay' application-label-el:'Mifos Pay' application-label-en-AU:'Mifos Pay' application-label-en-CA:'Mifos Pay' application-label-en-GB:'Mifos Pay' application-label-en-IN:'Mifos Pay' application-label-en-XC:'Mifos Pay' +application-label-eo:'Mifos Pay' application-label-es:'Mifos Pay' application-label-es-US:'Mifos Pay' application-label-et:'Mifos Pay' @@ -42,6 +46,8 @@ application-label-fa:'Mifos Pay' application-label-fi:'Mifos Pay' application-label-fr:'Mifos Pay' application-label-fr-CA:'Mifos Pay' +application-label-ga:'Mifos Pay' +application-label-gd:'Mifos Pay' application-label-gl:'Mifos Pay' application-label-gu:'Mifos Pay' application-label-hi:'Mifos Pay' @@ -49,17 +55,21 @@ application-label-hr:'Mifos Pay' application-label-hu:'Mifos Pay' application-label-hy:'Mifos Pay' application-label-in:'Mifos Pay' +application-label-in-ID:'Mifos Pay' application-label-is:'Mifos Pay' application-label-it:'Mifos Pay' application-label-it-IT:'Mifos Pay' application-label-iw:'Mifos Pay' application-label-ja:'Mifos Pay' +application-label-jv:'Mifos Pay' application-label-ka:'Mifos Pay' application-label-kk:'Mifos Pay' application-label-km:'Mifos Pay' application-label-kn:'Mifos Pay' application-label-ko:'Mifos Pay' +application-label-ku:'Mifos Pay' application-label-ky:'Mifos Pay' +application-label-lb:'Mifos Pay' application-label-lo:'Mifos Pay' application-label-lt:'Mifos Pay' application-label-lv:'Mifos Pay' @@ -72,6 +82,7 @@ application-label-my:'Mifos Pay' application-label-nb:'Mifos Pay' application-label-ne:'Mifos Pay' application-label-nl:'Mifos Pay' +application-label-no:'Mifos Pay' application-label-or:'Mifos Pay' application-label-pa:'Mifos Pay' application-label-pl:'Mifos Pay' @@ -120,12 +131,14 @@ feature-group: label='' uses-feature-not-required: name='android.hardware.camera' uses-feature: name='android.hardware.faketouch' uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps' + uses-feature: name='android.hardware.screen.portrait' + uses-implied-feature: name='android.hardware.screen.portrait' reason='one or more activities have specified a portrait orientation' main other-activities other-receivers other-services supports-screens: 'small' 'normal' 'large' 'xlarge' supports-any-density: 'true' -locales: '--_--' 'af' 'am' 'ar' 'as' 'az' 'be' 'bg' 'bn' 'bs' 'ca' 'cs' 'da' 'de' 'el' 'en-AU' 'en-CA' 'en-GB' 'en-IN' 'en-XC' 'es' 'es-US' 'et' 'eu' 'fa' 'fi' 'fr' 'fr-CA' 'gl' 'gu' 'hi' 'hr' 'hu' 'hy' 'in' 'is' 'it' 'it-IT' 'iw' 'ja' 'ka' 'kk' 'km' 'kn' 'ko' 'ky' 'lo' 'lt' 'lv' 'mk' 'ml' 'mn' 'mr' 'ms' 'my' 'nb' 'ne' 'nl' 'or' 'pa' 'pl' 'pt' 'pt-BR' 'pt-PT' 'ro' 'ru' 'ru-RU' 'si' 'sk' 'sl' 'so' 'sq' 'sr' 'sr-Latn' 'sv' 'sw' 'ta' 'te' 'th' 'tl' 'tr' 'tr-TR' 'uk' 'ur' 'uz' 'vi' 'zh' 'zh-CN' 'zh-HK' 'zh-TW' 'zu' +locales: '--_--' 'af' 'am' 'ar' 'as' 'az' 'be' 'bg' 'bn' 'bs' 'ca' 'cs' 'da' 'de' 'de-DE' 'el' 'en-AU' 'en-CA' 'en-GB' 'en-IN' 'en-XC' 'eo' 'es' 'es-US' 'et' 'eu' 'fa' 'fi' 'fr' 'fr-CA' 'ga' 'gd' 'gl' 'gu' 'hi' 'hr' 'hu' 'hy' 'in' 'in-ID' 'is' 'it' 'it-IT' 'iw' 'ja' 'jv' 'ka' 'kk' 'km' 'kn' 'ko' 'ku' 'ky' 'lb' 'lo' 'lt' 'lv' 'mk' 'ml' 'mn' 'mr' 'ms' 'my' 'nb' 'ne' 'nl' 'no' 'or' 'pa' 'pl' 'pt' 'pt-BR' 'pt-PT' 'ro' 'ru' 'ru-RU' 'si' 'sk' 'sl' 'so' 'sq' 'sr' 'sr-Latn' 'sv' 'sw' 'ta' 'te' 'th' 'tl' 'tr' 'tr-TR' 'uk' 'ur' 'uz' 'vi' 'zh' 'zh-CN' 'zh-HK' 'zh-TW' 'zu' densities: '160' '240' '320' '480' '640' native-code: 'arm64-v8a' 'armeabi-v7a' 'x86' 'x86_64' diff --git a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt index bf04c86cd..35ddb856e 100644 --- a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt +++ b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/MifosPayViewModel.kt @@ -7,28 +7,29 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.shared +package org.mifospay import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.mifospay.core.model.UserData import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import org.mifospay.core.datastore.UserPreferencesRepository -import org.mifospay.core.model.user.UserInfo -import proto.org.mifos.library.passcode.data.PasscodeManager +import org.mifos.library.passcode.data.PasscodeManager +import org.mifospay.core.data.repository.auth.UserDataRepository -class MifosPayViewModel( - private val userDataRepository: UserPreferencesRepository, +class MainActivityViewModel( + private val userDataRepository: UserDataRepository, private val passcodeManager: PasscodeManager, ) : ViewModel() { - val uiState: StateFlow = userDataRepository.userInfo.map { - MainUiState.Success(it) + + val uiState: StateFlow = userDataRepository.userData.map { + MainActivityUiState.Success(it) }.stateIn( scope = viewModelScope, - initialValue = MainUiState.Loading, + initialValue = MainActivityUiState.Loading, started = SharingStarted.WhileSubscribed(5_000), ) @@ -40,7 +41,7 @@ class MifosPayViewModel( } } -sealed interface MainUiState { - data object Loading : MainUiState - data class Success(val userData: UserInfo) : MainUiState +sealed interface MainActivityUiState { + data object Loading : MainActivityUiState + data class Success(val userData: UserData) : MainActivityUiState } diff --git a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt index 84030f534..0829c434c 100644 --- a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt +++ b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosApp.kt @@ -7,21 +7,30 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.shared.ui +package org.mifospay.ui -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -30,27 +39,29 @@ import androidx.compose.material3.SnackbarDuration.Indefinite 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.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +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.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hierarchy -import mobile_wallet.mifospay_shared.generated.resources.Res -import mobile_wallet.mifospay_shared.generated.resources.not_connected -import org.jetbrains.compose.resources.stringResource -import org.mifospay.core.data.util.NetworkMonitor -import org.mifospay.core.data.util.TimeZoneMonitor +import org.mifospay.R import org.mifospay.core.designsystem.component.IconBox import org.mifospay.core.designsystem.component.MifosBackground import org.mifospay.core.designsystem.component.MifosGradientBackground @@ -58,37 +69,37 @@ import org.mifospay.core.designsystem.component.MifosNavigationBar import org.mifospay.core.designsystem.component.MifosNavigationBarItem import org.mifospay.core.designsystem.component.MifosNavigationRail import org.mifospay.core.designsystem.component.MifosNavigationRailItem +import org.mifospay.core.designsystem.component.MifosTopAppBar import org.mifospay.core.designsystem.icon.MifosIcons import org.mifospay.core.designsystem.theme.LocalGradientColors -import org.mifospay.feature.notification.navigateToNotification +import org.mifospay.feature.faq.navigation.navigateToFAQ import org.mifospay.feature.profile.navigation.navigateToEditProfile import org.mifospay.feature.settings.navigation.navigateToSettings -import org.mifospay.shared.navigation.MifosNavHost -import org.mifospay.shared.utils.TopLevelDestination +import org.mifospay.navigation.MifosNavHost +import org.mifospay.navigation.TopLevelDestination +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalComposeUiApi::class, +) @Composable -internal fun MifosApp( - networkMonitor: NetworkMonitor, - timeZoneMonitor: TimeZoneMonitor, +fun MifosApp( + appState: MifosAppState, onClickLogout: () -> Unit, modifier: Modifier = Modifier, ) { + var showHomeMenuOption by rememberSaveable { mutableStateOf(false) } + MifosBackground(modifier) { MifosGradientBackground( gradientColors = LocalGradientColors.current, ) { - val appState = rememberMifosAppState( - networkMonitor = networkMonitor, - timeZoneMonitor = timeZoneMonitor, - ) - val snackbarHostState = remember { SnackbarHostState() } - val destination = appState.currentTopLevelDestination val isOffline by appState.isOffline.collectAsStateWithLifecycle() // If user is not connected to the internet show a snack bar to inform them. - val notConnectedMessage = stringResource(Res.string.not_connected) + val notConnectedMessage = stringResource(R.string.not_connected) LaunchedEffect(isOffline) { if (isOffline) { snackbarHostState.showSnackbar( @@ -98,13 +109,59 @@ internal fun MifosApp( } } + if (showHomeMenuOption) { + AnimatedVisibility(true) { + Box( + modifier = + Modifier + .fillMaxWidth() + .wrapContentSize(Alignment.TopEnd) + .padding(end = 24.dp) + .background(color = MaterialTheme.colorScheme.surface), + ) { + DropdownMenu( + modifier = Modifier.background(color = MaterialTheme.colorScheme.surface), + expanded = showHomeMenuOption, + onDismissRequest = { showHomeMenuOption = false }, + ) { + DropdownMenuItem( + text = { + Text( + stringResource(id = R.string.faq), + color = MaterialTheme.colorScheme.onSurface, + ) + }, + onClick = { + showHomeMenuOption = false + appState.navController.navigateToFAQ() + }, + ) + DropdownMenuItem( + text = { + Text( + stringResource(id = R.string.feature_profile_settings), + color = MaterialTheme.colorScheme.onSurface, + ) + }, + onClick = { + showHomeMenuOption = false + appState.navController.navigateToSettings() + }, + ) + } + } + } + } + Scaffold( - modifier = Modifier, - containerColor = Color.Transparent, + modifier = Modifier.semantics { + testTagsAsResourceId = true + }, + containerColor = MaterialTheme.colorScheme.surface, contentColor = MaterialTheme.colorScheme.onBackground, snackbarHost = { SnackbarHost(snackbarHostState) }, bottomBar = { - if (appState.shouldShowBottomBar && destination != null) { + if (appState.shouldShowBottomBar) { MifosBottomBar( destinations = appState.topLevelDestinations, destinationsWithUnreadResources = emptySet(), @@ -126,35 +183,47 @@ internal fun MifosApp( ), ), ) { - if (appState.shouldShowNavRail && destination != null) { + if (appState.shouldShowNavRail) { MifosNavRail( destinations = appState.topLevelDestinations, destinationsWithUnreadResources = emptySet(), onNavigateToDestination = appState::navigateToTopLevelDestination, currentDestination = appState.currentDestination, modifier = Modifier - .testTag("NiaNavRail") + .testTag("MifosNavRail") .safeDrawingPadding(), ) } Column(Modifier.fillMaxSize()) { // Show the top app bar on top level destinations. + val destination = appState.currentTopLevelDestination if (destination != null) { - MifosAppBar( - title = stringResource(destination.titleText), - onClickLogout = onClickLogout, - onNavigateToFaq = {}, - onNavigateToSettings = { - appState.navController.navigateToSettings() - }, - onNavigateToEditProfile = { - appState.navController.navigateToEditProfile() - }, - onNavigateToNotification = { - appState.navController.navigateToNotification() + MifosTopAppBar( + titleRes = destination.titleTextId, + actions = { + when (destination) { + TopLevelDestination.HOME -> { + IconBox( + icon = MifosIcons.SettingsOutlined, + onClick = { + appState.navController.navigateToSettings() + }, + ) + } + + TopLevelDestination.PROFILE -> { + IconBox( + icon = MifosIcons.Edit2, + onClick = { + appState.navController.navigateToEditProfile() + }, + ) + } + + else -> {} + } }, - destination = destination, ) } @@ -169,57 +238,6 @@ internal fun MifosApp( } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun MifosAppBar( - title: String, - onClickLogout: () -> Unit, - onNavigateToFaq: () -> Unit, - onNavigateToSettings: () -> Unit, - onNavigateToEditProfile: () -> Unit, - onNavigateToNotification: () -> Unit, - destination: TopLevelDestination?, - modifier: Modifier = Modifier, -) { - TopAppBar( - title = { Text(text = title) }, - actions = { - Box { - when (destination) { - TopLevelDestination.HOME -> { - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - IconBox( - icon = MifosIcons.OutlinedNotifications, - onClick = onNavigateToNotification, - ) - - IconBox( - icon = MifosIcons.SettingsOutlined, - onClick = onNavigateToSettings, - ) - } - } - - TopLevelDestination.PROFILE -> { - IconBox( - icon = MifosIcons.Edit2, - onClick = onNavigateToEditProfile, - ) - } - - else -> {} - } - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = Color.Transparent, - ), - modifier = modifier.testTag("mifosTopAppBar"), - ) -} - @Composable private fun MifosNavRail( destinations: List, @@ -248,7 +266,7 @@ private fun MifosNavRail( contentDescription = null, ) }, - label = { Text(stringResource(destination.iconText)) }, + label = { Text(stringResource(destination.iconTextId)) }, ) } } @@ -275,16 +293,26 @@ private fun MifosBottomBar( Icon( imageVector = destination.unselectedIcon, contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), ) }, modifier = if (hasUnread) Modifier.notificationDot() else Modifier, selectedIcon = { - Icon( - imageVector = destination.selectedIcon, - contentDescription = null, - ) + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = destination.selectedIcon, + contentDescription = null, + ) + Spacer( + modifier = Modifier + .padding(top = 31.dp) + .height(4.dp) + .width(11.dp) + .clip(RoundedCornerShape(100)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.7f)), + ) + } }, - label = { Text(stringResource(destination.iconText)) }, ) } } diff --git a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosAppState.kt b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosAppState.kt index 55604f92b..4e4a790f9 100644 --- a/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosAppState.kt +++ b/mifospay-shared/src/commonMain/kotlin/org/mifospay/shared/ui/MifosAppState.kt @@ -7,23 +7,24 @@ * * See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md */ -package org.mifospay.shared.ui +package org.mifospay.ui -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.WindowSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass -import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.util.trace +import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions +import androidx.tracing.trace +import com.mifos.library.material3.navigation.BottomSheetNavigator +import com.mifos.library.material3.navigation.rememberBottomSheetNavigator import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map @@ -31,6 +32,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.datetime.TimeZone import org.mifospay.core.data.util.NetworkMonitor import org.mifospay.core.data.util.TimeZoneMonitor +import org.mifospay.core.ui.TrackDisposableJank import org.mifospay.feature.finance.navigation.FINANCE_ROUTE import org.mifospay.feature.finance.navigation.navigateToFinance import org.mifospay.feature.home.navigation.HOME_ROUTE @@ -39,19 +41,22 @@ import org.mifospay.feature.payments.PAYMENTS_ROUTE import org.mifospay.feature.payments.navigateToPayments import org.mifospay.feature.profile.navigation.PROFILE_ROUTE import org.mifospay.feature.profile.navigation.navigateToProfile -import org.mifospay.shared.utils.TopLevelDestination +import org.mifospay.navigation.MifosNavGraph +import org.mifospay.navigation.TopLevelDestination -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @Composable -internal fun rememberMifosAppState( +fun rememberMifosAppState( + windowSizeClass: WindowSizeClass, networkMonitor: NetworkMonitor, timeZoneMonitor: TimeZoneMonitor, - windowSizeClass: WindowSizeClass = calculateWindowSizeClass(), coroutineScope: CoroutineScope = rememberCoroutineScope(), - navController: NavHostController = rememberNavController(), + bottomSheetNavigator: BottomSheetNavigator = rememberBottomSheetNavigator(), + navController: NavHostController = rememberNavController(bottomSheetNavigator), ): MifosAppState { + NavigationTrackingSideEffect(navController) return remember( navController, + bottomSheetNavigator, coroutineScope, windowSizeClass, networkMonitor, @@ -59,6 +64,7 @@ internal fun rememberMifosAppState( ) { MifosAppState( navController = navController, + bottomSheetNavigator = bottomSheetNavigator, coroutineScope = coroutineScope, windowSizeClass = windowSizeClass, networkMonitor = networkMonitor, @@ -68,8 +74,9 @@ internal fun rememberMifosAppState( } @Stable -internal class MifosAppState( +class MifosAppState( val navController: NavHostController, + val bottomSheetNavigator: BottomSheetNavigator, coroutineScope: CoroutineScope, val windowSizeClass: WindowSizeClass, networkMonitor: NetworkMonitor, @@ -102,6 +109,10 @@ internal class MifosAppState( initialValue = false, ) + /** + * Map of top level destinations to be used in the TopBar, BottomBar and NavRail. The key is the + * route. + */ val topLevelDestinations: List = TopLevelDestination.entries val currentTimeZone = timeZoneMonitor.currentTimeZone @@ -111,6 +122,13 @@ internal class MifosAppState( TimeZone.currentSystemDefault(), ) + /** + * UI logic for navigating to a top level destination in the app. Top level destinations have + * only one copy of the destination of the back stack, and save and restore state whenever you + * navigate to and from it. + * + * @param topLevelDestination: The destination the app needs to navigate to. + */ fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) { trace("Navigation: ${topLevelDestination.name}") { val topLevelNavOptions = navOptions { @@ -136,3 +154,39 @@ internal class MifosAppState( } } } + +/** + * Stores information about navigation events to be used with JankStats + */ +@Composable +private fun NavigationTrackingSideEffect(navController: NavHostController) { + TrackDisposableJank(navController) { metricsHolder -> + val listener = NavController.OnDestinationChangedListener { _, destination, _ -> + metricsHolder.state?.putState("Navigation", destination.route.toString()) + } + + navController.addOnDestinationChangedListener(listener) + + onDispose { + navController.removeOnDestinationChangedListener(listener) + } + } +} + +fun NavController.navigateToMainGraph() { + val options = navOptions { + // Pop up to the start destination of the graph to + // avoid building up a large stack of destinations + // on the back stack as users select items + popUpTo(graph.findStartDestination().id) { + saveState = false + } + // Avoid multiple copies of the same destination when + // reselecting the same item + launchSingleTop = true + // Restore state when reselecting a previously selected item + restoreState = false + } + + navigate(MifosNavGraph.MAIN_GRAPH, options) +} diff --git a/mifospay/dependencies/prodReleaseRuntimeClasspath.tree.txt b/mifospay/dependencies/prodReleaseRuntimeClasspath.tree.txt new file mode 100644 index 000000000..675d55138 --- /dev/null +++ b/mifospay/dependencies/prodReleaseRuntimeClasspath.tree.txt @@ -0,0 +1,2624 @@ ++--- androidx.databinding:databinding-common:8.5.2 ++--- androidx.databinding:databinding-runtime:8.5.2 +| +--- androidx.collection:collection:1.0.0 -> 1.4.2 +| | \--- androidx.collection:collection-jvm:1.4.2 +| | +--- androidx.annotation:annotation:1.8.1 +| | | \--- androidx.annotation:annotation-jvm:1.8.1 +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.7.10 -> 2.0.20 +| | | +--- org.jetbrains:annotations:13.0 -> 23.0.0 +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0 -> 1.9.20 (c) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0 -> 1.9.20 (c) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:2.0.20 (c) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | +--- androidx.collection:collection-ktx:1.4.2 (c) +| | \--- androidx.collection:collection-ktx:1.3.0 -> 1.4.2 (c) +| +--- androidx.databinding:databinding-common:8.5.2 +| +--- androidx.databinding:viewbinding:8.5.2 +| | \--- androidx.annotation:annotation:1.0.0 -> 1.8.1 (*) +| \--- androidx.lifecycle:lifecycle-runtime:2.6.1 -> 2.8.4 +| \--- androidx.lifecycle:lifecycle-runtime-android:2.8.4 +| +--- androidx.annotation:annotation:1.8.0 -> 1.8.1 (*) +| +--- androidx.arch.core:core-common:2.2.0 +| | \--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| +--- androidx.arch.core:core-runtime:2.2.0 +| | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | \--- androidx.arch.core:core-common:2.2.0 (*) +| +--- androidx.lifecycle:lifecycle-common:2.8.4 +| | \--- androidx.lifecycle:lifecycle-common-jvm:2.8.4 +| | +--- androidx.annotation:annotation:1.8.0 -> 1.8.1 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 +| | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1 +| | | +--- org.jetbrains:annotations:23.0.0 +| | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1 +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1 (c) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1 (c) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1 (c) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.8.1 (c) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.8.1 (c) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.8.1 (c) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1 (c) +| | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.8.1 (c) +| | +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-livedata:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-process:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-service:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (c) +| | \--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (c) +| +--- androidx.profileinstaller:profileinstaller:1.3.1 +| | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | +--- androidx.concurrent:concurrent-futures:1.1.0 +| | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | \--- com.google.guava:listenablefuture:1.0 -> 9999.0-empty-to-avoid-conflict-with-guava +| | +--- androidx.startup:startup-runtime:1.1.1 +| | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | \--- androidx.tracing:tracing:1.0.0 -> 1.3.0-alpha02 +| | | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | \--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (c) +| | \--- com.google.guava:listenablefuture:1.0 -> 9999.0-empty-to-avoid-conflict-with-guava +| +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 -> 1.8.1 +| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1 (*) +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.21 -> 2.0.20 (*) +| +--- androidx.lifecycle:lifecycle-common:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-livedata:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-process:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-service:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (c) +| \--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (c) ++--- androidx.databinding:databinding-adapters:8.5.2 +| +--- androidx.databinding:databinding-runtime:8.5.2 (*) +| \--- androidx.databinding:databinding-common:8.5.2 ++--- androidx.databinding:databinding-ktx:8.5.2 +| +--- androidx.databinding:databinding-runtime:8.5.2 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.20 +| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.20 -> 2.0.20 (*) +| | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.20 +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.20 -> 2.0.20 (*) +| +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1 -> 1.8.1 (*) +| +--- androidx.lifecycle:lifecycle-runtime-ktx:2.6.1 -> 2.8.4 +| | \--- androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.4 +| | +--- androidx.annotation:annotation:1.8.0 -> 1.8.1 (*) +| | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 -> 1.8.1 (*) +| | +--- androidx.lifecycle:lifecycle-common:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-livedata:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-process:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-service:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (c) +| | \--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-livedata:2.6.1 -> 2.8.4 +| | +--- androidx.arch.core:core-common:2.2.0 (*) +| | +--- androidx.arch.core:core-runtime:2.2.0 (*) +| | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 +| | | +--- androidx.arch.core:core-common:2.2.0 (*) +| | | +--- androidx.arch.core:core-runtime:2.2.0 (*) +| | | +--- androidx.lifecycle:lifecycle-common:2.8.4 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | +--- androidx.lifecycle:lifecycle-common:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-livedata:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-process:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-service:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (c) +| | | \--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 +| | | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | +--- androidx.lifecycle:lifecycle-common:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-livedata:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-process:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-service:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (c) +| | | \--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (c) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) +| | +--- androidx.lifecycle:lifecycle-common:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-process:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-service:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (c) +| | \--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-process:2.6.1 -> 2.8.4 +| | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (*) +| | +--- androidx.startup:startup-runtime:1.1.1 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | +--- androidx.lifecycle:lifecycle-common:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-livedata:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-service:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (c) +| | \--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-service:2.6.1 -> 2.8.4 +| | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | +--- androidx.lifecycle:lifecycle-common:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-livedata:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-process:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (c) +| | \--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (c) +| \--- androidx.lifecycle:lifecycle-viewmodel:2.6.1 -> 2.8.4 +| \--- androidx.lifecycle:lifecycle-viewmodel-android:2.8.4 +| +--- androidx.annotation:annotation:1.8.0 -> 1.8.1 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 -> 1.8.1 (*) +| +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) +| +--- androidx.lifecycle:lifecycle-common:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-livedata:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-process:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-service:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (c) +| +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (c) +| \--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (c) ++--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) ++--- androidx.compose:compose-bom:2024.08.00 +| +--- androidx.compose.material3:material3-window-size-class:1.2.1 (c) +| +--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-tooling-preview:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.foundation:foundation:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.foundation:foundation-layout:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.material:material-icons-extended:1.6.8 (c) +| +--- androidx.compose.material3:material3:1.2.1 (c) +| +--- androidx.compose.ui:ui-util:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.runtime:runtime-saveable:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.animation:animation:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.material3:material3-window-size-class-android:1.2.1 (c) +| +--- androidx.compose.animation:animation-graphics:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-geometry:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.animation:animation-core:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.foundation:foundation-layout-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.runtime:runtime-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.runtime:runtime-saveable-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.material3:material3-android:1.2.1 (c) +| +--- androidx.compose.material:material-icons-extended-android:1.6.8 (c) +| +--- androidx.compose.animation:animation-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-unit:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-test-junit4:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-tooling:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.animation:animation-core-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-graphics:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-text:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.material:material-icons-core:1.6.8 (c) +| +--- androidx.compose.material:material-ripple:1.6.8 (c) +| +--- androidx.compose.ui:ui-unit-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-util-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.foundation:foundation-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-geometry-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-test-junit4-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-tooling-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-tooling-preview-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-graphics-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-text-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.material:material-icons-core-android:1.6.8 (c) +| +--- androidx.compose.material:material-ripple-android:1.6.8 (c) +| +--- androidx.compose.animation:animation-graphics-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-test:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.material:material:1.6.8 (c) +| +--- androidx.compose.ui:ui-tooling-data:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-test-android:1.6.8 -> 1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-tooling-data-android:1.6.8 -> 1.7.0-rc01 (c) +| \--- androidx.compose.material:material-android:1.6.8 (c) ++--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 +| \--- androidx.compose.ui:ui-tooling-preview-android:1.7.0-rc01 +| +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| +--- androidx.compose.runtime:runtime:1.7.0-rc01 +| | \--- androidx.compose.runtime:runtime-android:1.7.0-rc01 +| | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.7.10 -> 2.0.20 (*) +| | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 -> 1.8.1 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) +| | \--- androidx.compose.runtime:runtime-saveable:1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui:1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-geometry:1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-test:1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-test-junit4:1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-text:1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-tooling:1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-tooling-data:1.7.0-rc01 (c) +| +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (c) +| \--- androidx.compose.ui:ui-util:1.7.0-rc01 (c) ++--- com.google.firebase:firebase-bom:33.1.2 +| +--- com.google.firebase:firebase-perf-ktx:21.0.1 (c) +| +--- com.google.firebase:firebase-crashlytics-ktx:19.0.3 (c) +| +--- com.google.firebase:firebase-analytics-ktx:22.0.2 (c) +| +--- com.google.firebase:firebase-messaging-ktx:24.0.0 (c) +| +--- com.google.firebase:firebase-perf:21.0.1 (c) +| +--- com.google.firebase:firebase-common:21.0.0 (c) +| +--- com.google.firebase:firebase-common-ktx:21.0.0 (c) +| +--- com.google.firebase:firebase-crashlytics:19.0.3 (c) +| +--- com.google.firebase:firebase-analytics:22.0.2 (c) +| +--- com.google.firebase:firebase-messaging:24.0.0 (c) +| +--- com.google.firebase:firebase-encoders:17.0.0 (c) +| +--- com.google.firebase:firebase-config:22.0.0 (c) +| \--- com.google.firebase:firebase-installations:18.0.0 (c) ++--- com.google.firebase:firebase-analytics-ktx -> 22.0.2 +| +--- com.google.firebase:firebase-analytics:22.0.2 +| | +--- com.google.android.gms:play-services-measurement:22.0.2 +| | | +--- androidx.collection:collection:1.0.0 -> 1.4.2 (*) +| | | +--- androidx.legacy:legacy-support-core-utils:1.0.0 +| | | | +--- androidx.annotation:annotation:1.0.0 -> 1.8.1 (*) +| | | | +--- androidx.core:core:1.0.0 -> 1.13.1 +| | | | | +--- androidx.annotation:annotation:1.6.0 -> 1.8.1 (*) +| | | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | | | +--- androidx.collection:collection:1.0.0 -> 1.4.2 (*) +| | | | | +--- androidx.concurrent:concurrent-futures:1.0.0 -> 1.1.0 (*) +| | | | | +--- androidx.interpolator:interpolator:1.0.0 +| | | | | | \--- androidx.annotation:annotation:1.0.0 -> 1.8.1 (*) +| | | | | +--- androidx.lifecycle:lifecycle-runtime:2.6.2 -> 2.8.4 (*) +| | | | | +--- androidx.versionedparcelable:versionedparcelable:1.1.1 +| | | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | | | \--- androidx.collection:collection:1.0.0 -> 1.4.2 (*) +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | \--- androidx.core:core-ktx:1.13.1 (c) +| | | | +--- androidx.documentfile:documentfile:1.0.0 +| | | | | \--- androidx.annotation:annotation:1.0.0 -> 1.8.1 (*) +| | | | +--- androidx.loader:loader:1.0.0 -> 1.1.0 +| | | | | +--- androidx.annotation:annotation:1.0.0 -> 1.8.1 (*) +| | | | | +--- androidx.core:core:1.0.0 -> 1.13.1 (*) +| | | | | +--- androidx.lifecycle:lifecycle-livedata-core:2.0.0 -> 2.8.4 (*) +| | | | | +--- androidx.lifecycle:lifecycle-viewmodel:2.0.0 -> 2.8.4 (*) +| | | | | \--- androidx.collection:collection:1.0.0 -> 1.4.2 (*) +| | | | +--- androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 +| | | | | \--- androidx.annotation:annotation:1.0.0 -> 1.8.1 (*) +| | | | \--- androidx.print:print:1.0.0 +| | | | \--- androidx.annotation:annotation:1.0.0 -> 1.8.1 (*) +| | | +--- com.google.android.gms:play-services-ads-identifier:18.0.0 +| | | | \--- com.google.android.gms:play-services-basement:18.0.0 -> 18.4.0 +| | | | +--- androidx.collection:collection:1.0.0 -> 1.4.2 (*) +| | | | +--- androidx.core:core:1.2.0 -> 1.13.1 (*) +| | | | \--- androidx.fragment:fragment:1.1.0 -> 1.8.2 +| | | | +--- androidx.activity:activity:1.8.1 -> 1.9.1 +| | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | | +--- androidx.collection:collection:1.0.0 -> 1.4.2 (*) +| | | | | +--- androidx.core:core:1.13.0 -> 1.13.1 (*) +| | | | | +--- androidx.lifecycle:lifecycle-runtime:2.6.1 -> 2.8.4 (*) +| | | | | +--- androidx.lifecycle:lifecycle-viewmodel:2.6.1 -> 2.8.4 (*) +| | | | | +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1 -> 2.8.4 +| | | | | | +--- androidx.annotation:annotation:1.0.0 -> 1.8.1 (*) +| | | | | | +--- androidx.core:core-ktx:1.2.0 -> 1.13.1 +| | | | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | | | | +--- androidx.core:core:1.13.1 (*) +| | | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | | | \--- androidx.core:core:1.13.1 (c) +| | | | | | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (*) +| | | | | | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (*) +| | | | | | +--- androidx.savedstate:savedstate:1.2.1 +| | | | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | | | | +--- androidx.arch.core:core-common:2.1.0 -> 2.2.0 (*) +| | | | | | | +--- androidx.lifecycle:lifecycle-common:2.6.1 -> 2.8.4 (*) +| | | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.10 -> 2.0.20 (*) +| | | | | | | \--- androidx.savedstate:savedstate-ktx:1.2.1 (c) +| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 -> 1.8.1 (*) +| | | | | | +--- androidx.lifecycle:lifecycle-common:2.8.4 (c) +| | | | | | +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 (c) +| | | | | | +--- androidx.lifecycle:lifecycle-livedata:2.8.4 (c) +| | | | | | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) +| | | | | | +--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 (c) +| | | | | | +--- androidx.lifecycle:lifecycle-process:2.8.4 (c) +| | | | | | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (c) +| | | | | | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (c) +| | | | | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 (c) +| | | | | | +--- androidx.lifecycle:lifecycle-service:2.8.4 (c) +| | | | | | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (c) +| | | | | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (c) +| | | | | | \--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (c) +| | | | | +--- androidx.profileinstaller:profileinstaller:1.3.1 (*) +| | | | | +--- androidx.savedstate:savedstate:1.2.1 (*) +| | | | | +--- androidx.tracing:tracing:1.0.0 -> 1.3.0-alpha02 (*) +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | +--- androidx.activity:activity-compose:1.9.1 (c) +| | | | | \--- androidx.activity:activity-ktx:1.9.1 (c) +| | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | | +--- androidx.collection:collection:1.1.0 -> 1.4.2 (*) +| | | | +--- androidx.core:core-ktx:1.2.0 -> 1.13.1 (*) +| | | | +--- androidx.lifecycle:lifecycle-livedata-core:2.6.1 -> 2.8.4 (*) +| | | | +--- androidx.lifecycle:lifecycle-runtime:2.6.1 -> 2.8.4 (*) +| | | | +--- androidx.lifecycle:lifecycle-viewmodel:2.6.1 -> 2.8.4 (*) +| | | | +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1 -> 2.8.4 (*) +| | | | +--- androidx.loader:loader:1.0.0 -> 1.1.0 (*) +| | | | +--- androidx.profileinstaller:profileinstaller:1.3.1 (*) +| | | | +--- androidx.savedstate:savedstate:1.2.1 (*) +| | | | +--- androidx.viewpager:viewpager:1.0.0 +| | | | | +--- androidx.annotation:annotation:1.0.0 -> 1.8.1 (*) +| | | | | +--- androidx.core:core:1.0.0 -> 1.13.1 (*) +| | | | | \--- androidx.customview:customview:1.0.0 -> 1.1.0 +| | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | | +--- androidx.core:core:1.3.0 -> 1.13.1 (*) +| | | | | \--- androidx.collection:collection:1.1.0 -> 1.4.2 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | \--- androidx.fragment:fragment-ktx:1.8.2 (c) +| | | +--- com.google.android.gms:play-services-basement:18.4.0 (*) +| | | +--- com.google.android.gms:play-services-measurement-base:22.0.2 +| | | | \--- com.google.android.gms:play-services-basement:18.4.0 (*) +| | | +--- com.google.android.gms:play-services-measurement-impl:22.0.2 +| | | | +--- androidx.collection:collection:1.0.0 -> 1.4.2 (*) +| | | | +--- androidx.core:core:1.9.0 -> 1.13.1 (*) +| | | | +--- androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05 +| | | | | +--- androidx.annotation:annotation:1.6.0 -> 1.8.1 (*) +| | | | | +--- androidx.core:core-ktx:1.8.0 -> 1.13.1 (*) +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.21 -> 2.0.20 (*) +| | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | | | \--- androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05 (c) +| | | | +--- androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05 +| | | | | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | | | +--- androidx.concurrent:concurrent-futures:1.1.0 (*) +| | | | | +--- androidx.core:core-ktx:1.8.0 -> 1.13.1 (*) +| | | | | +--- androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05 (*) +| | | | | +--- com.google.guava:guava:31.1-android +| | | | | | +--- com.google.guava:failureaccess:1.0.1 +| | | | | | +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava +| | | | | | +--- com.google.code.findbugs:jsr305:3.0.2 +| | | | | | +--- org.checkerframework:checker-qual:3.12.0 +| | | | | | +--- com.google.errorprone:error_prone_annotations:2.11.0 -> 2.26.0 +| | | | | | \--- com.google.j2objc:j2objc-annotations:1.3 +| | | | | +--- com.google.guava:listenablefuture:1.0 -> 9999.0-empty-to-avoid-conflict-with-guava +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.21 -> 2.0.20 (*) +| | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | | | \--- androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05 (c) +| | | | +--- com.google.android.gms:play-services-ads-identifier:18.0.0 (*) +| | | | +--- com.google.android.gms:play-services-basement:18.4.0 (*) +| | | | +--- com.google.android.gms:play-services-measurement-base:22.0.2 (*) +| | | | +--- com.google.android.gms:play-services-stats:17.0.2 +| | | | | +--- androidx.legacy:legacy-support-core-utils:1.0.0 (*) +| | | | | \--- com.google.android.gms:play-services-basement:18.0.0 -> 18.4.0 (*) +| | | | \--- com.google.guava:guava:31.1-android (*) +| | | \--- com.google.android.gms:play-services-stats:17.0.2 (*) +| | +--- com.google.android.gms:play-services-measurement-api:22.0.2 +| | | +--- com.google.android.gms:play-services-ads-identifier:18.0.0 (*) +| | | +--- com.google.android.gms:play-services-basement:18.4.0 (*) +| | | +--- com.google.android.gms:play-services-measurement-base:22.0.2 (*) +| | | +--- com.google.android.gms:play-services-measurement-sdk-api:22.0.2 +| | | | +--- com.google.android.gms:play-services-basement:18.4.0 (*) +| | | | \--- com.google.android.gms:play-services-measurement-base:22.0.2 (*) +| | | +--- com.google.android.gms:play-services-tasks:18.2.0 +| | | | \--- com.google.android.gms:play-services-basement:18.4.0 (*) +| | | +--- com.google.firebase:firebase-common:21.0.0 +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4 -> 1.8.1 +| | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1 (*) +| | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1 (*) +| | | | | +--- com.google.android.gms:play-services-tasks:16.0.1 -> 18.2.0 (*) +| | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.21 -> 2.0.20 (*) +| | | | +--- com.google.firebase:firebase-components:18.0.0 +| | | | | +--- com.google.firebase:firebase-annotations:16.2.0 +| | | | | | \--- javax.inject:javax.inject:1 +| | | | | +--- androidx.annotation:annotation:1.5.0 -> 1.8.1 (*) +| | | | | \--- com.google.errorprone:error_prone_annotations:2.26.0 +| | | | +--- com.google.firebase:firebase-annotations:16.2.0 (*) +| | | | +--- androidx.annotation:annotation:1.5.0 -> 1.8.1 (*) +| | | | +--- androidx.concurrent:concurrent-futures:1.1.0 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | +--- com.google.android.gms:play-services-basement:18.3.0 -> 18.4.0 (*) +| | | | \--- com.google.android.gms:play-services-tasks:18.1.0 -> 18.2.0 (*) +| | | +--- com.google.firebase:firebase-common-ktx:21.0.0 +| | | | +--- com.google.firebase:firebase-common:21.0.0 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | | +--- com.google.firebase:firebase-components:18.0.0 (*) +| | | | \--- com.google.firebase:firebase-annotations:16.2.0 (*) +| | | +--- com.google.firebase:firebase-components:18.0.0 (*) +| | | +--- com.google.firebase:firebase-installations:17.0.1 -> 18.0.0 +| | | | +--- com.google.android.gms:play-services-tasks:18.0.1 -> 18.2.0 (*) +| | | | +--- com.google.firebase:firebase-annotations:16.2.0 (*) +| | | | +--- com.google.firebase:firebase-common:21.0.0 (*) +| | | | +--- com.google.firebase:firebase-common-ktx:21.0.0 (*) +| | | | +--- com.google.firebase:firebase-components:18.0.0 (*) +| | | | +--- com.google.firebase:firebase-installations-interop:17.1.1 -> 17.2.0 +| | | | | +--- com.google.android.gms:play-services-tasks:18.0.1 -> 18.2.0 (*) +| | | | | \--- com.google.firebase:firebase-annotations:16.2.0 (*) +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | +--- com.google.firebase:firebase-installations-interop:17.0.0 -> 17.2.0 (*) +| | | +--- com.google.firebase:firebase-measurement-connector:19.0.0 -> 20.0.1 +| | | | +--- com.google.android.gms:play-services-basement:18.0.0 -> 18.4.0 (*) +| | | | \--- com.google.firebase:firebase-annotations:16.0.0 -> 16.2.0 (*) +| | | +--- com.google.guava:guava:31.1-android (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.7.10 -> 2.0.20 (*) +| | \--- com.google.android.gms:play-services-measurement-sdk:22.0.2 +| | +--- androidx.collection:collection:1.0.0 -> 1.4.2 (*) +| | +--- com.google.android.gms:play-services-basement:18.4.0 (*) +| | +--- com.google.android.gms:play-services-measurement-base:22.0.2 (*) +| | \--- com.google.android.gms:play-services-measurement-impl:22.0.2 (*) +| +--- com.google.firebase:firebase-common:21.0.0 (*) +| +--- com.google.firebase:firebase-common-ktx:21.0.0 (*) +| +--- com.google.firebase:firebase-components:18.0.0 (*) +| \--- org.jetbrains.kotlin:kotlin-stdlib:1.7.10 -> 2.0.20 (*) ++--- com.google.firebase:firebase-perf-ktx -> 21.0.1 +| +--- com.google.firebase:firebase-perf:21.0.1 +| | +--- com.google.firebase:firebase-annotations:16.2.0 (*) +| | +--- com.google.firebase:firebase-installations-interop:17.1.0 -> 17.2.0 (*) +| | +--- com.google.firebase:protolite-well-known-types:18.0.0 +| | | \--- com.google.protobuf:protobuf-javalite:3.14.0 -> 4.26.0 +| | +--- com.google.firebase:firebase-common:21.0.0 (*) +| | +--- com.google.firebase:firebase-common-ktx:21.0.0 (*) +| | +--- com.google.firebase:firebase-components:18.0.0 (*) +| | +--- com.google.firebase:firebase-config:21.5.0 -> 22.0.0 +| | | +--- com.google.firebase:firebase-config-interop:16.0.1 +| | | | +--- com.google.firebase:firebase-encoders-json:18.0.1 +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10 -> 1.9.20 (*) +| | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | | \--- com.google.firebase:firebase-encoders:17.0.0 +| | | | | \--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | \--- com.google.firebase:firebase-encoders:17.0.0 (*) +| | | +--- com.google.firebase:firebase-annotations:16.2.0 (*) +| | | +--- com.google.firebase:firebase-installations-interop:17.1.0 -> 17.2.0 (*) +| | | +--- com.google.firebase:firebase-abt:21.1.1 +| | | | +--- com.google.firebase:firebase-measurement-connector:18.0.0 -> 20.0.1 (*) +| | | | \--- com.google.android.gms:play-services-basement:18.1.0 -> 18.4.0 (*) +| | | +--- com.google.firebase:firebase-measurement-connector:18.0.0 -> 20.0.1 (*) +| | | +--- com.google.firebase:firebase-common:21.0.0 (*) +| | | +--- com.google.firebase:firebase-common-ktx:21.0.0 (*) +| | | +--- com.google.firebase:firebase-components:18.0.0 (*) +| | | +--- com.google.firebase:firebase-installations:17.2.0 -> 18.0.0 (*) +| | | +--- com.google.android.gms:play-services-tasks:18.0.1 -> 18.2.0 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | \--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | +--- com.google.firebase:firebase-installations:17.2.0 -> 18.0.0 (*) +| | +--- com.google.firebase:firebase-sessions:2.0.0 -> 2.0.3 +| | | +--- com.google.firebase:firebase-common:21.0.0 (*) +| | | +--- com.google.firebase:firebase-common-ktx:21.0.0 (*) +| | | +--- com.google.firebase:firebase-components:18.0.0 (*) +| | | +--- com.google.firebase:firebase-installations-interop:17.2.0 (*) +| | | +--- com.google.firebase:firebase-annotations:16.2.0 (*) +| | | +--- com.google.firebase:firebase-encoders:17.0.0 (*) +| | | +--- com.google.firebase:firebase-encoders-json:18.0.1 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | +--- com.google.firebase:firebase-installations:18.0.0 (*) +| | | +--- com.google.firebase:firebase-datatransport:19.0.0 +| | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | +--- com.google.android.datatransport:transport-api:3.1.0 -> 3.2.0 +| | | | | \--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | +--- com.google.android.datatransport:transport-backend-cct:3.2.0 -> 3.3.0 +| | | | | +--- com.google.android.datatransport:transport-api:3.2.0 (*) +| | | | | +--- com.google.android.datatransport:transport-runtime:3.3.0 +| | | | | | +--- com.google.android.datatransport:transport-api:3.2.0 (*) +| | | | | | +--- androidx.annotation:annotation:1.3.0 -> 1.8.1 (*) +| | | | | | +--- javax.inject:javax.inject:1 +| | | | | | +--- com.google.firebase:firebase-encoders:17.0.0 (*) +| | | | | | \--- com.google.firebase:firebase-encoders-proto:16.0.0 +| | | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | | | \--- com.google.firebase:firebase-encoders:17.0.0 (*) +| | | | | +--- com.google.firebase:firebase-encoders:17.0.0 (*) +| | | | | +--- com.google.firebase:firebase-encoders-json:18.0.0 -> 18.0.1 (*) +| | | | | \--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | +--- com.google.android.datatransport:transport-runtime:3.2.0 -> 3.3.0 (*) +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | +--- androidx.datastore:datastore-preferences:1.0.0 -> 1.1.1 +| | | | \--- androidx.datastore:datastore-preferences-android:1.1.1 +| | | | +--- androidx.datastore:datastore:1.1.1 +| | | | | \--- androidx.datastore:datastore-android:1.1.1 +| | | | | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | | | +--- androidx.datastore:datastore-core:1.1.1 +| | | | | | \--- androidx.datastore:datastore-core-android:1.1.1 +| | | | | | +--- androidx.annotation:annotation:1.7.0 -> 1.8.1 (*) +| | | | | | +--- org.jetbrains.kotlin:kotlin-parcelize-runtime:1.9.22 -> 2.0.20 +| | | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | | | | | | \--- org.jetbrains.kotlin:kotlin-android-extensions-runtime:2.0.20 +| | | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) +| | | | | | +--- androidx.datastore:datastore:1.1.1 (c) +| | | | | | +--- androidx.datastore:datastore-core-okio:1.1.1 (c) +| | | | | | +--- androidx.datastore:datastore-preferences:1.1.1 (c) +| | | | | | \--- androidx.datastore:datastore-preferences-core:1.1.1 (c) +| | | | | +--- androidx.datastore:datastore-core-okio:1.1.1 +| | | | | | \--- androidx.datastore:datastore-core-okio-jvm:1.1.1 +| | | | | | +--- androidx.datastore:datastore-core:1.1.1 (*) +| | | | | | +--- com.squareup.okio:okio:3.4.0 -> 3.9.0 +| | | | | | | \--- com.squareup.okio:okio-jvm:3.9.0 +| | | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.21 -> 2.0.20 (*) +| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) +| | | | | | +--- androidx.datastore:datastore-core:1.1.1 (c) +| | | | | | +--- androidx.datastore:datastore:1.1.1 (c) +| | | | | | +--- androidx.datastore:datastore-preferences:1.1.1 (c) +| | | | | | \--- androidx.datastore:datastore-preferences-core:1.1.1 (c) +| | | | | +--- com.squareup.okio:okio:3.4.0 -> 3.9.0 (*) +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) +| | | | | +--- androidx.datastore:datastore-core:1.1.1 (c) +| | | | | +--- androidx.datastore:datastore-core-okio:1.1.1 (c) +| | | | | +--- androidx.datastore:datastore-preferences:1.1.1 (c) +| | | | | \--- androidx.datastore:datastore-preferences-core:1.1.1 (c) +| | | | +--- androidx.datastore:datastore-preferences-core:1.1.1 +| | | | | \--- androidx.datastore:datastore-preferences-core-jvm:1.1.1 +| | | | | +--- androidx.datastore:datastore-core:1.1.1 (*) +| | | | | +--- androidx.datastore:datastore-core-okio:1.1.1 (*) +| | | | | +--- com.squareup.okio:okio:3.4.0 -> 3.9.0 (*) +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | +--- androidx.datastore:datastore:1.1.1 (c) +| | | | | +--- androidx.datastore:datastore-core:1.1.1 (c) +| | | | | +--- androidx.datastore:datastore-core-okio:1.1.1 (c) +| | | | | \--- androidx.datastore:datastore-preferences:1.1.1 (c) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) +| | | | +--- androidx.datastore:datastore:1.1.1 (c) +| | | | +--- androidx.datastore:datastore-core:1.1.1 (c) +| | | | +--- androidx.datastore:datastore-core-okio:1.1.1 (c) +| | | | \--- androidx.datastore:datastore-preferences-core:1.1.1 (c) +| | | +--- com.google.android.datatransport:transport-api:3.2.0 (*) +| | | \--- androidx.annotation:annotation:1.5.0 -> 1.8.1 (*) +| | +--- com.google.firebase:firebase-datatransport:18.1.8 -> 19.0.0 (*) +| | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | +--- androidx.lifecycle:lifecycle-process:2.3.1 -> 2.8.4 (*) +| | +--- com.google.android.gms:play-services-tasks:18.0.1 -> 18.2.0 (*) +| | +--- com.google.protobuf:protobuf-javalite:3.21.11 -> 4.26.0 +| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | +--- androidx.appcompat:appcompat:1.2.0 -> 1.7.0 +| | | +--- androidx.activity:activity:1.7.0 -> 1.9.1 (*) +| | | +--- androidx.annotation:annotation:1.3.0 -> 1.8.1 (*) +| | | +--- androidx.appcompat:appcompat-resources:1.7.0 +| | | | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | | +--- androidx.collection:collection:1.0.0 -> 1.4.2 (*) +| | | | +--- androidx.core:core:1.6.0 -> 1.13.1 (*) +| | | | +--- androidx.vectordrawable:vectordrawable:1.1.0 +| | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | | +--- androidx.core:core:1.1.0 -> 1.13.1 (*) +| | | | | \--- androidx.collection:collection:1.1.0 -> 1.4.2 (*) +| | | | +--- androidx.vectordrawable:vectordrawable-animated:1.1.0 +| | | | | +--- androidx.vectordrawable:vectordrawable:1.1.0 (*) +| | | | | +--- androidx.interpolator:interpolator:1.0.0 (*) +| | | | | \--- androidx.collection:collection:1.1.0 -> 1.4.2 (*) +| | | | \--- androidx.appcompat:appcompat:1.7.0 (c) +| | | +--- androidx.collection:collection:1.0.0 -> 1.4.2 (*) +| | | +--- androidx.core:core:1.13.0 -> 1.13.1 (*) +| | | +--- androidx.core:core-ktx:1.13.0 -> 1.13.1 (*) +| | | +--- androidx.cursoradapter:cursoradapter:1.0.0 +| | | | \--- androidx.annotation:annotation:1.0.0 -> 1.8.1 (*) +| | | +--- androidx.drawerlayout:drawerlayout:1.0.0 +| | | | +--- androidx.annotation:annotation:1.0.0 -> 1.8.1 (*) +| | | | +--- androidx.core:core:1.0.0 -> 1.13.1 (*) +| | | | \--- androidx.customview:customview:1.0.0 -> 1.1.0 (*) +| | | +--- androidx.emoji2:emoji2:1.3.0 +| | | | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | | +--- androidx.collection:collection:1.1.0 -> 1.4.2 (*) +| | | | +--- androidx.core:core:1.3.0 -> 1.13.1 (*) +| | | | +--- androidx.lifecycle:lifecycle-process:2.4.1 -> 2.8.4 (*) +| | | | +--- androidx.startup:startup-runtime:1.0.0 -> 1.1.1 (*) +| | | | \--- androidx.emoji2:emoji2-views-helper:1.3.0 (c) +| | | +--- androidx.emoji2:emoji2-views-helper:1.2.0 -> 1.3.0 +| | | | +--- androidx.collection:collection:1.1.0 -> 1.4.2 (*) +| | | | +--- androidx.core:core:1.3.0 -> 1.13.1 (*) +| | | | +--- androidx.emoji2:emoji2:1.3.0 (*) +| | | | \--- androidx.emoji2:emoji2:1.3.0 (c) +| | | +--- androidx.fragment:fragment:1.5.4 -> 1.8.2 (*) +| | | +--- androidx.lifecycle:lifecycle-runtime:2.6.1 -> 2.8.4 (*) +| | | +--- androidx.lifecycle:lifecycle-viewmodel:2.6.1 -> 2.8.4 (*) +| | | +--- androidx.profileinstaller:profileinstaller:1.3.1 (*) +| | | +--- androidx.resourceinspection:resourceinspection-annotation:1.0.1 +| | | | \--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | +--- androidx.savedstate:savedstate:1.2.1 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | \--- androidx.appcompat:appcompat-resources:1.7.0 (c) +| | +--- com.google.android.datatransport:transport-api:3.0.0 -> 3.2.0 (*) +| | +--- com.google.dagger:dagger:2.27 +| | | \--- javax.inject:javax.inject:1 +| | \--- com.squareup.okhttp3:okhttp:3.12.1 -> 4.12.0 +| | +--- com.squareup.okio:okio:3.6.0 -> 3.9.0 (*) +| | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21 -> 1.9.20 (*) +| +--- com.google.firebase:firebase-common:21.0.0 (*) +| +--- com.google.firebase:firebase-common-ktx:21.0.0 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| \--- com.google.firebase:firebase-components:18.0.0 (*) ++--- com.google.firebase:firebase-crashlytics-ktx -> 19.0.3 +| +--- com.google.firebase:firebase-crashlytics:19.0.3 +| | +--- com.google.firebase:firebase-sessions:2.0.3 (*) +| | +--- com.google.android.gms:play-services-tasks:18.1.0 -> 18.2.0 (*) +| | +--- com.google.firebase:firebase-annotations:16.2.0 (*) +| | +--- com.google.firebase:firebase-common:21.0.0 (*) +| | +--- com.google.firebase:firebase-common-ktx:21.0.0 (*) +| | +--- com.google.firebase:firebase-components:18.0.0 (*) +| | +--- com.google.firebase:firebase-config-interop:16.0.1 (*) +| | +--- com.google.firebase:firebase-encoders:17.0.0 (*) +| | +--- com.google.firebase:firebase-encoders-json:18.0.1 (*) +| | +--- com.google.firebase:firebase-installations:18.0.0 (*) +| | +--- com.google.firebase:firebase-installations-interop:17.2.0 (*) +| | +--- com.google.firebase:firebase-measurement-connector:20.0.1 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | +--- com.google.android.datatransport:transport-api:3.2.0 (*) +| | +--- com.google.android.datatransport:transport-backend-cct:3.3.0 (*) +| | +--- com.google.android.datatransport:transport-runtime:3.3.0 (*) +| | \--- androidx.annotation:annotation:1.5.0 -> 1.8.1 (*) +| +--- com.google.firebase:firebase-common:21.0.0 (*) +| +--- com.google.firebase:firebase-common-ktx:21.0.0 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| \--- com.google.firebase:firebase-components:18.0.0 (*) ++--- com.google.firebase:firebase-messaging-ktx -> 24.0.0 +| +--- com.google.firebase:firebase-messaging:24.0.0 +| | +--- com.google.firebase:firebase-common:21.0.0 (*) +| | +--- com.google.firebase:firebase-common-ktx:21.0.0 (*) +| | +--- com.google.firebase:firebase-components:18.0.0 (*) +| | +--- com.google.firebase:firebase-datatransport:18.2.0 -> 19.0.0 (*) +| | +--- com.google.firebase:firebase-encoders:17.0.0 (*) +| | +--- com.google.firebase:firebase-encoders-json:18.0.0 -> 18.0.1 (*) +| | +--- com.google.firebase:firebase-encoders-proto:16.0.0 (*) +| | +--- com.google.firebase:firebase-iid-interop:17.1.0 +| | | +--- com.google.android.gms:play-services-basement:17.0.0 -> 18.4.0 (*) +| | | \--- com.google.android.gms:play-services-tasks:17.0.0 -> 18.2.0 (*) +| | +--- com.google.firebase:firebase-installations:17.2.0 -> 18.0.0 (*) +| | +--- com.google.firebase:firebase-installations-interop:17.1.0 -> 17.2.0 (*) +| | +--- com.google.firebase:firebase-measurement-connector:19.0.0 -> 20.0.1 (*) +| | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | +--- com.google.android.datatransport:transport-api:3.1.0 -> 3.2.0 (*) +| | +--- com.google.android.datatransport:transport-backend-cct:3.1.8 -> 3.3.0 (*) +| | +--- com.google.android.datatransport:transport-runtime:3.1.8 -> 3.3.0 (*) +| | +--- com.google.android.gms:play-services-base:18.0.1 -> 18.3.0 +| | | +--- androidx.collection:collection:1.0.0 -> 1.4.2 (*) +| | | +--- androidx.core:core:1.2.0 -> 1.13.1 (*) +| | | +--- androidx.fragment:fragment:1.0.0 -> 1.8.2 (*) +| | | +--- com.google.android.gms:play-services-basement:18.3.0 -> 18.4.0 (*) +| | | \--- com.google.android.gms:play-services-tasks:18.1.0 -> 18.2.0 (*) +| | +--- com.google.android.gms:play-services-basement:18.1.0 -> 18.4.0 (*) +| | +--- com.google.android.gms:play-services-cloud-messaging:17.2.0 +| | | +--- com.google.android.gms:play-services-basement:18.3.0 -> 18.4.0 (*) +| | | \--- com.google.android.gms:play-services-tasks:18.1.0 -> 18.2.0 (*) +| | +--- com.google.android.gms:play-services-stats:17.0.2 (*) +| | +--- com.google.android.gms:play-services-tasks:18.0.1 -> 18.2.0 (*) +| | +--- com.google.errorprone:error_prone_annotations:2.9.0 -> 2.26.0 +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| +--- com.google.firebase:firebase-common:21.0.0 (*) +| +--- com.google.firebase:firebase-common-ktx:21.0.0 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| \--- com.google.firebase:firebase-components:18.0.0 (*) ++--- project :shared +| +--- org.jetbrains.compose.ui:ui-tooling-preview:1.6.11 +| | \--- androidx.compose.ui:ui-tooling-preview:1.6.7 -> 1.7.0-rc01 (*) +| +--- androidx.activity:activity-compose:1.9.1 +| | +--- androidx.activity:activity-ktx:1.9.1 +| | | +--- androidx.activity:activity:1.9.1 (*) +| | | +--- androidx.core:core-ktx:1.13.0 -> 1.13.1 (*) +| | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.6.1 -> 2.8.4 (*) +| | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1 -> 2.8.4 +| | | | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 -> 1.8.1 (*) +| | | | +--- androidx.lifecycle:lifecycle-livedata:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-process:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-service:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-common:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) +| | | | \--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 (c) +| | | +--- androidx.savedstate:savedstate-ktx:1.2.1 +| | | | +--- androidx.savedstate:savedstate:1.2.1 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.10 -> 2.0.20 (*) +| | | | \--- androidx.savedstate:savedstate:1.2.1 (c) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | +--- androidx.activity:activity:1.9.1 (c) +| | | \--- androidx.activity:activity-compose:1.9.1 (c) +| | +--- androidx.compose.runtime:runtime:1.0.1 -> 1.7.0-rc01 (*) +| | +--- androidx.compose.runtime:runtime-saveable:1.0.1 -> 1.7.0-rc01 +| | | \--- androidx.compose.runtime:runtime-saveable-android:1.7.0-rc01 +| | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | \--- androidx.compose.runtime:runtime:1.7.0-rc01 (c) +| | +--- androidx.compose.ui:ui:1.0.1 -> 1.7.0-rc01 +| | | \--- androidx.compose.ui:ui-android:1.7.0-rc01 +| | | +--- androidx.activity:activity-ktx:1.7.0 -> 1.9.1 (*) +| | | +--- androidx.annotation:annotation:1.6.0 -> 1.8.1 (*) +| | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | +--- androidx.autofill:autofill:1.0.0 +| | | | \--- androidx.core:core:1.1.0 -> 1.13.1 (*) +| | | +--- androidx.collection:collection:1.0.0 -> 1.4.2 (*) +| | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | +--- androidx.compose.runtime:runtime-saveable:1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-geometry:1.7.0-rc01 +| | | | \--- androidx.compose.ui:ui-geometry-android:1.7.0-rc01 +| | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 +| | | | | \--- androidx.compose.ui:ui-util-android:1.7.0-rc01 +| | | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | +--- androidx.compose.ui:ui:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-geometry:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-test-junit4:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-text:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-tooling:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-tooling-preview:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-test:1.7.0-rc01 (c) +| | | | | \--- androidx.compose.ui:ui-tooling-data:1.7.0-rc01 (c) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | +--- androidx.compose.ui:ui:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-test-junit4:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-text:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-tooling:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-tooling-preview:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-test:1.7.0-rc01 (c) +| | | | \--- androidx.compose.ui:ui-tooling-data:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 +| | | | \--- androidx.compose.ui:ui-graphics-android:1.7.0-rc01 +| | | | +--- androidx.annotation:annotation:1.7.0 -> 1.8.1 (*) +| | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 +| | | | | \--- androidx.compose.ui:ui-unit-android:1.7.0-rc01 +| | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | | | | +--- androidx.collection:collection-ktx:1.2.0 -> 1.4.2 +| | | | | | +--- androidx.collection:collection:1.4.2 (*) +| | | | | | \--- androidx.collection:collection:1.4.2 (c) +| | | | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | | | +--- androidx.compose.ui:ui-geometry:1.7.0-rc01 (*) +| | | | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (*) +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | +--- androidx.compose.ui:ui:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-geometry:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-test-junit4:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-text:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-tooling:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-tooling-preview:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (c) +| | | | | +--- androidx.compose.ui:ui-test:1.7.0-rc01 (c) +| | | | | \--- androidx.compose.ui:ui-tooling-data:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (*) +| | | | +--- androidx.core:core:1.12.0 -> 1.13.1 (*) +| | | | +--- androidx.graphics:graphics-path:1.0.1 +| | | | | +--- androidx.core:core:1.12.0 -> 1.13.1 (*) +| | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | +--- androidx.compose.ui:ui:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-geometry:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-test:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-test-junit4:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-text:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-tooling:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-tooling-data:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-tooling-preview:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (c) +| | | | \--- androidx.compose.ui:ui-util:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-text:1.7.0-rc01 +| | | | \--- androidx.compose.ui:ui-text-android:1.7.0-rc01 +| | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | | +--- androidx.compose.runtime:runtime-saveable:1.6.0 -> 1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (*) +| | | | +--- androidx.core:core:1.7.0 -> 1.13.1 (*) +| | | | +--- androidx.emoji2:emoji2:1.2.0 -> 1.3.0 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) +| | | | +--- androidx.compose.ui:ui:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-geometry:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-test:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-test-junit4:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-tooling:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-tooling-data:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-tooling-preview:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (c) +| | | | \--- androidx.compose.ui:ui-util:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (*) +| | | +--- androidx.core:core:1.12.0 -> 1.13.1 (*) +| | | +--- androidx.customview:customview-poolingcontainer:1.0.0 +| | | | +--- androidx.core:core-ktx:1.5.0 -> 1.13.1 (*) +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.21 -> 2.0.20 (*) +| | | +--- androidx.emoji2:emoji2:1.2.0 -> 1.3.0 (*) +| | | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.3 -> 2.8.4 +| | | | \--- androidx.lifecycle:lifecycle-runtime-compose-android:2.8.4 +| | | | +--- androidx.annotation:annotation:1.8.0 -> 1.8.1 (*) +| | | | +--- androidx.compose.runtime:runtime:1.6.5 -> 1.7.0-rc01 (*) +| | | | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (*) +| | | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 (*) +| | | | +--- androidx.lifecycle:lifecycle-common:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-livedata:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-process:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-service:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (c) +| | | | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) +| | | | \--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-viewmodel:2.6.1 -> 2.8.4 (*) +| | | +--- androidx.profileinstaller:profileinstaller:1.3.1 (*) +| | | +--- androidx.savedstate:savedstate-ktx:1.2.1 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 -> 1.8.1 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) +| | | +--- androidx.compose.ui:ui-geometry:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-test-junit4:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-text:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-tooling:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-tooling-preview:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (c) +| | | +--- androidx.compose.foundation:foundation:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-test:1.7.0-rc01 (c) +| | | \--- androidx.compose.ui:ui-tooling-data:1.7.0-rc01 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel:2.6.1 -> 2.8.4 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | +--- androidx.activity:activity:1.9.1 (c) +| | \--- androidx.activity:activity-ktx:1.9.1 (c) +| +--- io.insert-koin:koin-android:4.0.0-RC2 +| | +--- io.insert-koin:koin-core:4.0.0-RC2 +| | | \--- io.insert-koin:koin-core-jvm:4.0.0-RC2 +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | | +--- co.touchlab:stately-concurrency:2.0.7 +| | | | \--- co.touchlab:stately-concurrency-jvm:2.0.7 +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.23 -> 2.0.20 (*) +| | | | \--- co.touchlab:stately-strict:2.0.7 +| | | | \--- co.touchlab:stately-strict-jvm:2.0.7 +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.23 -> 2.0.20 (*) +| | | \--- co.touchlab:stately-concurrent-collections:2.0.7 +| | | \--- co.touchlab:stately-concurrent-collections-jvm:2.0.7 +| | | +--- co.touchlab:stately-concurrency:2.0.7 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.23 -> 2.0.20 (*) +| | +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 +| | | \--- io.insert-koin:koin-core-viewmodel-jvm:4.0.0-RC2 +| | | +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.0 +| | | | \--- androidx.lifecycle:lifecycle-viewmodel:2.8.0 -> 2.8.4 (*) +| | | +--- org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.0 +| | | | \--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.0 -> 2.8.4 (*) +| | | +--- org.jetbrains.androidx.core:core-bundle:1.0.0 +| | | | \--- org.jetbrains.androidx.core:core-bundle-android:1.0.0 +| | | | +--- androidx.core:core-ktx:1.2.0 -> 1.13.1 (*) +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.23 -> 2.0.20 (*) +| | | +--- org.jetbrains.androidx.savedstate:savedstate:1.2.0 +| | | | \--- androidx.savedstate:savedstate:1.2.1 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | +--- androidx.appcompat:appcompat:1.7.0 (*) +| | +--- androidx.activity:activity-ktx:1.9.1 (*) +| | +--- androidx.fragment:fragment-ktx:1.8.2 +| | | +--- androidx.activity:activity-ktx:1.8.1 -> 1.9.1 (*) +| | | +--- androidx.collection:collection-ktx:1.1.0 -> 1.4.2 (*) +| | | +--- androidx.core:core-ktx:1.2.0 -> 1.13.1 (*) +| | | +--- androidx.fragment:fragment:1.8.2 (*) +| | | +--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.6.1 -> 2.8.4 (*) +| | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1 -> 2.8.4 (*) +| | | +--- androidx.savedstate:savedstate-ktx:1.2.1 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | \--- androidx.fragment:fragment:1.8.2 (c) +| | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (*) +| | +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 +| | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | +--- androidx.lifecycle:lifecycle-common:2.8.4 (*) +| | | +--- androidx.lifecycle:lifecycle-common:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-livedata:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-process:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-service:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (c) +| | | \--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (c) +| | \--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 +| | +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-compose:4.0.0-RC2 +| | | \--- io.insert-koin:koin-compose-jvm:4.0.0-RC2 +| | | +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| | | +--- org.jetbrains.compose.runtime:runtime:1.6.11 +| | | | \--- androidx.compose.runtime:runtime:1.6.7 -> 1.7.0-rc01 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | +--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (*) +| | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 +| | | \--- androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.4 +| | | +--- androidx.annotation:annotation:1.8.0 -> 1.8.1 (*) +| | | +--- androidx.compose.runtime:runtime:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.lifecycle:lifecycle-common:2.8.4 (*) +| | | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (*) +| | | +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | +--- androidx.lifecycle:lifecycle-common:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-common-java8:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-livedata:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-process:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-runtime:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-service:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-viewmodel:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 (c) +| | | +--- androidx.lifecycle:lifecycle-livedata-core:2.8.4 (c) +| | | \--- androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 (c) +| | \--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- com.squareup.wire:wire-runtime:5.0.0 +| | \--- com.squareup.wire:wire-runtime-jvm:5.0.0 +| | +--- com.squareup.okio:okio:3.9.0 (*) +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.0.20 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- org.jetbrains.compose.runtime:runtime:1.6.11 (*) +| +--- org.jetbrains.compose.material3:material3:1.6.11 +| | \--- androidx.compose.material3:material3:1.2.1 +| | \--- androidx.compose.material3:material3-android:1.2.1 +| | +--- androidx.activity:activity-compose:1.5.0 -> 1.9.1 (*) +| | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | +--- androidx.compose.animation:animation-core:1.6.0 -> 1.7.0-rc01 +| | | \--- androidx.compose.animation:animation-core-android:1.7.0-rc01 +| | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-unit:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) +| | | +--- androidx.compose.animation:animation:1.7.0-rc01 (c) +| | | \--- androidx.compose.animation:animation-graphics:1.7.0-rc01 (c) +| | +--- androidx.compose.foundation:foundation:1.6.0 -> 1.7.0-rc01 +| | | \--- androidx.compose.foundation:foundation-android:1.7.0-rc01 +| | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | | +--- androidx.compose.animation:animation:1.7.0-rc01 +| | | | \--- androidx.compose.animation:animation-android:1.7.0-rc01 +| | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | | | +--- androidx.compose.animation:animation-core:1.7.0-rc01 (*) +| | | | +--- androidx.compose.foundation:foundation-layout:1.6.0 -> 1.7.0-rc01 +| | | | | \--- androidx.compose.foundation:foundation-layout-android:1.7.0-rc01 +| | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | | | | +--- androidx.compose.animation:animation-core:1.2.1 -> 1.7.0-rc01 (*) +| | | | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | | | +--- androidx.compose.ui:ui:1.6.0 -> 1.7.0-rc01 (*) +| | | | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (*) +| | | | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (*) +| | | | | +--- androidx.core:core:1.7.0 -> 1.13.1 (*) +| | | | | \--- androidx.compose.foundation:foundation:1.7.0-rc01 (c) +| | | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-geometry:1.6.0 -> 1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (*) +| | | | +--- androidx.compose.animation:animation-core:1.7.0-rc01 (c) +| | | | \--- androidx.compose.animation:animation-graphics:1.7.0-rc01 (c) +| | | +--- androidx.compose.foundation:foundation-layout:1.7.0-rc01 (*) +| | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui:1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-text:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-util:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.core:core:1.13.1 (*) +| | | +--- androidx.emoji2:emoji2:1.3.0 (*) +| | | \--- androidx.compose.foundation:foundation-layout:1.7.0-rc01 (c) +| | +--- androidx.compose.foundation:foundation-layout:1.6.0 -> 1.7.0-rc01 (*) +| | +--- androidx.compose.material:material-icons-core:1.6.0 -> 1.6.8 +| | | \--- androidx.compose.material:material-icons-core-android:1.6.8 +| | | +--- androidx.compose.ui:ui:1.6.8 -> 1.7.0-rc01 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | +--- androidx.compose.material:material:1.6.8 (c) +| | | +--- androidx.compose.material:material-icons-extended:1.6.8 (c) +| | | \--- androidx.compose.material:material-ripple:1.6.8 (c) +| | +--- androidx.compose.material:material-ripple:1.6.0 -> 1.6.8 +| | | \--- androidx.compose.material:material-ripple-android:1.6.8 +| | | +--- androidx.compose.animation:animation:1.6.8 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.foundation:foundation:1.6.8 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-util:1.6.8 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.material:material:1.6.8 (c) +| | | +--- androidx.compose.material:material-icons-core:1.6.8 (c) +| | | \--- androidx.compose.material:material-icons-extended:1.6.8 (c) +| | +--- androidx.compose.runtime:runtime:1.6.0 -> 1.7.0-rc01 (*) +| | +--- androidx.compose.ui:ui-graphics:1.6.0 -> 1.7.0-rc01 (*) +| | +--- androidx.compose.ui:ui-text:1.6.0 -> 1.7.0-rc01 (*) +| | +--- androidx.compose.ui:ui-util:1.6.0 -> 1.7.0-rc01 (*) +| | +--- androidx.lifecycle:lifecycle-common-java8:2.6.1 -> 2.8.4 (*) +| | +--- androidx.lifecycle:lifecycle-runtime:2.6.1 -> 2.8.4 (*) +| | +--- androidx.lifecycle:lifecycle-viewmodel:2.6.1 -> 2.8.4 (*) +| | +--- androidx.savedstate:savedstate-ktx:1.2.1 (*) +| | \--- androidx.compose.material3:material3-window-size-class:1.2.1 (c) +| +--- org.jetbrains.compose.ui:ui:1.6.11 +| | \--- androidx.compose.ui:ui:1.6.7 -> 1.7.0-rc01 (*) +| +--- org.jetbrains.compose.components:components-resources:1.6.11 +| | \--- org.jetbrains.compose.components:components-resources-android:1.6.11 +| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.23 -> 2.0.20 (*) +| | +--- org.jetbrains.compose.runtime:runtime:1.6.11 (*) +| | +--- org.jetbrains.compose.foundation:foundation:1.6.11 +| | | \--- androidx.compose.foundation:foundation:1.6.7 -> 1.7.0-rc01 (*) +| | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0 -> 1.8.1 (*) +| +--- org.jetbrains.compose.components:components-ui-tooling-preview:1.6.11 +| | \--- org.jetbrains.compose.components:components-ui-tooling-preview-android:1.6.11 +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.23 -> 2.0.20 (*) +| +--- org.jetbrains.kotlinx:kotlinx-datetime:0.6.0 +| | \--- org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.6.0 +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.21 -> 2.0.20 (*) +| +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1 +| | \--- org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.1 +| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.0 -> 2.0.20 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.1 +| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1 (c) +| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.1 (c) +| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1 (c) +| | | \--- org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.1 (c) +| | \--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1 +| | \--- org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.1 +| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.0 -> 2.0.20 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.1 (*) +| | \--- org.jetbrains.kotlin:kotlin-stdlib-common:2.0.0 -> 2.0.20 +| | \--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-compose:1.2.0-Beta4 -> 4.0.0-RC2 (*) +| +--- androidx.datastore:datastore-core-okio:1.1.1 (*) +| \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.20 (*) ++--- project :core:data +| +--- project :core:common +| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | +--- io.insert-koin:koin-bom:4.0.0-RC2 +| | | +--- io.insert-koin:koin-core:4.0.0-RC2 (c) +| | | +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (c) +| | | +--- io.insert-koin:koin-android:4.0.0-RC2 (c) +| | | +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (c) +| | | +--- io.insert-koin:koin-compose:4.0.0-RC2 (c) +| | | \--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (c) +| | +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-annotations:1.4.0-RC4 +| | | \--- io.insert-koin:koin-annotations-jvm:1.4.0-RC4 +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.0.20 (*) +| | +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 +| | | +--- androidx.tracing:tracing:1.3.0-alpha02 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | \--- androidx.tracing:tracing:1.3.0-alpha02 (c) +| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1 (*) +| | \--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1 (*) +| +--- project :core:model +| | +--- org.jetbrains.kotlinx:kotlinx-datetime:0.6.0 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| | +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| | +--- com.squareup.retrofit2:converter-gson:2.11.0 +| | | +--- com.squareup.retrofit2:retrofit:2.11.0 +| | | | \--- com.squareup.okhttp3:okhttp:3.14.9 -> 4.12.0 (*) +| | | \--- com.google.code.gson:gson:2.10.1 +| | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1 (*) +| | \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.20 (*) +| +--- project :core:network +| | +--- org.jetbrains.kotlinx:kotlinx-datetime:0.6.0 (*) +| | +--- project :core:common (*) +| | +--- project :core:model (*) +| | +--- project :core:datastore +| | | +--- org.jetbrains.kotlinx:kotlinx-datetime:0.6.0 (*) +| | | +--- androidx.datastore:datastore:1.1.1 (*) +| | | +--- project :core:datastore-proto +| | | | +--- com.google.protobuf:protobuf-kotlin-lite:4.26.0 +| | | | | +--- com.google.protobuf:protobuf-javalite:4.26.0 +| | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.0 -> 2.0.20 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | | | +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| | | | +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| | | | +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| | | | \--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| | | +--- project :core:common (*) +| | | +--- project :core:model (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | | +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| | | +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| | | +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| | | +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| | | +--- com.squareup.retrofit2:converter-gson:2.11.0 (*) +| | | \--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| | +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| | +--- com.squareup.okhttp3:okhttp:4.12.0 (*) +| | +--- com.squareup.okhttp3:logging-interceptor:4.12.0 +| | | +--- com.squareup.okhttp3:okhttp:4.12.0 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21 -> 1.9.20 (*) +| | +--- com.squareup.retrofit2:retrofit:2.11.0 (*) +| | +--- com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0 +| | | +--- com.squareup.retrofit2:retrofit:2.9.0 -> 2.11.0 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.0 -> 1.7.1 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10 -> 1.9.20 (*) +| | +--- com.squareup.retrofit2:adapter-rxjava:2.11.0 +| | | +--- com.squareup.retrofit2:retrofit:2.11.0 (*) +| | | \--- io.reactivex:rxjava:1.3.8 +| | +--- com.squareup.retrofit2:converter-gson:2.11.0 (*) +| | +--- io.reactivex:rxandroid:1.1.0 +| | | \--- io.reactivex:rxjava:1.1.0 -> 1.3.8 +| | +--- io.reactivex:rxjava:1.3.8 +| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.0 -> 2.0.20 (*) +| | +--- io.ktor:ktor-client-core:2.3.4 +| | | \--- io.ktor:ktor-client-core-jvm:2.3.4 +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1 (*) +| | | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1 (*) +| | | +--- org.slf4j:slf4j-api:1.7.36 +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | +--- io.ktor:ktor-http:2.3.4 +| | | | \--- io.ktor:ktor-http-jvm:2.3.4 +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | | +--- org.slf4j:slf4j-api:1.7.36 +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | | +--- io.ktor:ktor-utils:2.3.4 +| | | | | \--- io.ktor:ktor-utils-jvm:2.3.4 +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | | | +--- org.slf4j:slf4j-api:1.7.36 +| | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | | | +--- io.ktor:ktor-io:2.3.4 +| | | | | | \--- io.ktor:ktor-io-jvm:2.3.4 +| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | | | | +--- org.slf4j:slf4j-api:1.7.36 +| | | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | | | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | | +--- io.ktor:ktor-events:2.3.4 +| | | | \--- io.ktor:ktor-events-jvm:2.3.4 +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | | +--- org.slf4j:slf4j-api:1.7.36 +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | | +--- io.ktor:ktor-http:2.3.4 (*) +| | | | +--- io.ktor:ktor-utils:2.3.4 (*) +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | | +--- io.ktor:ktor-websocket-serialization:2.3.4 +| | | | \--- io.ktor:ktor-websocket-serialization-jvm:2.3.4 +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | | +--- org.slf4j:slf4j-api:1.7.36 +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | | +--- io.ktor:ktor-http:2.3.4 (*) +| | | | +--- io.ktor:ktor-serialization:2.3.4 +| | | | | \--- io.ktor:ktor-serialization-jvm:2.3.4 +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | | | +--- org.slf4j:slf4j-api:1.7.36 +| | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | | | +--- io.ktor:ktor-http:2.3.4 (*) +| | | | | +--- io.ktor:ktor-websockets:2.3.4 +| | | | | | \--- io.ktor:ktor-websockets-jvm:2.3.4 +| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | | | | +--- org.slf4j:slf4j-api:1.7.36 +| | | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | | | | +--- io.ktor:ktor-http:2.3.4 (*) +| | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | | | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.7.2 -> 1.8.1 +| | | +--- org.slf4j:slf4j-api:1.7.32 -> 1.7.36 +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.21 -> 2.0.20 (*) +| | +--- io.ktor:ktor-client-android:2.3.4 +| | | \--- io.ktor:ktor-client-android-jvm:2.3.4 +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | +--- org.slf4j:slf4j-api:1.7.36 +| | | +--- io.ktor:ktor-client-core:2.3.4 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | +--- io.ktor:ktor-client-serialization:2.3.4 +| | | \--- io.ktor:ktor-client-serialization-jvm:2.3.4 +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | +--- org.slf4j:slf4j-api:1.7.36 +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | +--- io.ktor:ktor-client-core:2.3.4 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1 -> 1.7.1 (*) +| | | +--- io.ktor:ktor-client-json:2.3.4 +| | | | \--- io.ktor:ktor-client-json-jvm:2.3.4 +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | | +--- org.slf4j:slf4j-api:1.7.36 +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | | +--- io.ktor:ktor-client-core:2.3.4 (*) +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | +--- io.ktor:ktor-client-logging:2.3.4 +| | | \--- io.ktor:ktor-client-logging-jvm:2.3.4 +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | +--- org.slf4j:slf4j-api:1.7.36 +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | +--- io.ktor:ktor-client-core:2.3.4 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | +--- io.ktor:ktor-client-content-negotiation:2.3.4 +| | | \--- io.ktor:ktor-client-content-negotiation-jvm:2.3.4 +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | +--- org.slf4j:slf4j-api:1.7.36 +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | +--- io.ktor:ktor-client-core:2.3.4 (*) +| | | +--- io.ktor:ktor-serialization:2.3.4 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | +--- io.ktor:ktor-client-json:2.3.4 (*) +| | +--- io.ktor:ktor-client-websockets:2.3.4 +| | | \--- io.ktor:ktor-client-websockets-jvm:2.3.4 +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | +--- org.slf4j:slf4j-api:1.7.36 +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | +--- io.ktor:ktor-client-core:2.3.4 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | +--- io.ktor:ktor-serialization-kotlinx-json:2.3.4 +| | | \--- io.ktor:ktor-serialization-kotlinx-json-jvm:2.3.4 +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | +--- org.slf4j:slf4j-api:1.7.36 +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | +--- io.ktor:ktor-http:2.3.4 (*) +| | | +--- io.ktor:ktor-serialization-kotlinx:2.3.4 +| | | | \--- io.ktor:ktor-serialization-kotlinx-jvm:2.3.4 +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | | +--- org.slf4j:slf4j-api:1.7.36 +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | | +--- io.ktor:ktor-http:2.3.4 (*) +| | | | +--- io.ktor:ktor-serialization:2.3.4 (*) +| | | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1 -> 1.7.1 (*) +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1 -> 1.7.1 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | +--- ch.qos.logback:logback-classic:1.2.3 +| | | +--- ch.qos.logback:logback-core:1.2.3 +| | | \--- org.slf4j:slf4j-api:1.7.25 -> 1.7.36 +| | +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1 (*) +| | \--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- com.squareup.retrofit2:retrofit:2.11.0 (*) +| +--- com.squareup.retrofit2:adapter-rxjava:2.11.0 (*) +| +--- com.squareup.retrofit2:converter-gson:2.11.0 (*) +| +--- com.squareup.okhttp3:okhttp:4.12.0 (*) +| +--- com.squareup.okhttp3:logging-interceptor:4.12.0 (*) +| +--- io.reactivex:rxandroid:1.1.0 (*) +| +--- io.reactivex:rxjava:1.3.8 +| +--- org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| \--- org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.20 (*) ++--- project :core:ui +| +--- project :core:designsystem +| | +--- androidx.compose.ui:ui:1.6.8 -> 1.7.0-rc01 (*) +| | +--- androidx.compose.foundation:foundation -> 1.7.0-rc01 (*) +| | +--- androidx.compose.foundation:foundation-layout -> 1.7.0-rc01 (*) +| | +--- androidx.compose.material:material-icons-extended -> 1.6.8 +| | | \--- androidx.compose.material:material-icons-extended-android:1.6.8 +| | | +--- androidx.compose.material:material-icons-core:1.6.8 (*) +| | | +--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.material:material:1.6.8 (c) +| | | +--- androidx.compose.material:material-icons-core:1.6.8 (c) +| | | \--- androidx.compose.material:material-ripple:1.6.8 (c) +| | +--- androidx.compose.material3:material3 -> 1.2.1 (*) +| | +--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (*) +| | +--- androidx.compose.ui:ui-util:1.6.8 -> 1.7.0-rc01 (*) +| | +--- androidx.activity:activity-compose:1.9.1 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| | +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| | +--- androidx.compose:compose-bom:2024.08.00 (*) +| | +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| | \--- project :core:model (*) +| +--- project :core:model (*) +| +--- project :core:common (*) +| +--- androidx.metrics:metrics-performance:1.0.0-beta01 +| | +--- androidx.collection:collection:1.1.0 -> 1.4.2 (*) +| | +--- androidx.core:core:1.5.0 -> 1.13.1 (*) +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| +--- project :core:analytics +| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| | +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| | +--- androidx.compose:compose-bom:2024.08.00 (*) +| | +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| | +--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (*) +| | +--- com.google.firebase:firebase-bom:33.1.2 (*) +| | \--- com.google.firebase:firebase-analytics-ktx -> 22.0.2 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| +--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (*) +| +--- com.google.accompanist:accompanist-pager:0.34.0 +| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 -> 1.8.1 (*) +| | +--- androidx.compose.foundation:foundation:1.6.0 -> 1.7.0-rc01 (*) +| | +--- dev.chrisbanes.snapper:snapper:0.2.2 -> 0.3.0 +| | | +--- androidx.compose.foundation:foundation:1.2.1 -> 1.7.0-rc01 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21 -> 1.9.20 (*) +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.22 -> 2.0.20 (*) +| +--- androidx.browser:browser:1.8.0 +| | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | +--- androidx.collection:collection:1.1.0 -> 1.4.2 (*) +| | +--- androidx.concurrent:concurrent-futures:1.0.0 -> 1.1.0 (*) +| | +--- androidx.core:core:1.1.0 -> 1.13.1 (*) +| | +--- androidx.interpolator:interpolator:1.0.0 (*) +| | \--- com.google.guava:listenablefuture:1.0 -> 9999.0-empty-to-avoid-conflict-with-guava +| +--- io.coil-kt:coil:2.6.0 +| | +--- io.coil-kt:coil-base:2.6.0 +| | | +--- androidx.annotation:annotation:1.7.1 -> 1.8.1 (*) +| | | +--- androidx.appcompat:appcompat-resources:1.6.1 -> 1.7.0 (*) +| | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | | +--- androidx.core:core-ktx:1.12.0 -> 1.13.1 (*) +| | | +--- androidx.exifinterface:exifinterface:1.3.7 +| | | | \--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | +--- androidx.profileinstaller:profileinstaller:1.3.1 (*) +| | | +--- androidx.lifecycle:lifecycle-runtime:2.7.0 -> 2.8.4 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 -> 1.8.1 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.9.22 -> 2.0.20 (*) +| | | +--- com.squareup.okhttp3:okhttp:4.12.0 (*) +| | | \--- com.squareup.okio:okio:3.8.0 -> 3.9.0 (*) +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.22 -> 2.0.20 (*) +| \--- io.coil-kt:coil-compose:2.6.0 +| +--- io.coil-kt:coil-compose-base:2.6.0 +| | +--- androidx.core:core-ktx:1.12.0 -> 1.13.1 (*) +| | +--- com.google.accompanist:accompanist-drawablepainter:0.32.0 +| | | +--- androidx.compose.ui:ui:1.5.0 -> 1.7.0-rc01 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 -> 1.8.1 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0 -> 1.9.20 (*) +| | +--- io.coil-kt:coil-base:2.6.0 (*) +| | +--- androidx.compose.foundation:foundation:1.6.1 -> 1.7.0-rc01 (*) +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.22 -> 2.0.20 (*) +| +--- io.coil-kt:coil:2.6.0 (*) +| \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.22 -> 2.0.20 (*) ++--- project :core:designsystem (*) ++--- project :feature:receipt +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation +| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| | +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| | +--- androidx.compose:compose-bom:2024.08.00 (*) +| | +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| | +--- androidx.navigation:navigation-compose:2.8.0-rc01 +| | | +--- androidx.activity:activity-compose:1.8.0 -> 1.9.1 (*) +| | | +--- androidx.compose.animation:animation:1.7.0-rc01 (*) +| | | +--- androidx.compose.foundation:foundation-layout:1.7.0-rc01 (*) +| | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | +--- androidx.compose.runtime:runtime-saveable:1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui:1.7.0-rc01 (*) +| | | +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2 -> 2.8.4 (*) +| | | +--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 +| | | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 +| | | | | +--- androidx.navigation:navigation-common:2.8.0-rc01 +| | | | | | +--- androidx.annotation:annotation:1.8.1 (*) +| | | | | | +--- androidx.collection:collection-ktx:1.4.2 (*) +| | | | | | +--- androidx.core:core-ktx:1.1.0 -> 1.13.1 (*) +| | | | | | +--- androidx.lifecycle:lifecycle-common:2.6.2 -> 2.8.4 (*) +| | | | | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.6.2 -> 2.8.4 (*) +| | | | | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2 -> 2.8.4 (*) +| | | | | | +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2 -> 2.8.4 (*) +| | | | | | +--- androidx.profileinstaller:profileinstaller:1.3.1 (*) +| | | | | | +--- androidx.savedstate:savedstate-ktx:1.2.1 (*) +| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3 -> 1.7.1 (*) +| | | | | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 (c) +| | | | | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 (c) +| | | | | | +--- androidx.navigation:navigation-fragment:2.8.0-rc01 (c) +| | | | | | +--- androidx.navigation:navigation-fragment-ktx:2.8.0-rc01 (c) +| | | | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 (c) +| | | | | | \--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 (c) +| | | | | +--- androidx.navigation:navigation-common:2.8.0-rc01 (c) +| | | | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 (c) +| | | | | +--- androidx.navigation:navigation-fragment:2.8.0-rc01 (c) +| | | | | +--- androidx.navigation:navigation-fragment-ktx:2.8.0-rc01 (c) +| | | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 (c) +| | | | | \--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 (c) +| | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 +| | | | | +--- androidx.activity:activity-ktx:1.7.1 -> 1.9.1 (*) +| | | | | +--- androidx.annotation:annotation-experimental:1.4.1 (*) +| | | | | +--- androidx.collection:collection:1.4.2 (*) +| | | | | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.6.2 -> 2.8.4 (*) +| | | | | +--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2 -> 2.8.4 (*) +| | | | | +--- androidx.navigation:navigation-common:2.8.0-rc01 (*) +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3 -> 1.7.1 (*) +| | | | | +--- androidx.navigation:navigation-common:2.8.0-rc01 (c) +| | | | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 (c) +| | | | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 (c) +| | | | | +--- androidx.navigation:navigation-fragment:2.8.0-rc01 (c) +| | | | | +--- androidx.navigation:navigation-fragment-ktx:2.8.0-rc01 (c) +| | | | | \--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 (c) +| | | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 (c) +| | | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 (c) +| | | | +--- androidx.navigation:navigation-fragment-ktx:2.8.0-rc01 (c) +| | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 (c) +| | | | +--- androidx.navigation:navigation-fragment:2.8.0-rc01 (c) +| | | | \--- androidx.navigation:navigation-common:2.8.0-rc01 (c) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3 -> 1.7.1 (*) +| | | +--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 (c) +| | | +--- androidx.navigation:navigation-fragment-ktx:2.8.0-rc01 (c) +| | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 (c) +| | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 (c) +| | | +--- androidx.navigation:navigation-fragment:2.8.0-rc01 (c) +| | | \--- androidx.navigation:navigation-common:2.8.0-rc01 (c) +| | +--- androidx.compose.material3:material3 -> 1.2.1 (*) +| | +--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (*) +| | \--- androidx.compose.ui:ui-util:1.6.8 -> 1.7.0-rc01 (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 +| | \--- org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.3.7 +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.21 -> 2.0.20 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 +| | +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| | +--- androidx.navigation:navigation-fragment-ktx:2.7.7 -> 2.8.0-rc01 +| | | +--- androidx.navigation:navigation-fragment:2.8.0-rc01 +| | | | +--- androidx.fragment:fragment-ktx:1.6.2 -> 1.8.2 (*) +| | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 (*) +| | | | +--- androidx.slidingpanelayout:slidingpanelayout:1.2.0 +| | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | | +--- androidx.customview:customview:1.1.0 (*) +| | | | | +--- androidx.core:core:1.1.0 -> 1.13.1 (*) +| | | | | +--- androidx.window:window:1.0.0 -> 1.3.0-rc01 +| | | | | | +--- androidx.annotation:annotation:1.3.0 -> 1.8.1 (*) +| | | | | | +--- androidx.collection:collection:1.1.0 -> 1.4.2 (*) +| | | | | | +--- androidx.core:core:1.8.0 -> 1.13.1 (*) +| | | | | | +--- androidx.window.extensions.core:core:1.0.0 +| | | | | | | +--- androidx.annotation:annotation:1.6.0 -> 1.8.1 (*) +| | | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.20 -> 2.0.20 (*) +| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3 -> 1.8.1 (*) +| | | | | | \--- androidx.window:window-core:1.3.0-rc01 (c) +| | | | | \--- androidx.transition:transition:1.4.1 +| | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | | +--- androidx.core:core:1.1.0 -> 1.13.1 (*) +| | | | | \--- androidx.collection:collection:1.1.0 -> 1.4.2 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | +--- org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.3 -> 1.7.1 (*) +| | | | +--- androidx.navigation:navigation-common:2.8.0-rc01 (c) +| | | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 (c) +| | | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 (c) +| | | | +--- androidx.navigation:navigation-fragment-ktx:2.8.0-rc01 (c) +| | | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 (c) +| | | | \--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 (c) +| | | +--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 (*) +| | | +--- androidx.navigation:navigation-common-ktx:2.8.0-rc01 (c) +| | | +--- androidx.navigation:navigation-compose:2.8.0-rc01 (c) +| | | +--- androidx.navigation:navigation-fragment:2.8.0-rc01 (c) +| | | +--- androidx.navigation:navigation-runtime:2.8.0-rc01 (c) +| | | +--- androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 (c) +| | | \--- androidx.navigation:navigation-common:2.8.0-rc01 (c) +| | \--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| \--- com.squareup.okhttp3:okhttp:4.12.0 (*) ++--- project :feature:profile +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| +--- project :libs:country-code-picker +| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| | +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| | +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| | +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| | +--- androidx.compose:compose-bom:2024.08.00 (*) +| | +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| | +--- androidx.compose.foundation:foundation -> 1.7.0-rc01 (*) +| | +--- androidx.compose.foundation:foundation-layout -> 1.7.0-rc01 (*) +| | +--- androidx.compose.material:material-icons-extended -> 1.6.8 (*) +| | +--- androidx.compose.material3:material3 -> 1.2.1 (*) +| | +--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (*) +| | +--- androidx.compose.ui:ui-util:1.6.8 -> 1.7.0-rc01 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| | \--- io.michaelrocks:libphonenumber-android:8.13.35 +| +--- network.chaintech:qr-kit:2.0.0 +| | \--- network.chaintech:qr-kit-android:2.0.0 +| | +--- androidx.compose.ui:ui-test-junit4:1.6.8 -> 1.7.0-rc01 +| | | \--- androidx.compose.ui:ui-test-junit4-android:1.7.0-rc01 +| | | +--- androidx.activity:activity:1.2.1 -> 1.9.1 (*) +| | | +--- androidx.activity:activity-compose:1.3.0 -> 1.9.1 (*) +| | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | +--- androidx.compose.runtime:runtime-saveable:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-test:1.7.0-rc01 +| | | | \--- androidx.compose.ui:ui-test-android:1.7.0-rc01 +| | | | +--- androidx.activity:activity-compose:1.3.0 -> 1.9.1 (*) +| | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-text:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (*) +| | | | +--- androidx.core:core-ktx:1.12.0 -> 1.13.1 (*) +| | | | +--- androidx.test:monitor:1.6.1 +| | | | | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | | | +--- androidx.test:annotation:1.0.1 +| | | | | | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | | | | \--- androidx.annotation:annotation-experimental:1.1.0 -> 1.4.1 (*) +| | | | | \--- androidx.tracing:tracing:1.0.0 -> 1.3.0-alpha02 (*) +| | | | +--- androidx.test.espresso:espresso-core:3.5.0 +| | | | | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | | | +--- androidx.test:core:1.5.0 +| | | | | | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | | | | +--- androidx.test:monitor:1.6.0 -> 1.6.1 (*) +| | | | | | +--- androidx.test.services:storage:1.4.2 +| | | | | | | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | | | | | +--- androidx.test:monitor:1.6.0 -> 1.6.1 (*) +| | | | | | | +--- com.google.code.findbugs:jsr305:2.0.2 -> 3.0.2 +| | | | | | | \--- androidx.test:annotation:1.0.1 (*) +| | | | | | +--- androidx.lifecycle:lifecycle-common:2.3.1 -> 2.8.4 (*) +| | | | | | +--- androidx.tracing:tracing:1.0.0 -> 1.3.0-alpha02 (*) +| | | | | | +--- com.google.guava:listenablefuture:1.0 -> 9999.0-empty-to-avoid-conflict-with-guava +| | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.7.10 -> 2.0.20 (*) +| | | | | | \--- androidx.concurrent:concurrent-futures:1.1.0 (*) +| | | | | +--- androidx.test:runner:1.5.0 +| | | | | | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | | | | +--- androidx.test:annotation:1.0.1 (*) +| | | | | | +--- androidx.test:monitor:1.6.0 -> 1.6.1 (*) +| | | | | | +--- androidx.test.services:storage:1.4.2 (*) +| | | | | | +--- androidx.tracing:tracing:1.0.0 -> 1.3.0-alpha02 (*) +| | | | | | \--- junit:junit:4.13.2 +| | | | | | \--- org.hamcrest:hamcrest-core:1.3 +| | | | | +--- androidx.test.espresso:espresso-idling-resource:3.5.0 +| | | | | +--- com.squareup:javawriter:2.1.1 +| | | | | +--- javax.inject:javax.inject:1 +| | | | | +--- org.hamcrest:hamcrest-library:1.3 +| | | | | | \--- org.hamcrest:hamcrest-core:1.3 +| | | | | +--- org.hamcrest:hamcrest-integration:1.3 +| | | | | | \--- org.hamcrest:hamcrest-library:1.3 (*) +| | | | | +--- com.google.code.findbugs:jsr305:2.0.2 -> 3.0.2 +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.7.10 -> 2.0.20 (*) +| | | | | \--- androidx.test:annotation:1.0.1 (*) +| | | | +--- androidx.test.espresso:espresso-idling-resource:3.5.0 +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3 -> 1.8.1 +| | | | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.8.1 +| | | | | +--- org.jetbrains:annotations:23.0.0 +| | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1 (*) +| | | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1 (*) +| | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.21 -> 2.0.20 (*) +| | | | +--- androidx.compose.ui:ui:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-geometry:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-test-junit4:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-text:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-tooling:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-tooling-data:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-tooling-preview:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (c) +| | | | \--- androidx.compose.ui:ui-util:1.7.0-rc01 (c) +| | | +--- androidx.lifecycle:lifecycle-common:2.5.1 -> 2.8.4 (*) +| | | +--- androidx.lifecycle:lifecycle-runtime:2.5.1 -> 2.8.4 (*) +| | | +--- androidx.test:core:1.5.0 (*) +| | | +--- androidx.test:monitor:1.6.1 (*) +| | | +--- androidx.test.ext:junit:1.1.5 +| | | | +--- junit:junit:4.13.2 (*) +| | | | +--- androidx.test:core:1.5.0 (*) +| | | | +--- androidx.test:monitor:1.6.1 (*) +| | | | \--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | +--- junit:junit:4.13.2 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3 -> 1.8.1 (*) +| | | +--- androidx.compose.ui:ui:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-geometry:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-test:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-text:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-tooling:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-tooling-preview:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (c) +| | | \--- androidx.compose.ui:ui-tooling-data:1.7.0-rc01 (c) +| | +--- androidx.core:core-ktx:1.13.1 (*) +| | +--- androidx.activity:activity-compose:1.9.1 (*) +| | +--- androidx.appcompat:appcompat:1.7.0 (*) +| | +--- androidx.compose.ui:ui-tooling:1.6.8 -> 1.7.0-rc01 +| | | \--- androidx.compose.ui:ui-tooling-android:1.7.0-rc01 +| | | +--- androidx.activity:activity-compose:1.7.0 -> 1.9.1 (*) +| | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | +--- androidx.compose.animation:animation:1.7.0-rc01 (*) +| | | +--- androidx.compose.material:material:1.0.0 -> 1.6.8 +| | | | \--- androidx.compose.material:material-android:1.6.8 +| | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | +--- androidx.compose.animation:animation:1.6.8 -> 1.7.0-rc01 (*) +| | | | +--- androidx.compose.animation:animation-core:1.6.8 -> 1.7.0-rc01 (*) +| | | | +--- androidx.compose.foundation:foundation:1.6.8 -> 1.7.0-rc01 (*) +| | | | +--- androidx.compose.foundation:foundation-layout:1.6.8 -> 1.7.0-rc01 (*) +| | | | +--- androidx.compose.material:material-icons-core:1.6.8 (*) +| | | | +--- androidx.compose.material:material-ripple:1.6.8 (*) +| | | | +--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui:1.6.8 -> 1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-text:1.6.8 -> 1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui-util:1.6.8 -> 1.7.0-rc01 (*) +| | | | +--- androidx.lifecycle:lifecycle-runtime:2.6.1 -> 2.8.4 (*) +| | | | +--- androidx.lifecycle:lifecycle-viewmodel:2.6.1 -> 2.8.4 (*) +| | | | +--- androidx.savedstate:savedstate:1.2.1 (*) +| | | | +--- androidx.compose.material:material-icons-core:1.6.8 (c) +| | | | +--- androidx.compose.material:material-icons-extended:1.6.8 (c) +| | | | \--- androidx.compose.material:material-ripple:1.6.8 (c) +| | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui:1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-tooling-data:1.7.0-rc01 +| | | | \--- androidx.compose.ui:ui-tooling-data-android:1.7.0-rc01 +| | | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | | +--- androidx.compose.ui:ui:1.7.0-rc01 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | | +--- androidx.compose.ui:ui:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-geometry:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-test:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-test-junit4:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-text:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-tooling:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-tooling-preview:1.7.0-rc01 (c) +| | | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (c) +| | | | \--- androidx.compose.ui:ui-util:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-tooling-preview:1.7.0-rc01 (*) +| | | +--- androidx.lifecycle:lifecycle-common:2.6.1 -> 2.8.4 (*) +| | | +--- androidx.savedstate:savedstate-ktx:1.2.1 (*) +| | | +--- androidx.compose.ui:ui:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-geometry:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-graphics:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-test:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-test-junit4:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-text:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-tooling-data:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-tooling-preview:1.7.0-rc01 (c) +| | | +--- androidx.compose.ui:ui-unit:1.7.0-rc01 (c) +| | | \--- androidx.compose.ui:ui-util:1.7.0-rc01 (c) +| | +--- androidx.camera:camera-core:1.3.4 +| | | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | +--- androidx.annotation:annotation-experimental:1.1.0 -> 1.4.1 (*) +| | | +--- androidx.concurrent:concurrent-futures:1.0.0 -> 1.1.0 (*) +| | | +--- androidx.core:core:1.1.0 -> 1.13.1 (*) +| | | +--- androidx.exifinterface:exifinterface:1.3.2 -> 1.3.7 (*) +| | | +--- androidx.lifecycle:lifecycle-common:2.1.0 -> 2.8.4 (*) +| | | +--- androidx.lifecycle:lifecycle-livedata:2.1.0 -> 2.8.4 (*) +| | | +--- com.google.auto.value:auto-value-annotations:1.6.3 +| | | +--- com.google.guava:listenablefuture:1.0 -> 9999.0-empty-to-avoid-conflict-with-guava +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | | +--- androidx.camera:camera-camera2:1.3.4 (c) +| | | +--- androidx.camera:camera-lifecycle:1.3.4 (c) +| | | +--- androidx.camera:camera-video:1.3.4 (c) +| | | \--- androidx.camera:camera-view:1.3.4 (c) +| | +--- androidx.camera:camera-camera2:1.3.4 +| | | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | +--- androidx.camera:camera-core:1.3.4 (*) +| | | +--- androidx.concurrent:concurrent-futures:1.0.0 -> 1.1.0 (*) +| | | +--- androidx.core:core:1.1.0 -> 1.13.1 (*) +| | | +--- com.google.auto.value:auto-value-annotations:1.6.3 +| | | +--- com.google.guava:listenablefuture:1.0 -> 9999.0-empty-to-avoid-conflict-with-guava +| | | +--- androidx.camera:camera-core:1.3.4 (c) +| | | +--- androidx.camera:camera-lifecycle:1.3.4 (c) +| | | +--- androidx.camera:camera-video:1.3.4 (c) +| | | \--- androidx.camera:camera-view:1.3.4 (c) +| | +--- androidx.camera:camera-lifecycle:1.3.4 +| | | +--- androidx.camera:camera-core:1.3.4 (*) +| | | +--- androidx.concurrent:concurrent-futures:1.0.0 -> 1.1.0 (*) +| | | +--- androidx.core:core:1.1.0 -> 1.13.1 (*) +| | | +--- androidx.lifecycle:lifecycle-common:2.1.0 -> 2.8.4 (*) +| | | +--- com.google.auto.value:auto-value-annotations:1.6.3 +| | | +--- com.google.guava:listenablefuture:1.0 -> 9999.0-empty-to-avoid-conflict-with-guava +| | | +--- androidx.camera:camera-core:1.3.4 (c) +| | | +--- androidx.camera:camera-video:1.3.4 (c) +| | | +--- androidx.camera:camera-view:1.3.4 (c) +| | | \--- androidx.camera:camera-camera2:1.3.4 (c) +| | +--- androidx.camera:camera-view:1.3.4 +| | | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | +--- androidx.annotation:annotation-experimental:1.3.1 -> 1.4.1 (*) +| | | +--- androidx.appcompat:appcompat:1.1.0 -> 1.7.0 (*) +| | | +--- androidx.camera:camera-core:1.3.4 (*) +| | | +--- androidx.camera:camera-lifecycle:1.3.4 (*) +| | | +--- androidx.camera:camera-video:1.3.4 +| | | | +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| | | | +--- androidx.camera:camera-core:1.3.4 (*) +| | | | +--- androidx.concurrent:concurrent-futures:1.0.0 -> 1.1.0 (*) +| | | | +--- androidx.core:core:1.1.0 -> 1.13.1 (*) +| | | | +--- com.google.auto.value:auto-value-annotations:1.6.3 +| | | | +--- androidx.camera:camera-camera2:1.3.4 (c) +| | | | +--- androidx.camera:camera-core:1.3.4 (c) +| | | | +--- androidx.camera:camera-lifecycle:1.3.4 (c) +| | | | \--- androidx.camera:camera-view:1.3.4 (c) +| | | +--- androidx.concurrent:concurrent-futures:1.0.0 -> 1.1.0 (*) +| | | +--- androidx.core:core:1.3.2 -> 1.13.1 (*) +| | | +--- androidx.lifecycle:lifecycle-common:2.0.0 -> 2.8.4 (*) +| | | +--- com.google.auto.value:auto-value-annotations:1.6.3 +| | | +--- com.google.guava:listenablefuture:1.0 -> 9999.0-empty-to-avoid-conflict-with-guava +| | | +--- androidx.camera:camera-core:1.3.4 (c) +| | | +--- androidx.camera:camera-lifecycle:1.3.4 (c) +| | | +--- androidx.camera:camera-video:1.3.4 (c) +| | | \--- androidx.camera:camera-camera2:1.3.4 (c) +| | +--- com.google.mlkit:barcode-scanning:17.2.0 +| | | +--- com.google.android.gms:play-services-basement:18.1.0 -> 18.4.0 (*) +| | | +--- com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.0 +| | | | +--- com.google.android.datatransport:transport-api:2.2.1 -> 3.2.0 (*) +| | | | +--- com.google.android.datatransport:transport-backend-cct:2.3.3 -> 3.3.0 (*) +| | | | +--- com.google.android.datatransport:transport-runtime:2.2.6 -> 3.3.0 (*) +| | | | +--- com.google.android.gms:play-services-base:18.1.0 -> 18.3.0 (*) +| | | | +--- com.google.android.gms:play-services-basement:18.1.0 -> 18.4.0 (*) +| | | | +--- com.google.android.gms:play-services-tasks:18.0.2 -> 18.2.0 (*) +| | | | +--- com.google.android.odml:image:1.0.0-beta1 +| | | | +--- com.google.firebase:firebase-components:16.1.0 -> 18.0.0 (*) +| | | | +--- com.google.firebase:firebase-encoders:16.1.0 -> 17.0.0 (*) +| | | | +--- com.google.firebase:firebase-encoders-json:17.1.0 -> 18.0.1 (*) +| | | | +--- com.google.mlkit:barcode-scanning-common:17.0.0 +| | | | | +--- com.google.android.gms:play-services-basement:18.0.0 -> 18.4.0 (*) +| | | | | \--- com.google.mlkit:vision-common:17.0.0 -> 17.3.0 +| | | | | +--- androidx.exifinterface:exifinterface:1.0.0 -> 1.3.7 (*) +| | | | | +--- com.google.android.datatransport:transport-api:2.2.1 -> 3.2.0 (*) +| | | | | +--- com.google.android.datatransport:transport-backend-cct:2.3.3 -> 3.3.0 (*) +| | | | | +--- com.google.android.datatransport:transport-runtime:2.2.6 -> 3.3.0 (*) +| | | | | +--- com.google.android.gms:play-services-base:18.1.0 -> 18.3.0 (*) +| | | | | +--- com.google.android.gms:play-services-basement:18.1.0 -> 18.4.0 (*) +| | | | | +--- com.google.android.gms:play-services-tasks:18.0.2 -> 18.2.0 (*) +| | | | | +--- com.google.android.odml:image:1.0.0-beta1 +| | | | | +--- com.google.firebase:firebase-components:16.1.0 -> 18.0.0 (*) +| | | | | +--- com.google.firebase:firebase-encoders:16.1.0 -> 17.0.0 (*) +| | | | | +--- com.google.firebase:firebase-encoders-json:17.1.0 -> 18.0.1 (*) +| | | | | \--- com.google.mlkit:common:18.6.0 -> 18.9.0 +| | | | | +--- androidx.core:core:1.0.0 -> 1.13.1 (*) +| | | | | +--- com.google.android.datatransport:transport-api:2.2.1 -> 3.2.0 (*) +| | | | | +--- com.google.android.datatransport:transport-backend-cct:2.3.3 -> 3.3.0 (*) +| | | | | +--- com.google.android.datatransport:transport-runtime:2.2.6 -> 3.3.0 (*) +| | | | | +--- com.google.android.gms:play-services-base:18.1.0 -> 18.3.0 (*) +| | | | | +--- com.google.android.gms:play-services-basement:18.1.0 -> 18.4.0 (*) +| | | | | +--- com.google.android.gms:play-services-tasks:18.0.2 -> 18.2.0 (*) +| | | | | +--- com.google.firebase:firebase-components:16.1.0 -> 18.0.0 (*) +| | | | | +--- com.google.firebase:firebase-encoders:16.1.0 -> 17.0.0 (*) +| | | | | \--- com.google.firebase:firebase-encoders-json:17.1.0 -> 18.0.1 (*) +| | | | +--- com.google.mlkit:common:18.9.0 (*) +| | | | +--- com.google.mlkit:vision-common:17.3.0 (*) +| | | | \--- com.google.mlkit:vision-interfaces:16.2.0 +| | | | +--- com.google.android.gms:play-services-basement:18.1.0 -> 18.4.0 (*) +| | | | \--- com.google.android.gms:play-services-tasks:18.0.2 -> 18.2.0 (*) +| | | +--- com.google.mlkit:barcode-scanning-common:17.0.0 (*) +| | | +--- com.google.mlkit:common:18.9.0 (*) +| | | \--- com.google.mlkit:vision-common:17.3.0 (*) +| | +--- com.google.accompanist:accompanist-permissions:0.34.0 +| | | +--- androidx.activity:activity-compose:1.7.2 -> 1.9.1 (*) +| | | +--- androidx.compose.foundation:foundation:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 -> 1.8.1 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.22 -> 2.0.20 (*) +| | +--- com.google.zxing:core:3.5.2 -> 3.5.3 +| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.0 -> 2.0.20 (*) +| | +--- org.jetbrains.compose.runtime:runtime:1.6.11 (*) +| | +--- org.jetbrains.compose.material3:material3:1.6.11 (*) +| | +--- org.jetbrains.compose.material:material-icons-extended:1.6.11 +| | | \--- androidx.compose.material:material-icons-extended:1.6.7 -> 1.6.8 (*) +| | +--- org.jetbrains.compose.components:components-resources:1.6.11 (*) +| | +--- io.github.qdsfdhvh:image-loader:1.6.4 +| | | \--- io.github.qdsfdhvh:image-loader-android:1.6.4 +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20 -> 1.9.20 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.2 -> 1.8.1 (*) +| | | +--- androidx.core:core-ktx:1.10.1 -> 1.13.1 (*) +| | | +--- androidx.appcompat:appcompat-resources:1.6.1 -> 1.7.0 (*) +| | | +--- androidx.exifinterface:exifinterface:1.3.6 -> 1.3.7 (*) +| | | +--- com.caverock:androidsvg-aar:1.4 +| | | +--- org.jetbrains.compose.ui:ui:1.4.3 -> 1.6.11 (*) +| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2 -> 1.8.1 (*) +| | | +--- com.squareup.okio:okio:3.4.0 -> 3.9.0 (*) +| | | +--- io.ktor:ktor-client-core:2.3.3 -> 2.3.4 (*) +| | | +--- com.eygraber:uri-kmp:0.0.12 +| | | | \--- com.eygraber:uri-kmp-android:0.0.12 +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | +--- io.ktor:ktor-client-okhttp:2.3.3 +| | | | \--- io.ktor:ktor-client-okhttp-jvm:2.3.3 +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.22 -> 1.9.20 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 -> 1.9.20 (*) +| | | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.1 -> 1.8.1 (*) +| | | | +--- org.slf4j:slf4j-api:1.7.36 +| | | | +--- io.ktor:ktor-client-core:2.3.3 -> 2.3.4 (*) +| | | | +--- com.squareup.okhttp3:okhttp:4.11.0 -> 4.12.0 (*) +| | | | +--- com.squareup.okio:okio:3.4.0 -> 3.9.0 (*) +| | | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.8.1 (*) +| | | \--- androidx.collection:collection:1.3.0-alpha04 -> 1.4.2 (*) +| | \--- network.chaintech:cmp-image-pick-n-crop:1.0.1 +| | \--- network.chaintech:cmp-image-pick-n-crop-android:1.0.1 +| | +--- androidx.compose.ui:ui-test-junit4:1.6.8 -> 1.7.0-rc01 (*) +| | +--- org.jetbrains.compose.ui:ui-tooling:1.6.11 +| | | \--- androidx.compose.ui:ui-tooling:1.6.7 -> 1.7.0-rc01 (*) +| | +--- androidx.activity:activity-compose:1.9.0 -> 1.9.1 (*) +| | +--- com.google.accompanist:accompanist-permissions:0.34.0 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.0 -> 2.0.20 (*) +| | +--- org.jetbrains.compose.runtime:runtime:1.6.11 (*) +| | +--- org.jetbrains.compose.foundation:foundation:1.6.11 (*) +| | +--- org.jetbrains.compose.material3:material3:1.6.11 (*) +| | +--- org.jetbrains.compose.components:components-resources:1.6.11 (*) +| | \--- org.jetbrains.compose.components:components-ui-tooling-preview:1.6.11 (*) +| +--- com.squareup.okhttp3:okhttp:4.12.0 (*) +| \--- io.coil-kt:coil-compose:2.6.0 (*) ++--- project :feature:auth +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| +--- project :libs:country-code-picker (*) +| +--- androidx.credentials:credentials:1.2.2 -> 1.3.0-beta01 +| | +--- androidx.annotation:annotation:1.5.0 -> 1.8.1 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) +| | \--- androidx.credentials:credentials-play-services-auth:1.3.0-beta01 (c) +| +--- androidx.credentials:credentials-play-services-auth:1.2.2 -> 1.3.0-beta01 +| | +--- androidx.credentials:credentials:1.3.0-beta01 (*) +| | +--- com.google.android.gms:play-services-auth:21.1.1 -> 21.2.0 +| | | +--- androidx.fragment:fragment:1.5.7 -> 1.8.2 (*) +| | | +--- androidx.loader:loader:1.1.0 (*) +| | | +--- com.google.android.gms:play-services-auth-api-phone:18.0.2 +| | | | +--- com.google.android.gms:play-services-base:18.0.1 -> 18.3.0 (*) +| | | | +--- com.google.android.gms:play-services-basement:18.0.2 -> 18.4.0 (*) +| | | | \--- com.google.android.gms:play-services-tasks:18.0.1 -> 18.2.0 (*) +| | | +--- com.google.android.gms:play-services-auth-base:18.0.10 +| | | | +--- androidx.collection:collection:1.0.0 -> 1.4.2 (*) +| | | | +--- com.google.android.gms:play-services-base:18.0.1 -> 18.3.0 (*) +| | | | +--- com.google.android.gms:play-services-basement:18.2.0 -> 18.4.0 (*) +| | | | \--- com.google.android.gms:play-services-tasks:18.0.1 -> 18.2.0 (*) +| | | +--- com.google.android.gms:play-services-base:18.3.0 (*) +| | | +--- com.google.android.gms:play-services-basement:18.3.0 -> 18.4.0 (*) +| | | +--- com.google.android.gms:play-services-fido:20.0.1 -> 21.0.0 +| | | | +--- com.google.android.gms:play-services-base:18.3.0 (*) +| | | | +--- com.google.android.gms:play-services-basement:18.3.0 -> 18.4.0 (*) +| | | | +--- com.google.android.gms:play-services-tasks:18.1.0 -> 18.2.0 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0 -> 1.9.20 (*) +| | | | \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3 -> 1.8.1 (*) +| | | \--- com.google.android.gms:play-services-tasks:18.1.0 -> 18.2.0 (*) +| | +--- com.google.android.gms:play-services-fido:21.0.0 (*) +| | +--- com.google.android.libraries.identity.googleid:googleid:1.1.0 -> 1.1.1 +| | | +--- androidx.credentials:credentials:1.3.0-beta01 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.0 -> 2.0.20 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0 -> 1.9.20 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | \--- androidx.credentials:credentials:1.3.0-beta01 (c) +| +--- com.google.android.libraries.identity.googleid:googleid:1.1.1 (*) +| \--- com.google.android.gms:play-services-auth:21.2.0 (*) ++--- project :feature:make-transfer +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| \--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) ++--- project :feature:faq +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| \--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) ++--- project :feature:editpassword +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| \--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) ++--- project :feature:notification +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| \--- project :libs:pullrefresh +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| +--- androidx.compose.animation:animation -> 1.7.0-rc01 (*) +| +--- androidx.compose.material3:material3 -> 1.2.1 (*) +| +--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (*) +| \--- androidx.compose.ui:ui-util:1.6.8 -> 1.7.0-rc01 (*) ++--- project :feature:request-money +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| +--- com.google.zxing:core:3.5.3 +| \--- io.coil-kt:coil-compose:2.6.0 (*) ++--- project :feature:upi-setup +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| \--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) ++--- project :feature:settings +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| \--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) ++--- project :feature:savedcards +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| \--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) ++--- project :feature:qr +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| +--- com.google.zxing:core:3.5.3 +| +--- androidx.camera:camera-view:1.3.4 (*) +| +--- androidx.camera:camera-lifecycle:1.3.4 (*) +| \--- com.google.guava:guava:27.0.1-android -> 31.1-android (*) ++--- project :feature:invoices +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| \--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) ++--- project :feature:merchants +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| \--- project :libs:pullrefresh (*) ++--- project :feature:history +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| \--- project :libs:pullrefresh (*) ++--- project :feature:kyc +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| +--- project :libs:country-code-picker (*) +| +--- project :libs:pullrefresh (*) +| +--- com.maxkeppeler.sheets-compose-dialogs:core:1.3.0 +| | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 -> 1.9.20 (*) +| | +--- androidx.core:core-ktx:1.9.0 -> 1.13.1 (*) +| | +--- androidx.compose:compose-bom:2024.02.00 -> 2024.08.00 (*) +| | +--- androidx.compose.ui:ui -> 1.7.0-rc01 (*) +| | +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| | +--- androidx.compose.animation:animation -> 1.7.0-rc01 (*) +| | +--- androidx.compose.animation:animation-graphics -> 1.7.0-rc01 +| | | \--- androidx.compose.animation:animation-graphics-android:1.7.0-rc01 +| | | +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| | | +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| | | +--- androidx.collection:collection:1.4.0 -> 1.4.2 (*) +| | | +--- androidx.compose.animation:animation:1.7.0-rc01 (*) +| | | +--- androidx.compose.foundation:foundation-layout:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.runtime:runtime:1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-geometry:1.6.0 -> 1.7.0-rc01 (*) +| | | +--- androidx.compose.ui:ui-util:1.7.0-rc01 (*) +| | | +--- androidx.core:core-ktx:1.5.0 -> 1.13.1 (*) +| | | +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| | | +--- androidx.compose.animation:animation:1.7.0-rc01 (c) +| | | \--- androidx.compose.animation:animation-core:1.7.0-rc01 (c) +| | +--- androidx.compose.runtime:runtime -> 1.7.0-rc01 (*) +| | \--- androidx.compose.material3:material3 -> 1.2.1 (*) +| +--- com.maxkeppeler.sheets-compose-dialogs:calendar:1.3.0 +| | +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10 -> 1.9.20 (*) +| | +--- androidx.core:core-ktx:1.9.0 -> 1.13.1 (*) +| | +--- androidx.compose:compose-bom:2024.02.00 -> 2024.08.00 (*) +| | +--- androidx.compose.ui:ui -> 1.7.0-rc01 (*) +| | +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| | +--- androidx.compose.animation:animation -> 1.7.0-rc01 (*) +| | +--- androidx.compose.animation:animation-graphics -> 1.7.0-rc01 (*) +| | +--- androidx.compose.runtime:runtime -> 1.7.0-rc01 (*) +| | +--- androidx.compose.material3:material3 -> 1.2.1 (*) +| | +--- dev.chrisbanes.snapper:snapper:0.3.0 (*) +| | \--- com.maxkeppeler.sheets-compose-dialogs:core:1.3.0 (*) +| \--- com.squareup.okhttp3:okhttp:4.12.0 (*) ++--- project :feature:home +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| \--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) ++--- project :feature:accounts +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| +--- project :libs:pullrefresh (*) +| \--- com.google.android.gms:play-services-auth:21.2.0 (*) ++--- project :feature:finance +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| \--- com.google.accompanist:accompanist-pager:0.34.0 (*) ++--- project :feature:payments +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| \--- com.google.accompanist:accompanist-pager:0.34.0 (*) ++--- project :feature:send-money +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| +--- project :libs:country-code-picker (*) +| \--- com.google.android.gms:play-services-code-scanner:16.1.0 +| +--- androidx.activity:activity:1.3.1 -> 1.9.1 (*) +| +--- com.google.android.datatransport:transport-api:2.2.1 -> 3.2.0 (*) +| +--- com.google.android.datatransport:transport-backend-cct:2.3.3 -> 3.3.0 (*) +| +--- com.google.android.datatransport:transport-runtime:2.2.6 -> 3.3.0 (*) +| +--- com.google.android.gms:play-services-base:18.1.0 -> 18.3.0 (*) +| +--- com.google.android.gms:play-services-basement:18.1.0 -> 18.4.0 (*) +| +--- com.google.android.gms:play-services-tasks:18.0.2 -> 18.2.0 (*) +| +--- com.google.firebase:firebase-components:16.1.0 -> 18.0.0 (*) +| +--- com.google.firebase:firebase-encoders:16.1.0 -> 17.0.0 (*) +| +--- com.google.firebase:firebase-encoders-json:17.1.0 -> 18.0.1 (*) +| +--- com.google.mlkit:barcode-scanning-common:17.0.0 (*) +| \--- com.google.mlkit:common:18.9.0 (*) ++--- project :feature:standing-instruction +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| \--- com.google.android.gms:play-services-code-scanner:16.1.0 (*) ++--- project :feature:search +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- project :core:ui (*) +| +--- project :core:designsystem (*) +| +--- project :core:data (*) +| +--- project :libs:material3-navigation (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| \--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) ++--- project :libs:mifos-passcode +| +--- org.jetbrains.kotlin:kotlin-stdlib:2.0.20 (*) +| +--- io.insert-koin:koin-bom:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-core:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-annotations:1.4.0-RC4 (*) +| +--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) +| +--- androidx.compose:compose-bom:2024.08.00 (*) +| +--- androidx.compose.ui:ui-tooling-preview -> 1.7.0-rc01 (*) +| +--- androidx.core:core-ktx:1.13.1 (*) +| +--- androidx.compose.foundation:foundation -> 1.7.0-rc01 (*) +| +--- androidx.compose.foundation:foundation-layout -> 1.7.0-rc01 (*) +| +--- androidx.compose.material:material-icons-extended -> 1.6.8 (*) +| +--- androidx.compose.material3:material3 -> 1.2.1 (*) +| +--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (*) +| +--- androidx.compose.ui:ui-util:1.6.8 -> 1.7.0-rc01 (*) +| +--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) +| +--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) +| +--- io.insert-koin:koin-android:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-navigation:4.0.0-RC2 (*) +| +--- io.insert-koin:koin-androidx-compose:4.0.0-RC2 (*) +| \--- io.insert-koin:koin-core-viewmodel:4.0.0-RC2 (*) ++--- project :libs:material3-navigation (*) ++--- androidx.core:core-ktx:1.13.1 (*) ++--- androidx.appcompat:appcompat:1.7.0 (*) ++--- androidx.activity:activity-compose:1.9.1 (*) ++--- androidx.activity:activity-ktx:1.9.1 (*) ++--- androidx.core:core-splashscreen:1.0.1 +| +--- androidx.annotation:annotation:1.2.0 -> 1.8.1 (*) +| \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.21 -> 2.0.20 (*) ++--- androidx.compose.material3.adaptive:adaptive:1.0.0-rc01 +| \--- androidx.compose.material3.adaptive:adaptive-android:1.0.0-rc01 +| +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| +--- androidx.compose.foundation:foundation:1.6.5 -> 1.7.0-rc01 (*) +| +--- androidx.compose.ui:ui-geometry:1.6.5 -> 1.7.0-rc01 (*) +| +--- androidx.window:window:1.3.0-rc01 (*) +| +--- androidx.window:window-core:1.3.0-rc01 +| | \--- androidx.window:window-core-android:1.3.0-rc01 +| | +--- androidx.annotation:annotation:1.7.0 -> 1.8.1 (*) +| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| | \--- androidx.window:window:1.3.0-rc01 (c) +| +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| +--- androidx.compose.material3.adaptive:adaptive-layout:1.0.0-rc01 (c) +| \--- androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-rc01 (c) ++--- androidx.compose.material3.adaptive:adaptive-layout:1.0.0-rc01 +| \--- androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0-rc01 +| +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| +--- androidx.compose.animation:animation:1.7.0-rc01 (*) +| +--- androidx.compose.animation:animation-core:1.7.0-rc01 (*) +| +--- androidx.compose.foundation:foundation:1.6.5 -> 1.7.0-rc01 (*) +| +--- androidx.compose.foundation:foundation-layout:1.6.5 -> 1.7.0-rc01 (*) +| +--- androidx.compose.material3.adaptive:adaptive:1.0.0-rc01 (*) +| +--- androidx.compose.ui:ui:1.7.0-rc01 (*) +| +--- androidx.compose.ui:ui-geometry:1.6.5 -> 1.7.0-rc01 (*) +| +--- androidx.compose.ui:ui-util:1.6.5 -> 1.7.0-rc01 (*) +| +--- androidx.window:window-core:1.3.0-rc01 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| +--- androidx.compose.material3.adaptive:adaptive:1.0.0-rc01 (c) +| \--- androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-rc01 (c) ++--- androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-rc01 +| \--- androidx.compose.material3.adaptive:adaptive-navigation-android:1.0.0-rc01 +| +--- androidx.activity:activity-compose:1.8.2 -> 1.9.1 (*) +| +--- androidx.annotation:annotation:1.1.0 -> 1.8.1 (*) +| +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| +--- androidx.compose.foundation:foundation:1.6.5 -> 1.7.0-rc01 (*) +| +--- androidx.compose.material3.adaptive:adaptive-layout:1.0.0-rc01 (*) +| +--- androidx.compose.ui:ui-util:1.6.5 -> 1.7.0-rc01 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.8.22 -> 2.0.20 (*) +| +--- androidx.compose.material3.adaptive:adaptive:1.0.0-rc01 (c) +| \--- androidx.compose.material3.adaptive:adaptive-layout:1.0.0-rc01 (c) ++--- androidx.compose.material3:material3-window-size-class -> 1.2.1 +| \--- androidx.compose.material3:material3-window-size-class-android:1.2.1 +| +--- androidx.annotation:annotation-experimental:1.4.0 -> 1.4.1 (*) +| +--- androidx.compose.runtime:runtime:1.6.0 -> 1.7.0-rc01 (*) +| +--- androidx.compose.ui:ui:1.6.0 -> 1.7.0-rc01 (*) +| +--- androidx.compose.ui:ui-unit:1.6.0 -> 1.7.0-rc01 (*) +| +--- androidx.compose.ui:ui-util:1.6.0 -> 1.7.0-rc01 (*) +| +--- androidx.window:window:1.0.0 -> 1.3.0-rc01 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| \--- androidx.compose.material3:material3:1.2.1 (c) ++--- androidx.compose.runtime:runtime-tracing:1.0.0-beta01 +| +--- androidx.annotation:annotation:1.3.0 -> 1.8.1 (*) +| +--- androidx.compose.runtime:runtime:1.3.3 -> 1.7.0-rc01 (*) +| +--- androidx.startup:startup-runtime:1.1.1 (*) +| +--- androidx.tracing:tracing-perfetto:1.0.0 +| | +--- androidx.annotation:annotation:1.3.0 -> 1.8.1 (*) +| | +--- androidx.startup:startup-runtime:1.1.1 (*) +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) +| \--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.0.20 (*) ++--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1 (*) ++--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1 (*) ++--- androidx.lifecycle:lifecycle-runtime-compose:2.8.4 (*) ++--- androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 (*) ++--- androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 (*) ++--- androidx.lifecycle:lifecycle-extensions:2.2.0 +| +--- androidx.lifecycle:lifecycle-runtime:2.2.0 -> 2.8.4 (*) +| +--- androidx.arch.core:core-common:2.1.0 -> 2.2.0 (*) +| +--- androidx.arch.core:core-runtime:2.1.0 -> 2.2.0 (*) +| +--- androidx.fragment:fragment:1.2.0 -> 1.8.2 (*) +| +--- androidx.lifecycle:lifecycle-common:2.2.0 -> 2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-livedata:2.2.0 -> 2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-process:2.2.0 -> 2.8.4 (*) +| +--- androidx.lifecycle:lifecycle-service:2.2.0 -> 2.8.4 (*) +| \--- androidx.lifecycle:lifecycle-viewmodel:2.2.0 -> 2.8.4 (*) ++--- androidx.navigation:navigation-compose:2.8.0-rc01 (*) ++--- androidx.profileinstaller:profileinstaller:1.3.1 (*) ++--- androidx.tracing:tracing-ktx:1.3.0-alpha02 (*) ++--- io.insert-koin:koin-android:4.0.0-RC2 (*) ++--- io.ktor:ktor-client-core:2.3.4 (*) +\--- androidx.compose.runtime:runtime:1.6.8 -> 1.7.0-rc01 (*) diff --git a/mifospay/dependencies/prodReleaseRuntimeClasspath.txt b/mifospay/dependencies/prodReleaseRuntimeClasspath.txt new file mode 100644 index 000000000..8567dba4d --- /dev/null +++ b/mifospay/dependencies/prodReleaseRuntimeClasspath.txt @@ -0,0 +1,408 @@ +:core:analytics +:core:common +:core:data +:core:datastore +:core:datastore-proto +:core:designsystem +:core:model +:core:network +:core:ui +:feature:accounts +:feature:auth +:feature:editpassword +:feature:faq +:feature:finance +:feature:history +:feature:home +:feature:invoices +:feature:kyc +:feature:make-transfer +:feature:merchants +:feature:notification +:feature:payments +:feature:profile +:feature:qr +:feature:receipt +:feature:request-money +:feature:savedcards +:feature:search +:feature:send-money +:feature:settings +:feature:standing-instruction +:feature:upi-setup +:libs:country-code-picker +:libs:material3-navigation +:libs:mifos-passcode +:libs:pullrefresh +:shared +androidx.activity:activity-compose:1.9.1 +androidx.activity:activity-ktx:1.9.1 +androidx.activity:activity:1.9.1 +androidx.annotation:annotation-experimental:1.4.1 +androidx.annotation:annotation-jvm:1.8.1 +androidx.annotation:annotation:1.8.1 +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.autofill:autofill:1.0.0 +androidx.browser:browser:1.8.0 +androidx.camera:camera-camera2:1.3.4 +androidx.camera:camera-core:1.3.4 +androidx.camera:camera-lifecycle:1.3.4 +androidx.camera:camera-video:1.3.4 +androidx.camera:camera-view:1.3.4 +androidx.collection:collection-jvm:1.4.2 +androidx.collection:collection-ktx:1.4.2 +androidx.collection:collection:1.4.2 +androidx.compose.animation:animation-android:1.7.0-rc01 +androidx.compose.animation:animation-core-android:1.7.0-rc01 +androidx.compose.animation:animation-core:1.7.0-rc01 +androidx.compose.animation:animation-graphics-android:1.7.0-rc01 +androidx.compose.animation:animation-graphics:1.7.0-rc01 +androidx.compose.animation:animation:1.7.0-rc01 +androidx.compose.foundation:foundation-android:1.7.0-rc01 +androidx.compose.foundation:foundation-layout-android:1.7.0-rc01 +androidx.compose.foundation:foundation-layout:1.7.0-rc01 +androidx.compose.foundation:foundation:1.7.0-rc01 +androidx.compose.material3.adaptive:adaptive-android:1.0.0-rc01 +androidx.compose.material3.adaptive:adaptive-layout-android:1.0.0-rc01 +androidx.compose.material3.adaptive:adaptive-layout:1.0.0-rc01 +androidx.compose.material3.adaptive:adaptive-navigation-android:1.0.0-rc01 +androidx.compose.material3.adaptive:adaptive-navigation:1.0.0-rc01 +androidx.compose.material3.adaptive:adaptive:1.0.0-rc01 +androidx.compose.material3:material3-android:1.2.1 +androidx.compose.material3:material3-window-size-class-android:1.2.1 +androidx.compose.material3:material3-window-size-class: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.7.0-rc01 +androidx.compose.runtime:runtime-saveable-android:1.7.0-rc01 +androidx.compose.runtime:runtime-saveable:1.7.0-rc01 +androidx.compose.runtime:runtime-tracing:1.0.0-beta01 +androidx.compose.runtime:runtime:1.7.0-rc01 +androidx.compose.ui:ui-android:1.7.0-rc01 +androidx.compose.ui:ui-geometry-android:1.7.0-rc01 +androidx.compose.ui:ui-geometry:1.7.0-rc01 +androidx.compose.ui:ui-graphics-android:1.7.0-rc01 +androidx.compose.ui:ui-graphics:1.7.0-rc01 +androidx.compose.ui:ui-test-android:1.7.0-rc01 +androidx.compose.ui:ui-test-junit4-android:1.7.0-rc01 +androidx.compose.ui:ui-test-junit4:1.7.0-rc01 +androidx.compose.ui:ui-test:1.7.0-rc01 +androidx.compose.ui:ui-text-android:1.7.0-rc01 +androidx.compose.ui:ui-text:1.7.0-rc01 +androidx.compose.ui:ui-tooling-android:1.7.0-rc01 +androidx.compose.ui:ui-tooling-data-android:1.7.0-rc01 +androidx.compose.ui:ui-tooling-data:1.7.0-rc01 +androidx.compose.ui:ui-tooling-preview-android:1.7.0-rc01 +androidx.compose.ui:ui-tooling-preview:1.7.0-rc01 +androidx.compose.ui:ui-tooling:1.7.0-rc01 +androidx.compose.ui:ui-unit-android:1.7.0-rc01 +androidx.compose.ui:ui-unit:1.7.0-rc01 +androidx.compose.ui:ui-util-android:1.7.0-rc01 +androidx.compose.ui:ui-util:1.7.0-rc01 +androidx.compose.ui:ui:1.7.0-rc01 +androidx.compose:compose-bom:2024.08.00 +androidx.concurrent:concurrent-futures:1.1.0 +androidx.core:core-ktx:1.13.1 +androidx.core:core-splashscreen:1.0.1 +androidx.core:core:1.13.1 +androidx.credentials:credentials-play-services-auth:1.3.0-beta01 +androidx.credentials:credentials:1.3.0-beta01 +androidx.cursoradapter:cursoradapter:1.0.0 +androidx.customview:customview-poolingcontainer:1.0.0 +androidx.customview:customview:1.1.0 +androidx.databinding:databinding-adapters:8.5.2 +androidx.databinding:databinding-common:8.5.2 +androidx.databinding:databinding-ktx:8.5.2 +androidx.databinding:databinding-runtime:8.5.2 +androidx.databinding:viewbinding:8.5.2 +androidx.datastore:datastore-android:1.1.1 +androidx.datastore:datastore-core-android:1.1.1 +androidx.datastore:datastore-core-okio-jvm:1.1.1 +androidx.datastore:datastore-core-okio:1.1.1 +androidx.datastore:datastore-core:1.1.1 +androidx.datastore:datastore-preferences-android:1.1.1 +androidx.datastore:datastore-preferences-core-jvm:1.1.1 +androidx.datastore:datastore-preferences-core:1.1.1 +androidx.datastore:datastore-preferences:1.1.1 +androidx.datastore:datastore:1.1.1 +androidx.documentfile:documentfile:1.0.0 +androidx.drawerlayout:drawerlayout:1.0.0 +androidx.emoji2:emoji2-views-helper:1.3.0 +androidx.emoji2:emoji2:1.3.0 +androidx.exifinterface:exifinterface:1.3.7 +androidx.fragment:fragment-ktx:1.8.2 +androidx.fragment:fragment:1.8.2 +androidx.graphics:graphics-path:1.0.1 +androidx.interpolator:interpolator:1.0.0 +androidx.legacy:legacy-support-core-utils:1.0.0 +androidx.lifecycle:lifecycle-common-java8:2.8.4 +androidx.lifecycle:lifecycle-common-jvm:2.8.4 +androidx.lifecycle:lifecycle-common:2.8.4 +androidx.lifecycle:lifecycle-extensions:2.2.0 +androidx.lifecycle:lifecycle-livedata-core-ktx:2.8.4 +androidx.lifecycle:lifecycle-livedata-core:2.8.4 +androidx.lifecycle:lifecycle-livedata:2.8.4 +androidx.lifecycle:lifecycle-process:2.8.4 +androidx.lifecycle:lifecycle-runtime-android:2.8.4 +androidx.lifecycle:lifecycle-runtime-compose-android:2.8.4 +androidx.lifecycle:lifecycle-runtime-compose:2.8.4 +androidx.lifecycle:lifecycle-runtime-ktx-android:2.8.4 +androidx.lifecycle:lifecycle-runtime-ktx:2.8.4 +androidx.lifecycle:lifecycle-runtime:2.8.4 +androidx.lifecycle:lifecycle-service:2.8.4 +androidx.lifecycle:lifecycle-viewmodel-android:2.8.4 +androidx.lifecycle:lifecycle-viewmodel-compose-android:2.8.4 +androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4 +androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 +androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.4 +androidx.lifecycle:lifecycle-viewmodel:2.8.4 +androidx.loader:loader:1.1.0 +androidx.localbroadcastmanager:localbroadcastmanager:1.0.0 +androidx.metrics:metrics-performance:1.0.0-beta01 +androidx.navigation:navigation-common-ktx:2.8.0-rc01 +androidx.navigation:navigation-common:2.8.0-rc01 +androidx.navigation:navigation-compose:2.8.0-rc01 +androidx.navigation:navigation-fragment-ktx:2.8.0-rc01 +androidx.navigation:navigation-fragment:2.8.0-rc01 +androidx.navigation:navigation-runtime-ktx:2.8.0-rc01 +androidx.navigation:navigation-runtime:2.8.0-rc01 +androidx.print:print:1.0.0 +androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta05 +androidx.privacysandbox.ads:ads-adservices:1.0.0-beta05 +androidx.profileinstaller:profileinstaller:1.3.1 +androidx.resourceinspection:resourceinspection-annotation:1.0.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.test.espresso:espresso-core:3.5.0 +androidx.test.espresso:espresso-idling-resource:3.5.0 +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.test:runner:1.5.0 +androidx.tracing:tracing-ktx:1.3.0-alpha02 +androidx.tracing:tracing-perfetto:1.0.0 +androidx.tracing:tracing:1.3.0-alpha02 +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.viewpager:viewpager:1.0.0 +androidx.window.extensions.core:core:1.0.0 +androidx.window:window-core-android:1.3.0-rc01 +androidx.window:window-core:1.3.0-rc01 +androidx.window:window:1.3.0-rc01 +ch.qos.logback:logback-classic:1.2.3 +ch.qos.logback:logback-core:1.2.3 +co.touchlab:stately-concurrency-jvm:2.0.7 +co.touchlab:stately-concurrency:2.0.7 +co.touchlab:stately-concurrent-collections-jvm:2.0.7 +co.touchlab:stately-concurrent-collections:2.0.7 +co.touchlab:stately-strict-jvm:2.0.7 +co.touchlab:stately-strict:2.0.7 +com.caverock:androidsvg-aar:1.4 +com.eygraber:uri-kmp-android:0.0.12 +com.eygraber:uri-kmp:0.0.12 +com.google.accompanist:accompanist-drawablepainter:0.32.0 +com.google.accompanist:accompanist-pager:0.34.0 +com.google.accompanist:accompanist-permissions:0.34.0 +com.google.android.datatransport:transport-api:3.2.0 +com.google.android.datatransport:transport-backend-cct:3.3.0 +com.google.android.datatransport:transport-runtime:3.3.0 +com.google.android.gms:play-services-ads-identifier:18.0.0 +com.google.android.gms:play-services-auth-api-phone:18.0.2 +com.google.android.gms:play-services-auth-base:18.0.10 +com.google.android.gms:play-services-auth:21.2.0 +com.google.android.gms:play-services-base:18.3.0 +com.google.android.gms:play-services-basement:18.4.0 +com.google.android.gms:play-services-cloud-messaging:17.2.0 +com.google.android.gms:play-services-code-scanner:16.1.0 +com.google.android.gms:play-services-fido:21.0.0 +com.google.android.gms:play-services-measurement-api:22.0.2 +com.google.android.gms:play-services-measurement-base:22.0.2 +com.google.android.gms:play-services-measurement-impl:22.0.2 +com.google.android.gms:play-services-measurement-sdk-api:22.0.2 +com.google.android.gms:play-services-measurement-sdk:22.0.2 +com.google.android.gms:play-services-measurement:22.0.2 +com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.0 +com.google.android.gms:play-services-stats:17.0.2 +com.google.android.gms:play-services-tasks:18.2.0 +com.google.android.libraries.identity.googleid:googleid:1.1.1 +com.google.android.odml:image:1.0.0-beta1 +com.google.auto.value:auto-value-annotations:1.6.3 +com.google.code.findbugs:jsr305:3.0.2 +com.google.code.gson:gson:2.10.1 +com.google.dagger:dagger:2.27 +com.google.errorprone:error_prone_annotations:2.26.0 +com.google.firebase:firebase-abt:21.1.1 +com.google.firebase:firebase-analytics-ktx:22.0.2 +com.google.firebase:firebase-analytics:22.0.2 +com.google.firebase:firebase-annotations:16.2.0 +com.google.firebase:firebase-bom:33.1.2 +com.google.firebase:firebase-common-ktx:21.0.0 +com.google.firebase:firebase-common:21.0.0 +com.google.firebase:firebase-components:18.0.0 +com.google.firebase:firebase-config-interop:16.0.1 +com.google.firebase:firebase-config:22.0.0 +com.google.firebase:firebase-crashlytics-ktx:19.0.3 +com.google.firebase:firebase-crashlytics:19.0.3 +com.google.firebase:firebase-datatransport:19.0.0 +com.google.firebase:firebase-encoders-json:18.0.1 +com.google.firebase:firebase-encoders-proto:16.0.0 +com.google.firebase:firebase-encoders:17.0.0 +com.google.firebase:firebase-iid-interop:17.1.0 +com.google.firebase:firebase-installations-interop:17.2.0 +com.google.firebase:firebase-installations:18.0.0 +com.google.firebase:firebase-measurement-connector:20.0.1 +com.google.firebase:firebase-messaging-ktx:24.0.0 +com.google.firebase:firebase-messaging:24.0.0 +com.google.firebase:firebase-perf-ktx:21.0.1 +com.google.firebase:firebase-perf:21.0.1 +com.google.firebase:firebase-sessions:2.0.3 +com.google.firebase:protolite-well-known-types:18.0.0 +com.google.guava:failureaccess:1.0.1 +com.google.guava:guava:31.1-android +com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava +com.google.j2objc:j2objc-annotations:1.3 +com.google.mlkit:barcode-scanning-common:17.0.0 +com.google.mlkit:barcode-scanning:17.2.0 +com.google.mlkit:common:18.9.0 +com.google.mlkit:vision-common:17.3.0 +com.google.mlkit:vision-interfaces:16.2.0 +com.google.protobuf:protobuf-javalite:4.26.0 +com.google.protobuf:protobuf-kotlin-lite:4.26.0 +com.google.zxing:core:3.5.3 +com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0 +com.maxkeppeler.sheets-compose-dialogs:calendar:1.3.0 +com.maxkeppeler.sheets-compose-dialogs:core:1.3.0 +com.squareup.okhttp3:logging-interceptor:4.12.0 +com.squareup.okhttp3:okhttp:4.12.0 +com.squareup.okio:okio-jvm:3.9.0 +com.squareup.okio:okio:3.9.0 +com.squareup.retrofit2:adapter-rxjava:2.11.0 +com.squareup.retrofit2:converter-gson:2.11.0 +com.squareup.retrofit2:retrofit:2.11.0 +com.squareup.wire:wire-runtime-jvm:5.0.0 +com.squareup.wire:wire-runtime:5.0.0 +com.squareup:javawriter:2.1.1 +dev.chrisbanes.snapper:snapper:0.3.0 +io.coil-kt:coil-base:2.6.0 +io.coil-kt:coil-compose-base:2.6.0 +io.coil-kt:coil-compose:2.6.0 +io.coil-kt:coil:2.6.0 +io.github.qdsfdhvh:image-loader-android:1.6.4 +io.github.qdsfdhvh:image-loader:1.6.4 +io.insert-koin:koin-android:4.0.0-RC2 +io.insert-koin:koin-androidx-compose:4.0.0-RC2 +io.insert-koin:koin-androidx-navigation:4.0.0-RC2 +io.insert-koin:koin-annotations-jvm:1.4.0-RC4 +io.insert-koin:koin-annotations:1.4.0-RC4 +io.insert-koin:koin-bom:4.0.0-RC2 +io.insert-koin:koin-compose-jvm:4.0.0-RC2 +io.insert-koin:koin-compose:4.0.0-RC2 +io.insert-koin:koin-core-jvm:4.0.0-RC2 +io.insert-koin:koin-core-viewmodel-jvm:4.0.0-RC2 +io.insert-koin:koin-core-viewmodel:4.0.0-RC2 +io.insert-koin:koin-core:4.0.0-RC2 +io.ktor:ktor-client-android-jvm:2.3.4 +io.ktor:ktor-client-android:2.3.4 +io.ktor:ktor-client-content-negotiation-jvm:2.3.4 +io.ktor:ktor-client-content-negotiation:2.3.4 +io.ktor:ktor-client-core-jvm:2.3.4 +io.ktor:ktor-client-core:2.3.4 +io.ktor:ktor-client-json-jvm:2.3.4 +io.ktor:ktor-client-json:2.3.4 +io.ktor:ktor-client-logging-jvm:2.3.4 +io.ktor:ktor-client-logging:2.3.4 +io.ktor:ktor-client-okhttp-jvm:2.3.3 +io.ktor:ktor-client-okhttp:2.3.3 +io.ktor:ktor-client-serialization-jvm:2.3.4 +io.ktor:ktor-client-serialization:2.3.4 +io.ktor:ktor-client-websockets-jvm:2.3.4 +io.ktor:ktor-client-websockets:2.3.4 +io.ktor:ktor-events-jvm:2.3.4 +io.ktor:ktor-events:2.3.4 +io.ktor:ktor-http-jvm:2.3.4 +io.ktor:ktor-http:2.3.4 +io.ktor:ktor-io-jvm:2.3.4 +io.ktor:ktor-io:2.3.4 +io.ktor:ktor-serialization-jvm:2.3.4 +io.ktor:ktor-serialization-kotlinx-json-jvm:2.3.4 +io.ktor:ktor-serialization-kotlinx-json:2.3.4 +io.ktor:ktor-serialization-kotlinx-jvm:2.3.4 +io.ktor:ktor-serialization-kotlinx:2.3.4 +io.ktor:ktor-serialization:2.3.4 +io.ktor:ktor-utils-jvm:2.3.4 +io.ktor:ktor-utils:2.3.4 +io.ktor:ktor-websocket-serialization-jvm:2.3.4 +io.ktor:ktor-websocket-serialization:2.3.4 +io.ktor:ktor-websockets-jvm:2.3.4 +io.ktor:ktor-websockets:2.3.4 +io.michaelrocks:libphonenumber-android:8.13.35 +io.reactivex:rxandroid:1.1.0 +io.reactivex:rxjava:1.3.8 +javax.inject:javax.inject:1 +junit:junit:4.13.2 +network.chaintech:cmp-image-pick-n-crop-android:1.0.1 +network.chaintech:cmp-image-pick-n-crop:1.0.1 +network.chaintech:qr-kit-android:2.0.0 +network.chaintech:qr-kit:2.0.0 +org.checkerframework:checker-qual:3.12.0 +org.hamcrest:hamcrest-core:1.3 +org.hamcrest:hamcrest-integration:1.3 +org.hamcrest:hamcrest-library:1.3 +org.jetbrains.androidx.core:core-bundle-android:1.0.0 +org.jetbrains.androidx.core:core-bundle:1.0.0 +org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.0 +org.jetbrains.androidx.lifecycle:lifecycle-viewmodel:2.8.0 +org.jetbrains.androidx.savedstate:savedstate:1.2.0 +org.jetbrains.compose.components:components-resources-android:1.6.11 +org.jetbrains.compose.components:components-resources:1.6.11 +org.jetbrains.compose.components:components-ui-tooling-preview-android:1.6.11 +org.jetbrains.compose.components:components-ui-tooling-preview:1.6.11 +org.jetbrains.compose.foundation:foundation:1.6.11 +org.jetbrains.compose.material3:material3:1.6.11 +org.jetbrains.compose.material:material-icons-extended:1.6.11 +org.jetbrains.compose.runtime:runtime:1.6.11 +org.jetbrains.compose.ui:ui-tooling-preview:1.6.11 +org.jetbrains.compose.ui:ui-tooling:1.6.11 +org.jetbrains.compose.ui:ui:1.6.11 +org.jetbrains.kotlin:kotlin-android-extensions-runtime:2.0.20 +org.jetbrains.kotlin:kotlin-parcelize-runtime:2.0.20 +org.jetbrains.kotlin:kotlin-stdlib-common:2.0.20 +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.20 +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.20 +org.jetbrains.kotlin:kotlin-stdlib:2.0.20 +org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.3.7 +org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7 +org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1 +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1 +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1 +org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.8.1 +org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.8.1 +org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.8.1 +org.jetbrains.kotlinx:kotlinx-coroutines-test-jvm:1.8.1 +org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1 +org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.6.0 +org.jetbrains.kotlinx:kotlinx-datetime:0.6.0 +org.jetbrains.kotlinx:kotlinx-serialization-bom:1.7.1 +org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.7.1 +org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1 +org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.1 +org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1 +org.jetbrains:annotations:23.0.0 +org.slf4j:slf4j-api:1.7.36 diff --git a/mifospay/src/main/java/org/mifospay/MainActivity.kt b/mifospay/src/main/java/org/mifospay/MainActivity.kt new file mode 100644 index 000000000..e17923bd8 --- /dev/null +++ b/mifospay/src/main/java/org/mifospay/MainActivity.kt @@ -0,0 +1,145 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay + +import android.os.Bundle +import android.view.Window +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.metrics.performance.JankStats +import androidx.navigation.compose.rememberNavController +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf +import org.mifospay.MainActivityUiState.Loading +import org.mifospay.MainActivityUiState.Success +import org.mifospay.core.analytics.AnalyticsHelper +import org.mifospay.core.analytics.LocalAnalyticsHelper +import org.mifospay.core.data.util.NetworkMonitor +import org.mifospay.core.data.util.TimeZoneMonitor +import org.mifospay.core.designsystem.theme.MifosTheme +import org.mifospay.core.ui.LocalTimeZone +import org.mifospay.navigation.MifosNavGraph.LOGIN_GRAPH +import org.mifospay.navigation.MifosNavGraph.PASSCODE_GRAPH +import org.mifospay.navigation.RootNavGraph +import org.mifospay.ui.rememberMifosAppState + +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +class MainActivity : ComponentActivity() { + + /** + * Lazily inject [JankStats], which is used to track jank throughout the app. + */ + + private val networkMonitor: NetworkMonitor by inject() + + private val timeZoneMonitor: TimeZoneMonitor by inject() + + private val analyticsHelper: AnalyticsHelper by inject() + + private val viewModel: MainActivityViewModel by viewModel() + + private val myWindow: Window by inject { parametersOf(this) } + + private val lazyStats: JankStats by inject { parametersOf(myWindow) } + + override fun onCreate(savedInstanceState: Bundle?) { + val splashScreen = installSplashScreen() + super.onCreate(savedInstanceState) + + var uiState: MainActivityUiState by mutableStateOf(Loading) + + // Update the uiState + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uiState + .onEach { uiState = it } + .collect() + } + } + + splashScreen.setKeepOnScreenCondition { + when (uiState) { + Loading -> true + is Success -> false + } + } + + enableEdgeToEdge() + + setContent { + val navController = rememberNavController() + + val appState = rememberMifosAppState( + windowSizeClass = calculateWindowSizeClass(this), + networkMonitor = networkMonitor, + timeZoneMonitor = timeZoneMonitor, + ) + + val currentTimeZone by appState.currentTimeZone.collectAsStateWithLifecycle() + + val navDestination = when (uiState) { + is Success -> if ((uiState as Success).userData.isAuthenticated) { + PASSCODE_GRAPH + } else { + LOGIN_GRAPH + } + + else -> LOGIN_GRAPH + } + + CompositionLocalProvider( + LocalAnalyticsHelper provides analyticsHelper, + LocalTimeZone provides currentTimeZone, + ) { + MifosTheme { + RootNavGraph( + appState = appState, + navHostController = navController, + startDestination = navDestination, + onClickLogout = { + viewModel.logOut() + navController.navigate(LOGIN_GRAPH) { + popUpTo(navController.graph.id) { + inclusive = true + } + } + }, + ) + } + } + } + } + + override fun onResume() { + super.onResume() + lazyStats.isTrackingEnabled = true + } + + override fun onPause() { + super.onPause() + lazyStats.isTrackingEnabled = false + } +} diff --git a/mifospay/src/main/java/org/mifospay/di/JankStatsModule.kt b/mifospay/src/main/java/org/mifospay/di/JankStatsModule.kt new file mode 100644 index 000000000..aa1210bf6 --- /dev/null +++ b/mifospay/src/main/java/org/mifospay/di/JankStatsModule.kt @@ -0,0 +1,36 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.di + +import android.app.Activity +import android.util.Log +import android.view.Window +import androidx.metrics.performance.JankStats +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module +import org.mifospay.MainActivityViewModel + +val JankStatsModule = module { + + factory { (activity: Activity) -> activity.window } + factory { (window: Window) -> + JankStats.createAndTrack(window) { frameData -> + // Make sure to only log janky frames. + if (frameData.isJank) { + // We're currently logging this but would better report it to a backend. + Log.v("Mifos Jank", frameData.toString()) + } + } + } + + viewModel { + MainActivityViewModel(userDataRepository = get(), passcodeManager = get()) + } +} diff --git a/mifospay/src/main/java/org/mifospay/navigation/MifosNavHost.kt b/mifospay/src/main/java/org/mifospay/navigation/MifosNavHost.kt new file mode 100644 index 000000000..745f43a20 --- /dev/null +++ b/mifospay/src/main/java/org/mifospay/navigation/MifosNavHost.kt @@ -0,0 +1,305 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +package org.mifospay.navigation + +import android.content.Context +import android.net.Uri +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.core.content.FileProvider +import androidx.navigation.compose.NavHost +import com.mifos.library.material3.navigation.ModalBottomSheetLayout +import org.mifospay.common.Constants +import org.mifospay.core.ui.utility.TabContent +import org.mifospay.feature.bank.accounts.AccountsScreen +import org.mifospay.feature.bank.accounts.navigation.bankAccountDetailScreen +import org.mifospay.feature.bank.accounts.navigation.linkBankAccountScreen +import org.mifospay.feature.bank.accounts.navigation.navigateToBankAccountDetail +import org.mifospay.feature.bank.accounts.navigation.navigateToLinkBankAccount +import org.mifospay.feature.editpassword.navigation.editPasswordScreen +import org.mifospay.feature.editpassword.navigation.navigateToEditPassword +import org.mifospay.feature.faq.navigation.faqScreen +import org.mifospay.feature.faq.navigation.navigateToFAQ +import org.mifospay.feature.finance.FinanceScreenContents +import org.mifospay.feature.finance.navigation.financeScreen +import org.mifospay.feature.history.HistoryScreen +import org.mifospay.feature.home.navigation.HOME_ROUTE +import org.mifospay.feature.home.navigation.homeScreen +import org.mifospay.feature.invoices.InvoiceScreenRoute +import org.mifospay.feature.invoices.navigation.invoiceDetailScreen +import org.mifospay.feature.invoices.navigation.navigateToInvoiceDetail +import org.mifospay.feature.kyc.KYCScreen +import org.mifospay.feature.kyc.navigation.kycLevel1Screen +import org.mifospay.feature.kyc.navigation.kycLevel2Screen +import org.mifospay.feature.kyc.navigation.kycLevel3Screen +import org.mifospay.feature.kyc.navigation.kycScreen +import org.mifospay.feature.kyc.navigation.navigateToKYCLevel1 +import org.mifospay.feature.kyc.navigation.navigateToKYCLevel2 +import org.mifospay.feature.kyc.navigation.navigateToKYCLevel3 +import org.mifospay.feature.make.transfer.navigation.makeTransferScreen +import org.mifospay.feature.make.transfer.navigation.navigateToMakeTransferScreen +import org.mifospay.feature.merchants.navigation.merchantTransferScreen +import org.mifospay.feature.merchants.ui.MerchantScreen +import org.mifospay.feature.notification.notificationScreen +import org.mifospay.feature.payments.PaymentsScreenContents +import org.mifospay.feature.payments.RequestScreen +import org.mifospay.feature.payments.paymentsScreen +import org.mifospay.feature.profile.navigation.editProfileScreen +import org.mifospay.feature.profile.navigation.navigateToEditProfile +import org.mifospay.feature.profile.navigation.profileScreen +import org.mifospay.feature.read.qr.navigation.readQrScreen +import org.mifospay.feature.receipt.navigation.navigateToReceipt +import org.mifospay.feature.receipt.navigation.receiptScreen +import org.mifospay.feature.request.money.navigation.navigateToShowQrScreen +import org.mifospay.feature.request.money.navigation.showQrScreen +import org.mifospay.feature.savedcards.CardsScreen +import org.mifospay.feature.savedcards.navigation.addCardScreen +import org.mifospay.feature.search.searchScreen +import org.mifospay.feature.send.money.SendScreenRoute +import org.mifospay.feature.send.money.navigation.navigateToSendMoneyScreen +import org.mifospay.feature.send.money.navigation.sendMoneyScreen +import org.mifospay.feature.settings.navigation.settingsScreen +import org.mifospay.feature.specific.transactions.navigation.navigateToSpecificTransactions +import org.mifospay.feature.specific.transactions.navigation.specificTransactionsScreen +import org.mifospay.feature.standing.instruction.StandingInstructionsScreenRoute +import org.mifospay.feature.standing.instruction.navigateToNewSiScreen +import org.mifospay.feature.standing.instruction.newSiScreen +import org.mifospay.feature.standing.instruction.siDetailsScreen +import org.mifospay.feature.upiSetup.navigation.navigateToSetupUpiPin +import org.mifospay.feature.upiSetup.navigation.setupUpiPinScreen +import org.mifospay.ui.MifosAppState +import java.io.File +import java.util.Objects + +/** + * Top-level navigation graph. Navigation is organized as explained at + * https://d.android.com/jetpack/compose/nav-adaptive + * + * The navigation graph defined in this file defines the different top level routes. Navigation + * within each route is handled using state and Back Handlers. + */ +@Suppress("MaxLineLength", "LongMethod") +@Composable +internal fun MifosNavHost( + appState: MifosAppState, + onClickLogout: () -> Unit, + modifier: Modifier = Modifier, +) { + val navController = appState.navController + + val tabContents = listOf( + TabContent(FinanceScreenContents.ACCOUNTS.name) { + AccountsScreen( + navigateToBankAccountDetailScreen = navController::navigateToBankAccountDetail, + navigateToLinkBankAccountScreen = navController::navigateToLinkBankAccount, + ) + }, + TabContent(FinanceScreenContents.CARDS.name) { + CardsScreen(onEditCard = {}) + }, + TabContent(FinanceScreenContents.MERCHANTS.name) { + MerchantScreen() + }, + TabContent(FinanceScreenContents.KYC.name) { + KYCScreen( + onLevel1Clicked = navController::navigateToKYCLevel1, + onLevel2Clicked = navController::navigateToKYCLevel2, + onLevel3Clicked = navController::navigateToKYCLevel3, + ) + }, + ) + + val paymentsTabContents = listOf( + TabContent(PaymentsScreenContents.SEND.name) { + SendScreenRoute( + onBackClick = {}, + showToolBar = false, + proceedWithMakeTransferFlow = navController::navigateToMakeTransferScreen, + ) + }, + TabContent(PaymentsScreenContents.REQUEST.name) { + RequestScreen(showQr = navController::navigateToShowQrScreen) + }, + TabContent(PaymentsScreenContents.HISTORY.name) { + HistoryScreen( + accountClicked = navController::navigateToSpecificTransactions, + viewReceipt = { + navController + .navigateToReceipt(Uri.parse(Constants.RECEIPT_DOMAIN + it)) + }, + ) + }, + TabContent(PaymentsScreenContents.SI.name) { + StandingInstructionsScreenRoute( + onNewSI = navController::navigateToNewSiScreen, + onBackPress = navController::popBackStack, + ) + }, + TabContent(PaymentsScreenContents.INVOICES.name) { + InvoiceScreenRoute( + navigateToInvoiceDetailScreen = { + navController.navigateToInvoiceDetail(it.toString()) + }, + ) + }, + ) + + ModalBottomSheetLayout( + bottomSheetNavigator = appState.bottomSheetNavigator, + modifier = modifier, + ) { + NavHost( + route = MifosNavGraph.MAIN_GRAPH, + startDestination = HOME_ROUTE, + navController = navController, + ) { + homeScreen( + onRequest = navController::navigateToShowQrScreen, + onPay = navController::navigateToSendMoneyScreen, + ) + paymentsScreen( + tabContents = paymentsTabContents, + ) + financeScreen( + tabContents = tabContents, + ) + addCardScreen( + onDismiss = navController::popBackStack, + onAddCard = { + // Handle adding the cards + navController.popBackStack() + }, + ) + profileScreen(onEditProfile = navController::navigateToEditProfile) + + sendMoneyScreen( + onBackClick = navController::popBackStack, + proceedWithMakeTransferFlow = navController::navigateToMakeTransferScreen, + ) + makeTransferScreen( + onDismiss = navController::popBackStack, + ) + showQrScreen( + onBackClick = navController::popBackStack, + ) + merchantTransferScreen( + proceedWithMakeTransferFlow = navController::navigateToMakeTransferScreen, + onBackPressed = navController::popBackStack, + ) + settingsScreen( + onBackPress = navController::popBackStack, + navigateToEditPasswordScreen = navController::navigateToEditPassword, + onLogout = onClickLogout, + onChangePasscode = { + // TODO:: Implement change passcode screen + }, + navigateToFaqScreen = navController::navigateToFAQ, + ) + + kycScreen( + onLevel1Clicked = navController::navigateToKYCLevel1, + onLevel2Clicked = navController::navigateToKYCLevel2, + onLevel3Clicked = navController::navigateToKYCLevel3, + ) + kycLevel1Screen( + navigateToKycLevel2 = navController::navigateToKYCLevel2, + ) + kycLevel2Screen( + onSuccessKyc2 = navController::navigateToKYCLevel3, + ) + + kycLevel3Screen() + + newSiScreen(onBackClick = navController::popBackStack) + + siDetailsScreen( + onClickCreateNew = navController::navigateToNewSiScreen, + onBackPress = navController::popBackStack, + ) + + editProfileScreen( + onBackPress = navController::popBackStack, + getUri = ::getUri, + ) + + faqScreen( + navigateBack = navController::popBackStack, + ) + readQrScreen( + onBackClick = navController::popBackStack, + ) + + specificTransactionsScreen( + onBackClick = navController::popBackStack, + onTransactionItemClicked = { transactionId -> + navController.navigateToReceipt(Uri.parse(Constants.RECEIPT_DOMAIN + transactionId)) + }, + ) + invoiceDetailScreen( + onBackPress = navController::popBackStack, +// navigateToReceiptScreen = { uri -> +// navController.navigateToReceipt(Uri.parse(Constants.RECEIPT_DOMAIN + uri)) +// }, + ) + receiptScreen( + openPassCodeActivity = { + // TODO: Implement Passcode Screen for Receipt + }, + onBackClick = navController::popBackStack, + ) + setupUpiPinScreen( + onBackPress = navController::popBackStack, + ) + + bankAccountDetailScreen( + onSetupUpiPin = { bankAccountDetails, index -> + navController.navigateToSetupUpiPin(bankAccountDetails, index, Constants.SETUP) + }, + onChangeUpiPin = { bankAccountDetails, index -> + navController.navigateToSetupUpiPin(bankAccountDetails, index, Constants.CHANGE) + }, + onForgotUpiPin = { bankAccountDetails, index -> + navController.navigateToSetupUpiPin(bankAccountDetails, index, Constants.FORGOT) + }, + onBackClick = { bankAccountDetails, index -> + navController.previousBackStackEntry?.savedStateHandle?.set( + Constants.UPDATED_BANK_ACCOUNT, + bankAccountDetails, + ) + navController.previousBackStackEntry?.savedStateHandle?.set( + Constants.INDEX, + index, + ) + navController.popBackStack() + }, + ) + linkBankAccountScreen( + onBackClick = navController::popBackStack, + ) + editPasswordScreen( + onBackPress = navController::popBackStack, + onCancelChanges = navController::popBackStack, + ) + + notificationScreen() + + searchScreen(onBackClick = navController::popBackStack) + } + } +} + +fun getUri(context: Context, file: File): Uri { + val uri = FileProvider.getUriForFile( + Objects.requireNonNull(context), + org.mifospay.BuildConfig.APPLICATION_ID + ".provider", + file, + ) + return uri +} diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts new file mode 100644 index 000000000..fec478782 --- /dev/null +++ b/shared/build.gradle.kts @@ -0,0 +1,119 @@ +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +/* + * 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/mobile-wallet/blob/master/LICENSE.md + */ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.wire) + id("kotlin-parcelize") +} + +kotlin { + androidTarget { + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + jvm("desktop") + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "shared" + isStatic = true + } + } + + sourceSets { + androidMain.dependencies { + implementation(compose.preview) + implementation(libs.androidx.activity.compose) + + implementation(libs.koin.android) + implementation(libs.koin.androidx.compose) + } + + commonMain.dependencies { + //put your multiplatform dependencies here + implementation(compose.runtime) + implementation(compose.material3) + implementation(compose.ui) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.serialization.json) + + api(libs.koin.core) + implementation(libs.koin.compose) + + implementation(libs.datastore) + } + + val desktopMain by getting { + dependencies { + // Desktop specific dependencies + implementation(compose.desktop.currentOs) + implementation(compose.desktop.common) + } + } + } + + task("testClasses") +} + +wire { + kotlin {} + sourcePath { + srcDir("src/commonMain/proto") + } +} + +android { + namespace = "org.mifospay.shared" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + dependencies { + debugImplementation(compose.uiTooling) + } +}