diff --git a/android/app/build.gradle b/android/app/build.gradle index cc7cecf2dd..f3d52ca951 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -263,6 +263,7 @@ dependencies { implementation project(":navi-cycs") implementation project(":navi-gold") implementation project(":navi-insurance") + implementation project(":navi-money-manager") implementation project(":navi-mqtt") implementation project(":navi-pay") implementation project(":navi-payment") diff --git a/android/app/src/main/java/com/naviapp/analytics/utils/NaviSDKHelper.kt b/android/app/src/main/java/com/naviapp/analytics/utils/NaviSDKHelper.kt index 7a81a78eb5..542a02ac4d 100644 --- a/android/app/src/main/java/com/naviapp/analytics/utils/NaviSDKHelper.kt +++ b/android/app/src/main/java/com/naviapp/analytics/utils/NaviSDKHelper.kt @@ -172,6 +172,7 @@ object NaviSDKHelper { DeeplinkManager().logOut() NaviApplication.instance.naviPayManager.get().onAppLogout() NaviApplication.instance.notificationManager.get().logout() + NaviApplication.instance.mmLibManager.get().clearMoneyManagerData() } fun setLocation(latitude: Double, longitude: Double) { diff --git a/android/app/src/main/java/com/naviapp/app/NaviApplication.kt b/android/app/src/main/java/com/naviapp/app/NaviApplication.kt index 99e689d99d..8c6d3d3506 100644 --- a/android/app/src/main/java/com/naviapp/app/NaviApplication.kt +++ b/android/app/src/main/java/com/naviapp/app/NaviApplication.kt @@ -10,6 +10,7 @@ package com.naviapp.app import coil.ImageLoader import coil.ImageLoaderFactory import com.navi.base.cache.repository.NaviCacheRepository +import com.navi.moneymanager.common.manager.MMLibManager import com.navi.pay.common.setup.NaviPayManager import com.naviapp.app.facades.ImageLoaderProvider import com.naviapp.app.initializers.ComponentInitializer @@ -29,6 +30,8 @@ open class NaviApplication : BaseApplication(), ImageLoaderFactory { // This will initialize NaviPayManager lazily i.e. when NaviPayManager::init() will be called @Inject lateinit var naviPayManager: Lazy + @Inject lateinit var mmLibManager: Lazy + @Inject lateinit var naviCacheRepository: Lazy @Inject lateinit var notificationManager: Lazy diff --git a/android/app/src/main/java/com/naviapp/common/navigator/NaviDeepLinkNavigator.kt b/android/app/src/main/java/com/naviapp/common/navigator/NaviDeepLinkNavigator.kt index fd6612d555..6392f1eba1 100644 --- a/android/app/src/main/java/com/naviapp/common/navigator/NaviDeepLinkNavigator.kt +++ b/android/app/src/main/java/com/naviapp/common/navigator/NaviDeepLinkNavigator.kt @@ -78,6 +78,8 @@ import com.navi.gold.ui.IconTitleDescBottomSheet import com.navi.insurance.BuildConfig import com.navi.insurance.navigator.NaviInsuranceDeeplinkNavigator import com.navi.insurance.util.VIDEO_ID_EXTRA +import com.navi.moneymanager.common.navigation.navigator.MMDeeplinkNavigator +import com.navi.moneymanager.common.navigation.navigator.MMDeeplinkNavigator.MONEY_MANAGER_ACTIVITY import com.navi.naviwidgets.R as WidgetsR import com.navi.naviwidgets.utils.CHAT_IDEMPOTENCY_KEY import com.navi.naviwidgets.utils.CHOOSER_TITLE_PARAM @@ -433,6 +435,21 @@ object NaviDeepLinkNavigator : DeepLinkListener { intent = getCrmWebViewIntent(activity = activity, ctaUrl = PRODUCT_HELP_PAGE) } + MONEY_MANAGER_ACTIVITY -> { + MMDeeplinkNavigator.navigate( + activity, + navArgs = + NavArgs( + ctaData = ctaData, + finish = finish.orFalse(), + bundle = bundle, + needsResult = needsResult, + requestCode = requestCode, + clearTask = clearTaskTemp + ) + ) + return + } CHAT_ACTIVITY -> { var shareableLink: String? = null var sourceId: String? = DEFAULT_SOURCE_ID_FOR_PL diff --git a/android/app/src/main/java/com/naviapp/home/common/handler/PostRenderTaskExecutor.kt b/android/app/src/main/java/com/naviapp/home/common/handler/PostRenderTaskExecutor.kt new file mode 100644 index 0000000000..295a07daa5 --- /dev/null +++ b/android/app/src/main/java/com/naviapp/home/common/handler/PostRenderTaskExecutor.kt @@ -0,0 +1,32 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.naviapp.home.common.handler + +import com.navi.base.sharedpref.PreferenceManager +import com.navi.moneymanager.common.manager.MMLibManager +import com.navi.moneymanager.common.utils.Constants.MM_IS_USER_ONBOARDED +import dagger.Lazy +import javax.inject.Inject + +class PostRenderTaskExecutor @Inject constructor(private val mmLibManager: Lazy) { + + private var isInitialized: Boolean = false + + fun initCommonTasks() { + if (isInitialized) return + isInitialized = true + + initMoneyManagerDB() + } + + private fun initMoneyManagerDB() { + if (PreferenceManager.getBooleanPreference(key = MM_IS_USER_ONBOARDED)) { + mmLibManager.get().initMoneyManagerDB() + } + } +} diff --git a/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeScreen.kt b/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeScreen.kt index 642bc5a585..4a6bc2367c 100644 --- a/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeScreen.kt +++ b/android/app/src/main/java/com/naviapp/home/compose/home/ui/screen/HomeScreen.kt @@ -77,6 +77,12 @@ fun HomeScreen( homeVM().sendEvent(HpEvents.UpdateShadowOnFrontLayer(true)) } + LaunchedEffect(hpStates().isHomePageRendered) { + if (hpStates().isHomePageRendered) { + homeVM().postRenderTaskExecutor.initCommonTasks() + } + } + when (hpStates().isError.not()) { true -> { if (hpStates().isLoading) { diff --git a/android/app/src/main/java/com/naviapp/home/viewmodel/HomeViewModel.kt b/android/app/src/main/java/com/naviapp/home/viewmodel/HomeViewModel.kt index f665a0b9f6..abedda6da5 100644 --- a/android/app/src/main/java/com/naviapp/home/viewmodel/HomeViewModel.kt +++ b/android/app/src/main/java/com/naviapp/home/viewmodel/HomeViewModel.kt @@ -31,6 +31,7 @@ import com.naviapp.analytics.utils.NaviAnalytics import com.naviapp.appsettings.utils.SettingsMedium import com.naviapp.common.navigator.NaviDeepLinkNavigator import com.naviapp.home.common.handler.HomePageSectionImpressionTracker +import com.naviapp.home.common.handler.PostRenderTaskExecutor import com.naviapp.home.compose.activity.HomePageActivity import com.naviapp.home.compose.listener.HomeScreenCallbackListener import com.naviapp.home.reducer.HomeReducer @@ -76,6 +77,7 @@ constructor( private val upiUseCase: HandleUpiUseCase, val videoViewHelper: Lazy, val sectionVisibilityTracker: HomePageSectionImpressionTracker, + val postRenderTaskExecutor: PostRenderTaskExecutor, ) : BaseMviViewModel( initialState = HpStates(), diff --git a/android/app/src/main/java/com/naviapp/nux/ui/NuxScaffoldRenderer.kt b/android/app/src/main/java/com/naviapp/nux/ui/NuxScaffoldRenderer.kt index 8ebd8c67a4..0c0f39440e 100644 --- a/android/app/src/main/java/com/naviapp/nux/ui/NuxScaffoldRenderer.kt +++ b/android/app/src/main/java/com/naviapp/nux/ui/NuxScaffoldRenderer.kt @@ -23,10 +23,10 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalView import com.navi.common.alchemist.model.AlchemistScreenStructure +import com.navi.common.commoncomposables.ui.AlchemistWidgetListRenderer import com.navi.design.utils.parseColorSafe import com.navi.uitron.model.ui.ScrollData import com.navi.uitron.utils.setVerticalScroll -import com.naviapp.alchemist.widgetfactory.AlchemistWidgetListRenderer import com.naviapp.forge.util.hideKeyboard import com.naviapp.nux.viewmodel.NuxViewModel diff --git a/android/app/src/main/java/com/naviapp/pushnotification/firebase/NaviFirebaseMessagingService.kt b/android/app/src/main/java/com/naviapp/pushnotification/firebase/NaviFirebaseMessagingService.kt index 1246371da4..c9b3482421 100644 --- a/android/app/src/main/java/com/naviapp/pushnotification/firebase/NaviFirebaseMessagingService.kt +++ b/android/app/src/main/java/com/naviapp/pushnotification/firebase/NaviFirebaseMessagingService.kt @@ -52,6 +52,7 @@ import com.naviapp.pushnotification.utils.cleanModuleDatabaseTable import com.naviapp.pushnotification.utils.cleanPreferenceKeys import com.naviapp.pushnotification.utils.clearAllSharedDb import com.naviapp.pushnotification.utils.clearImageCache +import com.naviapp.pushnotification.utils.clearMoneyManagerDatabase import com.naviapp.pushnotification.utils.clearSharedDbWithKeys import com.naviapp.pushnotification.utils.removeSpecificImageCache import com.naviapp.pushnotification.utils.removeSpecificPreferenceKeys @@ -173,6 +174,7 @@ class NaviFirebaseMessagingService : FirebaseMessagingService() { NotificationConstants.CLEAR_ALL_SHARED_DB -> clearAllSharedDb() NotificationConstants.CLEAR_SHARED_DB_WITH_KEYS -> clearSharedDbWithKeys(sharedDbKeys = data[NotificationConstants.SHARED_DB_KEYS]) + NotificationConstants.CLEAR_MONEY_MANAGER_DB -> clearMoneyManagerDatabase() NotificationConstants.REMOVE_SPECIFIC_IMAGE_CACHE -> removeSpecificImageCache( imageUrls = data[NotificationConstants.COMMA_SEPARATED_IMAGE_URLS], diff --git a/android/app/src/main/java/com/naviapp/pushnotification/utils/FMSUtils.kt b/android/app/src/main/java/com/naviapp/pushnotification/utils/FMSUtils.kt index 857e1291df..d86680ad5f 100644 --- a/android/app/src/main/java/com/naviapp/pushnotification/utils/FMSUtils.kt +++ b/android/app/src/main/java/com/naviapp/pushnotification/utils/FMSUtils.kt @@ -23,6 +23,12 @@ fun clearAllSharedDb() { } } +fun clearMoneyManagerDatabase() { + CoroutineScope(Dispatchers.IO).launch { + NaviApplication.instance.mmLibManager.get().clearMoneyManagerData() + } +} + fun clearSharedDbWithKeys(sharedDbKeys: String?) { sharedDbKeys?.let { CoroutineScope(Dispatchers.IO).launch { diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 2339c8056a..6ac7d766d7 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -362,7 +362,6 @@ Please enter at least %1$s characters HELP Popular Banks - All Banks OTP Onemoney is an RBI licensed account aggregator Link Account diff --git a/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/common/viewmodel/ApplicationPlatformVM.kt b/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/common/viewmodel/ApplicationPlatformVM.kt index ab64580b51..a0d8ad83f1 100644 --- a/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/common/viewmodel/ApplicationPlatformVM.kt +++ b/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/common/viewmodel/ApplicationPlatformVM.kt @@ -37,7 +37,6 @@ import com.navi.ap.network.model.ApplicationRequestBody import com.navi.ap.network.model.FillApplicationRequestBody import com.navi.ap.network.model.getBottomSheetStructure import com.navi.ap.utils.EventUtil -import com.navi.ap.utils.PeriodicTaskScheduler import com.navi.ap.utils.bundleToMap import com.navi.ap.utils.constants.APP_ACTION import com.navi.ap.utils.constants.APP_CONFIG_VERSION @@ -75,6 +74,7 @@ import com.navi.base.utils.orFalse import com.navi.common.constants.API_SUCCESS_CODE import com.navi.common.network.ApiConstants import com.navi.common.network.models.isSuccessWithData +import com.navi.common.scheduler.PeriodicTaskScheduler import com.navi.common.uitron.model.action.AnalyticsActionV2 import com.navi.common.uitron.model.action.AnalyticsActionV2.PredefinedEventProperty import com.navi.common.uitron.model.action.FillApiData diff --git a/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/utils/helper/PeriodicTaskSchedulerFacade.kt b/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/utils/helper/PeriodicTaskSchedulerFacade.kt index 499553208c..ec7207ed80 100644 --- a/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/utils/helper/PeriodicTaskSchedulerFacade.kt +++ b/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/utils/helper/PeriodicTaskSchedulerFacade.kt @@ -7,10 +7,10 @@ package com.navi.ap.utils.helper -import com.navi.ap.utils.PeriodicTaskScheduler import com.navi.ap.utils.constants.AP_POLL_INITIAL_DELAY import com.navi.ap.utils.constants.AP_POLL_INTERVAL import com.navi.ap.utils.constants.AP_POLL_RETRY_COUNT +import com.navi.common.scheduler.PeriodicTaskScheduler import com.navi.uitron.utils.orVal import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.sync.Mutex diff --git a/android/build.gradle b/android/build.gradle index cc5a40be66..35cb20c69b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -33,6 +33,7 @@ plugins { alias libs.plugins.hilt.android apply false alias libs.plugins.kotlin.android apply false alias libs.plugins.kotlin.compose apply false + alias libs.plugins.kotlin.jvm apply false alias libs.plugins.kotlin.kapt apply false alias libs.plugins.kotlin.parcelize apply false alias libs.plugins.ksp apply false @@ -76,7 +77,13 @@ allprojects { } maven { url 'https://phonepe.mycloudrepo.io/public/repositories/phonepe-intentsdk-android' } maven { url 'https://maven.juspay.in/jp-build-packages/hyper-sdk/' } - maven { url 'https://finarkein.jfrog.io/artifactory/anubhav-maven' } + maven { + url 'https://finarkein.jfrog.io/artifactory/anubhav-mvn' + credentials { + username 'client-navi' + password 'S*aJMPpkepwCKf2aWV1X97Wen4XrR46#' + } + } // jitpack should be last repository maven { url 'https://jitpack.io' } } diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 2efec86125..f327710238 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -62,7 +62,7 @@ digio = "v4.0.6" digitap = "1.4.4" facebook-applinks = "17.0.1" facebook-shimmer = "0.5.0" -finarkein = "0.5.2" +finarkein = "0.6.4" firebase-bom = "33.7.0" firebase-crashlytics = "3.0.2" firebase-perf = "1.4.2" @@ -112,6 +112,7 @@ retrofit = "2.11.0" room = "2.5.2" shawnLin-numberPicker = "2.4.13" spotless = "6.25.0" +squareup-kotlinpoet = "1.15.0" truecaller = "3.0.0" turbine = "1.2.0" uiautomator = "2.3.0" @@ -326,6 +327,8 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" } +ksp-symbolProcessingApi = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } + airbnb-lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" } airbnb-lottieCompose = { module = "com.airbnb.android:lottie-compose", version.ref = "lottie" } @@ -372,6 +375,9 @@ retrofit-retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = shawnLin-numberPicker = { module = "io.github.ShawnLin013:number-picker", version.ref = "shawnLin-numberPicker" } +squareup-kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "squareup-kotlinpoet" } +squareup-kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "squareup-kotlinpoet" } + truecaller = { module = "com.truecaller.android.sdk:truecaller-sdk", version.ref = "truecaller" } turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } @@ -393,6 +399,7 @@ google-services = { id = "com.google.gms.google-services", version.ref = "google hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/android/navi-base/src/main/java/com/navi/base/utils/BaseUtils.kt b/android/navi-base/src/main/java/com/navi/base/utils/BaseUtils.kt index c6f1a33cff..084f9e5bf8 100644 --- a/android/navi-base/src/main/java/com/navi/base/utils/BaseUtils.kt +++ b/android/navi-base/src/main/java/com/navi/base/utils/BaseUtils.kt @@ -24,6 +24,7 @@ import com.navi.base.sharedpref.CommonPrefConstants.UPDATED_VERSION_CODE import com.navi.base.sharedpref.PreferenceManager import java.io.File import java.text.SimpleDateFormat +import java.util.Calendar import java.util.Date import java.util.Locale @@ -565,4 +566,14 @@ object BaseUtils { CommonPrefConstants.PREVIOUS_LOGGED_IN_EXTERNAL_CUSTOMER_ID ) } + + fun getDayMonthAndYearFromTimestamp(timeStamp: Long): Triple { + val calendar = Calendar.getInstance() + calendar.timeInMillis = timeStamp + return Triple( + calendar.get(Calendar.DAY_OF_MONTH), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.YEAR) + ) + } } diff --git a/android/navi-base/src/main/java/com/navi/base/utils/Ext.kt b/android/navi-base/src/main/java/com/navi/base/utils/Ext.kt index 2dd337e2af..38ce823ea0 100644 --- a/android/navi-base/src/main/java/com/navi/base/utils/Ext.kt +++ b/android/navi-base/src/main/java/com/navi/base/utils/Ext.kt @@ -13,6 +13,8 @@ import com.google.gson.Gson import com.navi.base.model.AnalyticsEvent import com.navi.base.model.CtaData import com.navi.base.utils.AppLaunchUtils.TRUE +import java.text.NumberFormat +import java.util.Locale import kotlin.math.ceil import kotlin.math.floor import okhttp3.Headers @@ -273,3 +275,16 @@ fun convertObjectToJson(type: T): JSONObject? { } catch (ignore: Exception) {} return null } + +fun Double.formatToDecimalPlaces(digits: Int) = "%.${digits}f".format(this) + +fun Double.formatToInrWithTwoDecimals(): String { + val numberFormat = NumberFormat.getCurrencyInstance(Locale("en", "IN")) + numberFormat.minimumFractionDigits = 2 + numberFormat.maximumFractionDigits = 2 + return numberFormat.format(this).replace("\u00A0", "") +} + +fun List?.include(other: T?): List { + return this.orEmpty() + (other?.let { listOf(it) } ?: emptyList()) +} diff --git a/android/navi-code/.gitignore b/android/navi-code/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/android/navi-code/.gitignore @@ -0,0 +1 @@ +/build diff --git a/android/navi-code/build.gradle b/android/navi-code/build.gradle new file mode 100644 index 0000000000..f249024ed0 --- /dev/null +++ b/android/navi-code/build.gradle @@ -0,0 +1,17 @@ +plugins { + alias libs.plugins.kotlin.jvm +} + +sourceSets { + main { + java { + srcDir "${buildDir.absolutePath}/generated/ksp/" + } + } +} + +dependencies { + implementation libs.ksp.symbolProcessingApi + implementation libs.squareup.kotlinpoet + implementation libs.squareup.kotlinpoet.ksp +} diff --git a/android/navi-code/src/main/kotlin/com/navi/code/annotations/AutoGenerate.kt b/android/navi-code/src/main/kotlin/com/navi/code/annotations/AutoGenerate.kt new file mode 100644 index 0000000000..950b23c987 --- /dev/null +++ b/android/navi-code/src/main/kotlin/com/navi/code/annotations/AutoGenerate.kt @@ -0,0 +1,16 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.code.annotations + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +annotation class AutoGenerate + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.SOURCE) +annotation class EventName(val name: String) diff --git a/android/navi-code/src/main/kotlin/com/navi/code/generation/EventTrackerProcessorProvider.kt b/android/navi-code/src/main/kotlin/com/navi/code/generation/EventTrackerProcessorProvider.kt new file mode 100644 index 0000000000..74b5458d6a --- /dev/null +++ b/android/navi-code/src/main/kotlin/com/navi/code/generation/EventTrackerProcessorProvider.kt @@ -0,0 +1,19 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.code.generation + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import com.navi.code.processors.EventTrackerProcessor + +class EventTrackerProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return EventTrackerProcessor(environment.codeGenerator, environment.logger) + } +} diff --git a/android/navi-code/src/main/kotlin/com/navi/code/processors/EventTrackerProcessor.kt b/android/navi-code/src/main/kotlin/com/navi/code/processors/EventTrackerProcessor.kt new file mode 100644 index 0000000000..fdbe629fc5 --- /dev/null +++ b/android/navi-code/src/main/kotlin/com/navi/code/processors/EventTrackerProcessor.kt @@ -0,0 +1,140 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.code.processors + +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.google.devtools.ksp.validate +import com.navi.code.annotations.AutoGenerate +import com.navi.code.annotations.EventName +import com.navi.code.utils.isDefaultKotlinFunction +import com.navi.code.utils.toSnakeCase +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.ksp.toClassName +import java.io.OutputStreamWriter + +class EventTrackerProcessor( + private val codeGenerator: CodeGenerator, + private val logger: KSPLogger +) : SymbolProcessor { + + override fun process(resolver: Resolver): List { + + logger.info("Starting the processing of symbols with @AutoGenerate annotation.") + + val symbols = + resolver + .getSymbolsWithAnnotation(AutoGenerate::class.qualifiedName!!) + .filterIsInstance() + .validateAndGetSymbols() + + symbols.forEach(::generateAnalyticsClass) + + return symbols.filterNot { it.validate() }.toList() + } + + private fun Sequence.validateAndGetSymbols(): Sequence { + return filter { classDeclaration -> + when (classDeclaration.classKind) { + ClassKind.INTERFACE -> true + else -> { + throw IllegalArgumentException( + "Don't mark ${AutoGenerate::class.simpleName} on non-interface kind: ${classDeclaration.simpleName.asString()}." + ) + } + } + } + } + + private fun generateAnalyticsClass(classDeclaration: KSClassDeclaration) { + + val packageName = classDeclaration.packageName.asString() + + val className = classDeclaration.simpleName.asString() + + val generatedClassName = "${className}Impl" + + val fileSpecBuilder = + FileSpec.builder(packageName, generatedClassName) + .addImport("com.navi.analytics.utils", "NaviTrackEvent") + + val classBuilder = + TypeSpec.objectBuilder(generatedClassName).addModifiers(KModifier.INTERNAL) + + classDeclaration + .getAllFunctions() + .filterNot { it.isDefaultKotlinFunction() } + .forEach { function -> classBuilder.addFunction(generateMethod(function)) } + + fileSpecBuilder.addType(classBuilder.build()) + + val file = + codeGenerator.createNewFile( + dependencies = Dependencies.ALL_FILES, + packageName = packageName, + fileName = generatedClassName + ) + + OutputStreamWriter(file).use { writer -> fileSpecBuilder.build().writeTo(writer) } + } + + private fun generateMethod(function: KSFunctionDeclaration): FunSpec { + + val methodName = function.simpleName.asString() + + val parameters = function.parameters + + val eventName = + function.annotations + .find { it.shortName.asString() == EventName::class.simpleName } + ?.arguments + ?.firstOrNull() + ?.value as? String + ?: throw IllegalArgumentException("Event Name is missing for method $methodName.") + + if (eventName.isEmpty()) { + throw IllegalArgumentException("Event Name is missing for method $methodName.") + } + + val paramList = + parameters.map { param -> + val paramName = + param.name?.asString() + ?: throw IllegalArgumentException( + "Parameter name is missing for method $methodName." + ) + val paramType = param.type.resolve().toClassName() + ParameterSpec.builder(paramName, paramType).build() + } + + val paramEntries = + parameters.joinToString(", ") { param -> + val paramName = param.name?.asString() ?: "param" + "\"${paramName.toSnakeCase()}\" to $paramName.toString()" + } + + return FunSpec.builder(methodName) + .addParameters(paramList) + .addStatement( + "NaviTrackEvent.trackEventOnClickStream(eventName = %S, eventValues = mapOf($paramEntries))", + eventName + ) + .build() + } +} diff --git a/android/navi-code/src/main/kotlin/com/navi/code/utils/FuncExt.kt b/android/navi-code/src/main/kotlin/com/navi/code/utils/FuncExt.kt new file mode 100644 index 0000000000..b08459da7f --- /dev/null +++ b/android/navi-code/src/main/kotlin/com/navi/code/utils/FuncExt.kt @@ -0,0 +1,47 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.code.utils + +import com.google.devtools.ksp.symbol.KSFunctionDeclaration +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier + +fun KSFunctionDeclaration.isDefaultKotlinFunction(): Boolean { + return simpleName.asString() in listOf("equals", "hashCode", "toString") +} + +fun KSFunctionDeclaration.generateOverrideMethodWithDefaultSuperCall(): FunSpec { + val methodName = simpleName.asString() + + val methodBuilder = FunSpec.builder(methodName).addModifiers(KModifier.OVERRIDE) + + parameters.forEach { param -> + val paramName = + param.name?.asString() + ?: throw IllegalArgumentException( + "Parameter name is missing for method $methodName." + ) + + val paramType = param.type.resolve() + methodBuilder.addParameter( + paramName, + ClassName( + paramType.declaration.packageName.asString(), + paramType.declaration.simpleName.asString() + ) + ) + } + + methodBuilder.addCode( + "super.%L(%L)\n", + methodName, + parameters.joinToString(", ") { it.name?.asString().toString() } + ) + return methodBuilder.build() +} diff --git a/android/navi-code/src/main/kotlin/com/navi/code/utils/UtilExt.kt b/android/navi-code/src/main/kotlin/com/navi/code/utils/UtilExt.kt new file mode 100644 index 0000000000..71c155dd5d --- /dev/null +++ b/android/navi-code/src/main/kotlin/com/navi/code/utils/UtilExt.kt @@ -0,0 +1,12 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.code.utils + +fun String.toSnakeCase(): String { + return this.replace(Regex("([a-z])([A-Z])"), "$1_$2").lowercase() +} diff --git a/android/navi-code/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/android/navi-code/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 0000000000..944197142b --- /dev/null +++ b/android/navi-code/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +com.navi.code.generation.EventTrackerProcessorProvider \ No newline at end of file diff --git a/android/navi-coin/src/main/java/com/navi/coin/navigator/screens/CoinComposableRegistry.kt b/android/navi-coin/src/main/java/com/navi/coin/navigator/screens/CoinComposableRegistry.kt index fc383562dd..da818a875e 100644 --- a/android/navi-coin/src/main/java/com/navi/coin/navigator/screens/CoinComposableRegistry.kt +++ b/android/navi-coin/src/main/java/com/navi/coin/navigator/screens/CoinComposableRegistry.kt @@ -15,6 +15,7 @@ import com.navi.coin.ui.compose.screen.destinations.CoinHomeScreenDestination import com.navi.coin.ui.compose.screen.destinations.Destination import com.navi.coin.ui.compose.screen.destinations.RedemptionStatusScreenDestination import com.navi.coin.ui.compose.screen.destinations.ScratchCardHistoryScreenDestination +import com.navi.common.navigation.NavigationScreenData import com.navi.common.navigation.registry.ComposableRegistry import com.ramcosta.composedestinations.spec.DestinationSpec import com.ramcosta.composedestinations.spec.Direction @@ -28,7 +29,11 @@ import javax.inject.Inject @ActivityRetainedScoped class CoinComposableRegistry @Inject constructor() : ComposableRegistry { - override fun findDirectionByName(destinationName: String, bundle: Bundle?): Direction? { + override fun findDirectionByName( + destinationName: String, + bundle: Bundle?, + data: NavigationScreenData? + ): Direction? { return Composables.entries .find { it.destinationName == destinationName } ?.directionProvider diff --git a/android/navi-common/src/main/java/com/navi/common/alchemist/model/AlchemistScreenDefinition.kt b/android/navi-common/src/main/java/com/navi/common/alchemist/model/AlchemistScreenDefinition.kt index 27469ce65c..9429b602e7 100644 --- a/android/navi-common/src/main/java/com/navi/common/alchemist/model/AlchemistScreenDefinition.kt +++ b/android/navi-common/src/main/java/com/navi/common/alchemist/model/AlchemistScreenDefinition.kt @@ -13,6 +13,7 @@ import com.navi.common.model.common.UiTronActionAndResponse import com.navi.common.utils.Constants.DEFAULT_BACKGROUND_COLOR import com.navi.uitron.model.UiTronResponse import com.navi.uitron.model.data.UiTronActionData +import com.navi.uitron.model.ui.BrushData data class AlchemistScreenDefinition( @SerializedName("screenMetaData", alternate = ["metaData"]) @@ -49,6 +50,7 @@ data class AlchemistBottomSheetStructure( data class AlchemistWidgetGroup( val widgets: List>? = null, val backgroundColor: String? = DEFAULT_BACKGROUND_COLOR, + val backgroundBrushData: BrushData? = null ) data class AlchemistFloatingActionButton( diff --git a/android/navi-common/src/main/java/com/navi/common/basemvi/BaseMviViewModel.kt b/android/navi-common/src/main/java/com/navi/common/basemvi/BaseMviViewModel.kt index 2e8145a275..d920390605 100644 --- a/android/navi-common/src/main/java/com/navi/common/basemvi/BaseMviViewModel.kt +++ b/android/navi-common/src/main/java/com/navi/common/basemvi/BaseMviViewModel.kt @@ -7,6 +7,7 @@ package com.navi.common.basemvi +import androidx.annotation.CallSuper import androidx.lifecycle.viewModelScope import com.navi.common.viewmodel.BaseVM import kotlin.coroutines.CoroutineContext @@ -30,11 +31,13 @@ abstract class BaseMviViewModel(capacity = Channel.UNLIMITED) val effect = _effects.receiveAsFlow() - fun setEffect(dispatcher: CoroutineContext? = null, effect: () -> Effect) { + @CallSuper + open fun setEffect(dispatcher: CoroutineContext? = null, effect: () -> Effect) { viewModelScope.safeLaunch(dispatcher ?: Dispatchers.IO) { _effects.trySend(effect()) } } - fun sendEvent(event: Event) { + @CallSuper + open fun sendEvent(event: Event) { val newState = reducer.reduce(_state.value, event) _state.update { newState } } diff --git a/android/app/src/main/java/com/naviapp/alchemist/widgetfactory/AlchemistWidgetListRenderer.kt b/android/navi-common/src/main/java/com/navi/common/commoncomposables/ui/AlchemistWidgetListRenderer.kt similarity index 94% rename from android/app/src/main/java/com/naviapp/alchemist/widgetfactory/AlchemistWidgetListRenderer.kt rename to android/navi-common/src/main/java/com/navi/common/commoncomposables/ui/AlchemistWidgetListRenderer.kt index 2c2f9679ea..fc6a3debfe 100644 --- a/android/app/src/main/java/com/naviapp/alchemist/widgetfactory/AlchemistWidgetListRenderer.kt +++ b/android/navi-common/src/main/java/com/navi/common/commoncomposables/ui/AlchemistWidgetListRenderer.kt @@ -5,7 +5,7 @@ * */ -package com.naviapp.alchemist.widgetfactory +package com.navi.common.commoncomposables.ui import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column @@ -15,8 +15,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import com.navi.ap.utils.constants.SCROLL_TO_WIDGET import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition +import com.navi.common.constants.SCROLL_TO_WIDGET import com.navi.uitron.model.UiTronResponse import com.navi.uitron.viewmodel.UiTronViewModel diff --git a/android/app/src/main/java/com/naviapp/alchemist/widgetfactory/AlchemistWidgetRenderer.kt b/android/navi-common/src/main/java/com/navi/common/commoncomposables/ui/AlchemistWidgetRenderer.kt similarity index 95% rename from android/app/src/main/java/com/naviapp/alchemist/widgetfactory/AlchemistWidgetRenderer.kt rename to android/navi-common/src/main/java/com/navi/common/commoncomposables/ui/AlchemistWidgetRenderer.kt index 167616819a..2d232c8074 100644 --- a/android/app/src/main/java/com/naviapp/alchemist/widgetfactory/AlchemistWidgetRenderer.kt +++ b/android/navi-common/src/main/java/com/navi/common/commoncomposables/ui/AlchemistWidgetRenderer.kt @@ -5,7 +5,7 @@ * */ -package com.naviapp.alchemist.widgetfactory +package com.navi.common.commoncomposables.ui import androidx.compose.runtime.Composable import com.navi.common.alchemist.model.AlchemistWidgetModelDefinition diff --git a/android/navi-common/src/main/java/com/navi/common/constants/Constants.kt b/android/navi-common/src/main/java/com/navi/common/constants/Constants.kt index a9bc297a52..d26b319b2d 100644 --- a/android/navi-common/src/main/java/com/navi/common/constants/Constants.kt +++ b/android/navi-common/src/main/java/com/navi/common/constants/Constants.kt @@ -26,11 +26,13 @@ const val MONTHLY = "Monthly" const val MONTHLY_SMALL = "monthly" const val DAY = "day" const val MONTH = "month" +const val YEAR = "year" const val ENABLED_SMALL = "enabled" const val DISABLED_SMALL = "disabled" const val APP_UPGRADE_DATA = "APP_UPGRADE_DATA" const val QA = "qa" const val SCROLL_TO_TOP_STATE_KEY = "ScrollToTopStateKey_" +const val SCROLL_TO_WIDGET = "scroll_to_widget" // Chat constants const val HELP_CTA_TEXT = "HELP" diff --git a/android/navi-common/src/main/java/com/navi/common/extensions/Ext.kt b/android/navi-common/src/main/java/com/navi/common/extensions/Ext.kt index 50b978fdef..10852d452c 100644 --- a/android/navi-common/src/main/java/com/navi/common/extensions/Ext.kt +++ b/android/navi-common/src/main/java/com/navi/common/extensions/Ext.kt @@ -67,3 +67,5 @@ fun JSONObject.toMap(): Map { fun String.removeSpaces(): String { return this.replace(Regex("\\s+"), EMPTY) } + +fun T?.or(value: T) = this ?: value diff --git a/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt b/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt index aca51ebe6d..dde54df65f 100644 --- a/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt +++ b/android/navi-common/src/main/java/com/navi/common/firebaseremoteconfig/FirebaseRemoteConfigHelper.kt @@ -209,6 +209,8 @@ object FirebaseRemoteConfigHelper { const val RETRY_INTERCEPTOR_ENABLED = "RETRY_INTERCEPTOR_ENABLED" const val ROOT_CA_ADDITION_TARGET_SDK = "ROOT_CA_ADDITION_TARGET_SDK" + const val MONEY_MANAGER_DASHBOARD_SPEND_CATEGORIZATION_TIMEOUT = + "MONEY_MANAGER_DASHBOARD_SPEND_CATEGORIZATION_TIMEOUT" const val ANR_MONITOR_ENABLED_EXPERIMENT = "ANR_MONITOR_ENABLED_EXPERIMENT" @@ -240,6 +242,9 @@ object FirebaseRemoteConfigHelper { // CRM const val CRM_WEB_VIEW_ENABLED = "CRM_WEB_VIEW_ENABLED" + // MM + const val ENABLE_MM_MVI_EVENT_LOGGING = "ENABLE_MM_MVI_EVENT_LOGGING" + private fun getFirebaseRemoteConfig(): FirebaseRemoteConfig { val remoteConfig = Firebase.remoteConfig val configSettings = remoteConfigSettings { diff --git a/android/navi-common/src/main/java/com/navi/common/model/ModuleNameV2.kt b/android/navi-common/src/main/java/com/navi/common/model/ModuleNameV2.kt index c2a86868ea..18de8fdf84 100644 --- a/android/navi-common/src/main/java/com/navi/common/model/ModuleNameV2.kt +++ b/android/navi-common/src/main/java/com/navi/common/model/ModuleNameV2.kt @@ -28,5 +28,6 @@ enum class ModuleNameV2 { BBPS, GOLD, ALCHEMIST, - CRM + CRM, + MONEY_MANAGER, } diff --git a/android/navi-common/src/main/java/com/navi/common/model/NetworkInfo.kt b/android/navi-common/src/main/java/com/navi/common/model/NetworkInfo.kt index e9692c6e40..5d7fed11d3 100644 --- a/android/navi-common/src/main/java/com/navi/common/model/NetworkInfo.kt +++ b/android/navi-common/src/main/java/com/navi/common/model/NetworkInfo.kt @@ -38,5 +38,6 @@ enum class ModuleName { PG, // For Payments KRUZ_PROXY, CDS, // for device data - SAPHYRA // for marketing data after login to RnR + SAPHYRA, // for marketing data after login to RnR + MONEY_MANAGER, } diff --git a/android/navi-common/src/main/java/com/navi/common/navigation/NavArgs.kt b/android/navi-common/src/main/java/com/navi/common/navigation/NavArgs.kt index c6d7f133ba..596c0a6ec9 100644 --- a/android/navi-common/src/main/java/com/navi/common/navigation/NavArgs.kt +++ b/android/navi-common/src/main/java/com/navi/common/navigation/NavArgs.kt @@ -16,6 +16,7 @@ import com.navi.base.model.CtaData * @property ctaData The [CtaData] associated with the navigation action. * @property bundle Additional [Bundle] data to be passed along with the navigation action * (optional). + * @property screenData The [NavigationScreenData] needed for the Screen Destination (optional). * @property finish Indicates whether the current activity should be finished after navigation * (default is false). * @property needsResult Indicates whether the navigation action requires a result (optional). @@ -26,6 +27,7 @@ import com.navi.base.model.CtaData data class NavArgs( val ctaData: CtaData, val bundle: Bundle? = null, + val screenData: NavigationScreenData? = null, val finish: Boolean = false, val needsResult: Boolean? = null, val requestCode: Int? = null, diff --git a/android/navi-common/src/main/java/com/navi/common/navigation/NavigationScreenData.kt b/android/navi-common/src/main/java/com/navi/common/navigation/NavigationScreenData.kt new file mode 100644 index 0000000000..6de0919495 --- /dev/null +++ b/android/navi-common/src/main/java/com/navi/common/navigation/NavigationScreenData.kt @@ -0,0 +1,14 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.common.navigation + +/** + * If a screen destination has a data class that is used for its invocation, it must implement this + * interface. + */ +interface NavigationScreenData diff --git a/android/navi-common/src/main/java/com/navi/common/navigation/navigator/GenericNavigator.kt b/android/navi-common/src/main/java/com/navi/common/navigation/navigator/GenericNavigator.kt index b6b01fe6f7..60327c9298 100644 --- a/android/navi-common/src/main/java/com/navi/common/navigation/navigator/GenericNavigator.kt +++ b/android/navi-common/src/main/java/com/navi/common/navigation/navigator/GenericNavigator.kt @@ -63,8 +63,9 @@ open class GenericNavigator( val composableDirection = composableRegistry.findDirectionByName( - secondIdentifier.orElse(firstIdentifier.orEmpty()), - navArgs?.bundle + destinationName = secondIdentifier.orElse(firstIdentifier.orEmpty()), + bundle = navArgs?.bundle, + data = navArgs?.screenData ) if (activityClass?.name.equals(activity.localClassName)) { diff --git a/android/navi-common/src/main/java/com/navi/common/navigation/registry/ComposableRegistry.kt b/android/navi-common/src/main/java/com/navi/common/navigation/registry/ComposableRegistry.kt index a39f2f9aec..df326532f5 100644 --- a/android/navi-common/src/main/java/com/navi/common/navigation/registry/ComposableRegistry.kt +++ b/android/navi-common/src/main/java/com/navi/common/navigation/registry/ComposableRegistry.kt @@ -9,6 +9,7 @@ package com.navi.common.navigation.registry import android.os.Bundle import androidx.compose.runtime.Composable +import com.navi.common.navigation.NavigationScreenData import com.ramcosta.composedestinations.spec.DestinationSpec import com.ramcosta.composedestinations.spec.Direction @@ -26,7 +27,11 @@ interface ComposableRegistry { * pass data to the destination [Composable]. * @return The [Direction] instance representing the [Composable], or null if not found. */ - fun findDirectionByName(destinationName: String, bundle: Bundle?): Direction? + fun findDirectionByName( + destinationName: String, + bundle: Bundle?, + data: NavigationScreenData? = null + ): Direction? fun findDestinationByName(destinationName: String): DestinationSpec<*>? { return null diff --git a/android/navi-common/src/main/java/com/navi/common/navigation/utils/NavigatorFacade.kt b/android/navi-common/src/main/java/com/navi/common/navigation/utils/NavigatorFacade.kt index dcc96ff6a2..e030429181 100644 --- a/android/navi-common/src/main/java/com/navi/common/navigation/utils/NavigatorFacade.kt +++ b/android/navi-common/src/main/java/com/navi/common/navigation/utils/NavigatorFacade.kt @@ -85,7 +85,9 @@ class NavigatorFacade @Inject constructor() { activity = activity, ctaData = navArgs.ctaData, bundle = navArgs.bundle, - finish = navArgs.finish.orFalse() + finish = navArgs.finish.orFalse(), + requestCode = navArgs.requestCode, + needsResult = navArgs.needsResult ) } } diff --git a/android/navi-common/src/main/java/com/navi/common/pushnotification/NotificationConstants.kt b/android/navi-common/src/main/java/com/navi/common/pushnotification/NotificationConstants.kt index bc43c15151..4bf2e97f59 100644 --- a/android/navi-common/src/main/java/com/navi/common/pushnotification/NotificationConstants.kt +++ b/android/navi-common/src/main/java/com/navi/common/pushnotification/NotificationConstants.kt @@ -36,6 +36,7 @@ object NotificationConstants { const val CLEAR_ALL_SHARED_DB = "clearAllSharedDb" const val CLEAR_SHARED_DB_WITH_KEYS = "clearSharedDbWithKeys" const val SHARED_DB_KEYS = "shared_db_keys" + const val CLEAR_MONEY_MANAGER_DB = "clear_mm_db" // Cache clear constants const val REMOVE_SPECIFIC_IMAGE_CACHE = "removeSpecificImageCache" diff --git a/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/utils/PeriodicTaskScheduler.kt b/android/navi-common/src/main/java/com/navi/common/scheduler/PeriodicTaskScheduler.kt similarity index 68% rename from android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/utils/PeriodicTaskScheduler.kt rename to android/navi-common/src/main/java/com/navi/common/scheduler/PeriodicTaskScheduler.kt index ceb9b0539f..f81055ee00 100644 --- a/android/application-platform/navi-ap/src/main/kotlin/com/navi/ap/utils/PeriodicTaskScheduler.kt +++ b/android/navi-common/src/main/java/com/navi/common/scheduler/PeriodicTaskScheduler.kt @@ -5,12 +5,11 @@ * */ -package com.navi.ap.utils +package com.navi.common.scheduler import com.navi.base.utils.orFalse -import com.navi.common.utils.Constants.ERROR_MESSAGE import com.navi.common.utils.log -import kotlinx.coroutines.CoroutineDispatcher +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -29,7 +28,7 @@ class PeriodicTaskScheduler( private val taskIntervalSeconds: Long = TASK_REPEAT_INTERVAL_SECONDS, private val maxAttempts: Int = TASK_RETRY_COUNT, private val onTimeout: (() -> Unit)? = null, - private val task: () -> Unit + private val task: suspend () -> Unit ) { private var job: Job? = null @@ -50,31 +49,19 @@ class PeriodicTaskScheduler( } onTimeout?.invoke() } catch (e: Exception) { - logApEvent( - Pair(ERROR_MESSAGE, e.message.orEmpty()), - eventName = COROUTINE_JOB_ON_COMPLETION_CALLED - ) e.log() } } fun startTask( - coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO, + coroutineDispatcher: CoroutineContext = Dispatchers.IO, onResult: (() -> Unit)? = null ) { stopTask() job = - CoroutineScope(coroutineDispatcher + exceptionHandler) - .launch { scheduleTask().collect { onResult?.invoke() } } - .apply { - invokeOnCompletion { - val exception: Exception? = it?.cause as? Exception - logApEvent( - Pair(ERROR_MESSAGE, exception?.message.orEmpty()), - eventName = COROUTINE_JOB_ON_COMPLETION_CALLED - ) - } - } + CoroutineScope(coroutineDispatcher + exceptionHandler).launch { + scheduleTask().collect { onResult?.invoke() } + } } fun stopTask() { @@ -86,10 +73,9 @@ class PeriodicTaskScheduler( fun isJobActive() = job?.isActive.orFalse() - companion object { + private companion object { const val INITIAL_DELAY_FOR_PERIODIC_TASK_SECONDS = 5L const val TASK_REPEAT_INTERVAL_SECONDS = 10L const val TASK_RETRY_COUNT = 24 - const val COROUTINE_JOB_ON_COMPLETION_CALLED = "COROUTINE_JOB_ON_COMPLETION_CALLED" } } diff --git a/android/navi-common/src/main/java/com/navi/common/scheduler/TaskRepeater.kt b/android/navi-common/src/main/java/com/navi/common/scheduler/TaskRepeater.kt new file mode 100644 index 0000000000..1825a30a66 --- /dev/null +++ b/android/navi-common/src/main/java/com/navi/common/scheduler/TaskRepeater.kt @@ -0,0 +1,53 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.common.scheduler + +import kotlinx.coroutines.delay + +class TaskRepeater( + private val initialDelaySeconds: Long = INITIAL_DELAY_FOR_PERIODIC_TASK_SECONDS, + private val taskIntervalSeconds: Long = TASK_REPEAT_INTERVAL_SECONDS, + private val maxAttempts: Int = TASK_RETRY_COUNT, + private val onTimeout: (() -> Unit)? = null, + private val onStopped: (() -> Unit)? = null, + private val task: suspend () -> Unit +) { + private var isTaskActive = false + + suspend fun startTask() { + if (isTaskActive) return // Prevent multiple starts + + isTaskActive = true + + delay(initialDelaySeconds * 1000) + repeat(maxAttempts) { attempt -> + if (!isTaskActive) return // Stop immediately if task is stopped + task() + if (attempt < maxAttempts - 1) { + delay(taskIntervalSeconds * 1000) + } + } + if (isTaskActive) { + isTaskActive = false + onTimeout?.invoke() + } + } + + fun stopTask() { + if (isTaskActive) { + isTaskActive = false + onStopped?.invoke() + } + } + + private companion object { + const val INITIAL_DELAY_FOR_PERIODIC_TASK_SECONDS = 5L + const val TASK_REPEAT_INTERVAL_SECONDS = 10L + const val TASK_RETRY_COUNT = 24 + } +} diff --git a/android/navi-common/src/main/java/com/navi/common/uitron/deserializer/UiTronTriggerApiActionDeserializer.kt b/android/navi-common/src/main/java/com/navi/common/uitron/deserializer/UiTronTriggerApiActionDeserializer.kt index 20188145f9..ec0e1af20b 100644 --- a/android/navi-common/src/main/java/com/navi/common/uitron/deserializer/UiTronTriggerApiActionDeserializer.kt +++ b/android/navi-common/src/main/java/com/navi/common/uitron/deserializer/UiTronTriggerApiActionDeserializer.kt @@ -22,6 +22,7 @@ import com.navi.common.uitron.model.action.GetNextScreenApiAction import com.navi.common.uitron.model.action.GetScreenDefinitionApiAction import com.navi.common.uitron.model.action.InitiatePayAmountAction import com.navi.common.uitron.model.action.LambdaApiAction +import com.navi.common.uitron.model.action.MMFetchFinarkeinData import com.navi.common.uitron.model.action.PartialFillCallAction import com.navi.common.uitron.model.action.RedeemCoinAction import com.navi.common.uitron.model.action.RewardsAndReferralApiAction @@ -83,6 +84,8 @@ class UiTronTriggerApiActionDeserializer : BaseUiTronTriggerApiActionDeserialize context?.deserialize(jsonObject, CycsPostConsentApiAction::class.java) ApiType.CycsGetWebTokenUrl.name -> context?.deserialize(jsonObject, CycsGetWebTokenUrlApiAction::class.java) + ApiType.MMFetchFinarkeinData.name -> + context?.deserialize(jsonObject, MMFetchFinarkeinData::class.java) else -> super.deserialize(json, typeOfT, context) } } diff --git a/android/navi-common/src/main/java/com/navi/common/uitron/model/action/TriggerApiActions.kt b/android/navi-common/src/main/java/com/navi/common/uitron/model/action/TriggerApiActions.kt index c70458d947..ef77bf3478 100644 --- a/android/navi-common/src/main/java/com/navi/common/uitron/model/action/TriggerApiActions.kt +++ b/android/navi-common/src/main/java/com/navi/common/uitron/model/action/TriggerApiActions.kt @@ -52,6 +52,8 @@ class CycsGetNextActionApiAction : TriggerApiAction() class CycsGetWebTokenUrlApiAction : TriggerApiAction() +class MMFetchFinarkeinData : TriggerApiAction() + enum class ApiType { GetApplicationId, GetScreenDefinition, @@ -73,7 +75,8 @@ enum class ApiType { CycsGetWebTokenUrl, FetchUserRewardsDataAction, FetchUsersReferralDataAction, - GetScreenOverlayData + GetScreenOverlayData, + MMFetchFinarkeinData } enum class SourceType { diff --git a/android/navi-common/src/main/java/com/navi/common/uitron/serializer/UiTronTriggerApiActionSerializer.kt b/android/navi-common/src/main/java/com/navi/common/uitron/serializer/UiTronTriggerApiActionSerializer.kt index 0a4ef58e8a..3ee8826f9d 100644 --- a/android/navi-common/src/main/java/com/navi/common/uitron/serializer/UiTronTriggerApiActionSerializer.kt +++ b/android/navi-common/src/main/java/com/navi/common/uitron/serializer/UiTronTriggerApiActionSerializer.kt @@ -17,6 +17,7 @@ import com.navi.common.uitron.model.action.GetNextScreenApiAction import com.navi.common.uitron.model.action.GetScreenDefinitionApiAction import com.navi.common.uitron.model.action.InitiatePayAmountAction import com.navi.common.uitron.model.action.LambdaApiAction +import com.navi.common.uitron.model.action.MMFetchFinarkeinData import com.navi.common.uitron.model.action.PartialFillCallAction import com.navi.common.uitron.model.action.SdkExitAction import com.navi.common.uitron.model.action.SubmitFeedbackAction @@ -61,6 +62,9 @@ class UiTronTriggerApiActionSerializer : BaseUiTronTriggerApiActionSerializer() ApiType.SdkExitAction.name -> { context?.serialize(src, SdkExitAction::class.java) } + ApiType.MMFetchFinarkeinData.name -> { + context?.serialize(src, MMFetchFinarkeinData::class.java) + } else -> super.serialize(src, typeOfSrc, context) } } diff --git a/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt b/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt index 00b5ce2831..ae5f3316ce 100644 --- a/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/Constants.kt @@ -7,6 +7,8 @@ package com.navi.common.utils +import kotlin.time.Duration.Companion.milliseconds + object Constants { const val APP = "APP" const val BUSINESS_VERTICAL_GOLD = "GOLD" @@ -354,6 +356,8 @@ object Constants { const val ROTATING_VIEW_ANIMATION = "rotating view animation" const val SCROLL_FADE = "scroll_fade" + val DEFAULT_ON_CLICK_DEBOUNCE_TIME = 500.milliseconds + object ScreenLockConstants { const val LOGIN_SESSION_ID = "LOGIN_SESSION_ID" const val X_IS_SCREEN_LOCK_ENABLED = "X-IS-SCREEN-LOCK-ENABLED" diff --git a/android/navi-common/src/main/java/com/navi/common/utils/Ext.kt b/android/navi-common/src/main/java/com/navi/common/utils/Ext.kt index ede35bd36b..1b1a0034f5 100644 --- a/android/navi-common/src/main/java/com/navi/common/utils/Ext.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/Ext.kt @@ -34,6 +34,17 @@ import android.widget.EditText import android.widget.TextView import android.widget.Toast import androidx.annotation.StringRes +import androidx.compose.foundation.Indication +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed import androidx.compose.ui.graphics.Path import androidx.core.content.ContextCompat import androidx.core.content.FileProvider @@ -62,6 +73,7 @@ import com.navi.common.customview.CustomTypefaceSpan import com.navi.common.listeners.ClickableTextListener import com.navi.common.network.models.RepoResult import com.navi.common.network.retrofit.RetrofitService +import com.navi.common.utils.Constants.DEFAULT_ON_CLICK_DEBOUNCE_TIME import com.navi.design.font.FontWeightEnum import com.navi.design.font.getFontStyle import com.navi.naviwidgets.extensions.getOrientation @@ -70,6 +82,13 @@ import java.io.ByteArrayOutputStream import java.io.File import java.util.Locale import java.util.zip.GZIPOutputStream +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.time.Duration +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import okhttp3.Request import org.json.JSONObject import retrofit2.Invocation @@ -621,3 +640,35 @@ fun JsonObject.optString(key: String, defaultValue: String = ""): String = fun JsonObject.optBoolean(key: String, defaultValue: Boolean = false): Boolean = this.takeIf { it.has(key) && !it.get(key).isJsonNull }?.get(key)?.asBoolean ?: defaultValue + +@Composable +fun Modifier.onClickWithDebounce( + debounceInMillis: Duration = DEFAULT_ON_CLICK_DEBOUNCE_TIME, + interactionSource: MutableInteractionSource? = remember { MutableInteractionSource() }, + indication: Indication? = remember { ripple() }, + onClick: () -> Unit +): Modifier = composed { + var lastClickedTimeStamp by remember { mutableLongStateOf(0) } + clickable(indication = indication, interactionSource = interactionSource) { + val currentTimeStamp = System.currentTimeMillis() + if (currentTimeStamp - lastClickedTimeStamp >= debounceInMillis.inWholeMilliseconds) { + onClick() + lastClickedTimeStamp = currentTimeStamp + } + } +} + +fun CoroutineScope.safeLaunch( + coroutineContext: CoroutineContext = EmptyCoroutineContext, + launchBody: suspend () -> Unit +): Job { + return launch( + if (coroutineContext[CoroutineExceptionHandler.Key] == null) { + coroutineContext + CoroutineExceptionHandler { _, throwable -> throwable.log() } + } else { + coroutineContext + } + ) { + launchBody.invoke() + } +} diff --git a/android/navi-common/src/main/java/com/navi/common/utils/FirebaseEventFacade.kt b/android/navi-common/src/main/java/com/navi/common/utils/FirebaseEventFacade.kt index fb91b374b8..a1a2cd0255 100644 --- a/android/navi-common/src/main/java/com/navi/common/utils/FirebaseEventFacade.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/FirebaseEventFacade.kt @@ -15,7 +15,7 @@ import dagger.hilt.android.scopes.ViewModelScoped import javax.inject.Inject @ViewModelScoped -class FireabaseEventFacade @Inject constructor() { +class FirebaseEventFacade @Inject constructor() { fun setCurrentScreen(activity: Activity, screenName: String) { FcmAnalyticsUtil.analytics.firebaseAnalyticsInstance.logEvent( FirebaseAnalytics.Event.SCREEN_VIEW, diff --git a/android/navi-common/src/main/java/com/navi/common/utils/Keys.kt b/android/navi-common/src/main/java/com/navi/common/utils/Keys.kt index b1abb63f04..66d0dcabbf 100644 --- a/android/navi-common/src/main/java/com/navi/common/utils/Keys.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/Keys.kt @@ -13,3 +13,4 @@ const val SPACE = " " const val RETRY = "retry" const val ERROR = "error" const val SEPARATOR = "__" +const val PERCENT = "%" diff --git a/android/navi-common/src/main/java/com/navi/common/utils/Utility.kt b/android/navi-common/src/main/java/com/navi/common/utils/Utility.kt index d58f8877d2..b602879d2d 100644 --- a/android/navi-common/src/main/java/com/navi/common/utils/Utility.kt +++ b/android/navi-common/src/main/java/com/navi/common/utils/Utility.kt @@ -100,6 +100,9 @@ import okhttp3.MultipartBody import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody +val monthAbbreviations = + arrayOf("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") + fun isNetworkAvailable(): Boolean { return BaseUtils.isNetworkAvailable(AppServiceManager.application) } @@ -159,21 +162,7 @@ fun getDateSuffix(dayMonth: Int): String { fun getMonthPrefix(date: Date): String { val calendar = Calendar.getInstance() calendar.time = date - return when (calendar.get(Calendar.MONTH)) { - 0 -> "Jan" - 1 -> "Feb" - 2 -> "Mar" - 3 -> "Apr" - 4 -> "May" - 5 -> "Jun" - 6 -> "Jul" - 7 -> "Aug" - 8 -> "Sep" - 9 -> "Oct" - 10 -> "Nov" - 11 -> "Dec" - else -> EMPTY - } + return monthAbbreviations.getOrNull(calendar.get(Calendar.MONTH)) ?: EMPTY } fun getWeekDaysFirstLetters(): List { diff --git a/android/navi-rr/src/main/res/drawable/something_went_wrong_triangle.xml b/android/navi-common/src/main/res/drawable/something_went_wrong_triangle.xml similarity index 100% rename from android/navi-rr/src/main/res/drawable/something_went_wrong_triangle.xml rename to android/navi-common/src/main/res/drawable/something_went_wrong_triangle.xml diff --git a/android/navi-common/src/main/res/xml/default_remote_config.xml b/android/navi-common/src/main/res/xml/default_remote_config.xml index 76f46956d8..fe3c58e345 100644 --- a/android/navi-common/src/main/res/xml/default_remote_config.xml +++ b/android/navi-common/src/main/res/xml/default_remote_config.xml @@ -618,4 +618,12 @@ NAVI_GOLD_RETRY_POLICY_ENABLED false + + ENABLE_MM_MVI_EVENT_LOGGING + true + + + MONEY_MANAGER_DASHBOARD_SPEND_CATEGORIZATION_TIMEOUT + 40000 + \ No newline at end of file diff --git a/android/navi-money-manager/.gitignore b/android/navi-money-manager/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/android/navi-money-manager/.gitignore @@ -0,0 +1 @@ +/build diff --git a/android/navi-money-manager/build.gradle b/android/navi-money-manager/build.gradle new file mode 100644 index 0000000000..e0c5e8727d --- /dev/null +++ b/android/navi-money-manager/build.gradle @@ -0,0 +1,82 @@ +plugins { + alias libs.plugins.android.library + alias libs.plugins.hilt.android + alias libs.plugins.kotlin.android + alias libs.plugins.kotlin.compose + alias libs.plugins.kotlin.parcelize + alias libs.plugins.ksp +} + +android { + namespace 'com.navi.moneymanager' + compileSdk 34 + + defaultConfig { + minSdk 24 + targetSdk 34 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures { + compose true + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + benchmark { + initWith release + matchingFallbacks = ['release'] + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + freeCompilerArgs += ["-Xstring-concat=inline", "-Xcontext-receivers"] + jvmTarget = '17' + } + flavorDimensions = ["app"] + productFlavors { + qa { + isDefault true + dimension "app" + } + prod { + dimension "app" + } + } +} + +dependencies { + implementation project(':navi-code') + implementation project(":navi-common") + implementation libs.accompanist.permissions + implementation libs.accompanist.systemuicontroller + implementation libs.android.material + implementation libs.androidx.appcompat + implementation libs.androidx.compose.material3 + implementation libs.androidx.core.ktx + implementation libs.androidx.lifecycle.viewmodel.ktx + implementation libs.androidx.paging.compose + implementation libs.androidx.room.paging + implementation libs.dagger.hiltAndroid + implementation libs.raamcosta.composeDestinations.animation.core + implementation libs.androidx.activity + implementation libs.zetetic.sqlcipher.android + + ksp project(':navi-code') + ksp libs.androidx.hilt.compiler + ksp libs.androidx.room.compiler + ksp libs.dagger.hiltCompiler + ksp libs.raamcosta.composeDestinations.ksp + + androidTestImplementation libs.androidx.test.junit + + testImplementation libs.junit + testImplementation libs.kotlinx.coroutines.test + testImplementation libs.mockk +} diff --git a/android/navi-money-manager/proguard-rules.pro b/android/navi-money-manager/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/android/navi-money-manager/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/navi-money-manager/src/main/AndroidManifest.xml b/android/navi-money-manager/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..4906e5a06a --- /dev/null +++ b/android/navi-money-manager/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/base/viewmodel/MMBaseViewModel.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/base/viewmodel/MMBaseViewModel.kt new file mode 100644 index 0000000000..9782edb8ac --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/base/viewmodel/MMBaseViewModel.kt @@ -0,0 +1,50 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.base.viewmodel + +import androidx.annotation.Keep +import com.navi.common.basemvi.BaseMviViewModel +import com.navi.common.basemvi.BaseReducer +import com.navi.common.basemvi.UiEffect +import com.navi.common.basemvi.UiEvent +import com.navi.common.basemvi.UiState +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.ENABLE_MM_MVI_EVENT_LOGGING +import com.navi.moneymanager.common.analytics.MviEventTrackerImpl +import kotlin.coroutines.CoroutineContext + +@Keep +open class MMBaseViewModel( + initialState: State, + reducer: BaseReducer, +) : BaseMviViewModel(initialState, reducer) { + + override fun setEffect(dispatcher: CoroutineContext?, effect: () -> Effect) { + super.setEffect(dispatcher = dispatcher, effect = { effect() }) + if (isMviEventLoggingEnabled()) { + MviEventTrackerImpl.mviSetEffect( + viewmodel = this::class.simpleName.orEmpty(), + effect = effect().javaClass.simpleName.orEmpty() + ) + } + } + + override fun sendEvent(event: Event) { + super.sendEvent(event) + if (isMviEventLoggingEnabled()) { + MviEventTrackerImpl.mviSendEvent( + viewmodel = this::class.simpleName.orEmpty(), + event = event::class.simpleName.orEmpty() + ) + } + } + + private fun isMviEventLoggingEnabled(): Boolean { + return FirebaseRemoteConfigHelper.getBoolean(ENABLE_MM_MVI_EVENT_LOGGING) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/analytics/MMAnalytics.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/analytics/MMAnalytics.kt new file mode 100644 index 0000000000..5bc8190d75 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/analytics/MMAnalytics.kt @@ -0,0 +1,763 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.analytics + +import com.navi.code.annotations.AutoGenerate +import com.navi.code.annotations.EventName + +@AutoGenerate +interface AccountLinkingEventTracker { + + @EventName("mm_dev_account_link_success") fun onAccountLinkingSuccess() + + @EventName("mm_dev_account_link_error") fun onAccountLinkingFailure() + + @EventName("mm_dev_existing_accounts_and_config_fetched") + fun onExistingAccountsAndConfigFetched() + + @EventName("mm_dev_account_link_polling_started") fun onPollingStarted() +} + +@AutoGenerate +interface DataSyncEventTracker { + + @EventName("mm_dev_sync_s_flow_triggered") + fun sFlowTriggered( + currentTimestamp: String, + threshold: Long, + lastRefreshTimestamp: Long, + ) + + @EventName("mm_dev_sync_rs_flow_triggered") + fun rsFlowTriggered( + currentTimestamp: String, + threshold: Long, + lastRefreshTimestamp: Long, + ) + + @EventName("mm_dev_sync_refresh_started") + fun refreshStarted(requestId: String, lastRefreshTimestamp: Long) + + @EventName("mm_dev_sync_periodic_task_scheduler_created") fun periodicTaskSchedulerCreated() + + @EventName("mm_dev_sync_polling_started") fun pollingStarted() + + @EventName("mm_dev_sync_polling_stopped") fun pollingStopped() + + @EventName("mm_dev_sync_polling_timeout") fun pollingTimeout() + + @EventName("mm_dev_sync_db_sync_executor_init") fun dbSyncExecutorInit() + + @EventName("mm_dev_sync_db_sync_executor_triggered") fun dbSyncExecutorTriggered() + + @EventName("mm_dev_sync_success") fun syncSuccess(type: String) + + @EventName("mm_dev_sync_started") fun syncStarted(type: String) + + @EventName("mm_dev_sync_not_triggered") + fun syncNotTriggered(type: String, pollingStatus: String) + + @EventName("mm_dev_sync_accounts_failed") fun accountSyncFailed(type: String) + + @EventName("mm_dev_sync_transactions_failed") fun transactionSyncFailed(type: String) +} + +@AutoGenerate +interface FinarkeinEventTracker { + + @EventName("mm_finarkein_user_exit") fun finarkeinUserExit(exitData: Any) + + @EventName("mm_finarkein_success") fun finarkeinSuccess(successData: Any) + + @EventName("mm_finarkein_error") fun finarkeinError(errorData: Any) + + @EventName("mm_finarkein_journey_event") fun finarkeinJourneyEvent(eventData: Any) +} + +@AutoGenerate +interface MviEventTracker { + + @EventName("mm_dev_mvi_send_event") fun mviSendEvent(viewmodel: Any, event: Any) + + @EventName("mm_dev_mvi_set_effect") fun mviSetEffect(viewmodel: Any, effect: Any) +} + +@AutoGenerate +interface DataProviderEventTracker { + @EventName("mm_dev_dashboard_dp_bank_section_loading") fun dashboardDpBankSectionLoading() + + @EventName("mm_dev_dashboard_dp_bank_section_loaded") + fun dashboardDpBankSectionLoaded(accountsListSize: Any, isTotalSyncCompleted: Any) + + @EventName("mm_dev_dashboard_dp_bank_section_emit") + fun dashboardDpBankSectionEmission( + isCurrentMonthSynced: Boolean, + isTotalSyncCompleted: Boolean, + accountsListSize: Int + ) + + @EventName("mm_dev_dashboard_dp_spend_analysis_section_emit") + fun dashboardDpSpendAnalysisSectionEmission(month: Any, year: Any) + + @EventName("mm_dev_dashboard_dp_recent_transactions_section_emit") + fun dashboardDpRecentTransactionSectionEmission( + isFirstMonthSyncCompleted: Any, + isTotalSyncCompleted: Any + ) + + @EventName("mm_dev_dashboard_vm_bank_section_collect") + fun dashboardVmBankSectionCollect(isFirstMonthSyncCompleted: Any, bankSection: Any) + + @EventName("mm_dev_dashboard_vm_spend_analysis_section_collect") + fun dashboardVmSpendAnalysisSectionCollect(stateName: String) + + @EventName("mm_dev_dashboard_vm_recent_transactions_section_collect") + fun dashboardVmRecentTransactionSectionCollect(stateName: String) + + @EventName("mm_dev_dashboard_dp_spend_analysis_loading") + fun dashboardDpSpendAnalysisSectionLoading(month: Any, year: Any, isTotalSyncCompleted: Any) + + @EventName("mm_dev_dashboard_dp_spend_analysis_loaded") + fun dashboardDpSpendAnalysisSectionLoaded(month: Any, year: Any, isTotalSyncCompleted: Any) + + @EventName("mm_dev_dashboard_dp_spend_analysis_empty") + fun dashboardDpSpendAnalysisSectionEmpty(month: Any, year: Any, isTotalSyncCompleted: Any) + + @EventName("mm_dev_dashboard_vm_spend_analysis_update") + fun dashboardVmSpendAnalysisParamsUpdate(month: Any, year: Any) + + @EventName("mm_dev_dashboard_dp_recent_transactions_loading") + fun dashboardDpRecentTransactionSectionLoading( + isFirstMonthSyncCompleted: Any, + isTotalSyncCompleted: Any, + transactionsListSize: Any + ) + + @EventName("mm_dev_dashboard_dp_recent_transactions_loaded") + fun dashboardDpRecentTransactionSectionLoaded( + isFirstMonthSyncCompleted: Any, + isTotalSyncCompleted: Any, + transactionsListSize: Any + ) + + @EventName("mm_dev_dashboard_dp_recent_transactions_emit") + fun dashboardDpRecentTransactionSectionEmit() + + @EventName("mm_dev_spend_analysis_dp_data") + fun spendAnalysisDpData( + month: Any, + year: Any, + selectedBanksListSize: Any, + transactionsListSize: Any, + accountsListSize: Any, + currentMonthsTransactionsListSize: Any, + categoriesListSize: Any, + isTotalSyncCompleted: Any + ) + + @EventName("mm_dev_spend_analysis_vm_collect") + fun spendAnalysisVmCollect(month: Any, year: Any, selectedBanksListSize: Any) + + @EventName("mm_dev_spend_analysis_vm_params_update") + fun spendAnalysisVmUpdate(month: Any, year: Any, selectedBanksListSize: Any) + + @EventName("mm_dev_category_details_dp_data") + fun categoryDetailsDpData( + month: Any, + year: Any, + selectedCategory: Any, + selectedBanksListSize: Any, + transactionsListSize: Any, + currentMonthsTransactionsListSize: Any, + filteredTransactionsListSize: Any, + accountsListSize: Any, + isTotalSyncCompleted: Any + ) + + @EventName("mm_dev_category_details_vm_collect") + fun categoryDetailsVmCollect( + month: Any, + year: Any, + selectedCategory: Any, + selectedBanksListSize: Any + ) + + @EventName("mm_dev_category_details_vm_params_update") + fun categoryDetailsVmParamsUpdate( + month: Any, + year: Any, + selectedCategory: Any, + selectedBanksListSize: Any + ) + + @EventName("mm_dev_transaction_history_dp_data") + fun transactionHistoryDpData(query: Any, appliedFilters: Any, allFilters: Any) + + @EventName("mm_dev_transaction_history_vm_fetch_data") + fun transactionHistoryVmFetchData(query: Any, filterData: Any) + + @EventName("mm_dev_transaction_history_vm_collect") + fun transactionHistoryVmCollect(monthListSize: Any) + + @EventName("mm_dev_transaction_history_vm_params_update") + fun transactionHistoryVmUpdate(query: Any) + + @EventName("mm_dev_transaction_details_dp_data") + fun transactionDetailsDpData(isAccountFound: Any) + + @EventName("mm_dev_transaction_details_vm_collect") fun transactionDetailsVmCollect() +} + +@AutoGenerate +interface AddCategoryEventTracker { + + @EventName("mm_dev_add_category_bottom_sheet_fetch_data_called") + fun addCategoryBottomSheetFetchDataCalled( + hasOnlyOneSimilarTransaction: String, + counterPartyName: String, + transactionType: String + ) + + @EventName("mm_dev_similar_transactions_bottom_sheet_fetch_data_called") + fun similarTransactionsBottomSheetFetchDataCalled( + similarTransactionListSize: String, + counterPartyName: String, + transactionType: String + ) + + @EventName("mm_dev_post_transaction_category_loading") fun postTransactionCategoryLoading() + + @EventName("mm_dev_post_transaction_category_error") + fun postTransactionCategoryError(errorCode: String, errorMessage: String) + + @EventName("mm_dev_post_transaction_category_success") fun postTransactionCategorySuccess() + + @EventName("mm_dev_similar_txn_bottomsheet_header_back_button_clicked") + fun similarTxnHeaderBackButtonClicked() + + @EventName("mm_dev_system_back_button_clicked") + fun systemBackButtonClicked(bottomsheetType: String) + + @EventName("mm_dev_proceed_footer_button_clicked") + fun proceedFooterButtonClicked(isAutoCategoriseChecked: String, buttonType: String) + + @EventName("mm_dev_add_category_confirm_footer_button_clicked") + fun addCategoryConfirmFooterButtonClicked(selectedCategory: String) +} + +@AutoGenerate +interface ValuePropEventTracker { + + @EventName("mm_value_prop_screen_landed") fun onValuePropScreenLanded() + + @EventName("mm_value_prop_screen_exit") fun onValuePropScreenExit() + + @EventName("mm_value_prop_generic_error_bottom_sheet_appeared") + fun onValuePropGenericErrorBottomSheetAppeared() + + @EventName("mm_value_prop_generic_error_bottom_sheet_disappeared") + fun onValuePropGenericErrorBottomSheetDisappeared() + + @EventName("mm_value_prop_finarkein_error_bottom_sheet_appeared") + fun onValuePropFinarkeinErrorBottomSheetAppeared() + + @EventName("mm_value_prop_finarkein_error_bottom_sheet_disappeared") + fun onValuePropFinarkeinErrorBottomSheetDisappeared() + + @EventName("mm_value_prop_consent_revoked_bottom_sheet_appeared") + fun onValuePropConsentRevokedBottomSheetAppeared() + + @EventName("mm_value_prop_consent_revoked_bottom_sheet_disappeared") + fun onValuePropConsentRevokedBottomSheetDisappeared() +} + +@AutoGenerate +interface DashboardEventTracker { + + @EventName("mm_dashboard_screen_landed") fun onDashboardScreenLanded() + + @EventName("mm_dashboard_screen_exit") fun onDashboardScreenExit() + + @EventName("mm_dashboard_last_refresh_viewed") fun onDashboardLastRefreshViewed() + + @EventName("mm_dashboard_help_clicked") fun onDashboardHelpClicked() + + @EventName("mm_dashboard_bank_accounts_pills_viewed") + fun onDashboardBankAccountsPillsViewed(location: String, pillCount: Int) + + @EventName("mm_dashboard_bank_accounts_pills_clicked") + fun onDashboardBankAccountsPillsClicked(location: String, pillRank: Int) + + @EventName("mm_dashboard_total_spend_viewed") fun onDashboardTotalSpendViewed() + + @EventName("mm_dashboard_category_viewed") + fun onDashboardCategoryViewed(categoryRank: Int, categoryType: String) + + @EventName("mm_dashboard_total_spend_clicked") fun onDashboardTotalSpendClicked() + + @EventName("mm_dashbaord_category_clicked") + fun onDashboardCategoryClicked(categoryRank: Int, categoryType: String) + + @EventName("mm_dashboard_add_account_pill_clicked") + fun onDashboardAddAccountPillClicked(location: String) + + @EventName("mm_dashboard_month_dropdown_viewed") fun onDashboardMonthDropdownViewed() + + @EventName("mm_dashboard_month_clicked") fun onDashboardMonthClicked() + + @EventName("mm_dashboard_past_month_category_refresh_in_progress_bottom_sheet_appeared") + fun onDashboardPastMonthCategoryRefreshInProgressBottomsheetAppeared() + + @EventName("mm_dashboard_past_month_category_refresh_in_progress_bottom_sheet_disappeared") + fun onDashboardPastMonthCategoryRefreshInProgressBottomsheetDisappeared() + + @EventName("mm_post_bank_addition_progress_bottom_sheet_appeared") + fun onDashboardOnboardingLoadingBottomSheetAppeared() + + @EventName("mm_post_bank_addition_taking_long_bottom_sheet_appeared") + fun onDashboardOnboardingTakingLongBottomSheetAppeared() + + @EventName("mm_dashboard_transaction_clicked") + fun onDashboardTransactionClicked(transactionRank: Int) + + @EventName("mm_dashboard_transaction_category_clicked") + fun onDashboardTransactionCategoryClicked(transactionRank: Int, category: String) + + @EventName("mm_dashboard_transaction_category_selection_applied") + fun onDashboardTransactionCategorySelectionApplied( + previousCategory: String, + newCategory: String + ) + + @EventName("mm_dashboard_categorise_similar_transaction_skipped") + fun onDashboardCategoriseSimilarTransactionSkipped(category: String) + + @EventName("mm_dashboard_categorise_similar_transaction_confirmed") + fun onDashboardCategoriseSimilarTransactionConfirmed( + autoCategoriseSelected: Boolean, + allTransactionsSelected: Boolean, + numberOfTransactionsSelected: Int, + category: String + ) + + @EventName("mm_dashboard_month_selection_requested") fun onDashboardMonthSelectionRequested() + + @EventName("mm_dashboard_category_section_view_more_clicked") + fun onDashboardCategorySectionViewMoreClicked() + + @EventName("mm_dashboard_self_transfer_category_clicked") + fun onDashboardSelfTransferCategoryClicked() + + @EventName("mm_dashboard_view_all_transactions_clicked") + fun onDashboardViewAllTransactionsClicked() + + @EventName("mm_dashboard_month_selection_applied") + fun onDashboardMonthSelectionApplied(monthIndex: Int) + + @EventName("mm_dashboard_fetching_transactions_bottom_sheet_appeared") + fun onDashboardFetchingTransactionsBottomSheetAppeared() + + @EventName("mm_dashboard_fetching_transactions_bottom_sheet_disappeared") + fun onDashboardFetchingTransactionsBottomSheetDisappeared() + + @EventName("mm_dashboard_month_selection_bottom_sheet_appeared") + fun onDashboardMonthSelectionBottomSheetAppeared() + + @EventName("mm_dashboard_month_selection_bottom_sheet_disappeared") + fun onDashboardMonthSelectionBottomSheetDisappeared() + + @EventName("mm_dashboard_finarkein_error_bottom_sheet_disappeared") + fun onDashboardFinarkeinErrorBottomSheetDisappeared() + + @EventName("mm_dashboard_finarkein_error_bottom_sheet_appears") + fun onDashboardFinarkeinErrorBottomSheetAppeared() + + @EventName("mm_dashboard_help_bottom_sheet_appeared") fun onDashboardHelpBottomSheetAppeared() + + @EventName("mm_dashboard_help_bottom_sheet_disappeared") + fun onDashboardHelpBottomSheetDisappeared() + + @EventName("mm_dashboard_generic_error_bottom_sheet_appeared") + fun onDashboardGenericErrorBottomSheetAppeared() + + @EventName("mm_dashboard_generic_error_bottom_sheet_disappeared") + fun onDashboardGenericErrorBottomSheetDisappeared() + + @EventName("mm_dashboard_add_account_loading_bottom_sheet_appeared") + fun onDashboardAddAccountLoadingBottomSheetAppeared() + + @EventName("mm_dashboard_add_account_loading_bottom_sheet_disappeared") + fun onDashboardAddAccountLoadingBottomSheetDisappeared() + + @EventName("mm_dashboard_category_selection_bottom_sheet_appeared") + fun onDashboardCategorySelectionBottomSheetAppeared() + + @EventName("mm_dashboard_category_selection_bottom_sheet_disappeared") + fun onDashboardCategorySelectionBottomSheetDisappeared() + + @EventName("mm_dashboard_categorise_similar_transactions_bottom_sheet_appeared") + fun onDashboardCategoriseSimilarTransactionsBottomSheetAppeared() + + @EventName("mm_dashboard_categorise_similar_transactions_bottom_sheet_disappeared") + fun onDashboardCategoriseSimilarTransactionsBottomSheetDisappeared() + + @EventName("mm_dashboard_onboarding_loading_bottom_sheet_disappeared") + fun onDashboardOnboardingLoadingBottomSheetDisappeared() + + @EventName("mm_dashboard_onboarding_taking_long_bottom_sheet_disappeared") + fun onDashboardOnboardingTakingLongBottomSheetDisappeared() + + @EventName("mm_dashboard_onboarding_success_bottom_sheet_appeared") + fun onDashboardOnboardingSuccessBottomSheetAppeared() + + @EventName("mm_dashboard_onboarding_success_bottom_sheet_disappeared") + fun onDashboardOnboardingSuccessBottomSheetDisappeared() + + @EventName("mm_dashboard_help_bottom_sheet_faq_clicked") + fun onDashboardHelpBottomSheetFaqClicked() + + @EventName("mm_dashboard_help_bottom_sheet_manage_consent_clicked") + fun onDashboardHelpBottomSheetManageConsentClicked() +} + +@AutoGenerate +interface LauncherEventTracker { + @EventName("mm_launcher_screen_landed") fun onLauncherScreenLanded() + + @EventName("mm_launcher_screen_exit") fun onLauncherScreenExit() +} + +@AutoGenerate +interface SpendAnalysisEventTracker { + @EventName("mm_spend_analysis_screen_landed") + fun onSpendAnalysisScreenLanded(monthIndex: Int, numberOfBanks: Int) + + @EventName("mm_spend_analysis_screen_exit") fun onSpendAnalysisScreenExit() + + @EventName("mm_spend_analysis_screen_month_selection_requested") + fun onSpendAnalysisScreenMonthSelectionRequested() + + @EventName("mm_spend_analysis_screen_month_selection_applied") + fun onSpendAnalysisScreenMonthSelectionApplied(monthIndex: Int) + + @EventName("mm_spend_analysis_screen_bank_selection_requested") + fun onSpendAnalysisScreenBankSelectionRequested() + + @EventName("mm_spend_analysis_screen_bank_selection_applied") + fun onSpendAnalysisScreenBankSelectionApplied(numberOfSelectedBankAccounts: Int) + + @EventName("mm_spend_analysis_screen_graph_viewed") fun onSpendAnalysisScreenGraphViewed() + + @EventName("mm_spend_analysis_screen_graph_bar_clicked") + fun onSpendAnalysisScreenGraphBarClicked(monthIndex: Int) + + @EventName("mm_spend_analysis_screen_graph_average_info_clicked") + fun onSpendAnalysisScreenGraphAverageInfoClicked() + + @EventName("mm_spend_analysis_screen_category_viewed") + fun onSpendAnalysisCategoryViewed(categoryRank: Int, category: String) + + @EventName("mm_spend_analysis_screen_category_clicked") + fun onSpendAnalysisScreenCategoryClicked(categoryRank: Int, category: String) + + @EventName("mm_spend_analysis_screen_other_categories_clicked") + fun onSpendAnalysisScreenOtherCategoriesClicked() + + @EventName("mm_spend_analysis_screen_view_transaction_history_clicked") + fun onSpendAnalysisScreenViewTransactionHistoryClicked() + + @EventName("mm_spend_analysis_screen_average_info_bottom_sheet_appeared") + fun onSpendAnalysisScreenAverageInfoBottomSheetAppeared() + + @EventName("mm_spend_analysis_screen_average_info_bottom_sheet_disappeared") + fun onSpendAnalysisScreenAverageInfoBottomSheetDisappeared() + + @EventName("mm_spend_analysis_screen_help_clicked") fun onSpendAnalysisScreenHelpClicked() + + @EventName("mm_spend_analysis_screen_other_categories_bottom_sheet_appeared") + fun onSpendAnalysisScreenOtherCategoriesBottomSheetAppeared() + + @EventName("mm_spend_analysis_screen_other_categories_bottom_sheet_disappeared") + fun onSpendAnalysisScreenOtherCategoriesBottomSheetDisappeared() + + @EventName("mm_spend_analysis_screen_other_categories_bottom_sheet_category_clicked") + fun onSpendAnalysisScreenOtherCategoriesBottomSheetCategoryClicked( + categoryRank: Int, + category: String + ) + + @EventName("mm_spend_analysis_screen_other_categories_bottom_sheet_category_viewed") + fun onSpendAnalysisScreenOtherCategoriesBottomSheetCategoryViewed( + categoryRank: Int, + category: String + ) + + @EventName("mm_spend_analysis_screen_other_categories_bottom_sheet_acknowledged") + fun onSpendAnalysisScreenOtherCategoriesBottomSheetAcknowledged() + + @EventName("mm_spend_analysis_screen_average_info_bottom_sheet_acknowledged") + fun onSpendAnalysisScreenAverageInfoBottomSheetAcknowledged() + + @EventName("mm_spend_analysis_month_selection_bottom_sheet_appeared") + fun onSpendAnalysisMonthSelectionBottomSheetAppeared() + + @EventName("mm_spend_analysis_month_selection_bottom_sheet_disappeared") + fun onSpendAnalysisMonthSelectionBottomSheetDisappeared() + + @EventName("mm_spend_analysis_help_bottom_sheet_appeared") + fun onSpendAnalysisHelpBottomSheetAppeared() + + @EventName("mm_spend_analysis_help_bottom_sheet_disappeared") + fun onSpendAnalysisHelpBottomSheetDisappeared() + + @EventName("mm_spend_analysis_past_month_data_loading_bottom_sheet_appeared") + fun onSpendAnalysisPastMonthDataLoadingBottomSheetAppeared() + + @EventName("mm_spend_analysis_past_month_data_loading_bottom_sheet_disappeared") + fun onSpendAnalysisPastMonthDataLoadingBottomSheetDisappeared() + + @EventName("mm_spend_analysis_help_bottom_sheet_faq_clicked") + fun onSpendAnalysisHelpBottomSheetFaqClicked() + + @EventName("mm_spend_analysis_help_bottom_sheet_manage_consent_clicked") + fun onSpendAnalysisHelpBottomSheetManageConsentClicked() +} + +@AutoGenerate +interface CategoryDetailsEventTracker { + @EventName("mm_category_details_screen_landed") + fun onCategoryDetailsScreenLanded(category: String, monthIndex: Int, numberOfBanks: Int) + + @EventName("mm_category_details_screen_exit") fun onCategoryDetailsScreenExit() + + @EventName("mm_category_details_help_clicked") fun onCategoryDetailsHelpClicked() + + @EventName("mm_category_details_graph_viewed") fun onCategoryDetailsGraphView() + + @EventName("mm_category_details_category_selection_requested") + fun onCategoryDetailsCategorySelectionRequested() + + @EventName("mm_category_details_month_selection_requested") + fun onCategoryDetailsMonthSelectionRequested() + + @EventName("mm_category_details_bank_selection_requested") + fun onCategoryDetailsBankSelectionRequested() + + @EventName("mm_category_details_sort_transactions_requested") + fun onCategoryDetailsSortTransactionsRequested() + + @EventName("mm_category_details_category_selection_applied") + fun onCategoryDetailsCategorySelectionApplied(categoryRank: Int, categoryType: String) + + @EventName("mm_category_details_month_selection_applied") + fun onCategoryDetailsMonthSelectionApplied(monthIndex: Int) + + @EventName("mm_category_details_bank_selection_applied") + fun onCategoryDetailsBankSelectionApplied(numberOfSelectedBankAccounts: Int) + + @EventName("mm_category_details_sort_transactions_applied") + fun onCategoryDetailsSortTransactionsApplied(sortType: String) + + @EventName("mm_category_details_graph_bar_clicked") + fun onCategoryDetailsGraphBarClicked(monthIndex: Int) + + @EventName("mm_category_details_graph_average_info_clicked") + fun onCategoryDetailsGraphAverageInfoClicked() + + @EventName("mm_category_details_graph_average_info_bottom_sheet_acknowledged") + fun onCategoryDetailsGraphAverageInfoBottomSheetAcknowledged() + + @EventName("mm_category_details_transaction_clicked") + fun onCategoryDetailsTransactionClicked(transactionRank: Int) + + @EventName("mm_category_details_uncategorised_info_clicked") + fun onCategoryDetailsUncategorizedInfoClicked() + + @EventName("mm_category_details_uncategorized_info_bottom_sheet_acknowledged") + fun onCategoryDetailsUncategorizedInfoBottomSheetAcknowledged() + + @EventName("mm_category_details_transaction_category_clicked") + fun onCategoryDetailsTransactionCategoryClicked(transactionRank: Int, category: String) + + @EventName("mm_category_details_transaction_category_selection_applied") + fun onCategoryDetailsTransactionCategorySelectionApplied( + previousCategory: String, + newCategory: String + ) + + @EventName("mm_category_details_categorise_similar_transaction_skipped") + fun onCategoryDetailsCategoriseSimilarTransactionSkipped(category: String) + + @EventName("mm_category_details_categorise_similar_transaction_confirmed") + fun onCategoryDetailsCategoriseSimilarTransactionConfirmed( + autoCategoriseSelected: Boolean, + allTransactionsSelected: Boolean, + numberOfTransactionsSelected: Int, + category: String + ) + + @EventName("mm_category_details_month_selection_bottom_sheet_appeared") + fun onCategoryDetailsMonthSelectionBottomSheetAppeared() + + @EventName("mm_category_details_month_selection_bottom_sheet_disappeared") + fun onCategoryDetailsMonthSelectionBottomSheetDisappeared() + + @EventName("mm_category_details_past_month_data_loading_bottom_sheet_appeared") + fun onCategoryDetailsPastMonthDataLoadingBottomSheetAppeared() + + @EventName("mm_category_details_past_month_data_loading_bottom_sheet_disappeared") + fun onCategoryDetailsPastMonthDataLoadingBottomSheetDisappeared() + + @EventName("mm_category_details_help_bottom_sheet_appeared") + fun onCategoryDetailsHelpBottomSheetAppeared() + + @EventName("mm_category_details_help_bottom_sheet_disappeared") + fun onCategoryDetailsHelpBottomSheetDisappeared() + + @EventName("mm_category_details_transaction_category_selection_bottom_sheet_appeared") + fun onCategoryDetailsTransactionCategorySelectionBottomSheetAppeared() + + @EventName("mm_category_details_transaction_category_selection_bottom_sheet_disappeared") + fun onCategoryDetailsTransactionCategorySelectionBottomSheetDisappeared() + + @EventName("mm_category_details_categorise_similar_transaction_bottom_sheet_appeared") + fun onCategoryDetailsCategoriseSimilarTransactionBottomSheetAppeared() + + @EventName("mm_category_details_categorise_similar_transaction_bottom_sheet_disappeared") + fun onCategoryDetailsCategoriseSimilarTransactionBottomSheetDisappeared() + + @EventName("mm_category_details_help_bottom_sheet_faq_clicked") + fun onCategoryDetailsHelpBottomSheetFaqClicked() + + @EventName("mm_category_details_help_bottom_sheet_manage_consent_clicked") + fun onCategoryDetailsHelpBottomSheetManageConsentClicked() + + @EventName("mm_category_details_average_info_bottom_sheet_appeared") + fun onCategoryDetailsAverageInfoBottomSheetAppeared() + + @EventName("mm_category_details_average_info_bottom_sheet_disappeared") + fun onCategoryDetailsAverageInfoBottomSheetDisappeared() +} + +@AutoGenerate +interface TransactionHistoryEventTracker { + @EventName("mm_transaction_history_screen_landed") fun onTransactionHistoryScreenLanded() + + @EventName("mm_transaction_history_screen_exit") fun onTransactionHistoryScreenExit() + + @EventName("mm_transaction_history_screen_transaction_clicked") + fun onTransactionHistoryTransactionClicked(transactionRank: Int) + + @EventName("mm_transaction_history_search_query_changed") + fun onTransactionHistorySearchQueryChanged(query: String) + + @EventName("mm_transaction_history_filter_icon_clicked") + fun onTransactionHistoryFilterIconClicked() + + @EventName("mm_transaction_history_filter_applied") + fun onTransactionHistoryFilterApplied(numberOfSelections: String) + + @EventName("mm_transaction_history_filter_bottom_sheet_clear_all_clicked") + fun onTransactionHistoryFilterBottomsheetClearAllClicked() + + @EventName("mm_transaction_history_transaction_category_clicked") + fun onTransactionHistoryTransactionCategoryClicked(transactionRank: Int, category: String) + + @EventName("mm_transaction_history_transaction_category_selection_applied") + fun onTransactionHistoryTransactionCategorySelectionApplied( + previousCategory: String, + newCategory: String + ) + + @EventName("mm_transaction_history_filter_removed") + fun onTransactionHistoryFilterRemoved(filterRank: Int) + + @EventName("mm_transaction_history_categorise_similar_transaction_skipped") + fun onTransactionHistoryCategoriseSimilarTransactionSkipped(category: String) + + @EventName("mm_transaction_history_categorise_similar_transaction_confirmed") + fun onTransactionHistoryCategoriseSimilarTransactionConfirmed( + autoCategoriseSelected: Boolean, + allTransactionsSelected: Boolean, + numberOfTransactionsSelected: Int, + category: String + ) + + @EventName("mm_transaction_history_category_selection_bottom_sheet_appeared") + fun onTransactionHistoryCategorySelectionBottomSheetAppeared() + + @EventName("mm_transaction_history_category_selection_bottom_sheet_disappeared") + fun onTransactionHistoryCategorySelectionBottomSheetDisappeared() + + @EventName("mm_transaction_history_categorise_similar_transaction_bottom_sheet_appeared") + fun onTransactionHistoryCategoriseSimilarTransactionBottomSheetAppeared() + + @EventName("mm_transaction_history_categorise_similar_transaction_bottom_sheet_disappeared") + fun onTransactionHistoryCategoriseSimilarTransactionBottomSheetDisappeared() +} + +@AutoGenerate +interface TransactionDetailsEventTracker { + @EventName("mm_transaction_details_screen_landed") fun onTransactionDetailsScreenLanded() + + @EventName("mm_transaction_details_screen_exit") fun onTransactionDetailsScreenExit() + + @EventName("mm_transaction_details_help_clicked") fun onTransactionDetailsHelpClicked() + + @EventName("mm_transaction_details_transaction_category_clicked") + fun onTransactionDetailsTransactionCategoryClicked(category: String) + + @EventName("mm_transaction_details_transaction_category_selection_applied") + fun onTransactionDetailsTransactionCategorySelectionApplied( + previousCategory: String, + newCategory: String + ) + + @EventName("mm_transaction_details_categorise_similar_transaction_skipped") + fun onTransactionDetailsCategoriseSimilarTransactionSkipped(category: String) + + @EventName("mm_transaction_details_categorise_similar_transaction_confirmed") + fun onTransactionDetailsCategoriseSimilarTransactionConfirmed( + autoCategoriseSelected: Boolean, + allTransactionsSelected: Boolean, + numberOfTransactionsSelected: Int, + category: String + ) + + @EventName("mm_transaction_details_help_bottom_sheet_appeared") + fun onTransactionDetailsHelpBottomSheetAppeared() + + @EventName("mm_transaction_details_help_bottom_sheet_disappeared") + fun onTransactionDetailsHelpBottomSheetDisappeared() + + @EventName("mm_transaction_details_category_selection_bottom_sheet_appeared") + fun onTransactionDetailsCategorySelectionBottomSheetAppeared() + + @EventName("mm_transaction_details_category_selection_bottom_sheet_disappeared") + fun onTransactionDetailsCategorySelectionBottomSheetDisappeared() + + @EventName("mm_transaction_details_categorise_similar_transaction_bottom_sheet_appeared") + fun onTransactionDetailsCategoriseSimilarTransactionBottomSheetAppeared() + + @EventName("mm_transaction_details_categorise_similar_transaction_bottom_sheet_disappeared") + fun onTransactionDetailsCategoriseSimilarTransactionBottomSheetDisappeared() + + @EventName("mm_transaction_details_help_bottom_sheet_faq_clicked") + fun onTransactionDetailsHelpBottomSheetFaqClicked() + + @EventName("mm_transaction_details_help_bottom_sheet_manage_consent_clicked") + fun onTransactionDetailsHelpBottomSheetManageConsentClicked() +} + +@AutoGenerate +interface LauncherErrorEventTracker { + + @EventName("mm_launcher_error_screen_landed") fun onLauncherErrorScreenLanded() + + @EventName("mm_launcher_error_screen_exit") fun onLauncherErrorScreenExit() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/composable/bankselectionbottomsheet/BankSelectionBottomSheet.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/composable/bankselectionbottomsheet/BankSelectionBottomSheet.kt new file mode 100644 index 0000000000..29b9d334ca --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/composable/bankselectionbottomsheet/BankSelectionBottomSheet.kt @@ -0,0 +1,249 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.composable.bankselectionbottomsheet + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.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.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.elex.atoms.ElexCheckbox +import com.navi.elex.font.FontWeightEnum as ElexFontWeightEnum +import com.navi.elex.molecules.ElexButtonWithLoader +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.ALL_BANK_ICON +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.model.BankAccount +import com.navi.moneymanager.common.model.BankSelectionBottomSheetData +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor + +@Composable +fun BankSelectionBottomSheetContent( + bottomSheetData: BankSelectionBottomSheetData? = null, + onBackIconClicked: () -> Unit, + onBankSelectionApplied: (Set) -> Unit +) { + bottomSheetData?.let { data -> + var loading by remember { mutableStateOf(false) } + Column( + modifier = + Modifier.fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + BankSelectionBottomSheetHeaderSection( + title = data.headerText, + backIconUrl = data.backIconUrl, + onBackIconClicked = onBackIconClicked + ) + BankSelectionBottomSheetContentSection( + checkBoxEnabled = data.availableBanks.size > 1, + bankAccountsList = data.availableBanks, + selectedBankReferenceIds = data.selectedBankReferenceIds, + modifier = Modifier.weight(1f, false).padding(top = 32.dp), + loading = loading, + buttonText = data.ctaText, + loaderLottieUrl = data.buttonLoader, + onButtonClicked = { + loading = true + onBankSelectionApplied(it) + } + ) + } + } +} + +@Composable +fun BankSelectionBottomSheetHeaderSection( + title: String, + backIconUrl: IllustrationSource, + onBackIconClicked: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + MMText( + text = title, + modifier = Modifier.padding(end = 24.dp), + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + Illustration( + modifier = Modifier.size(24.dp).onClickWithDebounce { onBackIconClicked() }, + illustrationType = IllustrationType.Image(backIconUrl) + ) + } +} + +@Composable +fun BankSelectionBottomSheetContentSection( + checkBoxEnabled: Boolean, + bankAccountsList: List, + selectedBankReferenceIds: Set?, + modifier: Modifier = Modifier, + loading: Boolean, + buttonText: String, + loaderLottieUrl: String, + onButtonClicked: (Set) -> Unit, +) { + val scrollState = rememberScrollState() + var userSelectedBanks by remember { mutableStateOf(selectedBankReferenceIds ?: setOf()) } + Column { + Column(modifier = modifier.verticalScroll(scrollState).weight(1f, false)) { + bankAccountsList.forEach { bankAccount -> + BankAccountItem( + checkBoxEnabled = checkBoxEnabled, + bankAccount = bankAccount, + checked = userSelectedBanks.contains(bankAccount.linkedAccountReference), + onCheckedChange = { checked -> + userSelectedBanks = + if (checked) { + userSelectedBanks + bankAccount.linkedAccountReference + } else { + userSelectedBanks - bankAccount.linkedAccountReference + } + }, + modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp) + ) + } + } + Spacer(modifier = Modifier.height(12.dp)) + if (bankAccountsList.size > 1) { + ElexButtonWithLoader( + modifier = Modifier.fillMaxWidth().height(48.dp), + shape = RoundedCornerShape(4.dp), + colors = + ButtonDefaults.buttonColors( + disabledContainerColor = MMColor.disabledButtonColor, + containerColor = MMColor.ctaPrimary + ), + enabled = bankAccountsList.size > 1 && userSelectedBanks.isNotEmpty() && !loading, + text = buttonText, + textColor = MMColor.white, + fontSize = 14.sp, + fontWeight = ElexFontWeightEnum.NAVI_BODY_DEMI_BOLD, + loading = loading, + lottieSpec = LottieCompositionSpec.Url(url = loaderLottieUrl), + onClick = { onButtonClicked(userSelectedBanks) } + ) + } + } +} + +@Composable +fun BankAccountItem( + checkBoxEnabled: Boolean, + bankAccount: BankAccount, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = + modifier + .fillMaxWidth() + .border(width = 1.dp, color = MMColor.borderColor, shape = RoundedCornerShape(4.dp)) + .clip(RoundedCornerShape(4.dp)) + .clickable(enabled = checkBoxEnabled) { onCheckedChange(!checked) } + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier.size(40.dp).clip(CircleShape).background(MMColor.whiteSmoke), + contentAlignment = Alignment.Center + ) { + Illustration( + modifier = Modifier.size(32.dp).padding(4.dp), + illustrationType = + IllustrationType.Image( + IllustrationSource.Remote( + url = bankAccount.bankIconUrl, + placeholder = ALL_BANK_ICON + ) + ) + ) + } + Column( + modifier = Modifier.fillMaxWidth().padding(start = 8.dp, end = 24.dp).weight(1f), + horizontalAlignment = Alignment.Start + ) { + MMText( + text = bankAccount.bankName, + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + lineHeight = 24.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + Spacer(modifier = Modifier.height(2.dp)) + MMText( + text = bankAccount.accountHint, + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 18.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + } + ElexCheckbox( + checked = checked, + onCheckedChange = { onCheckedChange(it) }, + colors = + CheckboxDefaults.colors( + checkedColor = MMColor.ctaPrimary, + uncheckedColor = MMColor.gray, + checkmarkColor = MMColor.white, + disabledCheckedColor = MMColor.gray, + disabledUncheckedColor = MMColor.gray + ), + enabled = checkBoxEnabled, + modifier = Modifier.size(16.dp).clip(RoundedCornerShape(2.dp)) + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/composable/bankselectionbottomsheet/ZeroTransactionView.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/composable/bankselectionbottomsheet/ZeroTransactionView.kt new file mode 100644 index 0000000000..0bcff92496 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/composable/bankselectionbottomsheet/ZeroTransactionView.kt @@ -0,0 +1,53 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.composable.bankselectionbottomsheet + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor + +@Composable +fun ZeroTransactionView( + title: String, + illustrationSource: IllustrationSource, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxWidth().padding(horizontal = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Illustration( + illustrationType = IllustrationType.Image(source = illustrationSource), + modifier = Modifier.size(size = 180.dp), + ) + + MMText( + text = title, + color = MMColor.gray, + lineHeight = 22.sp, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + textAlign = TextAlign.Center, + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/addcategory/helper/AddCategoryProviderHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/addcategory/helper/AddCategoryProviderHelper.kt new file mode 100644 index 0000000000..6f48dafada --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/addcategory/helper/AddCategoryProviderHelper.kt @@ -0,0 +1,160 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.addcategory.helper + +import android.content.Context +import com.navi.base.utils.formatToInrWithTwoDecimals +import com.navi.base.utils.orZero +import com.navi.moneymanager.R +import com.navi.moneymanager.common.dataprovider.utils.getInitials +import com.navi.moneymanager.common.dataprovider.utils.getTransactionBackgroundColor +import com.navi.moneymanager.common.dataprovider.utils.getTransactionDate +import com.navi.moneymanager.common.dataprovider.utils.getWeekDayFromTimestamp +import com.navi.moneymanager.common.helper.TransactionsDataHelper +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.ALL_BANK_ICON_SMALL +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.COMMON_CROSS_BLACK +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.model.ChipData +import com.navi.moneymanager.common.model.ChipIllustration +import com.navi.moneymanager.common.model.SingleChipSelectionData +import com.navi.moneymanager.common.model.database.AccountOverview +import com.navi.moneymanager.common.model.database.TransactionSummaryData +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.utils.Constants.UNCATEGORIZED +import com.navi.moneymanager.postonboard.monthlysummary.model.AccountInfo +import com.navi.moneymanager.postonboard.monthlysummary.model.AddCategoryFooterData +import com.navi.moneymanager.postonboard.monthlysummary.model.AddCategoryHeaderData +import com.navi.moneymanager.postonboard.monthlysummary.model.MoreCategoriesData +import com.navi.moneymanager.postonboard.monthlysummary.model.RecommendedCategoriesData +import com.navi.moneymanager.postonboard.monthlysummary.model.StartHeaderIcon +import com.navi.moneymanager.postonboard.monthlysummary.model.TransactionDetails +import com.navi.naviwidgets.utils.NaviWidgetLottieUtils.TUK_TUK_LOTTIE +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class AddCategoryProviderHelper +@Inject +constructor( + @ApplicationContext private val context: Context, + private val transactionsDataHelper: TransactionsDataHelper +) { + fun getAddCategoryHeaderData(transaction: TransactionSummaryData): AddCategoryHeaderData { + val name = transaction.counterPartyName.orEmpty() + return AddCategoryHeaderData( + transactionAmount = transaction.txnAmount.orZero().formatToInrWithTwoDecimals(), + name = name, + startIcon = + StartHeaderIcon( + iconText = getInitials(name), + iconBackgroundColor = getTransactionBackgroundColor(name), + ), + endIcon = + IllustrationSource.Remote( + url = COMMON_CROSS_BLACK, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ) + } + + fun getTransactionDetails( + transaction: TransactionSummaryData, + account: AccountOverview + ): TransactionDetails { + return TransactionDetails( + transactionDate = getTransactionDate(transaction.txnTimestamp), + transactionDay = + String.format( + context.resources.getString(R.string.on_day), + getWeekDayFromTimestamp(transaction.txnTimestamp.orZero()) + ), + paymentMethod = transactionsDataHelper.getTransactionTypeLabel(transaction.type), + accountInfo = + AccountInfo( + account = "${account.bankCode} ${account.maskedAccNumber?.takeLast(4)}", + endIconUrl = + IllustrationSource.Remote( + url = account.bankIconUrl.orEmpty(), + placeholder = ALL_BANK_ICON_SMALL + ) + ) + ) + } + + fun getRecommendedCategoriesData(mmConfig: MMConfigResponse?): RecommendedCategoriesData { + val recommendedCategories = + mmConfig?.categories?.filter { it.categoryId != UNCATEGORIZED } ?: emptyList() + return RecommendedCategoriesData( + categoriesTitle = context.resources.getString(R.string.choose_a_category), + categoriesList = + recommendedCategories.map { + SingleChipSelectionData( + id = it.categoryId.orEmpty(), + chipData = + ChipData( + illustration = + ChipIllustration( + illustrationType = + IllustrationType.Image( + IllustrationSource.Remote( + url = it.categoryIcon.orEmpty(), + placeholder = + IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ), + size = 24, + contentDescription = it.categoryId.orEmpty() + ), + content = it.categoryName.orEmpty() + ) + ) + } + ) + } + + fun getMoreCategoriesData(mmConfig: MMConfigResponse?): MoreCategoriesData { + val moreCategories = + mmConfig?.categories?.filter { + it.isRecommended != true && it.categoryId != UNCATEGORIZED + } ?: emptyList() + return MoreCategoriesData( + moreCategoriesTitle = context.resources.getString(R.string.more_categories), + moreCategoriesList = + moreCategories.map { + SingleChipSelectionData( + id = it.categoryId.orEmpty(), + chipData = + ChipData( + illustration = + ChipIllustration( + illustrationType = + IllustrationType.Image( + IllustrationSource.Remote( + url = it.categoryIcon.orEmpty(), + placeholder = + IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ), + size = 24, + contentDescription = it.categoryId.orEmpty() + ), + content = it.categoryName.orEmpty() + ) + ) + } + ) + } + + fun getFooterData(): AddCategoryFooterData { + return AddCategoryFooterData( + buttonLabel = context.resources.getString(R.string.confirm), + lottieUrl = TUK_TUK_LOTTIE + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/addcategory/helper/SimilarTransactionProviderHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/addcategory/helper/SimilarTransactionProviderHelper.kt new file mode 100644 index 0000000000..04680e8914 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/addcategory/helper/SimilarTransactionProviderHelper.kt @@ -0,0 +1,131 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.addcategory.helper + +import android.content.Context +import com.navi.base.utils.SPACE +import com.navi.base.utils.formatToInrWithTwoDecimals +import com.navi.base.utils.orZero +import com.navi.moneymanager.R +import com.navi.moneymanager.common.dataprovider.utils.getTransactionDate +import com.navi.moneymanager.common.helper.TransactionsDataHelper +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.COMMON_ARROW_LEFT_PURPLE +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.model.database.AccountOverview +import com.navi.moneymanager.common.model.database.TransactionSummaryData +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryTransactionFooterData +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryTransactionHeaderData +import com.navi.moneymanager.postonboard.monthlysummary.model.FooterButtonData +import com.navi.moneymanager.postonboard.monthlysummary.model.FooterInfoData +import com.navi.moneymanager.postonboard.monthlysummary.model.TransactionItemData +import com.navi.moneymanager.postonboard.monthlysummary.model.TransactionPaidType +import com.navi.naviwidgets.utils.NaviWidgetLottieUtils.TUK_TUK_LOTTIE +import com.navi.naviwidgets.utils.formatAmount +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class SimilarTransactionProviderHelper +@Inject +constructor( + @ApplicationContext private val context: Context, + private val transactionsDataHelper: TransactionsDataHelper +) { + + fun getHeaderData( + categoryId: String, + mmConfig: MMConfigResponse?, + transaction: TransactionSummaryData, + similarTransactions: List + ): CategoryTransactionHeaderData { + val categoryName = + mmConfig + ?.categories + ?.firstOrNull { it.categoryId == categoryId } + ?.categoryName + .orEmpty() + return CategoryTransactionHeaderData( + topIconUrl = + IllustrationSource.Remote( + url = COMMON_ARROW_LEFT_PURPLE, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ), + title = + String.format( + context.resources.getString(R.string.n_similar_transactions_found), + similarTransactions.size.orZero().formatAmount() + ), + subTitle = + context.resources + .getString(R.string.paid_to) + .plus(SPACE) + .plus(transaction.counterPartyName.orEmpty()), + description = + context.resources + .getString(R.string.similar_transactions_heading_description) + .plus(SPACE), + annotatedDescription = "'$categoryName'" + ) + } + + fun getTransactionItems( + accounts: List, + transactions: List + ): List { + return transactions.map { transaction -> + val account = + accounts.firstOrNull { account -> account.linkedAccRef == transaction.linkedAccRef } + TransactionItemData( + id = transaction.txnId, + name = transaction.counterPartyName.orEmpty(), + date = getTransactionDate(transaction.txnTimestamp), + amount = transaction.txnAmount.orZero().formatToInrWithTwoDecimals(), + type = transaction.type.orEmpty(), + transactionPaidType = + TransactionPaidType( + label = transactionsDataHelper.getTransactionTypeLabel(transaction.type), + iconUrl = account?.bankIconUrl.orEmpty() + ) + ) + } + } + + fun getFooterData( + categoryId: String, + mmConfig: MMConfigResponse? + ): CategoryTransactionFooterData { + val categoryName = + mmConfig + ?.categories + ?.firstOrNull { it.categoryId == categoryId } + ?.categoryName + .orEmpty() + return CategoryTransactionFooterData( + footerInfoData = + FooterInfoData( + infoText = + context.resources + .getString(R.string.similar_transactions_checkbox_text) + .plus(SPACE), + annotatedInfoText = "'$categoryName'" + ), + proceedButtonData = + FooterButtonData( + buttonText = context.resources.getString(R.string.categorise_and_proceed), + lottieUrl = TUK_TUK_LOTTIE + ), + skipButtonData = + FooterButtonData( + buttonText = + context.resources.getString(R.string.skip_past_transactions_for_now), + lottieUrl = TUK_TUK_LOTTIE + ) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/addcategory/provider/AddCategoryDataProviderImpl.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/addcategory/provider/AddCategoryDataProviderImpl.kt new file mode 100644 index 0000000000..f439822614 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/addcategory/provider/AddCategoryDataProviderImpl.kt @@ -0,0 +1,137 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.addcategory.provider + +import com.navi.moneymanager.common.analytics.AddCategoryEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.data.addcategory.helper.AddCategoryProviderHelper +import com.navi.moneymanager.common.dataprovider.data.addcategory.helper.SimilarTransactionProviderHelper +import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.MMConfigResponseHelper +import com.navi.moneymanager.common.dataprovider.domain.AddCategoryDataProvider +import com.navi.moneymanager.common.db.database.MMEncryptedDatabase +import com.navi.moneymanager.common.utils.Constants +import com.navi.moneymanager.common.utils.calculateTimestamps +import com.navi.moneymanager.postonboard.monthlysummary.model.AddCategoryBottomSheetData +import com.navi.moneymanager.postonboard.monthlysummary.model.AddCategoryContentData +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryTransactionContentData +import com.navi.moneymanager.postonboard.monthlysummary.model.ChipsContainerData +import com.navi.moneymanager.postonboard.monthlysummary.model.SimilarTransactionBottomSheetData +import dagger.Lazy +import javax.inject.Inject +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow + +class AddCategoryDataProviderImpl +@Inject +constructor( + private val encryptedDatabase: Lazy, + private val addCategoryProviderHelper: AddCategoryProviderHelper, + private val similarTransactionProviderHelper: SimilarTransactionProviderHelper, + private val mmConfigResponseHelper: MMConfigResponseHelper +) : AddCategoryDataProvider { + override suspend fun fetchAddCategoryBottomSheetData(transactionId: String) = flow { + val transaction = + encryptedDatabase.get().transactionsDao().fetchTransaction(transactionId).first() + val account = + encryptedDatabase.get().accountsDao().fetchAccount(transaction.linkedAccRef.orEmpty()) + val mmConfig = mmConfigResponseHelper.getMMConfig() + val hasOnlyOneSimilarTransaction = + if (transaction.counterPartyName.equals(Constants.UNKNOWN, ignoreCase = true)) { + true + } else { + val (startDate, endDate) = calculateTimestamps() + val similarTransactionsCount = + encryptedDatabase + .get() + .transactionsDao() + .getTransactionCountByCounterPartyNameAndType( + counterPartyName = transaction.counterPartyName.orEmpty(), + transactionType = transaction.type.orEmpty(), + startDate, + endDate + ) + similarTransactionsCount == 1 + } + AddCategoryEventTrackerImpl.addCategoryBottomSheetFetchDataCalled( + hasOnlyOneSimilarTransaction = hasOnlyOneSimilarTransaction.toString(), + counterPartyName = transaction.counterPartyName.orEmpty(), + transactionType = transaction.type.orEmpty(), + ) + emit( + AddCategoryBottomSheetData( + headerData = addCategoryProviderHelper.getAddCategoryHeaderData(transaction), + contentData = + AddCategoryContentData( + transactionDetails = + addCategoryProviderHelper.getTransactionDetails(transaction, account), + chipsContainerData = + ChipsContainerData( + categoriesData = + addCategoryProviderHelper.getRecommendedCategoriesData( + mmConfig + ), + moreCategoriesData = null + ) + ), + footerData = addCategoryProviderHelper.getFooterData(), + hasOnlyOneSimilarTransaction = hasOnlyOneSimilarTransaction + ) + ) + } + + override suspend fun fetchSimilarTransactionBottomSheetData( + transactionId: String, + categoryId: String, + transactionType: String + ) = flow { + val transaction = + encryptedDatabase.get().transactionsDao().fetchTransaction(transactionId).first() + val (startDate, endDate) = calculateTimestamps() + val similarTransactions = + encryptedDatabase + .get() + .transactionsDao() + .fetchTransactionsByCounterPartyName( + transaction.counterPartyName.orEmpty(), + startDate, + endDate + ) + .filter { it.txnId != transaction.txnId && it.type == transactionType } + val accounts = encryptedDatabase.get().accountsDao().fetchAllAccounts().first() + val mmConfig = mmConfigResponseHelper.getMMConfig() + + AddCategoryEventTrackerImpl.similarTransactionsBottomSheetFetchDataCalled( + similarTransactionListSize = similarTransactions.size.toString(), + counterPartyName = transaction.counterPartyName.orEmpty(), + transactionType = transactionType + ) + emit( + SimilarTransactionBottomSheetData( + header = + similarTransactionProviderHelper.getHeaderData( + categoryId, + mmConfig, + transaction, + similarTransactions + ), + content = + CategoryTransactionContentData( + transactionItems = + similarTransactionProviderHelper.getTransactionItems( + accounts, + similarTransactions + ) + ), + footer = similarTransactionProviderHelper.getFooterData(categoryId, mmConfig) + ) + ) + } + + override suspend fun updateTransactionEntity(transactionId: String, categoryId: String) { + encryptedDatabase.get().transactionsDao().updateTransactionEntity(transactionId, categoryId) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/bankaccounts/BankAccountsDataProviderImpl.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/bankaccounts/BankAccountsDataProviderImpl.kt new file mode 100644 index 0000000000..57ea5dc8e9 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/bankaccounts/BankAccountsDataProviderImpl.kt @@ -0,0 +1,22 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.bankaccounts + +import com.navi.moneymanager.common.dataprovider.domain.BankAccountsDataProvider +import com.navi.moneymanager.common.db.database.MMEncryptedDatabase +import dagger.Lazy +import javax.inject.Inject + +class BankAccountsDataProviderImpl +@Inject +constructor( + private val encryptedDatabase: Lazy, +) : BankAccountsDataProvider { + override suspend fun getBankAccounts() = + encryptedDatabase.get().accountsDao().fetchAllAccounts() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/dashboard/helper/AccountUpdateTimeHandler.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/dashboard/helper/AccountUpdateTimeHandler.kt new file mode 100644 index 0000000000..0770ef6f0c --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/dashboard/helper/AccountUpdateTimeHandler.kt @@ -0,0 +1,71 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.dashboard.helper + +import android.content.Context +import com.navi.moneymanager.R +import com.navi.moneymanager.common.utils.formatDateFromTimestamp +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountUpdateData +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class AccountUpdateTimeHandler +@Inject +constructor(@ApplicationContext private val context: Context) { + + private val lastUpdateMap = mutableMapOf() + + fun getLastUpdatedMap(): Map { + return lastUpdateMap.toMap() + } + + fun initLastUpdatedTimeMap(accounts: List) { + accounts.forEach { account -> + lastUpdateMap[account.referenceId] = formatElapsedTime(account.lastUpdated) + } + } + + fun updateLastUpdatedTimeMap(accounts: List) { + accounts.forEach { account -> + val previousElapsedTime = lastUpdateMap[account.referenceId] ?: return@forEach + val currentElapsedTime = formatElapsedTime(account.lastUpdated) + + // Update only if the elapsed time has changed + if (currentElapsedTime != previousElapsedTime) { + lastUpdateMap[account.referenceId] = currentElapsedTime + } + } + } + + fun formatElapsedTime(lastUpdatedMillis: Long): String { + val elapsedMillis = System.currentTimeMillis() - lastUpdatedMillis + val seconds = (elapsedMillis / 1000).toInt() + val minutes = seconds / 60 + val hours = minutes / 60 + val days = hours / 24 + + return when { + seconds < 90 -> context.resources.getString(R.string.just_now) + minutes < 60 -> + if (minutes == 1) { + context.resources.getString(R.string.time_min_ago) + } else { + String.format(context.resources.getString(R.string.time_mins_ago), minutes) + } + hours < 24 -> + if (hours == 1) { + context.resources.getString(R.string.time_hr_ago) + } else { + String.format(context.resources.getString(R.string.time_hrs_ago), hours) + } + days == 1 -> context.resources.getString(R.string.time_day_ago) + days == 2 -> String.format(context.resources.getString(R.string.time_days_ago), days) + else -> formatDateFromTimestamp(lastUpdatedMillis) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/dashboard/helper/DashboardBankSectionProviderHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/dashboard/helper/DashboardBankSectionProviderHelper.kt new file mode 100644 index 0000000000..d7d6b4a587 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/dashboard/helper/DashboardBankSectionProviderHelper.kt @@ -0,0 +1,179 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.dashboard.helper + +import android.content.Context +import com.navi.base.utils.EMPTY +import com.navi.base.utils.SPACE +import com.navi.base.utils.formatToInrWithTwoDecimals +import com.navi.base.utils.orElse +import com.navi.base.utils.orZero +import com.navi.common.extensions.or +import com.navi.moneymanager.R +import com.navi.moneymanager.common.dataprovider.utils.DataProviderConstants.HYPHEN +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.ALL_BANK_ICON +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.ALL_BANK_ICON_SMALL +import com.navi.moneymanager.common.illustration.repository.LottieRepository.Companion.CHIP_LOADER_LOTTIE +import com.navi.moneymanager.common.model.database.AccountOverview +import com.navi.moneymanager.postonboard.dashboard.model.BankAccount +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountFooterSection +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountUpdateData +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class DashboardBankSectionProviderHelper +@Inject +constructor( + private val lastUpdatedTimeHandler: AccountUpdateTimeHandler, + @ApplicationContext private val context: Context +) { + + private val bankAccountUpdateDataList = mutableListOf() + + fun createAggregateAccount(accounts: List): BankAccount? { + if (accounts.size <= 1) return null + + val totalBalance = accounts.sumOf { it.currentBalance.orZero() } + val aggregatedFooterSection = getAggregatedFooterSection(accounts) + + return BankAccount( + isBalanceFetched = true, + balanceSectionTitlePrefix = context.resources.getString(R.string.total_balance), + bankChipContent = context.resources.getString(R.string.all_banks), + referenceId = context.resources.getString(R.string.all_banks), + balanceSectionTitleSuffix = EMPTY, + bankChipImage = + IllustrationSource.Remote( + url = ALL_BANK_ICON_SMALL, + placeholder = ALL_BANK_ICON_SMALL + ), + bankLoadingLottie = IllustrationSource.Resource(CHIP_LOADER_LOTTIE), + bankBalance = totalBalance.formatToInrWithTwoDecimals(), + lastUpdatedPrefixText = aggregatedFooterSection.lastUpdatedPrefixText, + lastUpdatedSuffixText = aggregatedFooterSection.lastUpdatedSuffixText, + bankBalanceSectionImage = + IllustrationSource.Remote(url = ALL_BANK_ICON, placeholder = ALL_BANK_ICON) + ) + } + + fun createAccountList(accounts: List): List { + val footerSection = getAccountsFooterSection(accounts) + return accounts.map { account -> + val footerItem = footerSection.first { it.referenceId == account.linkedAccRef } + BankAccount( + isBalanceFetched = (account.currentBalance != null), + balanceSectionTitlePrefix = + if (accounts.size > 1) account.bankName.orEmpty() + else context.resources.getString(R.string.total_balance), + bankChipContent = "${account.bankCode} - ${account.maskedAccNumber?.takeLast(4)}", + referenceId = account.linkedAccRef, + balanceSectionTitleSuffix = + if (accounts.size > 1) HYPHEN + account.maskedAccNumber?.takeLast(4) else EMPTY, + bankChipImage = + IllustrationSource.Remote( + url = account.bankIconUrl.orEmpty(), + placeholder = ALL_BANK_ICON_SMALL + ), + bankBalance = account.currentBalance.orZero().formatToInrWithTwoDecimals(), + lastUpdatedPrefixText = footerItem.lastUpdatedPrefixText, + lastUpdatedSuffixText = footerItem.lastUpdatedSuffixText, + bankBalanceSectionImage = + IllustrationSource.Remote( + url = account.bankIconUrl.orEmpty(), + placeholder = ALL_BANK_ICON_SMALL + ), + bankLoadingLottie = IllustrationSource.Resource(CHIP_LOADER_LOTTIE) + ) + } + } + + fun initLastUpdateTimeMap( + accounts: List, + ) { + bankAccountUpdateDataList.clear() + val allBanksText = context.resources.getString(R.string.all_banks) + val latestLastUpdatedTime = getLatestLastUpdatedTime(accounts) + bankAccountUpdateDataList.add(BankAccountUpdateData(allBanksText, latestLastUpdatedTime)) + accounts.forEach { account -> + val lastUpdatedTime = account.updatedAt.orElse(System.currentTimeMillis()) + bankAccountUpdateDataList.add( + BankAccountUpdateData(account.linkedAccRef, lastUpdatedTime) + ) + } + lastUpdatedTimeHandler.initLastUpdatedTimeMap(bankAccountUpdateDataList.toList()) + } + + fun getBanksLastUpdatedSection(): List { + lastUpdatedTimeHandler.updateLastUpdatedTimeMap(bankAccountUpdateDataList.toList()) + return lastUpdatedTimeHandler.getLastUpdatedMap().map { entry -> + buildFooterData(entry.key, R.string.last_updated, entry.value) + } + } + + fun getRefreshingLastUpdatedSection( + accounts: List + ): List { + val refreshingSections = + accounts.map { account -> + buildFooterData(account.linkedAccRef, R.string.refreshing, EMPTY) + } + + val allBanksSection = + buildFooterData( + context.resources.getString(R.string.all_banks), + R.string.refreshing, + EMPTY + ) + + return refreshingSections + allBanksSection + } + + private fun buildFooterData( + referenceId: String, + prefixResId: Int, + suffixText: String + ): BankAccountFooterSection { + return BankAccountFooterSection( + referenceId = referenceId, + lastUpdatedPrefixText = context.resources.getString(prefixResId).plus(SPACE), + lastUpdatedSuffixText = suffixText + ) + } + + private fun getAggregatedFooterSection( + accounts: List + ): BankAccountFooterSection { + val latestLastUpdatedTime = getLatestLastUpdatedTime(accounts) + val formattedTime = lastUpdatedTimeHandler.formatElapsedTime(latestLastUpdatedTime) + + return buildFooterData( + context.resources.getString(R.string.all_banks), + R.string.last_updated, + formattedTime + ) + } + + private fun getAccountsFooterSection( + accounts: List + ): List { + return accounts.map { account -> + val lastUpdatedTime = account.updatedAt.orElse(System.currentTimeMillis()) + buildFooterData( + account.linkedAccRef, + R.string.last_updated, + lastUpdatedTimeHandler.formatElapsedTime(lastUpdatedTime) + ) + } + } + + private fun getLatestLastUpdatedTime(accounts: List): Long { + val currentTime = System.currentTimeMillis() + return accounts.maxOfOrNull { it.updatedAt.or(currentTime) }.or(currentTime) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/dashboard/helper/MMConfigResponseHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/dashboard/helper/MMConfigResponseHelper.kt new file mode 100644 index 0000000000..c25a0454bf --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/dashboard/helper/MMConfigResponseHelper.kt @@ -0,0 +1,67 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.dashboard.helper + +import com.google.gson.Gson +import com.navi.base.cache.model.NaviCacheEntity +import com.navi.base.cache.repository.NaviCacheRepository +import com.navi.moneymanager.common.network.di.MoneyManagerGsonDeserializer +import com.navi.moneymanager.common.network.di.MoneyManagerGsonSerializer +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.utils.DbCacheConstants.MONEY_MANAGER_CONFIG_RESPONSE_KEY +import dagger.Lazy +import dagger.hilt.android.scopes.ActivityRetainedScoped +import javax.inject.Inject +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow + +@ActivityRetainedScoped +class MMConfigResponseHelper +@Inject +constructor( + private val naviCacheRepository: Lazy, + @MoneyManagerGsonDeserializer private val gsonDeserializer: Gson, + @MoneyManagerGsonSerializer private val gsonSerializer: Gson +) { + + private var mmConfigResponse: MMConfigResponse? = null + + suspend fun saveMMConfigToDB(data: MMConfigResponse) { + val dataJson = gsonSerializer.toJson(data) + val cacheEntity = + NaviCacheEntity(key = MONEY_MANAGER_CONFIG_RESPONSE_KEY, value = dataJson, version = 1) + naviCacheRepository.get().save(cacheEntity) + mmConfigResponse = + naviCacheRepository.get().get(MONEY_MANAGER_CONFIG_RESPONSE_KEY)?.value?.let { + gsonDeserializer.fromJson(it, MMConfigResponse::class.java) + } + } + + suspend fun getMMConfig(): MMConfigResponse? { + // Return cached response if available + if (mmConfigResponse != null) { + return mmConfigResponse + } + + // Collect the first non-null value from the flow + return getMMConfigFromDB() + .filterNotNull() // Only allow non-null responses + .first() // Get the first non-null response, or null if none + .also { response -> + mmConfigResponse = response // Cache the response + } + } + + private suspend fun getMMConfigFromDB() = flow { + val cacheEntity = naviCacheRepository.get().getAsFlow(MONEY_MANAGER_CONFIG_RESPONSE_KEY) + cacheEntity.collect { + emit(it?.value?.let { gsonDeserializer.fromJson(it, MMConfigResponse::class.java) }) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/dashboard/helper/SpendCategorizationHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/dashboard/helper/SpendCategorizationHelper.kt new file mode 100644 index 0000000000..e0f1a77fd0 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/dashboard/helper/SpendCategorizationHelper.kt @@ -0,0 +1,314 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.dashboard.helper + +import android.content.Context +import com.navi.base.utils.SPACE +import com.navi.base.utils.formatToDecimalPlaces +import com.navi.base.utils.formatToInrWithTwoDecimals +import com.navi.base.utils.orZero +import com.navi.common.utils.PERCENT +import com.navi.common.utils.monthAbbreviations +import com.navi.moneymanager.R +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.COMMON_EMPTY_PLACEHOLDER +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.OTHERS_CATEGORY_ICON +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.PLUS_ICON +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.TOTAL_SPEND_RUPEE_SYMBOL_WITHOUT_BG +import com.navi.moneymanager.common.illustration.repository.LottieRepository.Companion.CHIP_LOADER_LOTTIE +import com.navi.moneymanager.common.model.CategorySummary +import com.navi.moneymanager.common.model.SelfTransferCategoryData +import com.navi.moneymanager.common.model.SpendCategorizationState +import com.navi.moneymanager.common.model.SpendCategoryItemData +import com.navi.moneymanager.common.model.TotalSpends +import com.navi.moneymanager.common.model.sectionHeader.SectionHeaderData +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.ui.theme.color.MMColor.categoriesProgressBarColors +import com.navi.moneymanager.common.utils.Constants +import com.navi.moneymanager.common.utils.Constants.SELF_TRANSFER +import com.navi.naviwidgets.utils.NaviWidgetIconUtils.DOWN_ARROW_BLACK_16 +import com.navi.naviwidgets.utils.NaviWidgetIconUtils.HOME_PAGE_UPI_SECTION_RIGHT_ARROW_ICON +import com.navi.naviwidgets.utils.NaviWidgetIconUtils.ICON_ROUND_PURPLE_RIGHT_ARROW +import com.navi.naviwidgets.utils.NaviWidgetLottieUtils.TUK_TUK_LOTTIE +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class SpendCategorizationHelper +@Inject +constructor(@ApplicationContext private val context: Context) { + + fun getSpendAnalysisEmptyState( + month: Int, + year: Int, + isTotalSyncCompleted: Boolean + ): SpendCategorizationState.Empty { + return SpendCategorizationState.Empty( + header = + SectionHeaderData( + title = context.resources.getString(R.string.monthly_summary), + actionText = monthAbbreviations[month].plus(SPACE).plus(year), + suffixIcon = IllustrationSource.Resource(DOWN_ARROW_BLACK_16) + ), + totalSpends = + TotalSpends.Empty( + iconUrl = + IllustrationSource.Remote( + url = TOTAL_SPEND_RUPEE_SYMBOL_WITHOUT_BG, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ), + title = context.resources.getString(R.string.total_spends), + subtitle = context.resources.getString(R.string.zero_rupees), + actionIcon = IllustrationSource.Resource(ICON_ROUND_PURPLE_RIGHT_ARROW) + ), + categoryHeaderTitlePrefix = context.resources.getString(R.string.top_spend_categories), + title = context.resources.getString(R.string.no_transactions_to_categorise), + addAccountCtaText = context.resources.getString(R.string.add_another_account), + iconUrl = + IllustrationSource.Remote( + url = COMMON_EMPTY_PLACEHOLDER, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ), + ctaIconUrl = IllustrationSource.Resource(PLUS_ICON), + loadingLottieUrl = IllustrationSource.Resource(TUK_TUK_LOTTIE), + selectedYear = year, + selectedMonth = month, + isTotalSyncCompleted = isTotalSyncCompleted + ) + } + + fun getSpendAnalysisLoadingState(month: Int, year: Int): SpendCategorizationState.Loading { + return SpendCategorizationState.Loading( + header = + SectionHeaderData( + title = context.resources.getString(R.string.monthly_summary), + actionText = monthAbbreviations[month].plus(SPACE).plus(year), + suffixIcon = IllustrationSource.Resource(DOWN_ARROW_BLACK_16) + ), + totalSpends = + TotalSpends.Loading( + lottieUrl = IllustrationSource.Resource(CHIP_LOADER_LOTTIE), + title = context.resources.getString(R.string.total_spends), + animationUrl = IllustrationSource.Resource(TUK_TUK_LOTTIE) + ), + selectedYear = year, + selectedMonth = month, + categoryHeaderTitlePrefix = context.resources.getString(R.string.top_spend_categories), + categories = + (1..3).map { index -> + SpendCategoryItemData.Loading( + name = String.format(context.resources.getString(R.string.category), index), + lottieUrl = IllustrationSource.Resource(CHIP_LOADER_LOTTIE), + actionLottie = IllustrationSource.Resource(TUK_TUK_LOTTIE) + ) + } + ) + } + + fun getSpendAnalysisLoadedState( + categorySummary: List, + mmConfig: MMConfigResponse?, + month: Int, + year: Int + ): SpendCategorizationState.Loaded { + return SpendCategorizationState.Loaded( + header = + SectionHeaderData( + title = context.resources.getString(R.string.monthly_summary), + actionText = monthAbbreviations[month].plus(SPACE).plus(year), + suffixIcon = IllustrationSource.Resource(DOWN_ARROW_BLACK_16) + ), + totalSpends = + TotalSpends.Loaded( + iconUrl = + IllustrationSource.Remote( + url = TOTAL_SPEND_RUPEE_SYMBOL_WITHOUT_BG, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ), + title = context.resources.getString(R.string.total_spends), + subtitle = getTotalSpendsAmount(categorySummary), + actionIcon = IllustrationSource.Resource(ICON_ROUND_PURPLE_RIGHT_ARROW) + ), + selectedYear = year, + selectedMonth = month, + categoryHeaderTitlePrefix = context.resources.getString(R.string.top_spend_categories), + categories = getTopCategoriesData(categorySummary, mmConfig, 3), + viewMoreCtaText = context.resources.getString(R.string.view_more), + selfTransferCategory = getSelfTransferCategoryData(categorySummary, mmConfig) + ) + } + + fun getFilteredCategories(categorySummary: List): List { + return categorySummary.filter { it.finalCategory != SELF_TRANSFER } + } + + private fun getTotalSpendsAmount(categories: List): String { + // Filter categories excluding "SELF_TRANSFER" + val filteredCategories = getFilteredCategories(categories) + return filteredCategories.sumOf { it.totalAmount.orZero() }.formatToInrWithTwoDecimals() + } + + fun getTopCategoriesData( + categories: List, + mmConfig: MMConfigResponse?, + topN: Int + ): List { + // Filter categories excluding "SELF_TRANSFER" + val filteredCategories = getFilteredCategories(categories) + + // Sort categories by totalAmount in descending order + val sortedCategories = filteredCategories.sortedByDescending { it.totalAmount } + + // Calculate total amount of remaining categories + val totalAmount = sortedCategories.sumOf { it.totalAmount.orZero() } + + // Calculate percentage for each category, but take only as many as available + val categoriesWithPercentage = + sortedCategories.take(topN).mapIndexed { index, item -> + val effectiveTotalAmount = if (totalAmount == 0.0) 1.0 else totalAmount + val progress = item.totalAmount.orZero() / effectiveTotalAmount + val category = + mmConfig?.categories?.firstOrNull { it.categoryId == item.finalCategory } + ?: return@mapIndexed null + SpendCategoryItemData.Loaded( + categoryId = category.categoryId.orEmpty(), + name = category.categoryName.orEmpty(), + iconUrl = + IllustrationSource.Remote( + url = category.categoryIcon.orEmpty(), + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ), + actionIcon = + IllustrationSource.Resource(HOME_PAGE_UPI_SECTION_RIGHT_ARROW_ICON), + progress = progress.toFloat() + 0.03f, // Need to show min 3% for all progress + progressColor = categoriesProgressBarColors[index], + amount = item.totalAmount.orZero().formatToInrWithTwoDecimals(), + progressText = + if (progress < 0.01) context.getString(R.string.less_than_one_percent) + else (progress * 100).formatToDecimalPlaces(0).plus(PERCENT) + ) + } + + return categoriesWithPercentage.filterNotNull() + } + + fun getOtherCategoriesAccumulatedData( + categories: List, + topN: Int + ): SpendCategoryItemData.Loaded? { + // Filter categories excluding "SELF_TRANSFER" + val filteredCategories = getFilteredCategories(categories) + + // If there are fewer than topN categories, return null + if (filteredCategories.size <= topN) return null + + // Sort categories by totalAmount in descending order + val sortedCategories = filteredCategories.sortedByDescending { it.totalAmount } + + // Calculate total amount for all categories and the "Other" category + val totalAmount = sortedCategories.sumOf { it.totalAmount.orZero() } + val othersTotalAmount = sortedCategories.drop(topN).sumOf { it.totalAmount.orZero() } + + // Calculate the progress for the "Other" category + val effectiveTotalAmount = if (totalAmount == 0.0) 1.0 else totalAmount + val progress = + 1 - + (sortedCategories.take(topN).sumOf { it.totalAmount.orZero() } / + effectiveTotalAmount) + + // Create and return the "Other" category + return SpendCategoryItemData.Loaded( + categoryId = Constants.OTHERS_CATEGORY_ID, + name = context.getString(R.string.others), + iconUrl = + IllustrationSource.Remote( + url = OTHERS_CATEGORY_ICON, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ), + actionIcon = IllustrationSource.Resource(HOME_PAGE_UPI_SECTION_RIGHT_ARROW_ICON), + progress = progress.toFloat() + 0.03f, // Need to show min 3% for all progress + progressColor = categoriesProgressBarColors[topN], + amount = othersTotalAmount.formatToInrWithTwoDecimals(), + progressText = + if (progress < 0.01) context.getString(R.string.less_than_one_percent) + else (progress * 100).formatToDecimalPlaces(0).plus(PERCENT) + ) + } + + fun getOtherCategoriesCategorizedData( + categories: List, + mmConfig: MMConfigResponse?, + topN: Int + ): List { + // Filter categories excluding "SELF_TRANSFER" + val filteredCategories = getFilteredCategories(categories) + + // Sort categories by totalAmount in descending order + val sortedCategories = filteredCategories.sortedByDescending { it.totalAmount } + + // Get categories beyond the top N + val categoriesExcludingTopN = sortedCategories.drop(topN) + + // Calculate total amount of all categories + val totalAmount = sortedCategories.sumOf { it.totalAmount.orZero() } + + // Calculate and return data for categories excluding top N + return categoriesExcludingTopN + .mapIndexed { index, item -> + val effectiveTotalAmount = if (totalAmount == 0.0) 1.0 else totalAmount + val progress = item.totalAmount.orZero() / effectiveTotalAmount + val category = + mmConfig?.categories?.firstOrNull { it.categoryId == item.finalCategory } + ?: return@mapIndexed null + + SpendCategoryItemData.Loaded( + categoryId = category.categoryId.orEmpty(), + name = category.categoryName.orEmpty(), + iconUrl = + IllustrationSource.Remote( + url = category.categoryIcon.orEmpty(), + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ), + actionIcon = + IllustrationSource.Resource(HOME_PAGE_UPI_SECTION_RIGHT_ARROW_ICON), + progress = progress.toFloat() + 0.03f, // Need to show min 3% for all progress + progressColor = + categoriesProgressBarColors.getOrElse(topN + index) { + categoriesProgressBarColors.last() + }, + amount = item.totalAmount.orZero().formatToInrWithTwoDecimals(), + progressText = + if (progress < 0.01) context.getString(R.string.less_than_one_percent) + else (progress * 100).formatToDecimalPlaces(0).plus(PERCENT) + ) + } + .filterNotNull() + } + + private fun getSelfTransferCategoryData( + categories: List, + mmConfig: MMConfigResponse? + ): SelfTransferCategoryData { + val selfTransferCategory = categories.find { it.finalCategory == SELF_TRANSFER } + val category = mmConfig?.categories?.firstOrNull { it.categoryId == SELF_TRANSFER } + + return SelfTransferCategoryData( + categoryId = SELF_TRANSFER, + categoryName = + category?.categoryName ?: context.resources.getString(R.string.self_transfer), + subtitle = context.resources.getString(R.string.not_included_in_spends), + amount = selfTransferCategory?.totalAmount.orZero().formatToInrWithTwoDecimals(), + actionIcon = IllustrationSource.Resource(HOME_PAGE_UPI_SECTION_RIGHT_ARROW_ICON), + iconUrl = + IllustrationSource.Remote( + url = category?.categoryIcon.orEmpty(), + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/dashboard/provider/DashboardDataProviderImpl.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/dashboard/provider/DashboardDataProviderImpl.kt new file mode 100644 index 0000000000..f77e0aabdb --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/dashboard/provider/DashboardDataProviderImpl.kt @@ -0,0 +1,594 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.dashboard.provider + +import android.content.Context +import androidx.compose.runtime.MutableState +import com.navi.base.utils.BaseUtils.getDayMonthAndYearFromTimestamp +import com.navi.base.utils.EMPTY +import com.navi.base.utils.SPACE +import com.navi.base.utils.isNotNullAndNotEmpty +import com.navi.common.constants.HELP_CTA_TEXT +import com.navi.moneymanager.R +import com.navi.moneymanager.common.analytics.DataProviderEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.DashboardBankSectionProviderHelper +import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.MMConfigResponseHelper +import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.SpendCategorizationHelper +import com.navi.moneymanager.common.dataprovider.data.datastore.DataStoreInfoProvider +import com.navi.moneymanager.common.dataprovider.data.transaction.helper.TransactionProviderHelper +import com.navi.moneymanager.common.dataprovider.domain.DashboardDataProvider +import com.navi.moneymanager.common.dataprovider.utils.DataProviderConstants.COMMA +import com.navi.moneymanager.common.dataprovider.utils.DataProviderConstants.LAST_UPDATED_REFRESH_SCHEDULER_DELAY +import com.navi.moneymanager.common.dataprovider.utils.getLastTwelveMonths +import com.navi.moneymanager.common.dataprovider.utils.toCapitalizedWords +import com.navi.moneymanager.common.db.database.MMEncryptedDatabase +import com.navi.moneymanager.common.helper.convertMonthStringToPair +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.provider.IllustrationProvider +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.ALERT_CIRCLE +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.BLACK_ALERT_CIRCLE +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.COMMON_EMPTY_PLACEHOLDER +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.DASHBOARD_BOTTOMSHEET_LOADING +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.INFO +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.PLUS_ICON +import com.navi.moneymanager.common.illustration.repository.LottieRepository.Companion.CHIP_LOADER_LOTTIE +import com.navi.moneymanager.common.illustration.repository.LottieRepository.Companion.TUK_TUK_WHITE +import com.navi.moneymanager.common.model.CategorySummary +import com.navi.moneymanager.common.model.DataLoadingBottomSheetData +import com.navi.moneymanager.common.model.MonthSelectionBottomSheetData +import com.navi.moneymanager.common.model.SelectedMonth +import com.navi.moneymanager.common.model.SpendCategorizationState +import com.navi.moneymanager.common.model.ZeroTransactionData +import com.navi.moneymanager.common.model.database.AccountOverview +import com.navi.moneymanager.common.model.database.TransactionSummaryData +import com.navi.moneymanager.common.network.di.RoomDataStoreInfoProvider +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.utils.Constants.IS_FIRST_MONTH_SYNC_COMPLETED +import com.navi.moneymanager.common.utils.Constants.IS_TOTAL_SYNC_COMPLETED +import com.navi.moneymanager.common.utils.Constants.RECENT_TRANSACTION_COUNT +import com.navi.moneymanager.common.utils.Constants.USER_NAME +import com.navi.moneymanager.common.utils.calculateTimestamps +import com.navi.moneymanager.common.utils.combineWithFlatMapLatest +import com.navi.moneymanager.postonboard.dashboard.model.AddBankChipInfo +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountFooterSection +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountsData +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountsLoadingState +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountsState +import com.navi.moneymanager.postonboard.dashboard.model.BankTransactionsData +import com.navi.moneymanager.postonboard.dashboard.model.FinarkeinErrorBottomSheetData +import com.navi.moneymanager.postonboard.dashboard.model.NavBarData +import com.navi.moneymanager.postonboard.dashboard.model.RecentTransactionsLoadingItemData +import com.navi.moneymanager.postonboard.dashboard.model.RecentTransactionsState +import com.navi.moneymanager.postonboard.dashboard.model.RightCtaData +import com.navi.moneymanager.postonboard.dashboard.model.UserHeaderState +import com.navi.moneymanager.preonboard.launcher.model.ConsentRevokedBottomSheetData +import com.navi.naviwidgets.utils.NaviWidgetLottieUtils.TUK_TUK_LOTTIE +import dagger.Lazy +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import timber.log.Timber + +typealias IsCurrentMonthSynced = Boolean + +typealias IsTotalSyncCompleted = Boolean + +class DashboardDataProviderImpl +@Inject +constructor( + private val encryptedDatabase: Lazy, + private val bankSectionProviderHelper: DashboardBankSectionProviderHelper, + private val mmConfigResponseHelper: MMConfigResponseHelper, + @RoomDataStoreInfoProvider private val dbDataStoreProvider: DataStoreInfoProvider, + private val spendCategorizationHelper: SpendCategorizationHelper, + private val transactionProviderHelper: TransactionProviderHelper, + @ApplicationContext private val context: Context +) : DashboardDataProvider { + + @OptIn(FlowPreview::class) + private fun getTransactionCountFlow(): Flow { + return encryptedDatabase.get().transactionsDao().getTransactionCount().debounce(200) + } + + private suspend fun isFirstMonthSyncedFlow() = + dbDataStoreProvider.getBooleanData(IS_FIRST_MONTH_SYNC_COMPLETED).distinctUntilChanged() + + private suspend fun isTotalSyncCompletedFlow() = + dbDataStoreProvider.getBooleanData(IS_TOTAL_SYNC_COMPLETED).distinctUntilChanged() + + private suspend fun syncStatusFlow(): Flow> { + return combine(isFirstMonthSyncedFlow(), isTotalSyncCompletedFlow()) { + isFirstMonthSyncCompleted, + isTotalSyncCompleted -> + Pair(isFirstMonthSyncCompleted, isTotalSyncCompleted) + } + } + + override suspend fun saveMMConfigToDB(data: MMConfigResponse) = + mmConfigResponseHelper.saveMMConfigToDB(data) + + override suspend fun getMMConfig() = mmConfigResponseHelper.getMMConfig() + + override suspend fun getTopNavBarData() = NavBarData(actionLabel = HELP_CTA_TEXT) + + override suspend fun getUserHeader(customerProfileName: String?): UserHeaderState { + val displayName = + customerProfileName + ?: dbDataStoreProvider.getStringData(USER_NAME, EMPTY).first().takeIf { + it.isNotEmpty() + } + ?: context.resources.getString(R.string.hello) + + val greetingText = + if (displayName == context.resources.getString(R.string.hello)) { + displayName.plus(COMMA) + } else { + createGreeting(displayName) + } + + return UserHeaderState.Loaded( + greetingPrompt = greetingText, + expensePrompt = context.resources.getString(R.string.dashboard_heading_title) + ) + } + + private fun createGreeting(displayName: String) = + context + .getString(R.string.hi) + .plus(SPACE) + .plus(displayName.toCapitalizedWords()) + .plus(COMMA) + + override suspend fun getBankSectionStateFlow(): + Flow> { + return combineWithFlatMapLatest( + flow1 = syncStatusFlow(), + flow2 = encryptedDatabase.get().accountsDao().fetchAllAccounts().distinctUntilChanged(), + combineTransform = { (isCurrentMonthSynced, isTotalDataSynced), accounts -> + Triple(isCurrentMonthSynced, isTotalDataSynced, accounts) + }, + flatMapLatestTransform = { (isCurrentMonthSynced, isTotalDataSynced, accounts) -> + DataProviderEventTrackerImpl.dashboardDpBankSectionEmission( + isCurrentMonthSynced, + isTotalDataSynced, + accounts.size + ) + val state = getBankSectionState(accounts, isTotalDataSynced) + emit(state to isCurrentMonthSynced) + } + ) + } + + private fun getBankSectionState( + accounts: List, + isTotalSyncCompleted: Boolean + ): BankAccountsState { + return if (accounts.any { account -> account.currentBalance != null }) { + val aggregate = bankSectionProviderHelper.createAggregateAccount(accounts) + val accountsList = bankSectionProviderHelper.createAccountList(accounts) + DataProviderEventTrackerImpl.dashboardDpBankSectionLoaded( + accountsListSize = accountsList.size, + isTotalSyncCompleted + ) + BankAccountsState.Loaded( + data = + BankAccountsData( + accounts = accountsList, + aggregate = aggregate, + addBankChipInfo = + AddBankChipInfo( + addBankText = context.resources.getString(R.string.add_account), + addBankIcon = IllustrationSource.Resource(PLUS_ICON), + addBankLoaderLottie = + IllustrationSource.Resource(CHIP_LOADER_LOTTIE), + isTotalSyncCompleted = isTotalSyncCompleted + ) + ) + ) + } else { + DataProviderEventTrackerImpl.dashboardDpBankSectionLoading() + BankAccountsState.Loading( + data = + BankAccountsLoadingState( + chipText = context.resources.getString(R.string.fetching_bank), + chipLottie = IllustrationSource.Resource(CHIP_LOADER_LOTTIE), + balanceLottie = IllustrationSource.Resource(TUK_TUK_LOTTIE), + balanceSectionLottie = IllustrationSource.Resource(CHIP_LOADER_LOTTIE), + balanceSectionTitle = context.resources.getString(R.string.total_balance), + lastUpdated = + context.resources.getString(R.string.fetching_past_transactions) + ) + ) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + override suspend fun updateBankSectionRefreshingText( + refreshing: Boolean + ): Flow> = + encryptedDatabase + .get() + .accountsDao() + .fetchAllAccounts() + .distinctUntilChanged() + .flatMapLatest { accounts -> + flow { + Timber.tag("mm_dev_sync") + .d( + "DashboardDataProvider ${::updateBankSectionRefreshingText.name}: refreshing: $refreshing accounts: $accounts" + ) + if (refreshing) { + emit(bankSectionProviderHelper.getRefreshingLastUpdatedSection(accounts)) + } else { + bankSectionProviderHelper.initLastUpdateTimeMap(accounts) + while (true) { + Timber.tag("mm_dev_sync") + .d( + "DashboardDataProvider ${::updateBankSectionRefreshingText.name}: emit new time" + ) + emit(bankSectionProviderHelper.getBanksLastUpdatedSection()) + delay(LAST_UPDATED_REFRESH_SCHEDULER_DELAY) + } + } + } + } + + override suspend fun getSpendCategorizationSectionStateFlow( + screenParams: MutableStateFlow + ): Flow { + return combineWithFlatMapLatest( + flow1 = syncStatusFlow(), + flow2 = screenParams.filterNotNull(), + flow3 = getTransactionCountFlow(), + combineTransform = { syncStatus, params, _ -> + val (isFirstMonthSyncCompleted, isTotalSyncCompleted) = syncStatus + Triple(isFirstMonthSyncCompleted, isTotalSyncCompleted, params) + }, + flatMapLatestTransform = { (isFirstMonthSyncCompleted, isTotalSyncCompleted, params) -> + val dateComponents = getDayMonthAndYearFromTimestamp(System.currentTimeMillis()) + + DataProviderEventTrackerImpl.dashboardDpSpendAnalysisSectionEmission( + params.month.toString(), + params.year.toString() + ) + + val categorySummaryList = + encryptedDatabase + .get() + .transactionsDao() + .getCategorizedTransactionSummary( + month = params.month ?: dateComponents.second, + year = params.year ?: dateComponents.third + ) + .firstOrNull() ?: emptyList() + + val state = + getSpendCategorizationSectionState( + month = params.month ?: dateComponents.second, + year = params.year ?: dateComponents.third, + isCurrentMonthSyncCompleted = isFirstMonthSyncCompleted, + isTotalSyncCompleted = isTotalSyncCompleted, + categorySummaryList = categorySummaryList + ) + emit(state) + } + ) + } + + private suspend fun getSpendCategorizationSectionState( + isCurrentMonthSyncCompleted: Boolean, + month: Int, + year: Int, + isTotalSyncCompleted: Boolean, + categorySummaryList: List + ): SpendCategorizationState { + return if (!isCurrentMonthSyncCompleted) { + DataProviderEventTrackerImpl.dashboardDpSpendAnalysisSectionLoading( + month.toString(), + year.toString(), + isTotalSyncCompleted + ) + spendCategorizationHelper.getSpendAnalysisLoadingState(month = month, year = year) + } else { + if (spendCategorizationHelper.getFilteredCategories(categorySummaryList).isNotEmpty()) { + DataProviderEventTrackerImpl.dashboardDpSpendAnalysisSectionLoaded( + month.toString(), + year.toString(), + isTotalSyncCompleted + ) + spendCategorizationHelper.getSpendAnalysisLoadedState( + categorySummary = categorySummaryList, + mmConfig = getMMConfig(), + month = month, + year = year + ) + } else { + DataProviderEventTrackerImpl.dashboardDpSpendAnalysisSectionEmpty( + month.toString(), + year.toString(), + isTotalSyncCompleted + ) + spendCategorizationHelper.getSpendAnalysisEmptyState( + month = month, + year = year, + isTotalSyncCompleted = isTotalSyncCompleted + ) + } + } + } + + override suspend fun getRecentTransactionsSectionStateFlow( + currentMonthTag: MutableState + ): Flow { + return combineWithFlatMapLatest( + flow1 = syncStatusFlow(), + flow2 = getTransactionCountFlow(), + flow3 = encryptedDatabase.get().accountsDao().fetchAllAccounts(), + combineTransform = { (isFirstMonthSyncCompleted, isTotalSyncCompleted), _, accountList + -> + Triple(isFirstMonthSyncCompleted, isTotalSyncCompleted, accountList) + }, + flatMapLatestTransform = { + (isFirstMonthSyncCompleted, isTotalSyncCompleted, accountList) -> + DataProviderEventTrackerImpl.dashboardDpRecentTransactionSectionEmission( + isFirstMonthSyncCompleted, + isTotalSyncCompleted + ) + val state = + getRecentTransactionsSectionState( + isTotalSyncCompleted = isTotalSyncCompleted, + isCurrentMonthSyncCompleted = isFirstMonthSyncCompleted, + currentMonthTag = currentMonthTag, + accountList = accountList + ) + emit(state) + } + ) + } + + private suspend fun getRecentTransactionsSectionState( + currentMonthTag: MutableState, + isTotalSyncCompleted: Boolean, + isCurrentMonthSyncCompleted: Boolean, + accountList: List + ): RecentTransactionsState { + val (startDate, endDate) = calculateTimestamps() + val bankTransactionDataList = + accountList.map { account -> + val transactions: List = + encryptedDatabase + .get() + .transactionsDao() + .fetchTransactionsForBank( + linkedAccRef = account.linkedAccRef, + startDate = startDate, + endDate = endDate, + limit = RECENT_TRANSACTION_COUNT + ) + .first() + + val filteredTransactions: List = + filterTransactions(transactions, currentMonthTag, isTotalSyncCompleted) + + BankTransactionsData( + bankReferenceId = account.linkedAccRef, + transactions = + transactionProviderHelper.createTransactionList( + filteredTransactions.map { transaction -> + transaction.copy(bankIconUrl = account.bankIconUrl) + } + ) + ) + } + + return when { + isTotalSyncCompleted -> { + DataProviderEventTrackerImpl.dashboardDpRecentTransactionSectionLoaded( + isFirstMonthSyncCompleted = isCurrentMonthSyncCompleted, + isTotalSyncCompleted = true, + transactionsListSize = bankTransactionDataList.size + ) + createLoadedTransactions(bankTransactionDataList) + } + bankTransactionDataList.flatMap { it.transactions }.isEmpty() -> { + DataProviderEventTrackerImpl.dashboardDpRecentTransactionSectionLoading( + isFirstMonthSyncCompleted = isCurrentMonthSyncCompleted, + isTotalSyncCompleted = isTotalSyncCompleted, + transactionsListSize = bankTransactionDataList.size + ) + createLoadingTransactions() + } + else -> { + DataProviderEventTrackerImpl.dashboardDpRecentTransactionSectionLoaded( + isFirstMonthSyncCompleted = isCurrentMonthSyncCompleted, + isTotalSyncCompleted = isTotalSyncCompleted, + transactionsListSize = bankTransactionDataList.size + ) + createLoadedTransactions(bankTransactionDataList) + } + } + } + + private fun filterTransactions( + transactions: List, + currentMonthTag: MutableState, + isTotalSyncCompleted: Boolean + ): List { + return when { + isTotalSyncCompleted -> transactions + currentMonthTag.value.isNotNullAndNotEmpty() -> { + transactions + .filter { it.hasValidMonthYear() } + .filter { + val formattedMonthTag = it.getFormattedMonthTag() + formattedMonthTag == currentMonthTag.value + } + } + else -> { + transactions + .firstOrNull { it.hasValidMonthYear() } + ?.let { currentMonthTag.value = it.getFormattedMonthTag() } + transactions + } + } + } + + private fun createLoadedTransactions( + bankTransactionDataList: List + ): RecentTransactionsState { + return RecentTransactionsState.Loaded( + sectionTitle = context.getString(R.string.mm_transaction_dashboard_recent), + primaryCtaTitle = context.getString(R.string.mm_transaction_dashboard_view_all), + zeroTransactionData = createZeroTransactionData(), + aggregateTransactions = createAggregateTransactions(bankTransactionDataList), + accountTransactions = bankTransactionDataList + ) + } + + private fun createLoadingTransactions(): RecentTransactionsState { + return RecentTransactionsState.Loading( + sectionTitle = context.getString(R.string.mm_transaction_dashboard_recent), + transactions = + (1..3).map { + RecentTransactionsLoadingItemData( + name = context.resources.getString(R.string.fetching_transactions), + categoryText = context.resources.getString(R.string.category_placeholder), + lottieUrl = IllustrationSource.Resource(CHIP_LOADER_LOTTIE), + actionLottie = IllustrationSource.Resource(TUK_TUK_LOTTIE), + dateText = context.getString(R.string.date_time_placeholder) + ) + } + ) + } + + private fun createZeroTransactionData(): ZeroTransactionData { + return ZeroTransactionData( + title = context.getString(R.string.mm_transaction_zero_title), + illustrationSource = + IllustrationSource.Remote( + url = COMMON_EMPTY_PLACEHOLDER, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ) + ) + } + + private fun createAggregateTransactions( + groupedTransactions: List + ): BankTransactionsData? { + return if (groupedTransactions.size > 1) + BankTransactionsData( + bankReferenceId = context.resources.getString(R.string.all_banks), + transactions = + transactionProviderHelper.getLatestTransactionsForAllBanks( + groupedTransactions, + RECENT_TRANSACTION_COUNT + ) + ) + else null + } + + override suspend fun getMonthSelectionBottomSheetData( + displayMonthText: String + ): MonthSelectionBottomSheetData { + val last12Months = getLastTwelveMonths() + val selectedMonth = convertMonthStringToPair(displayMonthText) + return MonthSelectionBottomSheetData( + headerText = context.resources.getString(R.string.select_month), + monthList = last12Months, + selectedMonth = selectedMonth, + ctaText = context.resources.getString(R.string.apply_pascal_case) + ) + } + + override suspend fun getPastMonthDataLoadingBottomSheetData(): DataLoadingBottomSheetData { + return DataLoadingBottomSheetData( + loadingIcon = null, + loadingLottie = IllustrationSource.Resource(CHIP_LOADER_LOTTIE), + titleText = context.resources.getString(R.string.fetching_past_month_transactions), + subTitleText = + context.resources.getString(R.string.wait_for_sometime_to_see_past_months_summary), + ctaText = context.resources.getString(R.string.okay_got_it_no_comma) + ) + } + + override suspend fun getTransactionsFetchingBottomSheetData(): DataLoadingBottomSheetData { + return DataLoadingBottomSheetData( + loadingIcon = + IllustrationSource.Remote( + url = DASHBOARD_BOTTOMSHEET_LOADING, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ), + loadingLottie = null, + titleText = context.resources.getString(R.string.fetching_your_transactions), + subTitleText = + context.resources.getString(R.string.usually_takes_time_will_be_notified), + ctaText = context.resources.getString(R.string.okay_got_it_no_comma) + ) + } + + override suspend fun getFinarkeinErrorBottomSheetData(): FinarkeinErrorBottomSheetData { + return FinarkeinErrorBottomSheetData( + headerIcon = + IllustrationSource.Remote( + url = ALERT_CIRCLE, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ), + titleText = context.resources.getString(R.string.something_wrong), + subTitleText = context.resources.getString(R.string.please_try_again), + leftCtaText = context.resources.getString(R.string.do_later), + rightCtaData = + RightCtaData( + lottieUrl = IllustrationProvider.getLottieRemoteUrl(TUK_TUK_WHITE), + title = context.resources.getString(R.string.retry_pascal_case) + ) + ) + } + + override suspend fun getConsentRevokedBottomSheetData(): ConsentRevokedBottomSheetData { + return ConsentRevokedBottomSheetData( + headerIcon = + IllustrationSource.Remote( + url = BLACK_ALERT_CIRCLE, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ), + titleText = context.resources.getString(R.string.your_account_removed_successfully), + subTitleText = + context.resources.getString( + R.string.to_continue_tracking_complete_onboarding_process_again + ), + ctaText = context.resources.getString(R.string.okay_got_it_no_comma) + ) + } + + override suspend fun getAddAccountLoadingBottomSheetData(): DataLoadingBottomSheetData { + return DataLoadingBottomSheetData( + loadingIcon = + IllustrationSource.Remote(INFO, placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM), + loadingLottie = null, + titleText = + context.resources.getString(R.string.please_wait_for_sometime_to_add_more_banks), + subTitleText = + context.resources.getString(R.string.add_account_loading_bottomsheet_description), + ctaText = context.resources.getString(R.string.okay_got_it_no_comma) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/datastore/DbDataStoreProvider.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/datastore/DbDataStoreProvider.kt new file mode 100644 index 0000000000..e4d17c9077 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/datastore/DbDataStoreProvider.kt @@ -0,0 +1,108 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.datastore + +import com.navi.moneymanager.common.dataprovider.utils.getTransformedFlow +import com.navi.moneymanager.common.db.database.MMDatabase +import com.navi.moneymanager.common.db.entity.DataStoreEntity +import com.navi.moneymanager.common.network.di.RoomDataStoreInfoProvider +import dagger.Lazy +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.flow.Flow + +interface DataStoreInfoProvider { + suspend fun saveStringData(key: String, value: String) + + suspend fun getStringData(key: String, defaultValue: String): Flow + + suspend fun deleteStringData(key: String) + + suspend fun saveIntData(key: String, value: Int) + + suspend fun getIntData(key: String, defaultValue: Int): Flow + + suspend fun deleteIntData(key: String) + + suspend fun saveLongData(key: String, value: Long) + + suspend fun getLongData(key: String, defaultValue: Long): Flow + + suspend fun deleteLongData(key: String) + + suspend fun saveBooleanData(key: String, value: Boolean) + + suspend fun getBooleanData(key: String, defaultValue: Boolean = false): Flow + + suspend fun deleteBooleanData(key: String) + + suspend fun containsKey(key: String): Boolean +} + +@RoomDataStoreInfoProvider +@Singleton +class DbDataStoreProvider +@Inject +constructor( + database: Lazy, +) : DataStoreInfoProvider { + + private val dataStoreDao = database.get().getDataStoreDao() + + override suspend fun saveStringData(key: String, value: String) { + dataStoreDao.save(DataStoreEntity(key, value)) + } + + override suspend fun saveIntData(key: String, value: Int) { + dataStoreDao.save(DataStoreEntity(key, value.toString())) + } + + override suspend fun saveLongData(key: String, value: Long) { + dataStoreDao.save(DataStoreEntity(key, value.toString())) + } + + override suspend fun saveBooleanData(key: String, value: Boolean) { + dataStoreDao.save(DataStoreEntity(key, value.toString())) + } + + override suspend fun getStringData(key: String, defaultValue: String): Flow { + return dataStoreDao.getTransformedFlow(key, defaultValue) { it ?: defaultValue } + } + + override suspend fun getIntData(key: String, defaultValue: Int): Flow { + return dataStoreDao.getTransformedFlow(key, defaultValue) { it?.toInt() ?: defaultValue } + } + + override suspend fun getLongData(key: String, defaultValue: Long): Flow { + return dataStoreDao.getTransformedFlow(key, defaultValue) { it?.toLong() ?: defaultValue } + } + + override suspend fun getBooleanData(key: String, defaultValue: Boolean): Flow { + return dataStoreDao.getTransformedFlow(key, defaultValue) { + it?.toBoolean() ?: defaultValue + } + } + + override suspend fun deleteStringData(key: String) { + dataStoreDao.delete(key) + } + + override suspend fun deleteIntData(key: String) { + dataStoreDao.delete(key) + } + + override suspend fun deleteLongData(key: String) { + dataStoreDao.delete(key) + } + + override suspend fun deleteBooleanData(key: String) { + dataStoreDao.delete(key) + } + + override suspend fun containsKey(key: String) = dataStoreDao.containsKey(key) > 0 +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/remote/LocalDataSyncManagerImpl.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/remote/LocalDataSyncManagerImpl.kt new file mode 100644 index 0000000000..fca4a570c0 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/remote/LocalDataSyncManagerImpl.kt @@ -0,0 +1,100 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.remote + +import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.MMConfigResponseHelper +import com.navi.moneymanager.common.dataprovider.data.datastore.DataStoreInfoProvider +import com.navi.moneymanager.common.dataprovider.domain.LocalDataSyncManager +import com.navi.moneymanager.common.db.database.MMEncryptedDatabase +import com.navi.moneymanager.common.helper.AccountsDataHelper +import com.navi.moneymanager.common.helper.TransactionsDataHelper +import com.navi.moneymanager.common.network.di.RoomDataStoreInfoProvider +import com.navi.moneymanager.common.network.model.AccountData +import com.navi.moneymanager.common.network.model.TransactionData +import com.navi.moneymanager.common.utils.Constants +import com.navi.moneymanager.common.utils.Constants.NEW_TRANSACTION_COUNT +import com.navi.moneymanager.common.utils.calculateTimestamps +import dagger.Lazy +import javax.inject.Inject +import kotlinx.coroutines.flow.first + +class LocalDataSyncManagerImpl +@Inject +constructor( + private val encryptedDatabase: Lazy, + private val accountsDataHelper: AccountsDataHelper, + private val transactionsDataHelper: TransactionsDataHelper, + @RoomDataStoreInfoProvider private val dbDataStoreProvider: DataStoreInfoProvider, + private val mmConfigResponseHelper: Lazy +) : LocalDataSyncManager { + override suspend fun insertAccounts(accounts: List) { + encryptedDatabase + .get() + .accountsDao() + .deleteAccountsNotIn(accountsDataHelper.getAccountsEntity(accounts)) + encryptedDatabase + .get() + .accountsDao() + .insertAllAccounts(accountsDataHelper.getAccountsEntity(accounts)) + } + + override suspend fun insertTransactions( + transactions: List, + isCurrentMonthData: Boolean + ) { + val mmConfig = mmConfigResponseHelper.get().getMMConfig() + encryptedDatabase + .get() + .transactionsDao() + .insertAllTransactions( + transactionsDataHelper.getTransactionsEntity(transactions, mmConfig) + ) + + if (!isCurrentMonthData && transactions.isNotEmpty()) + updatedLastSyncTimestamp(transactions.last().mobileTimestamp) + } + + override suspend fun updatedLastSyncTimestamp(timestamp: Long?) { + timestamp?.let { + dbDataStoreProvider.saveLongData(Constants.LAST_TRANSACTION_SYNCED_TIMESTAMP, it) + } + } + + override suspend fun isTotalSyncCompleted() = + dbDataStoreProvider.getBooleanData(Constants.IS_TOTAL_SYNC_COMPLETED).first() + + override suspend fun isCurrentMonthSyncCompleted() = + dbDataStoreProvider.getBooleanData(Constants.IS_FIRST_MONTH_SYNC_COMPLETED).first() + + override suspend fun getLastSyncedTimestamp(defaultTimestamp: Long): Long { + return dbDataStoreProvider + .getLongData(Constants.LAST_TRANSACTION_SYNCED_TIMESTAMP, defaultTimestamp) + .first() + } + + override suspend fun updateCurrentMonthSyncFlag() { + dbDataStoreProvider.saveBooleanData(Constants.IS_FIRST_MONTH_SYNC_COMPLETED, true) + } + + override suspend fun updateTotalSyncCompleteFlag() { + dbDataStoreProvider.saveBooleanData(Constants.IS_TOTAL_SYNC_COMPLETED, true) + } + + override suspend fun getTransactionCount(): Int { + val (startDate, endDate) = calculateTimestamps() + return encryptedDatabase.get().transactionsDao().getTransactionsCount(startDate, endDate) + } + + override suspend fun updateNewTransactionCount(newTransaction: Int) { + dbDataStoreProvider.saveIntData(NEW_TRANSACTION_COUNT, newTransaction) + } + + override suspend fun getLatestTransactionTimestamp(): Long { + return encryptedDatabase.get().transactionsDao().getLatestTransactionTimestamp() + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/remote/RemoteDataProviderImpl.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/remote/RemoteDataProviderImpl.kt new file mode 100644 index 0000000000..a9eeed680b --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/remote/RemoteDataProviderImpl.kt @@ -0,0 +1,165 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.remote + +import com.google.gson.reflect.TypeToken +import com.navi.common.checkmate.model.MetricInfo +import com.navi.common.network.models.RepoResult +import com.navi.common.network.models.isSuccessWithData +import com.navi.common.network.retrofit.ResponseCallback +import com.navi.moneymanager.common.dataprovider.domain.RemoteDataProvider +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.network.model.CustomerProfileData +import com.navi.moneymanager.common.network.model.FetchConsentUrlResponse +import com.navi.moneymanager.common.network.model.FinarkeinDataResponse +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.network.model.PollingStatusResponse +import com.navi.moneymanager.common.network.model.RefreshDataResponse +import com.navi.moneymanager.common.network.model.TransactionResponse +import com.navi.moneymanager.common.network.service.RetrofitService +import com.navi.moneymanager.common.utils.Constants.CPS +import com.navi.moneymanager.common.utils.mockApiResponse +import com.navi.moneymanager.postonboard.monthlysummary.model.PostCategoryTransactionData +import javax.inject.Inject + +class RemoteDataProviderImpl +@Inject +constructor( + private val retrofitService: RetrofitService, +) : RemoteDataProvider, ResponseCallback() { + + override suspend fun fetchTransactions( + from: Long, + pageNo: Int, + pageSize: Int, + queryBy: String + ): RepoResult { + return apiResponseCallback( + response = + retrofitService.fetchTransactions( + from = from, + pageNo = pageNo, + pageSize = pageSize, + queryBy = queryBy + ), + metricInfo = + MetricInfo.AppMetric( + screen = MMScreen.DASHBOARD.screen, + isNae = { false }, + ) + ) + } + + override suspend fun fetchUserOnboardingStatus( + screenName: String, + naeApplicable: Boolean, + ) = + apiResponseCallback( + response = retrofitService.fetchUserOnboardingStatus(), + metricInfo = + MetricInfo.AppMetric( + screen = screenName, + isNae = { if (naeApplicable) !it.isSuccessWithData() else false }, + ) + ) + + override suspend fun fetchAlchemistScreen(screenName: String) = + apiResponseCallback( + response = retrofitService.fetchAlchemistScreen(screenName), + metricInfo = + MetricInfo.AppMetric( + screen = MMScreen.LAUNCHER.screen, + isNae = { false }, + ) + ) + + override suspend fun fetchFinarkeinData(screenName: String): RepoResult { + return apiResponseCallback( + response = retrofitService.fetchFinarkeinSDKInitData(), + metricInfo = + MetricInfo.AppMetric( + screen = screenName, + isNae = { !it.isSuccessWithData() }, + ) + ) + } + + override suspend fun fetchMMConfigResponse(screenName: String): RepoResult { + return apiResponseCallback( + response = retrofitService.fetchMMConfigResponse(), + metricInfo = + MetricInfo.AppMetric( + screen = screenName, + isNae = { false }, + ) + ) + } + + override suspend fun fetchUserAccounts(screenName: String) = + apiResponseCallback( + response = retrofitService.fetchUserAccounts(), + metricInfo = + MetricInfo.AppMetric( + screen = screenName, + isNae = { false }, + ) + ) + + override suspend fun postTransactionCategoryData( + screenName: String, + requestBody: PostCategoryTransactionData + ): RepoResult { + return apiResponseCallback( + response = retrofitService.postTransactionCategoryData(requestBody), + metricInfo = + MetricInfo.AppMetric( + screen = screenName, + isNae = { !it.isSuccessWithData() }, + ) + ) + } + + override suspend fun refreshData(): RepoResult { + return apiResponseCallback( + response = retrofitService.refreshData(), + metricInfo = + MetricInfo.AppMetric( + screen = MMScreen.DASHBOARD.screen, + isNae = { false }, + ) + ) + } + + override suspend fun pollSyncStatus(requestId: String): RepoResult { + return apiResponseCallback( + response = retrofitService.pollSyncStatus(requestId), + metricInfo = + MetricInfo.AppMetric( + screen = MMScreen.DASHBOARD.screen, + isNae = { false }, + ) + ) + } + + override suspend fun fetchConsentUrl(): RepoResult { + val type = object : TypeToken() {}.type + return mockApiResponse(type, "consentData") + // return apiResponseCallback(retrofitService.fetchConsentUrl()) + } + + override suspend fun getCustomerProfileData(): RepoResult { + return apiResponseCallback( + response = retrofitService.fetchCustomerName(CPS), + metricInfo = + MetricInfo.AppMetric( + screen = MMScreen.DASHBOARD.screen, + isNae = { false }, + ) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/helper/BankSelectionBottomSheetDataHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/helper/BankSelectionBottomSheetDataHelper.kt new file mode 100644 index 0000000000..204372f148 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/helper/BankSelectionBottomSheetDataHelper.kt @@ -0,0 +1,51 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.spendanalysis.helper + +import android.content.Context +import com.navi.moneymanager.R +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.provider.IllustrationProvider +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.COMMON_CROSS_BLACK +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.illustration.repository.LottieRepository.Companion.TUK_TUK_WHITE +import com.navi.moneymanager.common.model.BankAccount +import com.navi.moneymanager.common.model.BankSelectionBottomSheetData +import com.navi.moneymanager.common.model.database.AccountOverview +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class BankSelectionBottomSheetDataHelper +@Inject +constructor(@ApplicationContext private val context: Context) { + fun getBankSelectionBottomSheetData( + bankAccounts: List, + selectedBankReferenceIds: Set + ): BankSelectionBottomSheetData { + return BankSelectionBottomSheetData( + headerText = context.resources.getString(R.string.select_bank_accounts), + backIconUrl = + IllustrationSource.Remote( + url = COMMON_CROSS_BLACK, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ), + availableBanks = + bankAccounts.map { + BankAccount( + linkedAccountReference = it.linkedAccRef, + bankIconUrl = it.bankIconUrl.orEmpty(), + bankName = it.bankName.orEmpty(), + accountHint = "XXXX ${it.maskedAccNumber?.takeLast(4).orEmpty()}" + ) + }, + selectedBankReferenceIds = selectedBankReferenceIds, + ctaText = context.resources.getString(R.string.apply_pascal_case), + buttonLoader = IllustrationProvider.getLottieRemoteUrl(TUK_TUK_WHITE) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/helper/OtherCategoriesBottomSheetDataHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/helper/OtherCategoriesBottomSheetDataHelper.kt new file mode 100644 index 0000000000..482895aab2 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/helper/OtherCategoriesBottomSheetDataHelper.kt @@ -0,0 +1,56 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.spendanalysis.helper + +import android.content.Context +import com.navi.moneymanager.R +import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.SpendCategorizationHelper +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.OTHER_CATEGORIES +import com.navi.moneymanager.common.model.CategorySummary +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.utils.Constants +import com.navi.moneymanager.postonboard.spendanalysis.model.OtherCategoriesBottomSheetData +import com.navi.moneymanager.postonboard.spendanalysis.model.OtherCategoriesBottomSheetHeaderData +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class OtherCategoriesBottomSheetDataHelper +@Inject +constructor( + private val spendCategorizationHelper: SpendCategorizationHelper, + @ApplicationContext private val context: Context +) { + fun getOtherCategoriesBottomSheetData( + categories: List, + mmConfig: MMConfigResponse? + ): OtherCategoriesBottomSheetData { + val otherCategoriesCategorizedData = + spendCategorizationHelper.getOtherCategoriesCategorizedData( + categories = categories, + mmConfig = mmConfig, + topN = Constants.SPEND_CATEGORISATION_TOP_CATEGORIES_SIZE - 1 + ) + val otherCategoriesAccumulatedData = + spendCategorizationHelper.getOtherCategoriesAccumulatedData( + categories = categories, + topN = Constants.SPEND_CATEGORISATION_TOP_CATEGORIES_SIZE - 1 + ) + val otherCategoriesTotalAmount = otherCategoriesAccumulatedData?.amount.orEmpty() + val otherCategoriesTotalPercentage = otherCategoriesAccumulatedData?.progressText.orEmpty() + return OtherCategoriesBottomSheetData( + headerData = + OtherCategoriesBottomSheetHeaderData( + iconUrl = OTHER_CATEGORIES, + title = context.resources.getString(R.string.others), + subTitle = "$otherCategoriesTotalAmount • $otherCategoriesTotalPercentage" + ), + categories = otherCategoriesCategorizedData, + ctaText = context.resources.getString(R.string.okay_got_it_comma) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/helper/SpendAnalysisBarGraphSectionHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/helper/SpendAnalysisBarGraphSectionHelper.kt new file mode 100644 index 0000000000..ade155d444 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/helper/SpendAnalysisBarGraphSectionHelper.kt @@ -0,0 +1,161 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.spendanalysis.helper + +import android.content.Context +import com.navi.base.utils.formatToDecimalPlaces +import com.navi.base.utils.formatToInrWithTwoDecimals +import com.navi.base.utils.orZero +import com.navi.common.utils.monthAbbreviations +import com.navi.moneymanager.R +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.model.BarGraphData +import com.navi.moneymanager.common.model.BarGraphElement +import com.navi.moneymanager.common.model.BarGraphPageData +import com.navi.moneymanager.common.model.SelectedMonth +import com.navi.moneymanager.common.model.YAxisAverageData +import com.navi.moneymanager.common.model.database.TransactionSummaryData +import com.navi.moneymanager.common.utils.Constants.DEBIT +import com.navi.moneymanager.common.utils.Constants.SELF_TRANSFER +import com.navi.naviwidgets.utils.NaviWidgetIconUtils.NEW_INFO_ICON +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class SpendAnalysisBarGraphSectionHelper +@Inject +constructor(@ApplicationContext private val context: Context) { + fun getBarGraphSectionData( + selectedMonth: SelectedMonth, + transactions: List, + isTotalSyncCompleted: Boolean, + currentMonth: Int, + currentYear: Int, + selectedBankReferenceIds: Set, + ): BarGraphData { + val perPageElementSize = 6 + val numMonths = 12 + + val lastXMonthsInOrder = + (currentMonth - numMonths + 1..currentMonth).map { + if (it < 0) SelectedMonth(it + numMonths, currentYear - 1) + else SelectedMonth(it, currentYear) + } + + val spendTransactions = + getSpendTransactionsForLast12Months(transactions, lastXMonthsInOrder.toSet()) + val monthlyAmountForAllBanks = + getMonthlySumOfTxnAmount(spendTransactions, lastXMonthsInOrder) + + val spendTransactionsForSelectedBanks = + spendTransactions.filter { it.linkedAccRef in selectedBankReferenceIds } + val monthlyAmountForSelectedBanks = + getMonthlySumOfTxnAmount(spendTransactionsForSelectedBanks, lastXMonthsInOrder) + + val elementList = + lastXMonthsInOrder.mapIndexed { index, elementMonth -> + getBarGraphElement(index, monthlyAmountForSelectedBanks[index], elementMonth) + } + + val selectedBar = + lastXMonthsInOrder.indexOfFirst { it == selectedMonth }.takeIf { it >= 0 }.orZero() + val pageWiseElementList = elementList.chunked(perPageElementSize) + val numPages = pageWiseElementList.size + + val maxValueList = + monthlyAmountForAllBanks.chunked(perPageElementSize).map { it.maxOrNull() } + + return BarGraphData( + selectedBar = selectedBar, + pagedElementList = + pageWiseElementList.map { + val selectedPage = selectedBar.div(perPageElementSize) + BarGraphPageData( + elementList = it, + averageData = + getYAxisAverageData( + monthlyAmountForSelectedBanks = monthlyAmountForSelectedBanks, + perPageElementSize = perPageElementSize, + numPages = numPages, + isTotalSyncCompleted = isTotalSyncCompleted, + selectedPage = selectedPage, + ), + maxValue = maxValueList.getOrNull(selectedPage).orZero(), + ) + }, + isTotalSyncCompleted = isTotalSyncCompleted, + iconUrl = IllustrationSource.Resource(NEW_INFO_ICON), + elementsPerPage = perPageElementSize, + ) + } + + private fun getSpendTransactionsForLast12Months( + transactions: List, + lastXMonth: Set, + ): List { + return transactions.filter { + it.type == DEBIT && + it.finalCategory != SELF_TRANSFER && + SelectedMonth(it.txnMonth, it.txnYear) in lastXMonth + } + } + + private fun getMonthlySumOfTxnAmount( + transactions: List, + monthsInOrder: List, + ): List { + + val monthlySumsInOrder = mutableMapOf() + monthsInOrder.forEach { monthlySumsInOrder[it.month.orZero()] = 0.0 } + + // Iterate through the transactions and add amounts to the respective month + transactions.forEach { + monthlySumsInOrder[it.txnMonth.orZero()] = + monthlySumsInOrder[it.txnMonth.orZero()].orZero() + it.txnAmount.orZero() + } + + return monthlySumsInOrder.map { it.value } + } + + private fun getBarGraphElement( + barId: Int, + amount: Double, + selectedMonth: SelectedMonth + ): BarGraphElement { + return BarGraphElement( + xAxisValue = monthAbbreviations[selectedMonth.month.orZero()].uppercase(), + yAxisValue = amount.formatToDecimalPlaces(2).toDouble(), + formattedYAxisValue = amount.formatToInrWithTwoDecimals(), + barId = barId, + selectedMonth = selectedMonth + ) + } + + private fun getYAxisAverageData( + monthlyAmountForSelectedBanks: List, + perPageElementSize: Int, + numPages: Int, + isTotalSyncCompleted: Boolean, + selectedPage: Int, + ): YAxisAverageData? { + return if (selectedPage == numPages - 1) { + val avgMonthlySpend = + monthlyAmountForSelectedBanks + .subList(perPageElementSize * (numPages - 1), perPageElementSize * numPages - 1) + .average() + YAxisAverageData( + value = avgMonthlySpend.formatToDecimalPlaces(2).toDouble(), + formattedValue = + if (isTotalSyncCompleted) avgMonthlySpend.formatToInrWithTwoDecimals() + else null, + title = + if (isTotalSyncCompleted) context.resources.getString(R.string.avg_colon) + else context.resources.getString(R.string.fetching_past_spends), + ) + } else null + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/helper/SpendAnalysisCategoriesSectionHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/helper/SpendAnalysisCategoriesSectionHelper.kt new file mode 100644 index 0000000000..344cfacaeb --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/helper/SpendAnalysisCategoriesSectionHelper.kt @@ -0,0 +1,121 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.spendanalysis.helper + +import android.content.Context +import com.navi.base.utils.include +import com.navi.common.utils.SPACE +import com.navi.common.utils.monthAbbreviations +import com.navi.moneymanager.R +import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.SpendCategorizationHelper +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.COMMON_EMPTY_PLACEHOLDER +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM +import com.navi.moneymanager.common.model.CategorySummary +import com.navi.moneymanager.common.model.SpendCategorizationState +import com.navi.moneymanager.common.model.SpendCategoryItemData +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.utils.Constants +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class SpendAnalysisCategoriesSectionHelper +@Inject +constructor( + private val spendCategorizationHelper: SpendCategorizationHelper, + @ApplicationContext private val context: Context +) { + fun getCategoriesSectionData( + categories: List, + mmConfig: MMConfigResponse?, + month: Int, + year: Int + ): SpendCategorizationState { + if (categories.isEmpty()) { + return getEmptySpendCategorizationState(month, year) + } + // Get filtered categories + val filteredCategories = spendCategorizationHelper.getFilteredCategories(categories) + + // If filtered categories have exactly 5 items, return them directly + if (filteredCategories.size == Constants.SPEND_CATEGORISATION_TOP_CATEGORIES_SIZE) { + return createSpendCategorizationState( + month = month, + year = year, + categories = + spendCategorizationHelper.getTopCategoriesData( + categories = categories, + mmConfig = mmConfig, + topN = filteredCategories.size + ) + ) + } + + // Fetch top 4 categories + val topFourCategories = + spendCategorizationHelper.getTopCategoriesData( + categories = categories, + mmConfig = mmConfig, + topN = 4 + ) + + // Fetch remaining categories excluding top 4 + val otherCategories = + spendCategorizationHelper.getOtherCategoriesAccumulatedData( + categories = categories, + topN = 4 + ) + + // Combine the top 4 categories with "other" category + val categoriesToDisplay = topFourCategories.include(otherCategories) + + return createSpendCategorizationState( + month = month, + year = year, + categories = categoriesToDisplay + ) + } + + private fun createSpendCategorizationState( + month: Int, + year: Int, + categories: List + ): SpendCategorizationState.Loaded { + return SpendCategorizationState.Loaded( + categoryHeaderTitlePrefix = + context.getString(R.string.spend_categories).plus(SPACE).plus("•").plus(SPACE), + categoryHeaderTitleSuffix = getMonthDisplayText(month, year), + applyCategoryHorizontalPadding = false, + categories = categories, + selectedYear = year, + selectedMonth = month + ) + } + + private fun getEmptySpendCategorizationState( + month: Int, + year: Int + ): SpendCategorizationState.Empty { + return SpendCategorizationState.Empty( + title = context.resources.getString(R.string.no_transactions_to_categorise), + iconUrl = + IllustrationSource.Remote( + url = COMMON_EMPTY_PLACEHOLDER, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ), + categoryHeaderTitlePrefix = + context.getString(R.string.spend_categories).plus(SPACE).plus("•").plus(SPACE), + categoryHeaderTitleSuffix = getMonthDisplayText(month, year), + selectedYear = year, + selectedMonth = month + ) + } + + private fun getMonthDisplayText(month: Int, year: Int) = + monthAbbreviations[month].uppercase().plus("'").plus(year.toString().takeLast(2)) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/helper/TotalSpendSectionDataHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/helper/TotalSpendSectionDataHelper.kt new file mode 100644 index 0000000000..822dc91aa2 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/helper/TotalSpendSectionDataHelper.kt @@ -0,0 +1,160 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.spendanalysis.helper + +import android.content.Context +import com.navi.base.utils.SPACE +import com.navi.base.utils.formatToInrWithTwoDecimals +import com.navi.base.utils.orZero +import com.navi.common.utils.monthAbbreviations +import com.navi.moneymanager.R +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.ALL_BANK_ICON_SMALL +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.TOTAL_SPEND_RUPEE_SYMBOL +import com.navi.moneymanager.common.model.database.AccountOverview +import com.navi.moneymanager.common.model.database.TransactionSummaryData +import com.navi.moneymanager.common.model.sectionHeader.SectionHeaderData +import com.navi.moneymanager.common.network.model.CategoryItemData +import com.navi.moneymanager.common.utils.Constants.DEBIT +import com.navi.moneymanager.common.utils.Constants.SELF_TRANSFER +import com.navi.moneymanager.common.utils.Constants.UNCATEGORIZED +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryTotalSpendSectionCategoryDataData +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryTotalSpendSectionData +import com.navi.moneymanager.postonboard.spendanalysis.model.TotalSpendSectionData +import com.navi.naviwidgets.utils.NaviWidgetIconUtils.DOWN_ARROW_BLACK_16 +import com.navi.naviwidgets.utils.NaviWidgetIconUtils.NEW_INFO_ICON +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class TotalSpendSectionDataHelper +@Inject +constructor(@ApplicationContext private val context: Context) { + fun getTotalSpendSectionData( + month: Int, + year: Int, + currentMonthTransactions: List, + availableBanks: List, + selectedBankReferenceIds: Set? + ): TotalSpendSectionData { + val spendTransactions = getSpendTransactions(currentMonthTransactions) + val sumOfTransactions = spendTransactions.sumOf { it.txnAmount.orZero() } + return TotalSpendSectionData( + iconUrl = + IllustrationSource.Remote( + url = TOTAL_SPEND_RUPEE_SYMBOL, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ), + title = context.resources.getString(R.string.total_spend), + amount = sumOfTransactions.formatToInrWithTwoDecimals(), + selectedMonth = monthAbbreviations[month].plus(SPACE).plus(year), + actionIcon = IllustrationSource.Resource(DOWN_ARROW_BLACK_16), + selectedBankReferenceIds = selectedBankReferenceIds.orEmpty(), + spendingTrendSectionData = + getSpendingTrendSectionData(selectedBankReferenceIds, availableBanks) + ) + } + + fun getCategoryTotalSpendSectionData( + month: Int, + year: Int, + currentMonthTransactions: List, + availableBanks: List, + selectedBankReferenceIds: Set?, + categoryData: CategoryItemData?, + ): CategoryTotalSpendSectionData { + val spendTransactions = getSpendTransactions(currentMonthTransactions) + val sumOfTransactions = spendTransactions.sumOf { it.txnAmount.orZero() } + return CategoryTotalSpendSectionData( + iconUrl = + IllustrationSource.Remote( + url = categoryData?.categoryIcon ?: TOTAL_SPEND_RUPEE_SYMBOL, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ), + title = context.resources.getString(R.string.total_spend), + amount = sumOfTransactions.formatToInrWithTwoDecimals(), + selectedMonth = monthAbbreviations[month].plus(SPACE).plus(year), + actionIcon = IllustrationSource.Resource(DOWN_ARROW_BLACK_16), + spendingTrendSectionData = + getSpendingTrendSectionData(selectedBankReferenceIds, availableBanks), + selectedBankReferenceIds = selectedBankReferenceIds.orEmpty(), + category = + CategoryTotalSpendSectionCategoryDataData( + categoryId = categoryData?.categoryId.orEmpty(), + categoryName = categoryData?.categoryName.orEmpty(), + infoIconUrl = + if (categoryData?.categoryId == UNCATEGORIZED) + IllustrationSource.Resource(NEW_INFO_ICON) + else null + ) + ) + } + + private fun getSpendTransactions( + transactions: List + ): List { + return transactions.filter { it.type == DEBIT && it.finalCategory != SELF_TRANSFER } + } + + private fun getSpendingTrendSectionData( + selectedBankReferenceIds: Set?, + availableBanks: List + ): SectionHeaderData { + return SectionHeaderData( + title = context.resources.getString(R.string.past_spends), + actionText = + getSelectedBanksDisplayTitle(selectedBankReferenceIds.orEmpty(), availableBanks), + suffixIcon = IllustrationSource.Resource(DOWN_ARROW_BLACK_16), + prefixIcon = + IllustrationSource.Remote( + url = + getSelectedBanksIconUrl(selectedBankReferenceIds.orEmpty(), availableBanks), + placeholder = ALL_BANK_ICON_SMALL + ) + ) + } + + private fun getSelectedBanksDisplayTitle( + selectedBankReferenceIds: Set, + availableBanks: List + ): String { + return when (selectedBankReferenceIds.size) { + 1 -> { + val selectedAccountEntity = + availableBanks.find { it.linkedAccRef == selectedBankReferenceIds.first() } + val selectedBankTitle = + selectedAccountEntity + ?.let { + "${it.bankCode.orEmpty()} - ${it.maskedAccNumber?.takeLast(4).orEmpty()}" + } + .orEmpty() + selectedBankTitle + } + availableBanks.size -> { + context.resources.getString(R.string.all_accounts) + } + else -> { + "${selectedBankReferenceIds.size} accounts" + } + } + } + + private fun getSelectedBanksIconUrl( + selectedBankReferenceIds: Set, + availableBanks: List + ): String { + return if (selectedBankReferenceIds.size == 1) { + availableBanks + .find { it.linkedAccRef == selectedBankReferenceIds.first() } + ?.bankIconUrl + .orEmpty() + } else { + ALL_BANK_ICON_SMALL + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/provider/CategoryDetailsDataProviderImpl.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/provider/CategoryDetailsDataProviderImpl.kt new file mode 100644 index 0000000000..fdf4f00eec --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/provider/CategoryDetailsDataProviderImpl.kt @@ -0,0 +1,255 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.spendanalysis.provider + +import android.content.Context +import com.navi.base.utils.BaseUtils.getDayMonthAndYearFromTimestamp +import com.navi.moneymanager.R +import com.navi.moneymanager.common.analytics.DataProviderEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.MMConfigResponseHelper +import com.navi.moneymanager.common.dataprovider.data.datastore.DataStoreInfoProvider +import com.navi.moneymanager.common.dataprovider.data.spendanalysis.helper.BankSelectionBottomSheetDataHelper +import com.navi.moneymanager.common.dataprovider.data.spendanalysis.helper.SpendAnalysisBarGraphSectionHelper +import com.navi.moneymanager.common.dataprovider.data.spendanalysis.helper.TotalSpendSectionDataHelper +import com.navi.moneymanager.common.dataprovider.data.transaction.helper.TransactionProviderHelper +import com.navi.moneymanager.common.dataprovider.domain.CategoryDetailsLocalDataProvider +import com.navi.moneymanager.common.dataprovider.utils.getLastTwelveMonths +import com.navi.moneymanager.common.db.database.MMEncryptedDatabase +import com.navi.moneymanager.common.helper.convertMonthStringToPair +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.COMMON_EMPTY_PLACEHOLDER +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_LARGE +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.UNCATEGORIZED_INFO_BOTTOM_SHEET +import com.navi.moneymanager.common.illustration.repository.LottieRepository.Companion.CHIP_LOADER_LOTTIE +import com.navi.moneymanager.common.model.BankSelectionBottomSheetData +import com.navi.moneymanager.common.model.DataLoadingBottomSheetData +import com.navi.moneymanager.common.model.MonthSelectionBottomSheetData +import com.navi.moneymanager.common.model.SelectedMonth +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.common.model.ZeroTransactionData +import com.navi.moneymanager.common.model.database.TransactionSummaryData +import com.navi.moneymanager.common.network.di.RoomDataStoreInfoProvider +import com.navi.moneymanager.common.utils.Constants +import com.navi.moneymanager.common.utils.Constants.IS_TOTAL_SYNC_COMPLETED +import com.navi.moneymanager.common.utils.Constants.SELF_TRANSFER +import com.navi.moneymanager.common.utils.calculateTimestamps +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenData +import com.navi.moneymanager.postonboard.categorydetails.model.CategorySelectionBottomSheetCategoryData +import com.navi.moneymanager.postonboard.categorydetails.model.CategorySelectionBottomSheetData +import com.navi.moneymanager.postonboard.categorydetails.model.SortOption +import com.navi.moneymanager.postonboard.categorydetails.model.UncategorizedInfoBottomSheetData +import dagger.Lazy +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import timber.log.Timber + +class CategoryDetailsDataProviderImpl +@Inject +constructor( + private val encryptedDatabase: Lazy, + private val mmConfigResponseHelper: MMConfigResponseHelper, + private val totalSpendSectionHelper: TotalSpendSectionDataHelper, + private val barGraphSectionHelper: SpendAnalysisBarGraphSectionHelper, + private val bankSelectionBottomSheetDataHelper: BankSelectionBottomSheetDataHelper, + private val transactionProviderHelper: TransactionProviderHelper, + @RoomDataStoreInfoProvider private val dbDataStoreProvider: DataStoreInfoProvider, + @ApplicationContext private val context: Context, +) : CategoryDetailsLocalDataProvider { + + private var currentMonthTransactions = listOf() + + override suspend fun getSortedTransactions(sortOption: SortOption): List { + return when (sortOption) { + SortOption.HIGHEST_FIRST -> { + transactionProviderHelper.createTransactionList( + currentMonthTransactions.sortedByDescending { it.txnAmount ?: 0.0 } + ) + } + SortOption.LOWEST_FIRST -> { + transactionProviderHelper.createTransactionList( + currentMonthTransactions.sortedBy { it.txnAmount ?: 0.0 } + ) + } + SortOption.RECENT_FIRST -> { + transactionProviderHelper.createTransactionList( + currentMonthTransactions.sortedByDescending { it.txnTimestamp ?: 0L } + ) + } + } + } + + override suspend fun getCategoryDetailsScreenData( + month: Int?, + year: Int?, + sortOption: SortOption, + selectedBankReferenceIds: Set, + selectedCategory: String + ): Flow { + val (startDate, endDate) = calculateTimestamps() + return encryptedDatabase + .get() + .transactionsDao() + .fetchCategorisedTransactions(selectedCategory, startDate, endDate) + .map { transactions -> + Timber.tag("money_manager") + .d( + "CategoryDetailsDataProvider ${::getCategoryDetailsScreenData.name}: transactions fetched from db with category: $selectedCategory" + ) + val dateComponents = getDayMonthAndYearFromTimestamp(System.currentTimeMillis()) + val availableBanks = + encryptedDatabase.get().accountsDao().fetchAllAccounts().first() + val isTotalSyncCompleted = + dbDataStoreProvider.getBooleanData(IS_TOTAL_SYNC_COMPLETED).first() + val updatedSelectedBankReferenceIds = + selectedBankReferenceIds.ifEmpty { + availableBanks.map { it.linkedAccRef }.toSet() + } + val selectedMonth = month ?: dateComponents.second + val selectedYear = year ?: dateComponents.third + currentMonthTransactions = + transactions + .filter { + it.txnMonth == selectedMonth && + it.txnYear == selectedYear && + it.linkedAccRef in updatedSelectedBankReferenceIds + } + .map { transaction -> + val accountInfo = + availableBanks.find { it.linkedAccRef == transaction.linkedAccRef } + transaction.copy(bankIconUrl = accountInfo?.bankIconUrl.orEmpty()) + } + + val categoryData = + mmConfigResponseHelper.getMMConfig()?.categories?.find { + it.categoryId == selectedCategory + } + val filteredTransactions = + getSortedTransactions(sortOption).filter { it.type == Constants.DEBIT } + DataProviderEventTrackerImpl.categoryDetailsDpData( + month = month.toString(), + year = year.toString(), + selectedCategory = selectedCategory, + selectedBanksListSize = selectedBankReferenceIds.size.toString(), + transactionsListSize = transactions.size, + currentMonthsTransactionsListSize = currentMonthTransactions.size, + filteredTransactionsListSize = filteredTransactions.size, + accountsListSize = availableBanks.size, + isTotalSyncCompleted = isTotalSyncCompleted + ) + CategoryDetailsScreenData( + totalSpendSection = + totalSpendSectionHelper.getCategoryTotalSpendSectionData( + month = selectedMonth, + year = selectedYear, + currentMonthTransactions = currentMonthTransactions, + availableBanks = availableBanks, + selectedBankReferenceIds = updatedSelectedBankReferenceIds, + categoryData = categoryData + ), + barGraphData = + barGraphSectionHelper.getBarGraphSectionData( + selectedMonth = SelectedMonth(selectedMonth, selectedYear), + transactions = transactions, + isTotalSyncCompleted = isTotalSyncCompleted, + currentMonth = dateComponents.second, + currentYear = dateComponents.third, + selectedBankReferenceIds = updatedSelectedBankReferenceIds + ), + transactions = filteredTransactions, + sortOption = sortOption, + zeroTransactionData = + ZeroTransactionData( + title = context.getString(R.string.mm_transaction_zero_title), + illustrationSource = + IllustrationSource.Remote( + url = COMMON_EMPTY_PLACEHOLDER, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ), + ) + ) + } + } + + override suspend fun getMonthSelectionBottomSheetData( + displayMonthText: String + ): MonthSelectionBottomSheetData { + val last12Months = getLastTwelveMonths() + val selectedMonth = convertMonthStringToPair(displayMonthText) + return MonthSelectionBottomSheetData( + headerText = context.resources.getString(R.string.select_month), + monthList = last12Months, + selectedMonth = selectedMonth, + ctaText = context.resources.getString(R.string.apply_pascal_case) + ) + } + + override suspend fun getBankSelectionBottomSheetData( + selectedBankReferenceIds: Set + ): BankSelectionBottomSheetData { + val bankAccounts = encryptedDatabase.get().accountsDao().fetchAllAccounts().first() + return bankSelectionBottomSheetDataHelper.getBankSelectionBottomSheetData( + bankAccounts, + selectedBankReferenceIds + ) + } + + override suspend fun getPastMonthDataLoadingBottomSheetData(): DataLoadingBottomSheetData { + return DataLoadingBottomSheetData( + loadingIcon = null, + loadingLottie = IllustrationSource.Resource(CHIP_LOADER_LOTTIE), + titleText = context.resources.getString(R.string.fetching_past_month_transactions), + subTitleText = + context.resources.getString(R.string.wait_for_sometime_to_see_past_months_summary), + ctaText = context.resources.getString(R.string.okay_got_it_no_comma) + ) + } + + override suspend fun getCategorySelectionBottomSheetData( + selectedCategory: String + ): CategorySelectionBottomSheetData { + val categoryListFromConfig = + mmConfigResponseHelper.getMMConfig()?.categories?.filter { + it.categoryId != SELF_TRANSFER + } + val categoryList = + categoryListFromConfig + ?.map { + CategorySelectionBottomSheetCategoryData( + categoryId = it.categoryId.orEmpty(), + categoryName = it.categoryName.orEmpty(), + isSelected = selectedCategory == it.categoryId, + iconUrl = it.categoryIcon.orEmpty() + ) + } + .orEmpty() + + return CategorySelectionBottomSheetData( + selectedCategory = selectedCategory, + headerTitle = context.getString(R.string.select_category), + ctaText = context.getString(R.string.apply_pascal_case), + categoryList = categoryList, + ) + } + + override fun getUncategorizedInfoBottomSheetData(): UncategorizedInfoBottomSheetData { + return UncategorizedInfoBottomSheetData( + title = context.getString(R.string.uncategorized_bottomsheet_title), + description = context.getString(R.string.uncategorized_bottomsheet_description), + ctaText = context.getString(R.string.okay_got_it_comma), + describeIllustration = + IllustrationSource.Remote( + url = UNCATEGORIZED_INFO_BOTTOM_SHEET, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_LARGE + ), + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/provider/SpendAnalysisDataProviderImpl.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/provider/SpendAnalysisDataProviderImpl.kt new file mode 100644 index 0000000000..6ae6f97b35 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/spendanalysis/provider/SpendAnalysisDataProviderImpl.kt @@ -0,0 +1,201 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.spendanalysis.provider + +import android.content.Context +import com.navi.base.utils.BaseUtils.getDayMonthAndYearFromTimestamp +import com.navi.moneymanager.R +import com.navi.moneymanager.common.analytics.DataProviderEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.MMConfigResponseHelper +import com.navi.moneymanager.common.dataprovider.data.datastore.DataStoreInfoProvider +import com.navi.moneymanager.common.dataprovider.data.spendanalysis.helper.BankSelectionBottomSheetDataHelper +import com.navi.moneymanager.common.dataprovider.data.spendanalysis.helper.OtherCategoriesBottomSheetDataHelper +import com.navi.moneymanager.common.dataprovider.data.spendanalysis.helper.SpendAnalysisBarGraphSectionHelper +import com.navi.moneymanager.common.dataprovider.data.spendanalysis.helper.SpendAnalysisCategoriesSectionHelper +import com.navi.moneymanager.common.dataprovider.data.spendanalysis.helper.TotalSpendSectionDataHelper +import com.navi.moneymanager.common.dataprovider.domain.SpendAnalysisLocalDataProvider +import com.navi.moneymanager.common.dataprovider.utils.getLastTwelveMonths +import com.navi.moneymanager.common.db.database.MMEncryptedDatabase +import com.navi.moneymanager.common.helper.convertMonthStringToPair +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.repository.LottieRepository.Companion.CHIP_LOADER_LOTTIE +import com.navi.moneymanager.common.model.BankSelectionBottomSheetData +import com.navi.moneymanager.common.model.DataLoadingBottomSheetData +import com.navi.moneymanager.common.model.MonthSelectionBottomSheetData +import com.navi.moneymanager.common.model.SelectedMonth +import com.navi.moneymanager.common.network.di.RoomDataStoreInfoProvider +import com.navi.moneymanager.common.utils.Constants.IS_TOTAL_SYNC_COMPLETED +import com.navi.moneymanager.common.utils.calculateTimestamps +import com.navi.moneymanager.postonboard.spendanalysis.model.OtherCategoriesBottomSheetData +import com.navi.moneymanager.postonboard.spendanalysis.model.SpendAnalysisScreenData +import dagger.Lazy +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import timber.log.Timber + +class SpendAnalysisDataProviderImpl +@Inject +constructor( + private val encryptedDatabase: Lazy, + private val mmConfigResponseHelper: MMConfigResponseHelper, + private val totalSpendSectionHelper: TotalSpendSectionDataHelper, + private val categoriesSectionHelper: SpendAnalysisCategoriesSectionHelper, + private val barGraphSectionHelper: SpendAnalysisBarGraphSectionHelper, + private val bankSelectionBottomSheetDataHelper: BankSelectionBottomSheetDataHelper, + private val otherCategoriesBottomSheetDataHelper: OtherCategoriesBottomSheetDataHelper, + @RoomDataStoreInfoProvider private val dbDataStoreProvider: DataStoreInfoProvider, + @ApplicationContext private val context: Context, +) : SpendAnalysisLocalDataProvider { + + override suspend fun getSpendAnalysisScreenData( + month: Int?, + year: Int?, + selectedBankReferenceIds: Set? + ): Flow { + val (startDate, endDate) = calculateTimestamps() + return encryptedDatabase + .get() + .transactionsDao() + .fetchAllTransactions(startDate, endDate) + .map { transactions -> + Timber.tag("money_manager") + .d( + "SpendAnalysisDataProvider ${::getSpendAnalysisScreenData.name}: transactions fetched from DB" + ) + val dateComponents = getDayMonthAndYearFromTimestamp(System.currentTimeMillis()) + val availableBanks = + encryptedDatabase.get().accountsDao().fetchAllAccounts().first() + val updatedSelectedBankReferenceIds = + selectedBankReferenceIds ?: availableBanks.map { it.linkedAccRef }.toSet() + val isTotalSyncCompleted = + dbDataStoreProvider.getBooleanData(key = IS_TOTAL_SYNC_COMPLETED).first() + val currentMonthTransactions = + transactions.filter { + it.txnMonth == (month ?: dateComponents.second) && + it.txnYear == (year ?: dateComponents.third) && + it.linkedAccRef in updatedSelectedBankReferenceIds + } + val mmConfig = mmConfigResponseHelper.getMMConfig() + val categories = + encryptedDatabase + .get() + .transactionsDao() + .getCategorizedTransactionSummaryForSelectedBanks( + month = month ?: dateComponents.second, + year = year ?: dateComponents.third, + linkedAccRef = updatedSelectedBankReferenceIds + ) + .first() + DataProviderEventTrackerImpl.spendAnalysisDpData( + month = month.toString(), + year = year.toString(), + selectedBanksListSize = selectedBankReferenceIds?.size.toString(), + currentMonthsTransactionsListSize = currentMonthTransactions.size, + transactionsListSize = transactions.size, + accountsListSize = availableBanks.size, + categoriesListSize = categories.size, + isTotalSyncCompleted = isTotalSyncCompleted + ) + SpendAnalysisScreenData( + totalSpendSection = + totalSpendSectionHelper.getTotalSpendSectionData( + month = month ?: dateComponents.second, + year = year ?: dateComponents.third, + currentMonthTransactions = currentMonthTransactions, + availableBanks = availableBanks, + selectedBankReferenceIds = updatedSelectedBankReferenceIds + ), + spendCategorizationState = + categoriesSectionHelper.getCategoriesSectionData( + categories = categories, + mmConfig = mmConfig, + month = month ?: dateComponents.second, + year = year ?: dateComponents.third + ), + barGraphData = + barGraphSectionHelper.getBarGraphSectionData( + selectedMonth = + SelectedMonth( + month = month ?: dateComponents.second, + year = year ?: dateComponents.third + ), + transactions = transactions, + isTotalSyncCompleted = isTotalSyncCompleted, + currentMonth = dateComponents.second, + currentYear = dateComponents.third, + selectedBankReferenceIds = updatedSelectedBankReferenceIds, + ), + viewTransactionHistoryTitle = + context.resources.getString(R.string.mm_transaction_history) + ) + } + } + + override suspend fun getMonthSelectionBottomSheetData( + displayMonthText: String + ): MonthSelectionBottomSheetData { + val last12Months = getLastTwelveMonths() + val selectedMonth = convertMonthStringToPair(displayMonthText) + return MonthSelectionBottomSheetData( + headerText = context.resources.getString(R.string.select_month), + monthList = last12Months, + selectedMonth = selectedMonth, + ctaText = context.resources.getString(R.string.apply_pascal_case) + ) + } + + override suspend fun getBankSelectionBottomSheetData( + selectedBankReferenceIds: Set + ): BankSelectionBottomSheetData { + val bankAccounts = encryptedDatabase.get().accountsDao().fetchAllAccounts().first() + return bankSelectionBottomSheetDataHelper.getBankSelectionBottomSheetData( + bankAccounts, + selectedBankReferenceIds + ) + } + + override suspend fun getOtherCategoriesBottomSheetData( + month: Int?, + year: Int?, + selectedBankReferenceIds: Set? + ): OtherCategoriesBottomSheetData { + val dateComponents = getDayMonthAndYearFromTimestamp(System.currentTimeMillis()) + val availableBanks = encryptedDatabase.get().accountsDao().fetchAllAccounts().first() + val updatedSelectedBankReferenceIds = + selectedBankReferenceIds ?: availableBanks.map { it.linkedAccRef }.toSet() + val mmConfig = mmConfigResponseHelper.getMMConfig() + val categories = + encryptedDatabase + .get() + .transactionsDao() + .getCategorizedTransactionSummaryForSelectedBanks( + month = month ?: dateComponents.second, + year = year ?: dateComponents.third, + updatedSelectedBankReferenceIds + ) + .first() + return otherCategoriesBottomSheetDataHelper.getOtherCategoriesBottomSheetData( + categories, + mmConfig + ) + } + + override suspend fun getPastMonthDataLoadingBottomSheetData(): DataLoadingBottomSheetData { + return DataLoadingBottomSheetData( + loadingIcon = null, + loadingLottie = IllustrationSource.Resource(CHIP_LOADER_LOTTIE), + titleText = context.resources.getString(R.string.fetching_past_month_transactions), + subTitleText = + context.resources.getString(R.string.wait_for_sometime_to_see_past_months_summary), + ctaText = context.resources.getString(R.string.okay_got_it_no_comma) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/transaction/helper/FilterOptionDataHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/transaction/helper/FilterOptionDataHelper.kt new file mode 100644 index 0000000000..d4bc2424f1 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/transaction/helper/FilterOptionDataHelper.kt @@ -0,0 +1,115 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.transaction.helper + +import android.content.Context +import com.navi.moneymanager.R +import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.MMConfigResponseHelper +import com.navi.moneymanager.common.dataprovider.utils.getLastTwelveMonths +import com.navi.moneymanager.common.model.FilterAttribute +import com.navi.moneymanager.common.model.FilterItemData +import com.navi.moneymanager.common.model.database.AccountOverview +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.utils.Constants.CREDIT +import com.navi.moneymanager.common.utils.Constants.DEBIT +import com.navi.moneymanager.common.utils.MonthConstants +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class FilterOptionDataHelper +@Inject +constructor( + private val mmConfigResponseHelper: MMConfigResponseHelper, + @ApplicationContext private val context: Context +) { + suspend fun getFilterOptionsData(accounts: List): FilterItemData { + val mmConfig = mmConfigResponseHelper.getMMConfig() + return FilterItemData( + availableFilters = + listOf( + getMonthFilter(), + getCategoryFilter(mmConfig), + getTypeFilter(), + getBankFilter(accounts) + ) + ) + } + + private fun getMonthFilter(): FilterItemData.FilterItem { + return FilterItemData.FilterItem( + attributeKey = FilterAttribute.MONTH.value, + isSelected = true, + valueOptions = + getLastTwelveMonths().map { + FilterItemData.ValueData( + valueName = "${it.first} ${it.second}", + id = "${MonthConstants.monthNames.indexOf(it.first)}/${it.second}" + ) + } + ) + } + + private fun getCategoryFilter(mmConfig: MMConfigResponse?): FilterItemData.FilterItem { + return FilterItemData.FilterItem( + attributeKey = FilterAttribute.CATEGORY.value, + valueOptions = + mmConfig + ?.categories + ?.map { + FilterItemData.ValueData( + valueName = it.categoryName.orEmpty(), + id = it.categoryId.orEmpty() + ) + } + .orEmpty() + ) + } + + private fun getTypeFilter(): FilterItemData.FilterItem { + return FilterItemData.FilterItem( + attributeKey = FilterAttribute.TYPE.value, + valueOptions = + listOf( + FilterItemData.ValueData( + valueName = context.resources.getString(R.string.credit), + id = CREDIT + ), + FilterItemData.ValueData( + valueName = context.resources.getString(R.string.debit), + id = DEBIT + ) + ) + ) + } + + private fun getBankFilter(accounts: List): FilterItemData.FilterItem { + return FilterItemData.FilterItem( + attributeKey = FilterAttribute.BANK.value, + valueOptions = + accounts.map { account -> + FilterItemData.ValueData( + valueName = "${account.bankCode} - ${account.maskedAccNumber?.takeLast(4)}", + id = account.linkedAccRef + ) + } + ) + } + + // Helper function to get filter value from either applied or all filters + fun getFilterValue( + attribute: String, + appliedFilters: List>>, + allFilters: List>> + ): List { + val appliedFilterValues = + appliedFilters.firstOrNull { it.first == attribute }?.second.orEmpty() + return appliedFilterValues.ifEmpty { + allFilters.firstOrNull { it.first == attribute }?.second.orEmpty() + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/transaction/helper/TransactionProviderHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/transaction/helper/TransactionProviderHelper.kt new file mode 100644 index 0000000000..9130d2cc45 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/transaction/helper/TransactionProviderHelper.kt @@ -0,0 +1,152 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.transaction.helper + +import android.content.Context +import androidx.compose.ui.graphics.Color +import com.navi.base.utils.EMPTY +import com.navi.base.utils.SPACE +import com.navi.base.utils.formatToInrWithTwoDecimals +import com.navi.base.utils.orZero +import com.navi.moneymanager.R +import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.MMConfigResponseHelper +import com.navi.moneymanager.common.dataprovider.utils.getInitials +import com.navi.moneymanager.common.dataprovider.utils.getTransactionBackgroundColor +import com.navi.moneymanager.common.dataprovider.utils.getTransactionDate +import com.navi.moneymanager.common.dataprovider.utils.getTransactionMonth +import com.navi.moneymanager.common.dataprovider.utils.getTransactionYear +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.ALL_BANK_ICON_SMALL +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.PLUS +import com.navi.moneymanager.common.model.MonthAmountAggregate +import com.navi.moneymanager.common.model.MonthlyBalance +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.common.model.database.TransactionSummaryData +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.Constants.CREDIT +import com.navi.moneymanager.common.utils.Constants.DEBIT +import com.navi.moneymanager.common.utils.Constants.PLUS_SPACE +import com.navi.moneymanager.common.utils.Constants.UNCATEGORIZED +import com.navi.moneymanager.postonboard.dashboard.model.BankTransactionsData +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlin.math.absoluteValue + +class TransactionProviderHelper +@Inject +constructor( + private val mmConfigResponseHelper: MMConfigResponseHelper, + @ApplicationContext private val context: Context +) { + + suspend fun createTransactionList( + transactions: List + ): List { + return transactions.map { transaction -> createTransaction(transaction) } + } + + suspend fun createTransaction(transaction: TransactionSummaryData): Transaction { + val mmConfig = mmConfigResponseHelper.getMMConfig() + + return Transaction( + id = transaction.txnId, + initials = getInitials(transaction.counterPartyName.orEmpty()), + initialBackgroundColor = getTransactionBackgroundColor(transaction.counterPartyName), + title = transaction.counterPartyName ?: context.getString(R.string.unknown), + categoryName = getCategoryName(transaction.finalCategory, mmConfig), + categoryIcon = getCategoryIcon(transaction.finalCategory), + date = getTransactionDate(transaction.txnTimestamp), + monthTag = getMonthTag(transaction.txnTimestamp), + amount = getTransactionAmount(transaction.txnAmount, transaction.type), + amountColor = getAmountBackgroundColor(transaction.type), + transactionTypeLabel = getTransactionTypeLabel(transaction.type), + accountIcon = + IllustrationSource.Remote( + url = transaction.bankIconUrl.orEmpty(), + placeholder = ALL_BANK_ICON_SMALL + ), + categoryId = transaction.finalCategory, + counterPartyName = transaction.counterPartyName.orEmpty(), + type = transaction.type.orEmpty(), + txnTimestamp = transaction.txnTimestamp.orZero() + ) + } + + fun getLatestTransactionsForAllBanks( + groupedTransactionData: List, + transactionCount: Int + ): List { + return groupedTransactionData + .flatMap { it.transactions } + .sortedByDescending { it.txnTimestamp } + .take(transactionCount) + } + + fun createMonthlyBalanceList( + monthAmountAggregate: List + ): List { + return monthAmountAggregate.map { + MonthlyBalance( + monthTag = + getTransactionMonth(it.yearMonth.substring(startIndex = 4).toInt()) + .plus(SPACE) + .plus(it.yearMonth.substring(startIndex = 0, endIndex = 4).toInt()), + color = if (it.amount > 0) MMColor.creditAmountColor else MMColor.textPrimary, + balance = + if (it.amount > 0) PLUS_SPACE.plus(it.amount.formatToInrWithTwoDecimals()) + else it.amount.absoluteValue.formatToInrWithTwoDecimals() + ) + } + } + + private fun getCategoryName(categoryId: String?, mmConfig: MMConfigResponse?): String { + val categoryName = + mmConfig?.categories?.firstOrNull { it.categoryId == categoryId }?.categoryName + + return categoryName?.takeIf { it.isNotEmpty() && categoryId != UNCATEGORIZED } + ?: context.getString(R.string.add_category) + } + + private fun getCategoryIcon(categoryId: String?): IllustrationSource? { + return if (categoryId != UNCATEGORIZED) null + else + IllustrationSource.Remote(url = PLUS, placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL) + } + + private fun getMonthTag(transactionTime: Long?) = + getTransactionMonth(transactionTime).plus(SPACE).plus(getTransactionYear(transactionTime)) + + private fun getTransactionAmount(transactionAmount: Double?, transactionType: String?): String { + val formattedAmount = transactionAmount.orZero().formatToInrWithTwoDecimals() + return if (transactionType == CREDIT) { + PLUS_SPACE.plus(formattedAmount) + } else { + formattedAmount + } + } + + private fun getAmountBackgroundColor(transactionType: String?): Color { + return if (transactionType == CREDIT) MMColor.creditAmountColor else MMColor.textPrimary + } + + private fun getTransactionTypeLabel(transactionType: String?): String { + return when (transactionType) { + DEBIT -> { + context.getString(R.string.paid_via) + } + CREDIT -> { + context.getString(R.string.received_in) + } + else -> { + EMPTY + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/transaction/provider/TransactionDetailsDataProviderImpl.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/transaction/provider/TransactionDetailsDataProviderImpl.kt new file mode 100644 index 0000000000..80c020ac3f --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/transaction/provider/TransactionDetailsDataProviderImpl.kt @@ -0,0 +1,241 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.transaction.provider + +import android.content.Context +import com.navi.base.utils.EMPTY +import com.navi.base.utils.formatToInrWithTwoDecimals +import com.navi.base.utils.isNotNull +import com.navi.base.utils.isNotNullAndNotEmpty +import com.navi.base.utils.orZero +import com.navi.common.constants.HELP_CTA_TEXT +import com.navi.moneymanager.R +import com.navi.moneymanager.common.analytics.DataProviderEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.MMConfigResponseHelper +import com.navi.moneymanager.common.dataprovider.domain.TransactionDataProvider +import com.navi.moneymanager.common.dataprovider.utils.getTransactionDate +import com.navi.moneymanager.common.db.database.MMEncryptedDatabase +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.ADD_CATEGORY_PLUS +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.ALL_BANK_ICON_SMALL +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.EDIT_CATEGORY_PENCIL +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.OTHERS_CATEGORY_ICON +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.TRANSACTION_DETAIL_PAYMENT_RECEIVED +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.TRANSACTION_DETAIL_PAYMENT_SENT +import com.navi.moneymanager.common.model.database.TransactionDetails +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.utils.Constants.CREDIT +import com.navi.moneymanager.common.utils.Constants.DEBIT +import com.navi.moneymanager.common.utils.Constants.UNCATEGORIZED +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetTransactionData +import com.navi.moneymanager.postonboard.transactiondetails.model.AccountHolderInfo +import com.navi.moneymanager.postonboard.transactiondetails.model.BankAccountDisplayInfo +import com.navi.moneymanager.postonboard.transactiondetails.model.CategoryInfo +import com.navi.moneymanager.postonboard.transactiondetails.model.CounterpartyInfo +import com.navi.moneymanager.postonboard.transactiondetails.model.NavBarData +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionDetailsScreenData +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionInfo +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionMetadataItem +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionSummary +import dagger.Lazy +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlinx.coroutines.flow.flow + +class TransactionDetailsDataProviderImpl +@Inject +constructor( + private val encryptedDatabase: Lazy, + private val mmConfigResponseHelper: MMConfigResponseHelper, + @ApplicationContext private val context: Context +) : TransactionDataProvider { + override suspend fun getTransactionDetailScreenData(transactionId: String) = flow { + encryptedDatabase.get().transactionsDao().fetchTransactionDetails(transactionId).collect { + val mmConfig = mmConfigResponseHelper.getMMConfig() + val accountInfo = + encryptedDatabase.get().accountsDao().fetchAccount(it.linkedAccRef.orEmpty()) + DataProviderEventTrackerImpl.transactionDetailsDpData(accountInfo.isNotNull()) + emit( + TransactionDetailsScreenData( + topNavBar = NavBarData(actionLabel = HELP_CTA_TEXT), + transactionInfo = + TransactionInfo( + amount = it.txnAmount.orZero().formatToInrWithTwoDecimals(), + transactionOutcome = getTransactionOutcome(it.type), + transactionDate = getTransactionDate(it.txnTimestamp), + paymentDirectionIcon = getPaymentDirectionIcon(it.type) + ), + categoryInfo = getCategoryInfo(it, mmConfig), + transactionSummary = + TransactionSummary( + counterpartyInfo = + CounterpartyInfo( + paymentNarrative = getCounterPartyPaymentNarrative(it.type), + counterPartyName = it.counterPartyName.orEmpty() + ), + accountHolderInfo = + AccountHolderInfo( + paymentNarrative = getAccountHolderPaymentNarrative(it.type), + bankDetails = + BankAccountDisplayInfo( + bankAccountDisplayText = + "${accountInfo.bankCode} - ${ + accountInfo.maskedAccNumber?.takeLast( + 4 + ) + }", + bankIcon = + IllustrationSource.Remote( + url = accountInfo.bankIconUrl.orEmpty(), + placeholder = ALL_BANK_ICON_SMALL + ) + ) + ), + transactionMetadata = + listOf( + TransactionMetadataItem( + title = context.getString(R.string.mode), + subtitle = it.mode.orEmpty() + ), + TransactionMetadataItem( + title = context.getString(R.string.narration), + subtitle = it.narration.orEmpty() + ) + ) + ) + ) + ) + } + } + + override suspend fun getTransactionCategoryInfo(transactionId: String) = flow { + encryptedDatabase.get().transactionsDao().fetchTransactionDetails(transactionId).collect { + val mmConfig = mmConfigResponseHelper.getMMConfig() + emit(getCategoryInfo(it, mmConfig)) + } + } + + private fun getCategoryInfo( + transaction: TransactionDetails, + mmConfig: MMConfigResponse? + ): CategoryInfo { + return CategoryInfo( + isCategorized = isTransactionCategorized(transaction.finalCategory), + categoryIcon = getCategoryIcon(transaction.finalCategory, mmConfig), + categoryTransactionData = + CategoryBottomSheetTransactionData( + transactionId = transaction.txnId, + categoryId = transaction.finalCategory, + counterPartyName = transaction.counterPartyName.orEmpty(), + categoryName = transaction.finalCategory, + transactionType = transaction.type.orEmpty() + ), + categoryName = getCategoryName(transaction.finalCategory, mmConfig), + categoryActionIcon = getCategoryActionIcon(transaction.finalCategory), + categoryId = transaction.finalCategory + ) + } + + private fun getTransactionOutcome(transactionType: String?): String { + return when (transactionType) { + DEBIT -> { + context.getString(R.string.paid) + } + CREDIT -> { + context.getString(R.string.received) + } + else -> { + EMPTY + } + } + } + + private fun getPaymentDirectionIcon(transactionType: String?): IllustrationSource { + return if (transactionType == DEBIT) { + IllustrationSource.Remote( + url = TRANSACTION_DETAIL_PAYMENT_SENT, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + } else { + IllustrationSource.Remote( + url = TRANSACTION_DETAIL_PAYMENT_RECEIVED, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + } + } + + private fun isTransactionCategorized(transactionCategory: String?): Boolean { + return transactionCategory.isNotNullAndNotEmpty() && transactionCategory != UNCATEGORIZED + } + + private fun getCategoryIcon( + categoryId: String?, + mmConfig: MMConfigResponse? + ): IllustrationSource { + return mmConfig + ?.categories + ?.firstOrNull { it.categoryId == categoryId } + ?.let { + IllustrationSource.Remote( + url = it.categoryIcon.orEmpty(), + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + } ?: IllustrationSource.Resource(OTHERS_CATEGORY_ICON) + } + + private fun getCategoryName(categoryId: String?, mmConfig: MMConfigResponse?): String { + val categoryName = + mmConfig?.categories?.firstOrNull { it.categoryId == categoryId }?.categoryName + + return categoryName?.takeIf { it.isNotEmpty() && categoryId != UNCATEGORIZED } + ?: context.getString(R.string.add_category) + } + + private fun getCategoryActionIcon(categoryId: String?): IllustrationSource { + return if (isTransactionCategorized(categoryId)) { + IllustrationSource.Remote( + url = EDIT_CATEGORY_PENCIL, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + } else { + IllustrationSource.Remote( + url = ADD_CATEGORY_PLUS, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + } + } + + private fun getCounterPartyPaymentNarrative(transactionType: String?): String { + return when (transactionType) { + DEBIT -> { + context.getString(R.string.paid_to) + } + CREDIT -> { + context.getString(R.string.paid_by) + } + else -> { + EMPTY + } + } + } + + private fun getAccountHolderPaymentNarrative(transactionType: String?): String { + return when (transactionType) { + DEBIT -> { + context.getString(R.string.paid_via) + } + CREDIT -> { + context.getString(R.string.received_in) + } + else -> { + EMPTY + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/transaction/provider/TransactionHistoryDataProviderImpl.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/transaction/provider/TransactionHistoryDataProviderImpl.kt new file mode 100644 index 0000000000..85755ccb91 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/transaction/provider/TransactionHistoryDataProviderImpl.kt @@ -0,0 +1,164 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.transaction.provider + +import android.content.Context +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.map +import com.navi.moneymanager.R +import com.navi.moneymanager.common.analytics.DataProviderEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.data.transaction.helper.FilterOptionDataHelper +import com.navi.moneymanager.common.dataprovider.data.transaction.helper.TransactionProviderHelper +import com.navi.moneymanager.common.dataprovider.domain.TransactionHistoryDataProvider +import com.navi.moneymanager.common.db.database.MMEncryptedDatabase +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.COMMON_EMPTY_PLACEHOLDER +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM +import com.navi.moneymanager.common.model.FilterAttribute +import com.navi.moneymanager.common.model.MonthlyBalance +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.common.model.ZeroTransactionData +import com.navi.moneymanager.common.utils.Constants.TRANSACTION_HISTORY_PAGE_SIZE +import com.navi.moneymanager.common.utils.calculateTimestamps +import dagger.Lazy +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.map + +class TransactionHistoryDataProviderImpl +@Inject +constructor( + private val encryptedDatabase: Lazy, + private val transactionProviderHelper: TransactionProviderHelper, + private val filterOptionDataHelper: FilterOptionDataHelper, + @ApplicationContext private val context: Context, +) : TransactionHistoryDataProvider { + override fun getZeroTransactionData() = flow { + emit( + ZeroTransactionData( + title = context.getString(R.string.mm_transaction_zero_title), + illustrationSource = + IllustrationSource.Remote( + url = COMMON_EMPTY_PLACEHOLDER, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ), + ) + ) + } + + override fun getTransactionPagingSource( + query: String?, + appliedFilters: List>>, + allFilters: List>>, + onMonthlyBalanceCalculated: (List) -> Unit + ): Flow> { + DataProviderEventTrackerImpl.transactionHistoryDpData( + query.orEmpty(), + appliedFilters, + allFilters + ) + val updatedQuery = query.orEmpty().trim() + val appliedMonthAndYearFilter = + filterOptionDataHelper.getFilterValue( + FilterAttribute.MONTH.value, + appliedFilters, + allFilters + ) + val appliedCategoriesFilter = + filterOptionDataHelper.getFilterValue( + FilterAttribute.CATEGORY.value, + appliedFilters, + allFilters + ) + val appliedTypeFilter = + filterOptionDataHelper.getFilterValue( + FilterAttribute.TYPE.value, + appliedFilters, + allFilters + ) + val appliedBankFilter = + filterOptionDataHelper.getFilterValue( + FilterAttribute.BANK.value, + appliedFilters, + allFilters + ) + val dao = encryptedDatabase.get().transactionsDao() + val (startDate, endDate) = calculateTimestamps() + val data = + dao.fetchMonthAmountAggregateWithQuery( + input = "%$updatedQuery%", + appliedMonthAndYearFilter = appliedMonthAndYearFilter, + appliedCategoriesFilter = appliedCategoriesFilter, + appliedTypeFilter = appliedTypeFilter, + appliedBankFilter = appliedBankFilter, + startDate = startDate, + endDate = endDate + ) + val monthTagAggregateList = transactionProviderHelper.createMonthlyBalanceList(data) + onMonthlyBalanceCalculated(monthTagAggregateList) + + val pager = + Pager( + config = + PagingConfig( + pageSize = TRANSACTION_HISTORY_PAGE_SIZE, + prefetchDistance = TRANSACTION_HISTORY_PAGE_SIZE + ), + pagingSourceFactory = { + when { + isQueryEmptyAndNoFiltersApplied(updatedQuery, appliedFilters) -> + dao.fetchAllTransactionPaged(startDate, endDate) + noFiltersApplied(appliedFilters) -> + dao.fetchAllQueryTransactionPaged("%$updatedQuery%", startDate, endDate) + else -> + dao.fetchAllQueryAndFiltersTransactionPaged( + input = "%$updatedQuery%", + appliedMonthAndYearFilter = appliedMonthAndYearFilter, + appliedCategoriesFilter = appliedCategoriesFilter, + appliedTypeFilter = appliedTypeFilter, + appliedBankFilter = appliedBankFilter, + startDate = startDate, + endDate = endDate + ) + } + } + ) + + return pager.flow.map { pagingData -> + pagingData.map { transaction -> + val accountInfo = + encryptedDatabase + .get() + .accountsDao() + .fetchAccount(transaction.linkedAccRef.orEmpty()) + transactionProviderHelper.createTransaction( + transaction.copy(bankIconUrl = accountInfo.bankIconUrl) + ) + } + } + } + + private fun isQueryEmptyAndNoFiltersApplied( + query: String, + appliedFilters: List>> + ) = query.isEmpty() && appliedFilters.isEmpty() + + private fun noFiltersApplied(appliedFilters: List>>): Boolean = + appliedFilters.isEmpty() + + override fun getInitFilterData() = flow { + encryptedDatabase.get().accountsDao().fetchAllAccounts().collect { + val filterItemData = filterOptionDataHelper.getFilterOptionsData(it) + emit(filterItemData) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/valueprop/provider/LauncherDataProviderImpl.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/valueprop/provider/LauncherDataProviderImpl.kt new file mode 100644 index 0000000000..d612ad5205 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/data/valueprop/provider/LauncherDataProviderImpl.kt @@ -0,0 +1,59 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.data.valueprop.provider + +import com.google.gson.Gson +import com.navi.base.cache.model.NaviCacheEntity +import com.navi.base.cache.repository.NaviCacheRepository +import com.navi.common.alchemist.model.AlchemistScreenDefinition +import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.MMConfigResponseHelper +import com.navi.moneymanager.common.dataprovider.domain.LauncherDataProvider +import com.navi.moneymanager.common.network.di.MoneyManagerGsonDeserializer +import com.navi.moneymanager.common.network.di.MoneyManagerGsonSerializer +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.utils.DbCacheConstants +import dagger.Lazy +import javax.inject.Inject + +class LauncherDataProviderImpl +@Inject +constructor( + private val naviCacheRepository: Lazy, + private val mmConfigResponseHelper: MMConfigResponseHelper, + @MoneyManagerGsonDeserializer private val gsonDeserializer: Gson, + @MoneyManagerGsonSerializer private val gsonSerializer: Gson +) : LauncherDataProvider { + + override suspend fun fetchAlchemistScreenFromCache(key: String): AlchemistScreenDefinition? { + val cachedScreenDefinition = + naviCacheRepository + .get() + .get(DbCacheConstants.MONEY_MANAGER_VALUE_PROPOSITION_SCREEN_KEY) + return gsonDeserializer.fromJson( + cachedScreenDefinition?.value, + AlchemistScreenDefinition::class.java + ) + } + + override suspend fun saveAlchemistScreenToCache( + key: String, + screenDefinition: AlchemistScreenDefinition + ) { + val screenDefinitionJson = gsonSerializer.toJson(screenDefinition) + val cacheEntity = + NaviCacheEntity( + key = DbCacheConstants.MONEY_MANAGER_VALUE_PROPOSITION_SCREEN_KEY, + value = screenDefinitionJson, + version = 1 + ) + naviCacheRepository.get().save(cacheEntity) + } + + override suspend fun saveMMConfigToDB(data: MMConfigResponse) = + mmConfigResponseHelper.saveMMConfigToDB(data) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/di/DataProviderModule.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/di/DataProviderModule.kt new file mode 100644 index 0000000000..0a2abafa72 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/di/DataProviderModule.kt @@ -0,0 +1,88 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.di + +import com.navi.moneymanager.common.dataprovider.data.addcategory.provider.AddCategoryDataProviderImpl +import com.navi.moneymanager.common.dataprovider.data.bankaccounts.BankAccountsDataProviderImpl +import com.navi.moneymanager.common.dataprovider.data.dashboard.provider.DashboardDataProviderImpl +import com.navi.moneymanager.common.dataprovider.data.remote.LocalDataSyncManagerImpl +import com.navi.moneymanager.common.dataprovider.data.remote.RemoteDataProviderImpl +import com.navi.moneymanager.common.dataprovider.data.spendanalysis.provider.CategoryDetailsDataProviderImpl +import com.navi.moneymanager.common.dataprovider.data.spendanalysis.provider.SpendAnalysisDataProviderImpl +import com.navi.moneymanager.common.dataprovider.data.transaction.provider.TransactionDetailsDataProviderImpl +import com.navi.moneymanager.common.dataprovider.data.transaction.provider.TransactionHistoryDataProviderImpl +import com.navi.moneymanager.common.dataprovider.data.valueprop.provider.LauncherDataProviderImpl +import com.navi.moneymanager.common.dataprovider.domain.AddCategoryDataProvider +import com.navi.moneymanager.common.dataprovider.domain.BankAccountsDataProvider +import com.navi.moneymanager.common.dataprovider.domain.CategoryDetailsLocalDataProvider +import com.navi.moneymanager.common.dataprovider.domain.DashboardDataProvider +import com.navi.moneymanager.common.dataprovider.domain.LauncherDataProvider +import com.navi.moneymanager.common.dataprovider.domain.LocalDataSyncManager +import com.navi.moneymanager.common.dataprovider.domain.RemoteDataProvider +import com.navi.moneymanager.common.dataprovider.domain.SpendAnalysisLocalDataProvider +import com.navi.moneymanager.common.dataprovider.domain.TransactionDataProvider +import com.navi.moneymanager.common.dataprovider.domain.TransactionHistoryDataProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent + +@Module +@InstallIn(ActivityRetainedComponent::class) +abstract class DataProviderModule { + + @Binds + abstract fun bindRemoteDataProvider( + remoteDataProviderImpl: RemoteDataProviderImpl + ): RemoteDataProvider + + @Binds + abstract fun bindDashboardLocalDataProvider( + dashboardDataProviderImpl: DashboardDataProviderImpl + ): DashboardDataProvider + + @Binds + abstract fun bindLauncherLocalDataProvider( + launcherDataProviderImpl: LauncherDataProviderImpl + ): LauncherDataProvider + + @Binds + abstract fun bindSpendAnalysisLocalDataProvider( + spendAnalysisDataProviderImpl: SpendAnalysisDataProviderImpl + ): SpendAnalysisLocalDataProvider + + @Binds + abstract fun bindCategoryDetailsLocalDataProvider( + categoryDetailsDataProviderImpl: CategoryDetailsDataProviderImpl + ): CategoryDetailsLocalDataProvider + + @Binds + abstract fun bindTransactionHistoryLocalDataProvider( + transactionHistoryDataProviderImpl: TransactionHistoryDataProviderImpl + ): TransactionHistoryDataProvider + + @Binds + abstract fun bindTransactionDetailsLocalDataProvider( + transactionDetailsDataProviderImpl: TransactionDetailsDataProviderImpl + ): TransactionDataProvider + + @Binds + abstract fun bindAddCategoryLocalDataProvider( + addCategoryDataProviderImpl: AddCategoryDataProviderImpl + ): AddCategoryDataProvider + + @Binds + abstract fun bindBankAccountsDataProvider( + bankAccountsDataProviderImpl: BankAccountsDataProviderImpl + ): BankAccountsDataProvider + + @Binds + abstract fun bindLocalDataSyncManager( + localDataSyncManager: LocalDataSyncManagerImpl + ): LocalDataSyncManager +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/AddCategoryDataProvider.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/AddCategoryDataProvider.kt new file mode 100644 index 0000000000..cc509543b0 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/AddCategoryDataProvider.kt @@ -0,0 +1,27 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.domain + +import com.navi.moneymanager.postonboard.monthlysummary.model.AddCategoryBottomSheetData +import com.navi.moneymanager.postonboard.monthlysummary.model.SimilarTransactionBottomSheetData +import kotlinx.coroutines.flow.Flow + +interface AddCategoryDataProvider : LocalDataProvider { + + suspend fun fetchAddCategoryBottomSheetData( + transactionId: String + ): Flow + + suspend fun fetchSimilarTransactionBottomSheetData( + transactionId: String, + categoryId: String, + transactionType: String + ): Flow + + suspend fun updateTransactionEntity(transactionId: String, categoryId: String) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/BankAccountsDataProvider.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/BankAccountsDataProvider.kt new file mode 100644 index 0000000000..7e2d3dc4d2 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/BankAccountsDataProvider.kt @@ -0,0 +1,15 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.domain + +import com.navi.moneymanager.common.model.database.AccountOverview +import kotlinx.coroutines.flow.Flow + +interface BankAccountsDataProvider : LocalDataProvider { + suspend fun getBankAccounts(): Flow> +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/CategoryDetailsDataProvider.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/CategoryDetailsDataProvider.kt new file mode 100644 index 0000000000..873916e4a8 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/CategoryDetailsDataProvider.kt @@ -0,0 +1,47 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.domain + +import com.navi.moneymanager.common.model.BankSelectionBottomSheetData +import com.navi.moneymanager.common.model.DataLoadingBottomSheetData +import com.navi.moneymanager.common.model.MonthSelectionBottomSheetData +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenData +import com.navi.moneymanager.postonboard.categorydetails.model.CategorySelectionBottomSheetData +import com.navi.moneymanager.postonboard.categorydetails.model.SortOption +import com.navi.moneymanager.postonboard.categorydetails.model.UncategorizedInfoBottomSheetData +import kotlinx.coroutines.flow.Flow + +interface CategoryDetailsLocalDataProvider : LocalDataProvider { + + suspend fun getSortedTransactions(sortOption: SortOption): List + + suspend fun getCategoryDetailsScreenData( + month: Int?, + year: Int?, + sortOption: SortOption, + selectedBankReferenceIds: Set, + selectedCategory: String + ): Flow + + suspend fun getMonthSelectionBottomSheetData( + displayMonthText: String + ): MonthSelectionBottomSheetData + + suspend fun getBankSelectionBottomSheetData( + selectedBankReferenceIds: Set + ): BankSelectionBottomSheetData + + suspend fun getPastMonthDataLoadingBottomSheetData(): DataLoadingBottomSheetData + + suspend fun getCategorySelectionBottomSheetData( + selectedCategory: String + ): CategorySelectionBottomSheetData + + fun getUncategorizedInfoBottomSheetData(): UncategorizedInfoBottomSheetData +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/DashboardDataProvider.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/DashboardDataProvider.kt new file mode 100644 index 0000000000..af9ea86fbd --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/DashboardDataProvider.kt @@ -0,0 +1,63 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.domain + +import androidx.compose.runtime.MutableState +import com.navi.moneymanager.common.model.DataLoadingBottomSheetData +import com.navi.moneymanager.common.model.MonthSelectionBottomSheetData +import com.navi.moneymanager.common.model.SelectedMonth +import com.navi.moneymanager.common.model.SpendCategorizationState +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountFooterSection +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountsState +import com.navi.moneymanager.postonboard.dashboard.model.FinarkeinErrorBottomSheetData +import com.navi.moneymanager.postonboard.dashboard.model.NavBarData +import com.navi.moneymanager.postonboard.dashboard.model.RecentTransactionsState +import com.navi.moneymanager.postonboard.dashboard.model.UserHeaderState +import com.navi.moneymanager.preonboard.launcher.model.ConsentRevokedBottomSheetData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +interface DashboardDataProvider : LocalDataProvider { + + suspend fun saveMMConfigToDB(data: MMConfigResponse) + + suspend fun getMMConfig(): MMConfigResponse? + + suspend fun getTopNavBarData(): NavBarData + + suspend fun getUserHeader(customerProfileName: String?): UserHeaderState + + suspend fun getBankSectionStateFlow(): Flow> + + suspend fun updateBankSectionRefreshingText( + refreshing: Boolean + ): Flow> + + suspend fun getSpendCategorizationSectionStateFlow( + screenParams: MutableStateFlow + ): Flow + + suspend fun getRecentTransactionsSectionStateFlow( + currentMonthTag: MutableState + ): Flow + + suspend fun getMonthSelectionBottomSheetData( + displayMonthText: String + ): MonthSelectionBottomSheetData + + suspend fun getPastMonthDataLoadingBottomSheetData(): DataLoadingBottomSheetData + + suspend fun getTransactionsFetchingBottomSheetData(): DataLoadingBottomSheetData + + suspend fun getFinarkeinErrorBottomSheetData(): FinarkeinErrorBottomSheetData + + suspend fun getConsentRevokedBottomSheetData(): ConsentRevokedBottomSheetData + + suspend fun getAddAccountLoadingBottomSheetData(): DataLoadingBottomSheetData +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/LauncherDataProvider.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/LauncherDataProvider.kt new file mode 100644 index 0000000000..4d9086b605 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/LauncherDataProvider.kt @@ -0,0 +1,20 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.domain + +import com.navi.common.alchemist.model.AlchemistScreenDefinition +import com.navi.moneymanager.common.network.model.MMConfigResponse + +interface LauncherDataProvider : LocalDataProvider { + + suspend fun fetchAlchemistScreenFromCache(key: String): AlchemistScreenDefinition? + + suspend fun saveAlchemistScreenToCache(key: String, screenDefinition: AlchemistScreenDefinition) + + suspend fun saveMMConfigToDB(data: MMConfigResponse) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/LocalDataProvider.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/LocalDataProvider.kt new file mode 100644 index 0000000000..5fad19ca33 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/LocalDataProvider.kt @@ -0,0 +1,10 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.domain + +interface LocalDataProvider diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/LocalDataSyncManager.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/LocalDataSyncManager.kt new file mode 100644 index 0000000000..d8d97c61fb --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/LocalDataSyncManager.kt @@ -0,0 +1,38 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.domain + +import com.navi.moneymanager.common.network.model.AccountData +import com.navi.moneymanager.common.network.model.TransactionData + +interface LocalDataSyncManager { + suspend fun insertAccounts(accounts: List) + + suspend fun insertTransactions( + transactions: List, + isCurrentMonthData: Boolean = false + ) + + suspend fun updatedLastSyncTimestamp(timestamp: Long?) + + suspend fun isTotalSyncCompleted(): Boolean + + suspend fun isCurrentMonthSyncCompleted(): Boolean + + suspend fun getLastSyncedTimestamp(defaultTimestamp: Long): Long + + suspend fun updateCurrentMonthSyncFlag() + + suspend fun updateTotalSyncCompleteFlag() + + suspend fun getTransactionCount(): Int + + suspend fun updateNewTransactionCount(newTransaction: Int) + + suspend fun getLatestTransactionTimestamp(): Long +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/RemoteDataProvider.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/RemoteDataProvider.kt new file mode 100644 index 0000000000..1521d5e3c2 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/RemoteDataProvider.kt @@ -0,0 +1,56 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.domain + +import com.navi.common.alchemist.model.AlchemistScreenDefinition +import com.navi.common.network.models.RepoResult +import com.navi.moneymanager.common.network.model.AccountDetailResponse +import com.navi.moneymanager.common.network.model.CustomerProfileData +import com.navi.moneymanager.common.network.model.FetchConsentUrlResponse +import com.navi.moneymanager.common.network.model.FinarkeinDataResponse +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.network.model.OnboardingStatusResponse +import com.navi.moneymanager.common.network.model.PollingStatusResponse +import com.navi.moneymanager.common.network.model.RefreshDataResponse +import com.navi.moneymanager.common.network.model.TransactionResponse +import com.navi.moneymanager.postonboard.monthlysummary.model.PostCategoryTransactionData + +interface RemoteDataProvider { + suspend fun fetchTransactions( + from: Long, + pageNo: Int, + pageSize: Int, + queryBy: String, + ): RepoResult + + suspend fun fetchUserOnboardingStatus( + screenName: String, + naeApplicable: Boolean, + ): RepoResult + + suspend fun fetchAlchemistScreen(screenName: String): RepoResult + + suspend fun fetchFinarkeinData(screenName: String): RepoResult + + suspend fun fetchMMConfigResponse(screenName: String): RepoResult + + suspend fun fetchUserAccounts(screenName: String): RepoResult + + suspend fun postTransactionCategoryData( + screenName: String, + requestBody: PostCategoryTransactionData, + ): RepoResult + + suspend fun refreshData(): RepoResult + + suspend fun pollSyncStatus(requestId: String): RepoResult + + suspend fun fetchConsentUrl(): RepoResult + + suspend fun getCustomerProfileData(): RepoResult +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/SpendAnalysisDataProvider.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/SpendAnalysisDataProvider.kt new file mode 100644 index 0000000000..69e76abcd3 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/SpendAnalysisDataProvider.kt @@ -0,0 +1,40 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.domain + +import com.navi.moneymanager.common.model.BankSelectionBottomSheetData +import com.navi.moneymanager.common.model.DataLoadingBottomSheetData +import com.navi.moneymanager.common.model.MonthSelectionBottomSheetData +import com.navi.moneymanager.postonboard.spendanalysis.model.OtherCategoriesBottomSheetData +import com.navi.moneymanager.postonboard.spendanalysis.model.SpendAnalysisScreenData +import kotlinx.coroutines.flow.Flow + +interface SpendAnalysisLocalDataProvider : LocalDataProvider { + + suspend fun getSpendAnalysisScreenData( + month: Int?, + year: Int?, + selectedBankReferenceIds: Set? + ): Flow + + suspend fun getMonthSelectionBottomSheetData( + displayMonthText: String + ): MonthSelectionBottomSheetData + + suspend fun getBankSelectionBottomSheetData( + selectedBankReferenceIds: Set + ): BankSelectionBottomSheetData + + suspend fun getOtherCategoriesBottomSheetData( + month: Int?, + year: Int?, + selectedBankReferenceIds: Set? + ): OtherCategoriesBottomSheetData + + suspend fun getPastMonthDataLoadingBottomSheetData(): DataLoadingBottomSheetData +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/TransactionDataProvider.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/TransactionDataProvider.kt new file mode 100644 index 0000000000..a426b984b4 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/TransactionDataProvider.kt @@ -0,0 +1,20 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.domain + +import com.navi.moneymanager.postonboard.transactiondetails.model.CategoryInfo +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionDetailsScreenData +import kotlinx.coroutines.flow.Flow + +interface TransactionDataProvider : LocalDataProvider { + suspend fun getTransactionDetailScreenData( + transactionId: String + ): Flow + + suspend fun getTransactionCategoryInfo(transactionId: String): Flow +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/TransactionHistoryDataProvider.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/TransactionHistoryDataProvider.kt new file mode 100644 index 0000000000..ab4aafda47 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/domain/TransactionHistoryDataProvider.kt @@ -0,0 +1,28 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.domain + +import androidx.paging.PagingData +import com.navi.moneymanager.common.model.FilterItemData +import com.navi.moneymanager.common.model.MonthlyBalance +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.common.model.ZeroTransactionData +import kotlinx.coroutines.flow.Flow + +interface TransactionHistoryDataProvider : LocalDataProvider { + fun getZeroTransactionData(): Flow + + fun getTransactionPagingSource( + query: String?, + appliedFilters: List>>, + allFilters: List>>, + onMonthlyBalanceCalculated: (List) -> Unit + ): Flow> + + fun getInitFilterData(): Flow +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/utils/DataProviderConstants.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/utils/DataProviderConstants.kt new file mode 100644 index 0000000000..40ed694c26 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/utils/DataProviderConstants.kt @@ -0,0 +1,18 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.utils + +object DataProviderConstants { + const val COMMA = "," + const val HYPHEN = " - " + const val EE_DD_MMM_YYYY = "EEE, d MMM yyyy" + const val MMM = "MMM" + const val YYYY = "yyyy" + const val LAST_UPDATED_REFRESH_SCHEDULER_DELAY = 60000L + const val LAST_UPDATED_REFRESH_INITIAL_DELAY = 30000L +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/utils/DataProviderUtils.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/utils/DataProviderUtils.kt new file mode 100644 index 0000000000..6810af9c5e --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/utils/DataProviderUtils.kt @@ -0,0 +1,74 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.utils + +import androidx.compose.ui.graphics.Color +import com.navi.base.utils.EMPTY +import com.navi.base.utils.SPACE +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.MonthConstants +import com.navi.moneymanager.common.utils.formatDateFromTimestamp +import com.navi.moneymanager.common.utils.formatMonthFromInt +import com.navi.moneymanager.common.utils.formatMonthFromTimestamp +import com.navi.moneymanager.common.utils.formatYearFromTimestamp +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale +import kotlin.math.abs + +fun getInitials(name: String): String { + val words = name.split(SPACE).filter { it.isNotEmpty() } + + return if (words.size == 1) { + words[0].take(2).uppercase() + } else { + words.take(2).joinToString(EMPTY) { it[0].uppercase() } + } +} + +fun getTransactionBackgroundColor(name: String?): Color { + val hash = abs(name.hashCode()) + return MMColor.transactionIconColors[hash % MMColor.transactionIconColors.size] +} + +fun getWeekDayFromTimestamp(timestampMillis: Long): String { + val date = Date(timestampMillis) + val dayOfWeekFormatter = SimpleDateFormat("EEEE", Locale("en", "IN")) + return dayOfWeekFormatter.format(date) +} + +fun getTransactionDate(transactionTime: Long?): String { + return if (transactionTime == null) EMPTY else formatDateFromTimestamp(transactionTime) +} + +fun getTransactionMonth(transactionTime: Long?): String { + return if (transactionTime == null) EMPTY else formatMonthFromTimestamp(transactionTime) +} + +fun getTransactionYear(transactionTime: Long?): String { + return if (transactionTime == null) EMPTY else formatYearFromTimestamp(transactionTime) +} + +fun getTransactionMonth(monthInInt: Int): String { + return formatMonthFromInt(monthInInt) +} + +fun getLastTwelveMonths(): List> { + val calendar = Calendar.getInstance() + val monthsList = mutableListOf>() + val monthNames = MonthConstants.monthNames + for (i in 0 until 12) { + val monthIndex = calendar.get(Calendar.MONTH) + val year = calendar.get(Calendar.YEAR) + monthsList.add(Pair(monthNames[monthIndex], year)) + calendar.add(Calendar.MONTH, -1) + } + + return monthsList +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/utils/Ext.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/utils/Ext.kt new file mode 100644 index 0000000000..363a2dcf75 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/dataprovider/utils/Ext.kt @@ -0,0 +1,38 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.dataprovider.utils + +import com.navi.common.utils.log +import com.navi.moneymanager.common.db.dao.DataStoreDao +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.transform + +suspend fun DataStoreDao.getTransformedFlow( + key: String, + defaultValue: T, + transformValue: (String?) -> T +): Flow { + return getAsFlow(key).transform { + val value = + try { + transformValue(it?.value) + } catch (e: Exception) { + e.log() + defaultValue + } + emit(value) + } +} + +fun String.toCapitalizedWords(): String { + return this.lowercase() // Convert the entire string to lowercase + .split(" ") // Split the string by spaces + .joinToString(" ") { word -> + word.replaceFirstChar { it.uppercaseChar() } // Capitalize the first letter of each word + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/DBSyncExecutor.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/DBSyncExecutor.kt new file mode 100644 index 0000000000..a37410b17a --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/DBSyncExecutor.kt @@ -0,0 +1,131 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.datasync + +import com.navi.common.utils.log +import com.navi.common.utils.safeLaunch +import com.navi.moneymanager.common.analytics.DataSyncEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.domain.LocalDataSyncManager +import com.navi.moneymanager.common.datasync.helper.AllMonthsDataSyncHelper +import com.navi.moneymanager.common.datasync.helper.BaseDataSyncHelper.TransactionSyncType +import com.navi.moneymanager.common.datasync.helper.CurrentMonthDataSyncHelper +import com.navi.moneymanager.common.datasync.model.DBSyncConfig +import com.navi.moneymanager.common.datasync.model.DataSyncState +import com.navi.moneymanager.common.datasync.model.DataSyncStatus +import javax.inject.Inject +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update + +class DBSyncExecutor +@Inject +constructor( + private val allMonthsDataSyncHelper: AllMonthsDataSyncHelper, + private val currentMonthDataSyncHelper: CurrentMonthDataSyncHelper, + private val localDataProvider: LocalDataSyncManager +) { + private var currentMonthSyncStatus = MutableStateFlow(DataSyncState.NotStarted) + private var allMonthsSyncStatus = MutableStateFlow(DataSyncState.NotStarted) + private lateinit var scope: CoroutineScope + lateinit var dataSyncStatus: StateFlow + + private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> throwable.log() } + + suspend fun execute(dbSyncConfig: DBSyncConfig) { + DataSyncEventTrackerImpl.dbSyncExecutorTriggered() + syncCurrentMonthIfRequired(dbSyncConfig) + syncAllMonthsIfRequired(dbSyncConfig) + } + + fun initDBSyncExecutor(scope: CoroutineScope) { + DataSyncEventTrackerImpl.dbSyncExecutorInit() + this.scope = scope + dataSyncStatus = + combine(currentMonthSyncStatus, allMonthsSyncStatus) { + currentMonthStatus, + allMonthsStatus -> + DataSyncStatus(currentMonthStatus, allMonthsStatus) + } + .stateIn( + scope = scope, + started = SharingStarted.WhileSubscribed(), + initialValue = + DataSyncStatus(DataSyncState.NotStarted, DataSyncState.NotStarted) + ) + } + + private suspend fun syncCurrentMonthIfRequired( + dbSyncConfig: DBSyncConfig, + ) { + if ( + shouldSyncCurrentMonth() && + isReadyForSync(dbSyncConfig.pollingStatusResponse.currMonthTxnsStatus) + ) { + currentMonthSyncStatus.update { DataSyncState.InProgress } + scope.safeLaunch(Dispatchers.IO + exceptionHandler) { + currentMonthSyncStatus.update { + currentMonthDataSyncHelper.execute( + dbSyncConfig.timestampConfig, + dbSyncConfig.paginationConfig + ) + } + } + } else { + DataSyncEventTrackerImpl.syncNotTriggered( + type = TransactionSyncType.CURRENT_MONTH.name, + pollingStatus = dbSyncConfig.pollingStatusResponse.currMonthTxnsStatus.orEmpty() + ) + } + } + + private suspend fun syncAllMonthsIfRequired(dbSyncConfig: DBSyncConfig) { + val currentMonthPollingStatus = + isReadyForSync(dbSyncConfig.pollingStatusResponse.currMonthTxnsStatus) + val allMonthsPollingStatus = + isReadyForSync(dbSyncConfig.pollingStatusResponse.oldMonthTxnsStatus) + if (shouldSyncAllMonths() && currentMonthPollingStatus && allMonthsPollingStatus) { + allMonthsSyncStatus.update { DataSyncState.InProgress } + scope.safeLaunch(Dispatchers.IO + exceptionHandler) { + allMonthsSyncStatus.update { + allMonthsDataSyncHelper.execute( + dbSyncConfig.timestampConfig, + dbSyncConfig.paginationConfig + ) + } + } + } else { + DataSyncEventTrackerImpl.syncNotTriggered( + type = "ALL_MONTHS", + pollingStatus = dbSyncConfig.pollingStatusResponse.oldMonthTxnsStatus.orEmpty() + ) + } + } + + private fun isReadyForSync(status: String?): Boolean = status == SyncStatus.COMPLETED.name + + private suspend fun shouldSyncCurrentMonth(): Boolean = + localDataProvider.isCurrentMonthSyncCompleted().not() && + currentMonthSyncStatus.value.shouldInitiateSync() + + private fun shouldSyncAllMonths(): Boolean = allMonthsSyncStatus.value.shouldInitiateSync() +} + +private fun DataSyncState.shouldInitiateSync() = + this in listOf(DataSyncState.NotStarted, DataSyncState.Failed) + +enum class SyncStatus { + PENDING, + COMPLETED, + FAILED +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/helper/AllMonthsDataSyncHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/helper/AllMonthsDataSyncHelper.kt new file mode 100644 index 0000000000..18e72aab5b --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/helper/AllMonthsDataSyncHelper.kt @@ -0,0 +1,157 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.datasync.helper + +import com.navi.base.utils.orZero +import com.navi.moneymanager.common.analytics.DataSyncEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.domain.LocalDataSyncManager +import com.navi.moneymanager.common.dataprovider.domain.RemoteDataProvider +import com.navi.moneymanager.common.datasync.model.DataSyncState +import com.navi.moneymanager.common.datasync.model.TransactionsApiCallStatus +import com.navi.moneymanager.common.network.model.PaginationConfig +import com.navi.moneymanager.common.network.model.TimestampConfig +import com.navi.moneymanager.common.network.model.TransactionData +import com.navi.moneymanager.common.utils.Constants.PAGE_SIZE +import com.navi.moneymanager.common.utils.Constants.TXN_TIMESTAMP +import com.navi.moneymanager.common.utils.Constants.UPDATED_AT +import javax.inject.Inject + +class AllMonthsDataSyncHelper +@Inject +constructor(remoteDataProvider: RemoteDataProvider, localDataSyncManager: LocalDataSyncManager) : + BaseDataSyncHelper(remoteDataProvider, localDataSyncManager) { + + override suspend fun execute( + timestampConfig: TimestampConfig?, + paginationConfig: PaginationConfig? + ): DataSyncState { + val twelveMonthsOldTimestamp = timestampConfig?.twelveMonthsOldTimestamp ?: 0L + if (twelveMonthsOldTimestamp == 0L) return DataSyncState.Failed + val fromTimestamp = localDataSyncManager.getLastSyncedTimestamp(twelveMonthsOldTimestamp) + return if (localDataSyncManager.isTotalSyncCompleted()) { + DataSyncEventTrackerImpl.syncStarted(TransactionSyncType.ALL_MONTH_IN_ONE_SHOT.name) + syncAllDataAndInsertInOneShot( + fromTimestamp = fromTimestamp, + pageSize = paginationConfig?.pageSize ?: PAGE_SIZE, + queryBy = UPDATED_AT + ) + } else { + DataSyncEventTrackerImpl.syncStarted(TransactionSyncType.ALL_MONTH_PAGE_WISE.name) + syncAllDataAndInsertPageWise( + fromTimestamp = fromTimestamp, + pageSize = paginationConfig?.pageSize ?: PAGE_SIZE, + queryBy = TXN_TIMESTAMP + ) + } + } + + private suspend fun syncAllDataAndInsertPageWise( + fromTimestamp: Long, + pageSize: Int, + queryBy: String + ): DataSyncState { + val syncType = TransactionSyncType.ALL_MONTH_PAGE_WISE.name + val accounts = getAccounts() ?: return accountsApiFailure(syncType) + localDataSyncManager.insertAccounts(accounts) + val result = + fetchAllTransactionsAndInsertPageWise( + fromTimestamp = fromTimestamp, + pageSize = pageSize, + queryBy = queryBy, + syncType = syncType + ) + if (result is DataSyncState.Failed) return result + localDataSyncManager.updateCurrentMonthSyncFlag() + localDataSyncManager.updateTotalSyncCompleteFlag() + DataSyncEventTrackerImpl.syncSuccess(syncType) + return DataSyncState.Completed + } + + private suspend fun syncAllDataAndInsertInOneShot( + fromTimestamp: Long, + pageSize: Int, + queryBy: String + ): DataSyncState { + val syncType = TransactionSyncType.ALL_MONTH_IN_ONE_SHOT.name + val transactions = + fetchAllTransactions(fromTimestamp, pageSize, queryBy) + ?: return transactionsApiFailure(syncType) + + val accounts = getAccounts() ?: return accountsApiFailure(syncType) + + val latestTransactionTimestamp = localDataSyncManager.getLatestTransactionTimestamp() + + insertDataIntoDatabase(accounts, transactions) + + val newTransactionCount = + transactions.count { it.txnTimestamp.orZero() > latestTransactionTimestamp } + if (newTransactionCount > 0) { + localDataSyncManager.updateNewTransactionCount(newTransactionCount) + } + DataSyncEventTrackerImpl.syncSuccess(syncType) + return DataSyncState.Completed + } + + private suspend fun fetchAllTransactions( + fromTimestamp: Long, + pageSize: Int, + queryBy: String + ): List? { + val transactionsList = mutableListOf() + var pageNo = 0 + + while (true) { + when ( + val response = + callTransactionsApi( + from = fromTimestamp, + pageNo = pageNo, + pageSize = pageSize, + queryBy = queryBy + ) + ) { + is TransactionsApiCallStatus.Success -> { + transactionsList.addAll(response.data) + if (response.data.size < pageSize) break + pageNo++ + } + is TransactionsApiCallStatus.Failure -> return null + } + } + return transactionsList + } + + private suspend fun fetchAllTransactionsAndInsertPageWise( + fromTimestamp: Long, + pageSize: Int, + queryBy: String, + syncType: String, + ): DataSyncState { + var pageNo = 0 + + while (true) { + when ( + val response = + callTransactionsApi( + from = fromTimestamp, + pageNo = pageNo, + pageSize = pageSize, + queryBy = queryBy + ) + ) { + is TransactionsApiCallStatus.Success -> { + localDataSyncManager.insertTransactions(response.data) + if (response.data.size < pageSize) break + pageNo++ + } + is TransactionsApiCallStatus.Failure -> return transactionsApiFailure(syncType) + } + } + return DataSyncState.Completed + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/helper/BaseDataSyncHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/helper/BaseDataSyncHelper.kt new file mode 100644 index 0000000000..d80f1162d3 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/helper/BaseDataSyncHelper.kt @@ -0,0 +1,86 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.datasync.helper + +import com.navi.common.network.models.isSuccessWithData +import com.navi.moneymanager.common.analytics.DataSyncEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.domain.LocalDataSyncManager +import com.navi.moneymanager.common.dataprovider.domain.RemoteDataProvider +import com.navi.moneymanager.common.datasync.model.DataSyncState +import com.navi.moneymanager.common.datasync.model.TransactionsApiCallStatus +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.network.model.AccountData +import com.navi.moneymanager.common.network.model.PaginationConfig +import com.navi.moneymanager.common.network.model.TimestampConfig +import com.navi.moneymanager.common.network.model.TransactionData + +abstract class BaseDataSyncHelper( + protected val remoteDataProvider: RemoteDataProvider, + protected val localDataSyncManager: LocalDataSyncManager +) { + + abstract suspend fun execute( + timestampConfig: TimestampConfig?, + paginationConfig: PaginationConfig? + ): DataSyncState + + suspend fun callTransactionsApi( + from: Long, + pageNo: Int, + pageSize: Int, + queryBy: String + ): TransactionsApiCallStatus { + val response = + remoteDataProvider.fetchTransactions( + from = from, + pageNo = pageNo, + pageSize = pageSize, + queryBy = queryBy + ) + return if (response.isSuccessWithData()) { + TransactionsApiCallStatus.Success(response.data?.transactions.orEmpty()) + } else { + TransactionsApiCallStatus.Failure + } + } + + suspend fun getAccounts(): List? { + val accountsApiResponse = + remoteDataProvider.fetchUserAccounts(screenName = MMScreen.DASHBOARD.screen) + return if (accountsApiResponse.isSuccessWithData()) { + accountsApiResponse.data?.accountDetails.orEmpty() + } else { + null + } + } + + suspend fun insertDataIntoDatabase( + accounts: List, + transactions: List, + isCurrentMonthData: Boolean = false + ) { + localDataSyncManager.insertAccounts(accounts) + localDataSyncManager.insertTransactions(transactions, isCurrentMonthData) + } + + fun accountsApiFailure(type: String): DataSyncState { + DataSyncEventTrackerImpl.accountSyncFailed(type) + return DataSyncState.Failed + } + + fun transactionsApiFailure(type: String): DataSyncState { + DataSyncEventTrackerImpl.transactionSyncFailed(type) + return DataSyncState.Failed + } + + enum class TransactionSyncType { + CURRENT_MONTH, + ALL_MONTH_PAGE_WISE, + ALL_MONTH_IN_ONE_SHOT + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/helper/CurrentMonthDataSyncHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/helper/CurrentMonthDataSyncHelper.kt new file mode 100644 index 0000000000..fb14a67e7e --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/helper/CurrentMonthDataSyncHelper.kt @@ -0,0 +1,65 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.datasync.helper + +import com.navi.moneymanager.common.analytics.DataSyncEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.domain.LocalDataSyncManager +import com.navi.moneymanager.common.dataprovider.domain.RemoteDataProvider +import com.navi.moneymanager.common.datasync.model.DataSyncState +import com.navi.moneymanager.common.datasync.model.TransactionsApiCallStatus +import com.navi.moneymanager.common.network.model.PaginationConfig +import com.navi.moneymanager.common.network.model.TimestampConfig +import com.navi.moneymanager.common.network.model.TransactionData +import com.navi.moneymanager.common.utils.Constants.TXN_TIMESTAMP +import javax.inject.Inject + +class CurrentMonthDataSyncHelper +@Inject +constructor(remoteDataProvider: RemoteDataProvider, localDataSyncManager: LocalDataSyncManager) : + BaseDataSyncHelper(remoteDataProvider, localDataSyncManager) { + + override suspend fun execute( + timestampConfig: TimestampConfig?, + paginationConfig: PaginationConfig? + ): DataSyncState { + val syncType = TransactionSyncType.CURRENT_MONTH.name + val startTimestamp = timestampConfig?.currentMonthStartTimestamp ?: 0L + if (startTimestamp == 0L) return DataSyncState.Failed + DataSyncEventTrackerImpl.syncStarted(syncType) + val transactions = + fetchCurrentMonthTransactions(startTimestamp) ?: return transactionsApiFailure(syncType) + + val accounts = getAccounts() ?: return accountsApiFailure(syncType) + + insertDataIntoDatabase( + accounts = accounts, + transactions = transactions, + isCurrentMonthData = true + ) + localDataSyncManager.updateCurrentMonthSyncFlag() + DataSyncEventTrackerImpl.syncSuccess(syncType) + return DataSyncState.Completed + } + + private suspend fun fetchCurrentMonthTransactions( + startTimestamp: Long + ): List? { + return when ( + val response = + callTransactionsApi( + from = startTimestamp, + pageNo = -1, + pageSize = 1, + queryBy = TXN_TIMESTAMP + ) + ) { + is TransactionsApiCallStatus.Success -> response.data + is TransactionsApiCallStatus.Failure -> null + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/model/DBSyncConfig.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/model/DBSyncConfig.kt new file mode 100644 index 0000000000..0f5bc4bbf2 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/model/DBSyncConfig.kt @@ -0,0 +1,18 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.datasync.model + +import com.navi.moneymanager.common.network.model.PaginationConfig +import com.navi.moneymanager.common.network.model.PollingStatusResponse +import com.navi.moneymanager.common.network.model.TimestampConfig + +data class DBSyncConfig( + val pollingStatusResponse: PollingStatusResponse, + val timestampConfig: TimestampConfig?, + val paginationConfig: PaginationConfig?, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/model/DataSyncState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/model/DataSyncState.kt new file mode 100644 index 0000000000..5580e51f24 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/model/DataSyncState.kt @@ -0,0 +1,18 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.datasync.model + +sealed class DataSyncState { + data object NotStarted : DataSyncState() + + data object InProgress : DataSyncState() + + data object Completed : DataSyncState() + + data object Failed : DataSyncState() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/model/DataSyncStatus.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/model/DataSyncStatus.kt new file mode 100644 index 0000000000..a8065f840f --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/model/DataSyncStatus.kt @@ -0,0 +1,13 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.datasync.model + +data class DataSyncStatus( + val currentMonthSyncStatus: DataSyncState, + val allMonthsSyncStatus: DataSyncState +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/model/TransactionsApiCallStatus.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/model/TransactionsApiCallStatus.kt new file mode 100644 index 0000000000..af750f075a --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/datasync/model/TransactionsApiCallStatus.kt @@ -0,0 +1,16 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.datasync.model + +import com.navi.moneymanager.common.network.model.TransactionData + +sealed class TransactionsApiCallStatus { + data class Success(val data: List) : TransactionsApiCallStatus() + + data object Failure : TransactionsApiCallStatus() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/dao/AccountsDao.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/dao/AccountsDao.kt new file mode 100644 index 0000000000..827cb5758e --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/dao/AccountsDao.kt @@ -0,0 +1,53 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.db.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Upsert +import com.navi.moneymanager.common.db.entity.AccountEntity +import com.navi.moneymanager.common.model.database.AccountOverview +import com.navi.moneymanager.common.utils.Constants.ACCOUNT_TABLE +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first + +@Dao +interface AccountsDao { + @Upsert suspend fun insertAllAccounts(accounts: List) + + @Query( + "SELECT linkedAccRef, accountHolderName, maskedAccNumber, bankName, bankCode, bankIconUrl, currentBalance, updatedAt " + + "FROM $ACCOUNT_TABLE " + + "WHERE linkedAccRef = :linkedAccountReference" + ) + suspend fun fetchAccount(linkedAccountReference: String): AccountOverview + + @Query( + "SELECT linkedAccRef, accountHolderName, maskedAccNumber, bankName, bankCode, bankIconUrl, currentBalance, updatedAt " + + "FROM $ACCOUNT_TABLE " + + "ORDER BY bankName, maskedAccNumber ASC" + ) + fun fetchAllAccounts(): Flow> + + @Query("DELETE FROM $ACCOUNT_TABLE WHERE linkedAccRef = :linkedAccountReference") + suspend fun deleteAccount(linkedAccountReference: String) + + @Query("DELETE FROM $ACCOUNT_TABLE WHERE linkedAccRef IN (:linkedAccRefList)") + suspend fun deleteAccounts(linkedAccRefList: Set) + + @Query("SELECT COUNT(*) FROM $ACCOUNT_TABLE") suspend fun getAccountsCount(): Int + + @Transaction + suspend fun deleteAccountsNotIn(accounts: List) { + val localAccountIds = fetchAllAccounts().first().map { it.linkedAccRef }.toSet() + val updatedAccountIds = accounts.map { it.linkedAccRef }.toSet() + val accountsToDelete = localAccountIds - updatedAccountIds + deleteAccounts(accountsToDelete) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/dao/DataStoreDao.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/dao/DataStoreDao.kt new file mode 100644 index 0000000000..a71a3aa5f1 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/dao/DataStoreDao.kt @@ -0,0 +1,33 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.navi.moneymanager.common.db.entity.DataStoreEntity +import com.navi.moneymanager.common.utils.Constants.DATA_STORE_TABLE +import kotlinx.coroutines.flow.Flow + +@Dao +interface DataStoreDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun save(entity: DataStoreEntity) + + @Query("SELECT * FROM $DATA_STORE_TABLE WHERE `key` = :key LIMIT 1") + suspend fun get(key: String): DataStoreEntity? + + @Query("SELECT * FROM $DATA_STORE_TABLE WHERE `key` = :key LIMIT 1") + fun getAsFlow(key: String): Flow + + @Query("DELETE FROM $DATA_STORE_TABLE WHERE `key` = :key") suspend fun delete(key: String) + + @Query("SELECT COUNT(*) FROM $DATA_STORE_TABLE WHERE `key` = :key") + suspend fun containsKey(key: String): Int +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/dao/TransactionsDao.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/dao/TransactionsDao.kt new file mode 100644 index 0000000000..bda7aeae55 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/dao/TransactionsDao.kt @@ -0,0 +1,234 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.db.dao + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Upsert +import com.navi.moneymanager.common.db.entity.TransactionEntity +import com.navi.moneymanager.common.model.CategorySummary +import com.navi.moneymanager.common.model.MonthAmountAggregate +import com.navi.moneymanager.common.model.database.TransactionDetails +import com.navi.moneymanager.common.model.database.TransactionSummaryData +import com.navi.moneymanager.common.utils.Constants.TRANSACTION_TABLE +import kotlinx.coroutines.flow.Flow + +@Dao +interface TransactionsDao { + @Upsert suspend fun insertAllTransactions(transactions: List) + + @Query( + "SELECT txnId, linkedAccRef, txnTimestamp, txnMonth, txnYear, txnAmount, type, counterPartyName, finalCategory " + + "FROM $TRANSACTION_TABLE WHERE txnTimestamp BETWEEN :startDate AND :endDate" + ) + fun fetchAllTransactions(startDate: Long, endDate: Long): Flow> + + @Query( + "SELECT txnId, linkedAccRef, txnTimestamp, txnMonth, txnYear, txnAmount, type, counterPartyName, finalCategory " + + "FROM $TRANSACTION_TABLE " + + "WHERE txnTimestamp BETWEEN :startDate AND :endDate " + + "ORDER BY txnTimestamp DESC" + ) + fun fetchAllTransactionPaged( + startDate: Long, + endDate: Long + ): PagingSource + + @Query( + "SELECT txnId, linkedAccRef, txnTimestamp, txnMonth, txnYear, txnAmount, type, counterPartyName, finalCategory " + + "FROM $TRANSACTION_TABLE " + + "WHERE txnTimestamp BETWEEN :startDate AND :endDate AND categoryName LIKE :input OR txnAmount like :input OR counterPartyName like :input " + + "ORDER BY txnTimestamp DESC" + ) + fun fetchAllQueryTransactionPaged( + input: String, + startDate: Long, + endDate: Long + ): PagingSource + + @Query(""" + SELECT COUNT(*) FROM $TRANSACTION_TABLE + """) + fun getTransactionCount(): Flow + + @Query( + "SELECT" + + " txnId, linkedAccRef, txnTimestamp, txnMonth, txnYear, txnAmount, type, counterPartyName, finalCategory " + + "FROM $TRANSACTION_TABLE WHERE " + + "txnTimestamp BETWEEN :startDate AND :endDate AND (categoryName LIKE :input OR txnAmount like :input OR counterPartyName like :input)" + + "AND txnMonth || '/' || txnYear IN (:appliedMonthAndYearFilter)" + + "AND finalCategory IN (:appliedCategoriesFilter)" + + "AND type IN (:appliedTypeFilter)" + + "AND linkedAccRef IN (:appliedBankFilter)" + + "ORDER BY txnTimestamp DESC" + ) + fun fetchAllQueryAndFiltersTransactionPaged( + input: String, + appliedMonthAndYearFilter: List, + appliedCategoriesFilter: List, + appliedTypeFilter: List, + appliedBankFilter: List, + startDate: Long, + endDate: Long + ): PagingSource + + @Query( + """ + SELECT + txnYear || txnMonth AS yearMonth, + SUM( + CASE + WHEN type = 'CREDIT' THEN txnAmount + WHEN type = 'DEBIT' THEN -txnAmount + ELSE 0 + END + ) AS amount + FROM $TRANSACTION_TABLE + WHERE + txnTimestamp BETWEEN :startDate AND :endDate + AND ( + categoryName LIKE '%' || :input || '%' + OR txnAmount LIKE '%' || :input || '%' + OR counterPartyName LIKE '%' || :input || '%' + ) + AND (txnMonth || '/' || txnYear) IN (:appliedMonthAndYearFilter) + AND finalCategory IN (:appliedCategoriesFilter) + AND type IN (:appliedTypeFilter) + AND linkedAccRef IN (:appliedBankFilter) + GROUP BY yearMonth + """ + ) + fun fetchMonthAmountAggregateWithQuery( + input: String, + appliedMonthAndYearFilter: List, + appliedCategoriesFilter: List, + appliedTypeFilter: List, + appliedBankFilter: List, + startDate: Long, + endDate: Long + ): List + + @Query( + "SELECT txnId, linkedAccRef, txnTimestamp, txnMonth, txnYear, txnAmount, type, counterPartyName, finalCategory " + + "FROM $TRANSACTION_TABLE " + + "WHERE txnTimestamp BETWEEN :startDate AND :endDate " + + "ORDER BY txnTimestamp DESC LIMIT :recentCount" + ) + fun fetchRecentTransactions( + recentCount: Int, + startDate: Long, + endDate: Long + ): Flow> + + @Query( + "SELECT txnId, linkedAccRef, txnTimestamp, txnMonth, txnYear, txnAmount, type, counterPartyName, finalCategory " + + "FROM $TRANSACTION_TABLE " + + "WHERE txnId = :transactionId" + ) + fun fetchTransaction(transactionId: String): Flow + + @Query( + """ + SELECT txnId, + txnTimestamp, + txnMonth, + txnYear, + txnAmount, + type, + counterPartyName, + finalCategory + FROM $TRANSACTION_TABLE + WHERE linkedAccRef = :linkedAccRef + AND txnTimestamp BETWEEN :startDate AND :endDate + ORDER BY txnTimestamp DESC + LIMIT :limit + """ + ) + fun fetchTransactionsForBank( + linkedAccRef: String, + startDate: Long, + endDate: Long, + limit: Int + ): Flow> + + @Query( + "SELECT txnId, linkedAccRef, txnTimestamp, txnMonth, txnYear, txnAmount, type, counterPartyName, finalCategory FROM $TRANSACTION_TABLE WHERE finalCategory = :category AND txnTimestamp BETWEEN :startDate AND :endDate " + ) + fun fetchCategorisedTransactions( + category: String, + startDate: Long, + endDate: Long + ): Flow> + + @Query( + "SELECT txnId, linkedAccRef, txnTimestamp, txnAmount, narration, mode, type, counterPartyName, finalCategory " + + "FROM $TRANSACTION_TABLE " + + "WHERE txnId = :transactionId" + ) + fun fetchTransactionDetails(transactionId: String): Flow + + @Query( + "SELECT txnId, linkedAccRef, txnTimestamp, txnMonth, txnYear, txnAmount, type, counterPartyName, finalCategory " + + "FROM $TRANSACTION_TABLE" + + " WHERE counterPartyName = :counterPartyName AND txnTimestamp BETWEEN :startDate AND :endDate " + + "ORDER BY txnTimestamp DESC" + ) + fun fetchTransactionsByCounterPartyName( + counterPartyName: String, + startDate: Long, + endDate: Long + ): List + + @Query( + " SELECT finalCategory, COUNT(txnId) AS numberOfTransactions, SUM(txnAmount) AS totalAmount " + + "FROM $TRANSACTION_TABLE " + + "WHERE type = 'DEBIT' AND txnMonth = :month AND txnYear = :year " + + "GROUP BY finalCategory" + ) + fun getCategorizedTransactionSummary(month: Int, year: Int): Flow> + + @Query( + "SELECT finalCategory, COUNT(txnId) AS numberOfTransactions, SUM(txnAmount) AS totalAmount " + + "FROM $TRANSACTION_TABLE " + + "WHERE type = 'DEBIT' AND txnMonth = :month AND txnYear = :year AND linkedAccRef IN (:linkedAccRef) " + + "GROUP BY finalCategory" + ) + fun getCategorizedTransactionSummaryForSelectedBanks( + month: Int, + year: Int, + linkedAccRef: Set + ): Flow> + + @Query("DELETE FROM $TRANSACTION_TABLE") suspend fun deleteAllTransactions() + + @Query("DELETE FROM $TRANSACTION_TABLE WHERE txnTimestamp <= :timestamp") + suspend fun deleteTransactionsOlderThan(timestamp: Long) + + @Query( + "SELECT COUNT(*) FROM $TRANSACTION_TABLE WHERE txnTimestamp BETWEEN :startDate AND :endDate " + ) + suspend fun getTransactionsCount(startDate: Long, endDate: Long): Int + + @Query("UPDATE $TRANSACTION_TABLE SET finalCategory = :categoryId WHERE txnId = :transactionId") + suspend fun updateTransactionEntity(transactionId: String, categoryId: String) + + @Query( + "SELECT COUNT(txnId) FROM $TRANSACTION_TABLE " + + "WHERE counterPartyName = :counterPartyName AND type = :transactionType AND txnTimestamp BETWEEN :startDate AND :endDate " + ) + fun getTransactionCountByCounterPartyNameAndType( + counterPartyName: String, + transactionType: String, + startDate: Long, + endDate: Long + ): Int + + @Query("SELECT MAX(txnTimestamp) FROM $TRANSACTION_TABLE") + suspend fun getLatestTransactionTimestamp(): Long +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/database/MMDatabase.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/database/MMDatabase.kt new file mode 100644 index 0000000000..25824d10f7 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/database/MMDatabase.kt @@ -0,0 +1,19 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.db.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.navi.moneymanager.common.db.dao.DataStoreDao +import com.navi.moneymanager.common.db.entity.DataStoreEntity + +@Database(entities = [DataStoreEntity::class], version = 1, exportSchema = false) +abstract class MMDatabase : RoomDatabase() { + + abstract fun getDataStoreDao(): DataStoreDao +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/database/MMEncryptedDatabase.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/database/MMEncryptedDatabase.kt new file mode 100644 index 0000000000..1621b8430e --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/database/MMEncryptedDatabase.kt @@ -0,0 +1,27 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.db.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.navi.moneymanager.common.db.dao.AccountsDao +import com.navi.moneymanager.common.db.dao.TransactionsDao +import com.navi.moneymanager.common.db.entity.AccountEntity +import com.navi.moneymanager.common.db.entity.TransactionEntity + +@Database( + entities = [AccountEntity::class, TransactionEntity::class], + version = 1, + exportSchema = false +) +abstract class MMEncryptedDatabase : RoomDatabase() { + + abstract fun accountsDao(): AccountsDao + + abstract fun transactionsDao(): TransactionsDao +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/entity/AccountEntity.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/entity/AccountEntity.kt new file mode 100644 index 0000000000..9b187094d6 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/entity/AccountEntity.kt @@ -0,0 +1,53 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.navi.moneymanager.common.utils.Constants.ACCOUNT_TABLE + +@Entity(tableName = ACCOUNT_TABLE) +data class AccountEntity( + @PrimaryKey @ColumnInfo(name = "linkedAccRef") val linkedAccRef: String, + @ColumnInfo(name = "accountHolderName") val accountHolderName: String?, + @ColumnInfo(name = "accountHolderEmail") val accountHolderEmail: String?, + @ColumnInfo(name = "accountHolderDob") val accountHolderDob: Long?, + @ColumnInfo(name = "accountHolderLandline") val accountHolderLandline: String?, + @ColumnInfo(name = "accountHolderAddress") val accountHolderAddress: String?, + @ColumnInfo(name = "ckycCompliance") val ckycCompliance: Boolean?, + @ColumnInfo(name = "accountHolderMobileNumber") val accountHolderMobileNumber: String?, + @ColumnInfo(name = "accountHolderPan") val accountHolderPan: String?, + @ColumnInfo(name = "accountType") val accountType: String?, + @ColumnInfo(name = "accountOwnershipType") val accountOwnershipType: String?, + @ColumnInfo(name = "nominee") val nominee: String?, + @ColumnInfo(name = "maskedAccNumber") val maskedAccNumber: String?, + @ColumnInfo(name = "accountOpeningDate") val accountOpeningDate: String?, + @ColumnInfo(name = "bankName") val bankName: String?, + @ColumnInfo(name = "bankCode") val bankCode: String?, + @ColumnInfo(name = "bankIconUrl") val bankIconUrl: String?, + @ColumnInfo(name = "bankBranch") val bankBranch: String?, + @ColumnInfo(name = "currentBalance") val currentBalance: Double?, + @ColumnInfo(name = "accountDrawingLimit") val accountDrawingLimit: String?, + @ColumnInfo(name = "accountAgeInDays") val accountAgeInDays: Long?, + @ColumnInfo(name = "pendingType") val pendingType: String?, + @ColumnInfo(name = "accountStatus") val accountStatus: String?, + @ColumnInfo(name = "micrCode") val micrCode: String?, + @ColumnInfo(name = "balanceDateTime") val balanceDateTime: Long?, + @ColumnInfo(name = "currency") val currency: String?, + @ColumnInfo(name = "pendingAmount") val pendingAmount: Long?, + @ColumnInfo(name = "ifscCode") val ifscCode: String?, + @ColumnInfo(name = "accountSubType") val accountSubType: String?, + @ColumnInfo(name = "facility") val facility: String?, + @ColumnInfo(name = "exchangeRate") val exchangeRate: String?, + @ColumnInfo(name = "refreshedAt") val refreshedAt: Long?, + @ColumnInfo(name = "updatedAt") val updatedAt: Long?, + @ColumnInfo(name = "fipId") val fipId: String?, + @ColumnInfo(name = "consentStatus") val consentStatus: String?, + @ColumnInfo(name = "lastRefreshAttemptedAt") val lastRefreshAttemptedAt: Long?, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/entity/DataStoreEntity.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/entity/DataStoreEntity.kt new file mode 100644 index 0000000000..71d3942c8b --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/entity/DataStoreEntity.kt @@ -0,0 +1,15 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.db.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.navi.moneymanager.common.utils.Constants.DATA_STORE_TABLE + +@Entity(tableName = DATA_STORE_TABLE) +data class DataStoreEntity(@PrimaryKey val key: String, val value: String) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/entity/TransactionEntity.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/entity/TransactionEntity.kt new file mode 100644 index 0000000000..e21ed1cfff --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/db/entity/TransactionEntity.kt @@ -0,0 +1,47 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.navi.moneymanager.common.utils.Constants.TRANSACTION_TABLE + +@Entity( + tableName = TRANSACTION_TABLE, + foreignKeys = + [ + ForeignKey( + entity = AccountEntity::class, + parentColumns = arrayOf("linkedAccRef"), + childColumns = arrayOf("linkedAccRef"), + onDelete = ForeignKey.CASCADE + ) + ] +) +data class TransactionEntity( + @PrimaryKey @ColumnInfo(name = "txnId") val txnId: String, + @ColumnInfo(name = "linkedAccRef") val linkedAccRef: String?, + @ColumnInfo(name = "txnReference") val txnReference: String?, + @ColumnInfo(name = "txnTimestamp") val txnTimestamp: Long?, + @ColumnInfo(name = "txnAmount") val txnAmount: Double?, + @ColumnInfo(name = "valueDate") val valueDate: Long?, + @ColumnInfo(name = "narration") val narration: String?, + @ColumnInfo(name = "mode") val mode: String?, + @ColumnInfo(name = "type") val type: String?, + @ColumnInfo(name = "counterPartyName") val counterPartyName: String?, + @ColumnInfo(name = "txnDate") val txnDate: String?, + @ColumnInfo(name = "txnDay") val txnDay: Int?, + @ColumnInfo(name = "txnMonth") val txnMonth: Int?, + @ColumnInfo(name = "txnYear") val txnYear: Int?, + @ColumnInfo(name = "finalCategory") val finalCategory: String, + @ColumnInfo(name = "categoryName") val categoryName: String, + @ColumnInfo(name = "updatedAt") val updatedAt: Long?, + @ColumnInfo(name = "mobileTimestamp") val mobileTimestamp: Long?, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/helper/AccountsDataHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/helper/AccountsDataHelper.kt new file mode 100644 index 0000000000..cf71240e85 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/helper/AccountsDataHelper.kt @@ -0,0 +1,58 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.helper + +import com.navi.moneymanager.common.db.entity.AccountEntity +import com.navi.moneymanager.common.network.model.AccountData +import javax.inject.Inject + +class AccountsDataHelper @Inject constructor() { + + fun getAccountsEntity(accounts: List): List { + return accounts.map { + AccountEntity( + linkedAccRef = it.linkedAccRef.orEmpty(), + accountHolderName = it.accountHolderName, + accountHolderEmail = it.accountHolderEmail, + accountHolderDob = it.accountHolderDob, + accountHolderLandline = it.accountHolderLandline, + accountHolderAddress = it.accountHolderAddress, + ckycCompliance = it.ckycCompliance, + accountHolderMobileNumber = it.accountHolderMobileNumber, + accountHolderPan = it.accountHolderPan, + accountType = it.accountType, + accountOwnershipType = it.accountOwnershipType, + nominee = it.nominee, + maskedAccNumber = it.maskedAccNumber, + accountOpeningDate = it.accountOpeningDate, + bankName = it.bankName, + bankCode = it.bankCode, + bankIconUrl = it.bankIconUrl, + bankBranch = it.bankBranch, + currentBalance = it.currentBalance, + accountDrawingLimit = it.accountDrawingLimit, + accountAgeInDays = it.accountAgeInDays, + pendingType = it.pendingType, + accountStatus = it.accountStatus, + micrCode = it.micrCode, + balanceDateTime = it.balanceDateTime, + currency = it.currency, + pendingAmount = it.pendingAmount, + ifscCode = it.ifscCode, + accountSubType = it.accountSubType, + facility = it.facility, + exchangeRate = it.exchangeRate, + refreshedAt = it.refreshedAt, + updatedAt = it.updatedAt, + fipId = it.fipId, + consentStatus = it.consentStatus, + lastRefreshAttemptedAt = it.lastRefreshAttemptedAt + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/helper/TransactionsDataHelper.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/helper/TransactionsDataHelper.kt new file mode 100644 index 0000000000..882b0a03fd --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/helper/TransactionsDataHelper.kt @@ -0,0 +1,79 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.helper + +import android.annotation.SuppressLint +import android.content.Context +import com.navi.base.utils.orElse +import com.navi.base.utils.orZero +import com.navi.common.utils.EMPTY +import com.navi.moneymanager.R +import com.navi.moneymanager.common.db.entity.TransactionEntity +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.network.model.TransactionData +import com.navi.moneymanager.common.utils.Constants +import com.navi.moneymanager.common.utils.Constants.UNCATEGORIZED +import com.navi.moneymanager.common.utils.MonthConstants +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class TransactionsDataHelper @Inject constructor(@ApplicationContext private val context: Context) { + + @SuppressLint("DefaultLocale") + fun getTransactionsEntity( + transactionsList: List, + mmConfig: MMConfigResponse? + ): List { + return transactionsList.map { + val txnMonth = it.txnDate?.split("-")?.get(1)?.toInt().orZero() - 1 // Month is 0 based + val txnYear = it.txnDate?.split("-")?.get(0)?.toInt() + TransactionEntity( + txnId = it.txnId.orEmpty(), + linkedAccRef = it.linkedAccRef, + txnReference = it.txnReference, + txnTimestamp = it.txnTimestamp, + txnAmount = String.format("%.2f", it.txnAmount).toDouble(), + valueDate = it.valueDate, + narration = it.narration, + mode = it.mode, + type = it.type, + counterPartyName = it.counterPartyName, + txnDate = it.txnDate, + txnDay = it.txnDate?.split("-")?.get(2)?.toInt(), + txnMonth = txnMonth, + txnYear = txnYear, + finalCategory = it.finalCategory.orElse(UNCATEGORIZED), + categoryName = + mmConfig + ?.categories + ?.find { category -> category.categoryId == it.finalCategory } + ?.categoryName + .orElse(context.resources.getString(R.string.add_category)), + updatedAt = it.updatedAt, + mobileTimestamp = it.mobileTimestamp + ) + } + } + + fun getTransactionTypeLabel(transactionType: String?): String { + return when (transactionType) { + Constants.DEBIT -> context.getString(R.string.paid_via) + Constants.CREDIT -> context.getString(R.string.received_in) + else -> EMPTY + } + } +} + +fun convertMonthStringToPair(monthString: String): Pair { + val parts = monthString.split(" ") + val monthAbbreviation = parts[0] + val year = parts[1] + val monthList = MonthConstants.monthNames + val fullMonthName = monthList.find { it.startsWith(monthAbbreviation, ignoreCase = true) } ?: "" + return Pair(fullMonthName, year.toInt()) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/model/IllustrationSource.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/model/IllustrationSource.kt new file mode 100644 index 0000000000..884507a62a --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/model/IllustrationSource.kt @@ -0,0 +1,14 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.illustration.model + +sealed interface IllustrationSource { + data class Remote(val url: String, val placeholder: String? = null) : IllustrationSource + + data class Resource(val resId: String) : IllustrationSource +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/model/IllustrationType.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/model/IllustrationType.kt new file mode 100644 index 0000000000..8ab5c11f3e --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/model/IllustrationType.kt @@ -0,0 +1,48 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.illustration.model + +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultFilterQuality +import androidx.compose.ui.layout.ContentScale +import com.airbnb.lottie.compose.LottieConstants + +sealed interface IllustrationType { + data class Image( + val source: IllustrationSource, + val properties: ImageProperties = ImageProperties(), + val contentDescription: String? = null, + ) : IllustrationType + + data class Lottie( + val source: IllustrationSource, + val properties: LottieProperties = LottieProperties(), + val contentDescription: String? = null, + ) : IllustrationType +} + +@Stable +data class ImageProperties( + val contentScale: ContentScale = ContentScale.Fit, + val alignment: Alignment = Alignment.Center, + val alpha: Float = 1.0f, + val colorFilter: ColorFilter? = null, + val filterQuality: FilterQuality = DefaultFilterQuality, +) + +@Stable +data class LottieProperties( + val iterationCount: Int = LottieConstants.IterateForever, + val isPlaying: Boolean = true, + val contentScale: ContentScale = ContentScale.Fit, + val onAnimationStart: (() -> Unit)? = null, + val onAnimationEnd: (() -> Unit)? = null, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/provider/IllustrationProvider.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/provider/IllustrationProvider.kt new file mode 100644 index 0000000000..7bc58b0d02 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/provider/IllustrationProvider.kt @@ -0,0 +1,43 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.illustration.provider + +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.repository.ImageRepository +import com.navi.moneymanager.common.illustration.repository.LottieRepository + +object IllustrationProvider { + + private val imageRepository by lazy { ImageRepository() } + + private val lottieRepository by lazy { LottieRepository() } + + fun getResourceId(illustrationName: String, illustrationType: IllustrationType): Int { + return when (illustrationType) { + is IllustrationType.Image -> { + imageRepository.getResourceId(illustrationName) + } + is IllustrationType.Lottie -> { + lottieRepository.getResourceId(illustrationName) + } + } + } + + fun getRemoteUrl(illustrationName: String, illustrationType: IllustrationType): String { + return when (illustrationType) { + is IllustrationType.Image -> { + imageRepository.getRemoteUrl(illustrationName) + } + is IllustrationType.Lottie -> { + lottieRepository.getRemoteUrl(illustrationName) + } + } + } + + fun getLottieRemoteUrl(lottieName: String) = lottieRepository.getRemoteUrl(lottieName) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/repository/IllustrationRepository.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/repository/IllustrationRepository.kt new file mode 100644 index 0000000000..a15058d1ed --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/repository/IllustrationRepository.kt @@ -0,0 +1,14 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.illustration.repository + +interface IllustrationRepository { + fun getResourceId(illustrationName: String): Int + + fun getRemoteUrl(illustrationName: String): String +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/repository/ImageRepository.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/repository/ImageRepository.kt new file mode 100644 index 0000000000..30d44314c5 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/repository/ImageRepository.kt @@ -0,0 +1,143 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.illustration.repository + +import com.navi.common.utils.EMPTY +import com.navi.moneymanager.R +import com.navi.naviwidgets.R as NaviWidgetsR +import com.navi.naviwidgets.utils.NaviWidgetIconUtils.getIconResourceId + +class ImageRepository : IllustrationRepository { + companion object { + const val PLUS_ICON = "PLUS_ICON" + const val ALL_BANK_ICON = "ALL_BANK_ICON" + const val ALL_BANK_ICON_SMALL = "ALL_BANK_ICON_SMALL" + const val ERROR_TRIANGLE = "ERROR_TRIANGLE" + const val IMAGE_TRANSPARENT_PLACEHOLDER_SMALL = "IMAGE_TRANSPARENT_PLACEHOLDER_SMALL" + const val IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM = "IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM" + const val IMAGE_TRANSPARENT_PLACEHOLDER_LARGE = "IMAGE_TRANSPARENT_PLACEHOLDER_LARGE" + const val IMAGE_TRANSPARENT_PLACEHOLDER_X_LARGE = "IMAGE_TRANSPARENT_PLACEHOLDER_X_LARGE" + const val IMAGE_TRANSPARENT_PLACEHOLDER_XX_LARGE = "IMAGE_TRANSPARENT_PLACEHOLDER_XX_LARGE" + const val IMAGE_PLACEHOLDER_XX_LARGE = "IMAGE_PLACEHOLDER_XX_LARGE" + // Async Image Resource URL + const val TOTAL_SPEND_RUPEE_SYMBOL = "TOTAL_SPEND_RUPEE_SYMBOL" + const val TOTAL_SPEND_RUPEE_SYMBOL_WITHOUT_BG = "TOTAL_SPEND_RUPEE_SYMBOL_WITHOUT_BG" + const val OTHERS_CATEGORY_ICON = "OTHERS_CATEGORY_ICON" + const val EDIT_CATEGORY_PENCIL = "EDIT_CATEGORY_PENCIL" + const val ADD_CATEGORY_PLUS = "ADD_CATEGORY_PLUS" + const val TRANSACTION_DETAIL_PAYMENT_SENT = "TRANSACTION_DETAIL_PAYMENT_SENT" + const val TRANSACTION_DETAIL_PAYMENT_RECEIVED = "TRANSACTION_DETAIL_PAYMENT_RECEIVED" + const val COMMON_CROSS_BLACK = "COMMON_CROSS_BLACK" + const val COMMON_ARROW_LEFT_PURPLE = "COMMON_ARROW_LEFT_PURPLE" + const val COMMON_EMPTY_PLACEHOLDER = "COMMON_EMPTY_PLACEHOLDER" + const val COMMON_SEARCH = "COMMON_SEARCH" + const val COMMON_FILTER = "COMMON_FILTER" + const val ALERT_CIRCLE = "ALERT_CIRCLE" + const val UNCATEGORIZED_INFO_BOTTOM_SHEET = "UNCATEGORIZED_INFO_BOTTOM_SHEET" + const val NEW_TRANSACTION = "NEW_TRANSACTION" + const val COMMON_ARROW_RIGHT_WHITE = "COMMON_ARROW_RIGHT_WHITE" + const val SORT_ICON = "SORT_ICON" + const val BLACK_ALERT_CIRCLE = "BLACK_ALERT_CIRCLE" + const val FAQ_ICON = "FAQ_ICON" + const val CONSENT_ICON = "CONSENT_ICON" + const val CHEVRON_PURPLE_RIGHT_ICON = "CHEVRON_PURPLE_RIGHT_ICON" + const val TOTAL_SPEND = "TOTAL_SPEND" + const val DASHBOARD_FOOTER = "DASHBOARD_FOOTER" + const val INFO = "INFO" + const val PLUS = "PLUS" + const val DASHBOARD_BOTTOMSHEET_LOADING = "DASHBOARD_BOTTOMSHEET_LOADING" + const val LOADING_DONE_TICK = "LOADING_DONE_TICK" + const val OTHER_CATEGORIES = "OTHER_CATEGORIES" + } + + override fun getResourceId(illustrationName: String): Int { + return when (illustrationName) { + PLUS_ICON -> R.drawable.plus_icon + ALL_BANK_ICON -> R.drawable.all_bank_image + ALL_BANK_ICON_SMALL -> R.drawable.all_banks_icon_small + ERROR_TRIANGLE -> com.navi.common.R.drawable.something_went_wrong_triangle + IMAGE_TRANSPARENT_PLACEHOLDER_SMALL -> + NaviWidgetsR.drawable.image_transparent_placeholder_small + IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM -> + NaviWidgetsR.drawable.image_transparent_placeholder_medium + IMAGE_TRANSPARENT_PLACEHOLDER_LARGE -> + NaviWidgetsR.drawable.image_transparent_placeholder_large + IMAGE_TRANSPARENT_PLACEHOLDER_X_LARGE -> + NaviWidgetsR.drawable.image_transparent_placeholder_x_large + IMAGE_TRANSPARENT_PLACEHOLDER_XX_LARGE -> + NaviWidgetsR.drawable.image_transparent_placeholder_xx_large + IMAGE_PLACEHOLDER_XX_LARGE -> NaviWidgetsR.drawable.image_placeholder_xx_large + else -> { + getIconResourceId(illustrationName) + } + } + } + + override fun getRemoteUrl(illustrationName: String): String { + return when (illustrationName) { + ALL_BANK_ICON -> + "https://public-assets.prod.navi-sa.in/money-manager/png/all_banks_icon.svg" + ALL_BANK_ICON_SMALL -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/common/all_banks_icon_small.svg" + TOTAL_SPEND_RUPEE_SYMBOL -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/dashboard/monthly-summary/total-spend.svg" + TOTAL_SPEND_RUPEE_SYMBOL_WITHOUT_BG -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/dashboard/monthly-summary/total_spend_without_bg.svg" + OTHERS_CATEGORY_ICON -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/category/others.svg" + EDIT_CATEGORY_PENCIL -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/common/pencil.svg" + ADD_CATEGORY_PLUS -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/common/plus.svg" + TRANSACTION_DETAIL_PAYMENT_SENT -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/transaction-detail/payment-sent.svg" + TRANSACTION_DETAIL_PAYMENT_RECEIVED -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/transaction-detail/payment-received.svg" + COMMON_CROSS_BLACK -> + "https://public-assets.prod.navi-sa.in/cycs/png/common-cross-black.png" + COMMON_ARROW_LEFT_PURPLE -> + "https://public-assets.prod.navi-sa.in/cycs/png/common-arrow-left-purple.png" + COMMON_EMPTY_PLACEHOLDER -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/common/empty-placeholder.svg" + COMMON_SEARCH -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/common/search.svg" + COMMON_FILTER -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/common/filter.svg" + ALERT_CIRCLE -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/common/alert-circle.svg" + NEW_TRANSACTION -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/dashboard/monthly-summary/green-new-transaction-icon.svg" + COMMON_ARROW_RIGHT_WHITE -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/dashboard/monthly-summary/right-arrow-white.svg" + UNCATEGORIZED_INFO_BOTTOM_SHEET -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/common/uncategorised_info_bottom_sheet.svg" + SORT_ICON -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/category-details/sort-icon.svg" + BLACK_ALERT_CIRCLE -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/common/black-alert-circle.svg" + FAQ_ICON -> "https://public-assets.prod.navi-sa.in/money-manager/svg/common/faq.svg" + CONSENT_ICON -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/common/consent.svg" + CHEVRON_PURPLE_RIGHT_ICON -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/common/chevron-right.svg" + TOTAL_SPEND -> + "https://public-assets.prod.navi-sa.in/money-manager/svg/dashboard/monthly-summary/total-spend.svg" + DASHBOARD_FOOTER -> + "https://public-assets.prod.navi-sa.in/money-manager/png/dashboard-footer.png" + INFO -> "https://public-assets.prod.navi-sa.in/money-manager/svg/common/info.svg" + PLUS -> "https://public-assets.prod.navi-sa.in/money-manager/svg/common/plus.svg" + DASHBOARD_BOTTOMSHEET_LOADING -> + "https://public-assets.prod.navi-sa.in/money-manager/png/dashboard_bottomsheet_loading_icon.png" + LOADING_DONE_TICK -> + "https://public-assets.prod.navi-sa.in/money-manager/png/mm_loading_done_tick_icon.png" + OTHER_CATEGORIES -> + "https://public-assets.prod.navi-sa.in/money-manager/png/other_categories.png" + else -> EMPTY + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/repository/LottieRepository.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/repository/LottieRepository.kt new file mode 100644 index 0000000000..eb88741b41 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/repository/LottieRepository.kt @@ -0,0 +1,40 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.illustration.repository + +import com.navi.common.utils.EMPTY +import com.navi.moneymanager.R +import com.navi.naviwidgets.utils.NaviWidgetLottieUtils.getLottieFromLottieCode + +class LottieRepository : IllustrationRepository { + companion object { + const val CHIP_LOADER_LOTTIE = "CHIP_LOADER_LOTTIE" + const val CIRCULAR_LOADER_LOTTIE = "CIRCULAR_LOADER_LOTTIE" + const val SUCCESS_TICK_LOTTIE = "SUCCESS_TICK_LOTTIE" + const val TUK_TUK_WHITE = "TUK_TUK_WHITE" + } + + override fun getResourceId(illustrationName: String): Int { + return when (illustrationName) { + CHIP_LOADER_LOTTIE -> R.raw.chip_loader_lottie + SUCCESS_TICK_LOTTIE -> R.raw.success_tick_lottie + CIRCULAR_LOADER_LOTTIE -> R.raw.circular_loader_lottie + else -> { + getLottieFromLottieCode(illustrationName) + } + } + } + + override fun getRemoteUrl(illustrationName: String): String { + return when (illustrationName) { + TUK_TUK_WHITE -> + "https://public-assets.prod.navi-sa.in/money-manager/lottie/common/tuktuk-white.json" + else -> EMPTY + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/ui/Illustration.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/ui/Illustration.kt new file mode 100644 index 0000000000..c73c62a310 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/ui/Illustration.kt @@ -0,0 +1,70 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.illustration.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.navi.elex.atoms.ElexAsyncImage +import com.navi.elex.atoms.ElexImage +import com.navi.elex.atoms.ElexLottie +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.provider.IllustrationProvider +import com.navi.moneymanager.common.illustration.utils.lottieCompositionSpecBasedOnSource + +@Composable +fun Illustration(modifier: Modifier, illustrationType: IllustrationType) { + when (illustrationType) { + is IllustrationType.Image -> { + when (val source = illustrationType.source) { + is IllustrationSource.Resource -> { + val iconCode = + IllustrationProvider.getResourceId(source.resId, illustrationType) + ElexImage( + iconCode = iconCode, + modifier = modifier, + contentDescription = illustrationType.contentDescription, + alignment = illustrationType.properties.alignment, + alpha = illustrationType.properties.alpha, + colorFilter = illustrationType.properties.colorFilter + ) + } + is IllustrationSource.Remote -> { + val iconUrl = IllustrationProvider.getRemoteUrl(source.url, illustrationType) + val placeHolder = + IllustrationProvider.getResourceId( + source.placeholder.orEmpty(), + illustrationType + ) + ElexAsyncImage( + icon = iconUrl.ifEmpty { source.url }, + modifier = modifier, + contentScale = illustrationType.properties.contentScale, + contentDescription = illustrationType.contentDescription, + alignment = illustrationType.properties.alignment, + alpha = illustrationType.properties.alpha, + colorFilter = illustrationType.properties.colorFilter, + filterQuality = illustrationType.properties.filterQuality, + placeholder = placeHolder + ) + } + } + } + is IllustrationType.Lottie -> { + ElexLottie( + modifier = modifier, + spec = lottieCompositionSpecBasedOnSource(illustrationType.source), + iterations = illustrationType.properties.iterationCount, + isPlaying = illustrationType.properties.isPlaying, + contentScale = illustrationType.properties.contentScale, + onAnimationStart = illustrationType.properties.onAnimationStart, + onAnimationEnd = illustrationType.properties.onAnimationEnd, + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/utils/IllustrationUtils.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/utils/IllustrationUtils.kt new file mode 100644 index 0000000000..d01f5963ae --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/illustration/utils/IllustrationUtils.kt @@ -0,0 +1,31 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.illustration.utils + +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.provider.IllustrationProvider + +fun lottieCompositionSpecBasedOnSource(lottie: IllustrationSource): LottieCompositionSpec { + return when (lottie) { + is IllustrationSource.Remote -> { + val lottieUrl = + IllustrationProvider.getRemoteUrl( + illustrationName = lottie.url, + illustrationType = IllustrationType.Lottie(lottie) + ) + LottieCompositionSpec.Url(lottieUrl.ifEmpty { lottie.url }) + } + is IllustrationSource.Resource -> { + LottieCompositionSpec.RawRes( + IllustrationProvider.getResourceId(lottie.resId, IllustrationType.Lottie(lottie)) + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/manager/MMLibManager.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/manager/MMLibManager.kt new file mode 100644 index 0000000000..f610e5996a --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/manager/MMLibManager.kt @@ -0,0 +1,47 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.manager + +import com.navi.base.cache.repository.NaviCacheRepository +import com.navi.base.sharedpref.PreferenceManager +import com.navi.moneymanager.common.db.database.MMDatabase +import com.navi.moneymanager.common.db.database.MMEncryptedDatabase +import com.navi.moneymanager.common.utils.Constants.MM_IS_USER_ONBOARDED +import com.navi.moneymanager.common.utils.DbCacheConstants.MONEY_MANAGER_CONFIG_RESPONSE_KEY +import com.navi.moneymanager.common.utils.DbCacheConstants.MONEY_MANAGER_VALUE_PROPOSITION_SCREEN_KEY +import dagger.Lazy +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Singleton +class MMLibManager +@Inject +constructor( + private val mmEncryptedDatabase: Lazy, + private val mmDatabase: Lazy, + private val naviCacheRepository: NaviCacheRepository, +) { + fun initMoneyManagerDB() { + CoroutineScope(Dispatchers.IO).launch { + mmEncryptedDatabase.get().accountsDao().getAccountsCount() + } + } + + fun clearMoneyManagerData() { + CoroutineScope(Dispatchers.IO).launch { + PreferenceManager.setBooleanPreference(MM_IS_USER_ONBOARDED, false) + naviCacheRepository.clear(key = MONEY_MANAGER_VALUE_PROPOSITION_SCREEN_KEY) + naviCacheRepository.clear(key = MONEY_MANAGER_CONFIG_RESPONSE_KEY) + mmEncryptedDatabase.get().clearAllTables() + mmDatabase.get().clearAllTables() + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/BankSelectionBottomSheetData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/BankSelectionBottomSheetData.kt new file mode 100644 index 0000000000..c67aa3db39 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/BankSelectionBottomSheetData.kt @@ -0,0 +1,26 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model + +import com.navi.moneymanager.common.illustration.model.IllustrationSource + +data class BankSelectionBottomSheetData( + val headerText: String, + val backIconUrl: IllustrationSource, + val buttonLoader: String, + val availableBanks: List, + val ctaText: String, + var selectedBankReferenceIds: Set? = null +) + +data class BankAccount( + val linkedAccountReference: String, + val bankIconUrl: String, + val bankName: String, + val accountHint: String +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/BarChartData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/BarChartData.kt new file mode 100644 index 0000000000..ae13c4fc5b --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/BarChartData.kt @@ -0,0 +1,38 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model + +import com.navi.moneymanager.common.illustration.model.IllustrationSource + +data class BarGraphData( + val selectedBar: Int? = null, + val iconUrl: IllustrationSource, + val isTotalSyncCompleted: Boolean? = true, + val pagedElementList: List, + val elementsPerPage: Int, +) + +data class BarGraphPageData( + val elementList: List, + val averageData: YAxisAverageData? = null, + val maxValue: Double, +) + +data class BarGraphElement( + val xAxisValue: String, + val yAxisValue: Double, + val formattedYAxisValue: String, + val barId: Int, + val selectedMonth: SelectedMonth, +) + +data class YAxisAverageData( + val value: Double, + val title: String, + val formattedValue: String? = null, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/CategorySummary.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/CategorySummary.kt new file mode 100644 index 0000000000..1b11ee4fe2 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/CategorySummary.kt @@ -0,0 +1,14 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model + +data class CategorySummary( + val finalCategory: String, + val numberOfTransactions: Int?, + val totalAmount: Double? +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/ChipData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/ChipData.kt new file mode 100644 index 0000000000..0317ee0788 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/ChipData.kt @@ -0,0 +1,39 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model + +import androidx.compose.runtime.Stable +import com.navi.common.utils.EMPTY +import com.navi.moneymanager.common.illustration.model.IllustrationType + +/** + * Represents the data for a chip component, which includes an illustration and associated content. + * + * @property illustration The illustration associated with the chip. + * @property content The textual content displayed on the chip. + */ +@Stable +data class ChipData( + val illustration: ChipIllustration, + val content: String, +) + +/** + * Represents the illustration details for a chip component. + * + * @property illustrationType The type of illustration (e.g., icon, image, lottie). + * @property size The size of the illustration. Default is 0. + * @property contentDescription A description of the illustration for accessibility purposes. + * Default is an empty string. + */ +@Stable +data class ChipIllustration( + val illustrationType: IllustrationType, + val size: Int = 0, + val contentDescription: String = EMPTY, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/DataLoadingBottomSheetData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/DataLoadingBottomSheetData.kt new file mode 100644 index 0000000000..9a369d3e23 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/DataLoadingBottomSheetData.kt @@ -0,0 +1,18 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model + +import com.navi.moneymanager.common.illustration.model.IllustrationSource + +data class DataLoadingBottomSheetData( + val loadingIcon: IllustrationSource?, + val loadingLottie: IllustrationSource?, + val titleText: String, + val subTitleText: String, + val ctaText: String +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/FilterItemData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/FilterItemData.kt new file mode 100644 index 0000000000..aefa8b5b20 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/FilterItemData.kt @@ -0,0 +1,39 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model + +data class FilterItemData( + val availableFilters: List, +) { + data class FilterItem( + val attributeKey: String, + val isSelected: Boolean = false, + val selectedValueCount: Int = 0, + val valueOptions: List, + ) + + data class ValueData( + val valueName: String, + val isSelected: Boolean = false, + val id: String, + val type: FilterChipType = FilterChipType.ChipItem + ) { + sealed interface FilterChipType { + data object ChipItem : FilterChipType + + data object HiddenChipRepresentationItem : FilterChipType + } + } +} + +enum class FilterAttribute(val value: String) { + MONTH("Month"), + CATEGORY("Category"), + TYPE("Type"), + BANK("Bank") +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/MMUIState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/MMUIState.kt new file mode 100644 index 0000000000..3a0f34be29 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/MMUIState.kt @@ -0,0 +1,16 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model + +sealed class MMUIState { + data object Loading : MMUIState() + + data object Loaded : MMUIState() + + data object Empty : MMUIState() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/MonthAmountAggregate.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/MonthAmountAggregate.kt new file mode 100644 index 0000000000..0d07d1e412 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/MonthAmountAggregate.kt @@ -0,0 +1,17 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model + +import androidx.compose.ui.graphics.Color + +data class MonthAmountAggregate( + val yearMonth: String, + val amount: Double, +) + +data class MonthlyBalance(val monthTag: String, val balance: String, val color: Color) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/MonthSelectionBottomSheetData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/MonthSelectionBottomSheetData.kt new file mode 100644 index 0000000000..2a98e21ae6 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/MonthSelectionBottomSheetData.kt @@ -0,0 +1,15 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model + +data class MonthSelectionBottomSheetData( + val headerText: String, + val monthList: List>, + val selectedMonth: Pair, + val ctaText: String +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/ScreenInputParams.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/ScreenInputParams.kt new file mode 100644 index 0000000000..2caa4951a7 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/ScreenInputParams.kt @@ -0,0 +1,21 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model + +data class SpendAnalysisScreenInputParams( + val month: Int? = null, + val year: Int? = null, + val selectedBankReferenceIds: Set? = null +) + +data class CategoryDetailsScreenInputParams( + val month: Int? = null, + val year: Int? = null, + val selectedCategory: String? = null, + val selectedBankReferenceIds: Set? = null +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/SelectedMonth.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/SelectedMonth.kt new file mode 100644 index 0000000000..9816262475 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/SelectedMonth.kt @@ -0,0 +1,13 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize data class SelectedMonth(val month: Int? = null, val year: Int? = null) : Parcelable diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/SingleChipSelectionData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/SingleChipSelectionData.kt new file mode 100644 index 0000000000..b40db9d202 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/SingleChipSelectionData.kt @@ -0,0 +1,12 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model + +import com.navi.common.utils.EMPTY + +data class SingleChipSelectionData(val id: String = EMPTY, val chipData: ChipData) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/SpendCategorizationLoaded.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/SpendCategorizationLoaded.kt new file mode 100644 index 0000000000..5db580ca72 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/SpendCategorizationLoaded.kt @@ -0,0 +1,129 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model + +import androidx.compose.ui.graphics.Color +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.model.sectionHeader.SectionHeaderData + +sealed interface SpendCategorizationState { + val header: SectionHeaderData? + val totalSpends: TotalSpends? + val categoryHeaderTitlePrefix: String + val categoryHeaderTitleSuffix: String? + val selectedMonth: Int + val selectedYear: Int + + data class Loading( + override val header: SectionHeaderData? = null, + override val totalSpends: TotalSpends? = null, + override val categoryHeaderTitlePrefix: String, + override val categoryHeaderTitleSuffix: String? = null, + override val selectedMonth: Int, + override val selectedYear: Int, + val categories: List + ) : SpendCategorizationState + + data class Loaded( + override val header: SectionHeaderData? = null, + override val totalSpends: TotalSpends? = null, + override val categoryHeaderTitlePrefix: String, + override val categoryHeaderTitleSuffix: String? = null, + override val selectedMonth: Int, + override val selectedYear: Int, + val applyCategoryHorizontalPadding: Boolean? = true, + val categories: List, + val viewMoreCtaText: String? = null, + val selfTransferCategory: SelfTransferCategoryData? = null + ) : SpendCategorizationState + + data class Empty( + override val header: SectionHeaderData? = null, + override val totalSpends: TotalSpends? = null, + override val categoryHeaderTitlePrefix: String, + override val categoryHeaderTitleSuffix: String? = null, + override val selectedMonth: Int, + override val selectedYear: Int, + val title: String, + val iconUrl: IllustrationSource, + val addAccountCtaText: String? = null, + val ctaIconUrl: IllustrationSource? = null, + val loadingLottieUrl: IllustrationSource? = null, + val isTotalSyncCompleted: Boolean = false + ) : SpendCategorizationState +} + +sealed class TotalSpends { + abstract val title: String + + data class Empty( + val iconUrl: IllustrationSource, + override val title: String, + val subtitle: String, + val actionIcon: IllustrationSource? = null + ) : TotalSpends() + + data class Loaded( + val iconUrl: IllustrationSource, + override val title: String, + val subtitle: String, + val actionIcon: IllustrationSource? = null + ) : TotalSpends() + + data class Loading( + override val title: String, + val lottieUrl: IllustrationSource, + val animationUrl: IllustrationSource + ) : TotalSpends() +} + +data class SelfTransferCategoryData( + val categoryId: String, + val categoryName: String, + val subtitle: String, + val amount: String, + val iconUrl: IllustrationSource, + val actionIcon: IllustrationSource, +) + +sealed class SpendCategoryItemData { + data class Loaded( + val categoryId: String, + val name: String, + val iconUrl: IllustrationSource, + val actionIcon: IllustrationSource, + val progress: Float, + val progressColor: Color, + val amount: String, + val progressText: String + ) : SpendCategoryItemData() + + data class Loading( + val name: String, + val lottieUrl: IllustrationSource, + val actionLottie: IllustrationSource + ) : SpendCategoryItemData() +} + +sealed class SpendCategorizationAction { + data class ViewTotalSpends(val selectedMonth: SelectedMonth) : SpendCategorizationAction() + + data class SelectCategory(val categoryId: String, val selectedMonth: SelectedMonth) : + SpendCategorizationAction() + + data class ViewMoreCategories(val selectedMonth: SelectedMonth) : SpendCategorizationAction() + + data class SelectSelfTransferCategory( + val categoryName: String, + val selectedMonth: SelectedMonth + ) : SpendCategorizationAction() + + data class AddNewBankAccount(val isTotalSyncCompleted: Boolean) : SpendCategorizationAction() + + data class OnMonthChange(val displayMonthText: String) : SpendCategorizationAction() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/Transaction.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/Transaction.kt new file mode 100644 index 0000000000..ec854b5a85 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/Transaction.kt @@ -0,0 +1,30 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model + +import androidx.compose.ui.graphics.Color +import com.navi.moneymanager.common.illustration.model.IllustrationSource + +data class Transaction( + val id: String, + val initials: String, + val initialBackgroundColor: Color, + val title: String, + val categoryId: String, + val categoryName: String, + val categoryIcon: IllustrationSource?, + val date: String, + val monthTag: String, + val amount: String, + val amountColor: Color, + val transactionTypeLabel: String, + val accountIcon: IllustrationSource, + val counterPartyName: String, + val type: String, + val txnTimestamp: Long +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/ZeroTransactionData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/ZeroTransactionData.kt new file mode 100644 index 0000000000..0fcfa1514d --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/ZeroTransactionData.kt @@ -0,0 +1,15 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model + +import com.navi.moneymanager.common.illustration.model.IllustrationSource + +data class ZeroTransactionData( + val title: String, + val illustrationSource: IllustrationSource, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/BottomSheetConfig.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/BottomSheetConfig.kt new file mode 100644 index 0000000000..3fd9a982f1 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/BottomSheetConfig.kt @@ -0,0 +1,26 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model.bottomSheet + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp +import com.navi.common.basemvi.UiEvent +import com.navi.moneymanager.common.ui.theme.color.MMColor + +@Immutable +data class BottomSheetConfig( + val isCancellable: Boolean = true, + val screenOffset: Int = 0, + val containerColor: Color = MMColor.white, + val shape: Shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp), + val scrimColor: Color = Color.Black.copy(alpha = 0.4f), + val dismissEvent: uiEvent? = null +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/BottomSheetState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/BottomSheetState.kt new file mode 100644 index 0000000000..b183ce576e --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/BottomSheetState.kt @@ -0,0 +1,15 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model.bottomSheet + +import com.navi.common.basemvi.UiEvent + +data class BottomSheetState, uiEvent : UiEvent>( + val type: bottomSheetType, + val isVisible: Boolean = false +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/CategoryDetailsScreenBottomSheets.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/CategoryDetailsScreenBottomSheets.kt new file mode 100644 index 0000000000..14250cedeb --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/CategoryDetailsScreenBottomSheets.kt @@ -0,0 +1,118 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model.bottomSheet + +import com.navi.moneymanager.common.model.BankSelectionBottomSheetData +import com.navi.moneymanager.common.model.DataLoadingBottomSheetData +import com.navi.moneymanager.common.model.MonthSelectionBottomSheetData +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenUiEvent +import com.navi.moneymanager.postonboard.categorydetails.model.CategorySelectionBottomSheetData +import com.navi.moneymanager.postonboard.categorydetails.model.UncategorizedInfoBottomSheetData +import com.navi.moneymanager.postonboard.help.model.HelpBottomSheetState + +sealed class CategoryDetailsScreenBottomSheets( + val bottomSheetContent: Any? = null, + val bottomSheetConfig: BottomSheetConfig = BottomSheetConfig(), +) : + ScreenBottomSheetType( + sheetData = bottomSheetContent, + sheetConfig = bottomSheetConfig + ) { + data class BankSelection(val data: BankSelectionBottomSheetData? = null) : + CategoryDetailsScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + screenOffset = 80, + dismissEvent = + CategoryDetailsScreenUiEvent.DismissBottomSheet(BankSelection::class.java) + ) + ) + + data class MonthSelection(val data: MonthSelectionBottomSheetData? = null) : + CategoryDetailsScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + screenOffset = 80, + dismissEvent = + CategoryDetailsScreenUiEvent.DismissBottomSheet(MonthSelection::class.java) + ) + ) + + data class AverageInfo(val averageValue: String?) : + CategoryDetailsScreenBottomSheets( + bottomSheetContent = Any(), + bottomSheetConfig = + BottomSheetConfig( + dismissEvent = + CategoryDetailsScreenUiEvent.DismissBottomSheet(AverageInfo::class.java) + ) + ) + + data class PastMonthDataLoading(val data: DataLoadingBottomSheetData? = null) : + CategoryDetailsScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + dismissEvent = + CategoryDetailsScreenUiEvent.DismissBottomSheet( + PastMonthDataLoading::class.java + ) + ) + ) + + data class CategorySelection(val data: CategorySelectionBottomSheetData? = null) : + CategoryDetailsScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + screenOffset = 80, + dismissEvent = + CategoryDetailsScreenUiEvent.DismissBottomSheet( + CategorySelection::class.java + ) + ) + ) + + data class UncategorizedInfo(val data: UncategorizedInfoBottomSheetData? = null) : + CategoryDetailsScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + dismissEvent = + CategoryDetailsScreenUiEvent.DismissBottomSheet( + UncategorizedInfo::class.java + ) + ) + ) + + data object SortTransactions : + CategoryDetailsScreenBottomSheets( + bottomSheetContent = Any(), + bottomSheetConfig = + BottomSheetConfig( + dismissEvent = + CategoryDetailsScreenUiEvent.DismissBottomSheet( + SortTransactions::class.java + ) + ) + ) + + data class HelpBottomSheet(val state: HelpBottomSheetState = HelpBottomSheetState.Initial) : + CategoryDetailsScreenBottomSheets( + bottomSheetContent = state, + bottomSheetConfig = + BottomSheetConfig( + dismissEvent = + CategoryDetailsScreenUiEvent.DismissBottomSheet(HelpBottomSheet::class.java) + ) + ) + + data object NoBottomSheet : CategoryDetailsScreenBottomSheets() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/DashboardScreenBottomSheets.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/DashboardScreenBottomSheets.kt new file mode 100644 index 0000000000..a16d3657a9 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/DashboardScreenBottomSheets.kt @@ -0,0 +1,108 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model.bottomSheet + +import com.navi.moneymanager.common.model.DataLoadingBottomSheetData +import com.navi.moneymanager.common.model.MonthSelectionBottomSheetData +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiEvent +import com.navi.moneymanager.postonboard.dashboard.model.FinarkeinErrorBottomSheetData +import com.navi.moneymanager.postonboard.dashboard.model.OnboardingStatus +import com.navi.moneymanager.postonboard.help.model.HelpBottomSheetState +import com.navi.moneymanager.preonboard.launcher.model.GenericErrorBottomSheetData + +sealed class DashboardScreenBottomSheets( + val bottomSheetContent: Any? = null, + val bottomSheetConfig: BottomSheetConfig = BottomSheetConfig(), +) : + ScreenBottomSheetType( + sheetData = bottomSheetContent, + sheetConfig = bottomSheetConfig + ) { + data class OnBoarding(val state: OnboardingStatus = OnboardingStatus.Loading) : + DashboardScreenBottomSheets( + bottomSheetContent = state, + bottomSheetConfig = BottomSheetConfig(isCancellable = false) + ) + + data class FetchingTransactions(val data: DataLoadingBottomSheetData? = null) : + DashboardScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + dismissEvent = + DashboardScreenUiEvent.DismissBottomSheet(FetchingTransactions::class.java) + ) + ) + + data class PastMonthDataLoading(val data: DataLoadingBottomSheetData? = null) : + DashboardScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + dismissEvent = + DashboardScreenUiEvent.DismissBottomSheet(PastMonthDataLoading::class.java) + ) + ) + + data class MonthSelection(val data: MonthSelectionBottomSheetData? = null) : + DashboardScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + screenOffset = 80, + dismissEvent = + DashboardScreenUiEvent.DismissBottomSheet(MonthSelection::class.java) + ) + ) + + data class FinarkeinError(val data: FinarkeinErrorBottomSheetData? = null) : + DashboardScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + isCancellable = false, + dismissEvent = + DashboardScreenUiEvent.DismissBottomSheet(FinarkeinError::class.java) + ) + ) + + data class HelpBottomSheet(val state: HelpBottomSheetState = HelpBottomSheetState.Initial) : + DashboardScreenBottomSheets( + bottomSheetContent = state, + bottomSheetConfig = + BottomSheetConfig( + dismissEvent = + DashboardScreenUiEvent.DismissBottomSheet(HelpBottomSheet::class.java) + ) + ) + + data class GenericErrorBottomSheet(val data: GenericErrorBottomSheetData? = null) : + DashboardScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + isCancellable = true, + dismissEvent = + DashboardScreenUiEvent.DismissBottomSheet( + GenericErrorBottomSheet::class.java + ) + ) + ) + + data object NoBottomSheet : DashboardScreenBottomSheets() + + data class AddAccountLoading(val data: DataLoadingBottomSheetData? = null) : + DashboardScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + dismissEvent = + DashboardScreenUiEvent.DismissBottomSheet(AddAccountLoading::class.java) + ) + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/ScreenBottomSheetType.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/ScreenBottomSheetType.kt new file mode 100644 index 0000000000..85c7208fba --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/ScreenBottomSheetType.kt @@ -0,0 +1,19 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model.bottomSheet + +import com.navi.common.basemvi.UiEvent + +sealed class ScreenBottomSheetType( + val sheetData: Any? = null, + val sheetConfig: BottomSheetConfig = BottomSheetConfig() +) { + fun createDismissAction(onEvent: (uiEvent) -> Unit): () -> Unit = { + sheetConfig.dismissEvent?.let { event -> onEvent(event) } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/SpendAnalysisScreenBottomSheets.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/SpendAnalysisScreenBottomSheets.kt new file mode 100644 index 0000000000..b709278f32 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/SpendAnalysisScreenBottomSheets.kt @@ -0,0 +1,91 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model.bottomSheet + +import com.navi.moneymanager.common.model.BankSelectionBottomSheetData +import com.navi.moneymanager.common.model.DataLoadingBottomSheetData +import com.navi.moneymanager.common.model.MonthSelectionBottomSheetData +import com.navi.moneymanager.postonboard.help.model.HelpBottomSheetState +import com.navi.moneymanager.postonboard.spendanalysis.model.OtherCategoriesBottomSheetData +import com.navi.moneymanager.postonboard.spendanalysis.model.SpendAnalysisScreenUiEvent + +sealed class SpendAnalysisScreenBottomSheets( + val bottomSheetContent: Any? = null, + val bottomSheetConfig: BottomSheetConfig = BottomSheetConfig(), +) : + ScreenBottomSheetType( + sheetData = bottomSheetContent, + sheetConfig = bottomSheetConfig + ) { + data class BankSelection(val data: BankSelectionBottomSheetData? = null) : + SpendAnalysisScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + screenOffset = 80, + dismissEvent = + SpendAnalysisScreenUiEvent.DismissBottomSheet(BankSelection::class.java) + ) + ) + + data class MonthSelection(val data: MonthSelectionBottomSheetData? = null) : + SpendAnalysisScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + screenOffset = 80, + dismissEvent = + SpendAnalysisScreenUiEvent.DismissBottomSheet(MonthSelection::class.java) + ) + ) + + data class OtherCategories(val data: OtherCategoriesBottomSheetData? = null) : + SpendAnalysisScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + screenOffset = 80, + dismissEvent = + SpendAnalysisScreenUiEvent.DismissBottomSheet(OtherCategories::class.java) + ) + ) + + data class AverageInfo(val averageValue: String?) : + SpendAnalysisScreenBottomSheets( + bottomSheetContent = Any(), + bottomSheetConfig = + BottomSheetConfig( + dismissEvent = + SpendAnalysisScreenUiEvent.DismissBottomSheet(AverageInfo::class.java) + ) + ) + + data class PastMonthDataLoading(val data: DataLoadingBottomSheetData? = null) : + SpendAnalysisScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + dismissEvent = + SpendAnalysisScreenUiEvent.DismissBottomSheet( + PastMonthDataLoading::class.java + ) + ) + ) + + data class HelpBottomSheet(val state: HelpBottomSheetState = HelpBottomSheetState.Initial) : + SpendAnalysisScreenBottomSheets( + bottomSheetContent = state, + bottomSheetConfig = + BottomSheetConfig( + dismissEvent = + SpendAnalysisScreenUiEvent.DismissBottomSheet(HelpBottomSheet::class.java) + ) + ) + + data object NoBottomSheet : SpendAnalysisScreenBottomSheets() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/TransactionDetailsScreenBottomSheets.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/TransactionDetailsScreenBottomSheets.kt new file mode 100644 index 0000000000..c17181d2a0 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/TransactionDetailsScreenBottomSheets.kt @@ -0,0 +1,34 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model.bottomSheet + +import com.navi.moneymanager.postonboard.help.model.HelpBottomSheetState +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionDetailsScreenUiEvent + +sealed class TransactionDetailsScreenBottomSheets( + val bottomSheetContent: Any? = null, + val bottomSheetConfig: BottomSheetConfig = BottomSheetConfig(), +) : + ScreenBottomSheetType( + sheetData = bottomSheetContent, + sheetConfig = bottomSheetConfig + ) { + data class HelpBottomSheet(val state: HelpBottomSheetState = HelpBottomSheetState.Initial) : + TransactionDetailsScreenBottomSheets( + bottomSheetContent = state, + bottomSheetConfig = + BottomSheetConfig( + dismissEvent = + TransactionDetailsScreenUiEvent.DismissBottomSheet( + HelpBottomSheet::class.java + ) + ) + ) + + data object NoBottomSheet : TransactionDetailsScreenBottomSheets() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/ValuePropScreenBottomSheets.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/ValuePropScreenBottomSheets.kt new file mode 100644 index 0000000000..cf843eabfd --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/bottomSheet/ValuePropScreenBottomSheets.kt @@ -0,0 +1,61 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model.bottomSheet + +import com.navi.moneymanager.postonboard.dashboard.model.FinarkeinErrorBottomSheetData +import com.navi.moneymanager.preonboard.launcher.model.ConsentRevokedBottomSheetData +import com.navi.moneymanager.preonboard.launcher.model.GenericErrorBottomSheetData +import com.navi.moneymanager.preonboard.valueprop.model.ValuePropScreenUiEvent + +sealed class ValuePropScreenBottomSheets( + val bottomSheetContent: Any? = null, + val bottomSheetConfig: BottomSheetConfig = BottomSheetConfig(), +) : + ScreenBottomSheetType( + sheetData = bottomSheetContent, + sheetConfig = bottomSheetConfig + ) { + data class FinarkeinError(val data: FinarkeinErrorBottomSheetData? = null) : + ValuePropScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + isCancellable = false, + dismissEvent = + ValuePropScreenUiEvent.DismissBottomSheet(FinarkeinError::class.java) + ) + ) + + data class ConsentRevokedBottomSheet(val data: ConsentRevokedBottomSheetData? = null) : + ValuePropScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + isCancellable = true, + dismissEvent = + ValuePropScreenUiEvent.DismissBottomSheet( + ConsentRevokedBottomSheet::class.java + ) + ) + ) + + data class GenericErrorBottomSheet(val data: GenericErrorBottomSheetData? = null) : + ValuePropScreenBottomSheets( + bottomSheetContent = data, + bottomSheetConfig = + BottomSheetConfig( + isCancellable = true, + dismissEvent = + ValuePropScreenUiEvent.DismissBottomSheet( + GenericErrorBottomSheet::class.java + ) + ) + ) + + data object NoBottomSheet : ValuePropScreenBottomSheets() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/database/AccountOverview.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/database/AccountOverview.kt new file mode 100644 index 0000000000..89be208c54 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/database/AccountOverview.kt @@ -0,0 +1,19 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model.database + +data class AccountOverview( + val linkedAccRef: String, + val accountHolderName: String?, + val maskedAccNumber: String?, + val bankName: String?, + val bankCode: String?, + val bankIconUrl: String?, + val currentBalance: Double?, + val updatedAt: Long?, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/database/TransactionDetails.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/database/TransactionDetails.kt new file mode 100644 index 0000000000..7e4606b367 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/database/TransactionDetails.kt @@ -0,0 +1,20 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model.database + +data class TransactionDetails( + val txnId: String, + val linkedAccRef: String?, + val txnTimestamp: Long?, + val txnAmount: Double?, + val narration: String?, + val mode: String?, + val type: String?, + val counterPartyName: String?, + val finalCategory: String, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/database/TransactionSummaryData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/database/TransactionSummaryData.kt new file mode 100644 index 0000000000..e7810435a6 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/database/TransactionSummaryData.kt @@ -0,0 +1,42 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model.database + +import com.navi.base.utils.EMPTY +import com.navi.base.utils.SPACE +import com.navi.common.utils.monthAbbreviations +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +data class TransactionSummaryData( + val txnId: String, + val linkedAccRef: String?, + val txnTimestamp: Long?, + val txnMonth: Int?, + val txnYear: Int?, + val txnAmount: Double?, + val type: String?, + val counterPartyName: String?, + val finalCategory: String, + val bankIconUrl: String? = null, +) { + fun getFormattedMonthTag(): String { + if (!hasValidMonthYear(txnMonth, txnYear)) return EMPTY + return monthAbbreviations[txnMonth].plus(SPACE).plus(this.txnYear) + } + + fun hasValidMonthYear(): Boolean { + return hasValidMonthYear(txnMonth, txnYear) + } + + @OptIn(ExperimentalContracts::class) + private fun hasValidMonthYear(txnMonth: Int?, txnYear: Int?): Boolean { + contract { returns(true) implies (txnMonth != null && txnYear != null) } + return txnMonth != null && txnYear != null + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/sectionHeader/SectionHeaderData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/sectionHeader/SectionHeaderData.kt new file mode 100644 index 0000000000..61a3b81827 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/model/sectionHeader/SectionHeaderData.kt @@ -0,0 +1,17 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.model.sectionHeader + +import com.navi.moneymanager.common.illustration.model.IllustrationSource + +data class SectionHeaderData( + val title: String, + val actionText: String? = null, + val suffixIcon: IllustrationSource? = null, + val prefixIcon: IllustrationSource? = null +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/navigator/MMDeeplinkNavigator.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/navigator/MMDeeplinkNavigator.kt new file mode 100644 index 0000000000..1c357dba38 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/navigator/MMDeeplinkNavigator.kt @@ -0,0 +1,144 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.navigation.navigator + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import com.navi.base.deeplink.DeepLinkManager +import com.navi.base.deeplink.util.DeeplinkConstants +import com.navi.base.model.CtaData +import com.navi.base.utils.orFalse +import com.navi.base.utils.orZero +import com.navi.common.navigation.NavArgs +import com.navi.common.navigation.utils.NavigatorFacade +import com.navi.common.utils.Constants +import com.navi.moneymanager.common.navigation.registry.MMActivityRegistry +import com.navi.moneymanager.common.utils.startEnterAnimation +import com.navi.moneymanager.entry.ui.activity.MMActivity +import com.navi.naviwidgets.utils.FORWARD_SLASH +import timber.log.Timber + +object MMDeeplinkNavigator { + + const val MONEY_MANAGER_ACTIVITY = "MONEY_MANAGER" + private const val MM_NAVIGATION_LINK = "MM_NAVIGATION_LINK" + + private val mmActivityRegistry: MMActivityRegistry by lazy { MMActivityRegistry() } + private val navigationFacade: NavigatorFacade by lazy { NavigatorFacade() } + + fun navigate(activity: Activity?, navArgs: NavArgs) { + if (activity == null) return + + val deepLink = navArgs.ctaData.url + val bundle = navArgs.bundle ?: Bundle() + val navigatorFacade = navigationFacade + val pathUrl = navArgs.ctaData.url?.trim()?.split(FORWARD_SLASH) + val firstIdentifier = pathUrl?.firstOrNull().orEmpty() + + val destinationActivityClass = mmActivityRegistry.findScreenByName(firstIdentifier) + if (destinationActivityClass != null) { + navigateToActivity( + activity = activity, + navArgs = navArgs, + navigatorFacade = navigatorFacade, + destinationActivityClass = destinationActivityClass, + bundle = bundle + ) + } else { + handleDeepLink(activity, navArgs, deepLink, bundle) + } + } + + private fun navigateToActivity( + activity: Activity, + navArgs: NavArgs, + navigatorFacade: NavigatorFacade, + destinationActivityClass: Class, + bundle: Bundle + ) { + val intent = + navigatorFacade + .getIntent( + activity = activity, + navArgs = navArgs, + destinationActivityClass = destinationActivityClass + ) + .apply { + putExtras(bundle) + navArgs.ctaData.parameters?.forEach { putExtra(it.key, it.value) } + addFlagsIfNeeded(navArgs) + } + + startActivity(activity, navArgs, intent) + } + + private fun handleDeepLink( + activity: Activity, + navArgs: NavArgs, + deepLink: String?, + bundle: Bundle + ) { + try { + val splitDeepLink = deepLink?.split(FORWARD_SLASH.toString()) + bundle.putString(MM_NAVIGATION_LINK, deepLink) + val lastPath = splitDeepLink?.lastOrNull() + + val intent = + when (lastPath) { + MONEY_MANAGER_ACTIVITY -> Intent(activity, MMActivity::class.java) + else -> null + }?.apply { + putExtras(bundle) + putExtra(Constants.REDIRECT_STATUS, splitDeepLink?.getOrNull(1)) + putExtra(Constants.SUB_REDIRECT, splitDeepLink?.getOrNull(2)) + navArgs.ctaData.parameters?.forEach { putExtra(it.key, it.value) } + addFlagsIfNeeded(navArgs) + } + + if (intent != null) { + startActivity(activity, navArgs, intent) + activity.startEnterAnimation() + } + } catch (e: Exception) { + Timber.e(e) + DeepLinkManager.getDeepLinkListener() + ?.navigateTo( + activity, + CtaData(url = DeeplinkConstants.HOME), + navArgs.finish, + bundle, + navArgs.needsResult, + navArgs.requestCode, + navArgs.clearTask + ) + } + } + + private fun Intent.addFlagsIfNeeded(navArgs: NavArgs) { + if (navArgs.clearTask.orFalse()) { + addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP + ) + } + } + + private fun startActivity(activity: Activity, navArgs: NavArgs, intent: Intent) { + if (navArgs.needsResult.orFalse()) { + activity.startActivityForResult(intent, navArgs.requestCode.orZero()) + } else { + activity.startActivity(intent) + } + + if (navArgs.finish.orFalse()) { + activity.finish() + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/navigator/MMNavigator.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/navigator/MMNavigator.kt new file mode 100644 index 0000000000..81f2d76670 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/navigator/MMNavigator.kt @@ -0,0 +1,78 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.navigation.navigator + +import android.app.Activity +import android.os.Bundle +import androidx.compose.runtime.Composable +import com.navi.base.model.CtaData +import com.navi.base.utils.orFalse +import com.navi.common.navigation.NavArgs +import com.navi.common.navigation.NavigationAction +import com.navi.common.navigation.NavigationScreenData +import com.navi.common.navigation.navigator.GenericNavigator +import com.navi.common.navigation.navigator.def.ActivityNavigator +import com.navi.common.navigation.navigator.def.ComposableNavigator +import com.navi.common.navigation.utils.NavigatorFacade +import com.navi.moneymanager.common.navigation.registry.MMActivityRegistry +import com.navi.moneymanager.common.navigation.registry.MMComposableRegistry +import com.navi.moneymanager.common.ui.base.MMBaseActivity +import dagger.hilt.android.scopes.ActivityRetainedScoped +import javax.inject.Inject + +/** + * [MMNavigator] facilitates navigation within the **money-manager** module. It extends the + * [GenericNavigator] class to handle navigation for both [Activity] and [Composable]. + */ +@ActivityRetainedScoped +class MMNavigator +@Inject +constructor( + override val activityNavigator: ActivityNavigator, + override val composableNavigator: ComposableNavigator, + override val composableRegistry: MMComposableRegistry, + override val activityRegistry: MMActivityRegistry, + override val navigatorFacade: NavigatorFacade, +) : + GenericNavigator( + activityNavigator = activityNavigator, + composableNavigator = composableNavigator, + composableRegistry = composableRegistry, + activityRegistry = activityRegistry, + navigatorFacade = navigatorFacade + ) { + fun navigateTo( + activity: MMBaseActivity, + ctaData: CtaData, + screenData: NavigationScreenData? = null, + bundle: Bundle? = null, + navigationAction: NavigationAction = NavigationAction.Default + ) { + navigate( + activity = activity, + navHostOwner = activity, + navArgs = ctaData.toNavArgs(bundle, screenData), + navAction = navigationAction, + ) + } + + private fun CtaData.toNavArgs( + bundle: Bundle? = null, + screenData: NavigationScreenData? = null + ): NavArgs { + return NavArgs( + ctaData = this, + finish = this.finish.orFalse(), + bundle = bundle, + needsResult = this.needsResult, + requestCode = this.requestCode, + clearTask = this.clearTask, + screenData = screenData + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/registry/MMActivityRegistry.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/registry/MMActivityRegistry.kt new file mode 100644 index 0000000000..18bd92e9b0 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/registry/MMActivityRegistry.kt @@ -0,0 +1,39 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.navigation.registry + +import android.app.Activity +import com.navi.common.navigation.registry.ActivityRegistry +import com.navi.moneymanager.common.navigation.navigator.MMDeeplinkNavigator.MONEY_MANAGER_ACTIVITY +import com.navi.moneymanager.common.ui.base.MMBaseActivity +import com.navi.moneymanager.entry.ui.activity.MMActivity +import dagger.hilt.android.scopes.ActivityRetainedScoped +import javax.inject.Inject + +/** + * This registry is scoped to [Activity] within the **money-manager** module and is responsible for + * finding [MMBaseActivity] classes based on their screen names. + */ +@ActivityRetainedScoped +class MMActivityRegistry @Inject constructor() : ActivityRegistry { + + override fun findScreenByName(screenName: String): Class? { + return Activities.entries.find { it.screenName == screenName }?.activityClass + } + + /** + * Define the screen names and corresponding [MMBaseActivity] classes within the + * **money-manager** module. + */ + enum class Activities( + val screenName: String, + val activityClass: Class?, + ) { + MONEY_MANAGER(screenName = MONEY_MANAGER_ACTIVITY, activityClass = MMActivity::class.java), + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/registry/MMComposableRegistry.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/registry/MMComposableRegistry.kt new file mode 100644 index 0000000000..73162e7425 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/registry/MMComposableRegistry.kt @@ -0,0 +1,107 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.navigation.registry + +import android.os.Bundle +import androidx.compose.runtime.Composable +import com.navi.common.navigation.NavigationScreenData +import com.navi.common.navigation.registry.ComposableRegistry +import com.navi.moneymanager.destinations.AccountLinkingStatusScreenDestination +import com.navi.moneymanager.destinations.CategoryDetailsScreenDestination +import com.navi.moneymanager.destinations.DashboardScreenDestination +import com.navi.moneymanager.destinations.Destination +import com.navi.moneymanager.destinations.LauncherScreenDestination +import com.navi.moneymanager.destinations.SpendAnalysisScreenDestination +import com.navi.moneymanager.destinations.TransactionDetailsScreenDestination +import com.navi.moneymanager.destinations.TransactionHistoryScreenDestination +import com.navi.moneymanager.destinations.ValuePropScreenDestination +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenNavigationData +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenNavigationData +import com.ramcosta.composedestinations.spec.DestinationSpec +import com.ramcosta.composedestinations.spec.Direction +import dagger.hilt.android.scopes.ActivityRetainedScoped +import javax.inject.Inject + +/** + * This registry is scoped to [Composable] within the **money-manager** module and is responsible + * for finding [Direction] instances based on their destination names. + */ +@ActivityRetainedScoped +class MMComposableRegistry @Inject constructor() : ComposableRegistry { + + override fun findDirectionByName( + destinationName: String, + bundle: Bundle?, + data: NavigationScreenData? + ): Direction? { + return Composables.entries + .find { it.name == destinationName } + ?.directionProvider + ?.invoke(bundle, data) + } + + override fun findDestinationByName(destinationName: String): DestinationSpec<*>? { + return Composables.entries.find { it.name == destinationName }?.destination + } + + /** + * Enum defining the [Direction] names and corresponding [Direction] providers within the + * **money-manager** module. + */ + enum class Composables( + val destination: Destination, + val directionProvider: (bundle: Bundle?, data: NavigationScreenData?) -> Direction, + ) { + LAUNCHER( + destination = LauncherScreenDestination, + directionProvider = { bundle, _ -> LauncherScreenDestination(bundle = bundle) } + ), + DASHBOARD( + destination = DashboardScreenDestination, + directionProvider = { bundle, _ -> DashboardScreenDestination(bundle = bundle) } + ), + VALUE_PROP_SCREEN( + destination = ValuePropScreenDestination, + directionProvider = { bundle, _ -> ValuePropScreenDestination(bundle = bundle) } + ), + TRANSACTION_DETAILS( + destination = TransactionDetailsScreenDestination, + directionProvider = { bundle, _ -> TransactionDetailsScreenDestination(bundle) } + ), + ACCOUNT_LINKING_SUCCESS( + destination = AccountLinkingStatusScreenDestination, + directionProvider = { bundle, _ -> + AccountLinkingStatusScreenDestination(bundle = bundle) + } + ), + SPEND_ANALYSIS( + destination = SpendAnalysisScreenDestination, + directionProvider = { bundle, _ -> SpendAnalysisScreenDestination(bundle = bundle) } + ), + CAT( + destination = SpendAnalysisScreenDestination, + directionProvider = { bundle, _ -> SpendAnalysisScreenDestination(bundle = bundle) } + ), + TRANSACTION_HISTORY( + destination = TransactionHistoryScreenDestination, + directionProvider = { _, data -> + TransactionHistoryScreenDestination( + screenData = data as? TransactionHistoryScreenNavigationData + ) + } + ), + CATEGORY_DETAILS( + destination = CategoryDetailsScreenDestination, + directionProvider = { _, data -> + CategoryDetailsScreenDestination( + data = data as? CategoryDetailsScreenNavigationData + ) + } + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/utils/MMScreen.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/utils/MMScreen.kt new file mode 100644 index 0000000000..4d3c60bff4 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/utils/MMScreen.kt @@ -0,0 +1,27 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.navigation.utils + +import com.navi.moneymanager.common.navigation.registry.MMActivityRegistry +import com.navi.moneymanager.common.navigation.registry.MMComposableRegistry +import com.navi.naviwidgets.utils.FORWARD_SLASH + +val mmActivity: String = MMActivityRegistry.Activities.MONEY_MANAGER.screenName + FORWARD_SLASH + +enum class MMScreen(val screen: String) { + LAUNCHER("$mmActivity${MMComposableRegistry.Composables.LAUNCHER.name}"), + VALUE_PROP_SCREEN("$mmActivity${MMComposableRegistry.Composables.VALUE_PROP_SCREEN.name}"), + DASHBOARD("$mmActivity${MMComposableRegistry.Composables.DASHBOARD.name}"), + TRANSACTION_DETAILS("$mmActivity${MMComposableRegistry.Composables.TRANSACTION_DETAILS.name}"), + ACCOUNT_LINKING_SUCCESS( + "$mmActivity${MMComposableRegistry.Composables.ACCOUNT_LINKING_SUCCESS.name}" + ), + SPEND_ANALYSIS("$mmActivity${MMComposableRegistry.Composables.SPEND_ANALYSIS.name}"), + TRANSACTION_HISTORY("$mmActivity${MMComposableRegistry.Composables.TRANSACTION_HISTORY.name}"), + CATEGORY_DETAILS("$mmActivity${MMComposableRegistry.Composables.CATEGORY_DETAILS.name}") +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/utils/NavigationUtils.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/utils/NavigationUtils.kt new file mode 100644 index 0000000000..6511d3eefe --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/navigation/utils/NavigationUtils.kt @@ -0,0 +1,14 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.navigation.utils + +import com.navi.moneymanager.common.ui.base.MMBaseActivity + +fun navigateToPreviousScreen(activity: MMBaseActivity) { + activity.navController.popBackStack() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/HttpClient.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/HttpClient.kt new file mode 100644 index 0000000000..99e46d6de3 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/HttpClient.kt @@ -0,0 +1,42 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network + +import android.content.Context +import com.chuckerteam.chucker.api.ChuckerCollector +import com.chuckerteam.chucker.api.ChuckerInterceptor +import com.navi.common.model.NetworkInfo +import com.navi.common.network.BaseHttpClient +import com.navi.moneymanager.BuildConfig +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor + +class HttpClient(val networkInfo: NetworkInfo, private val context: Context) : + BaseHttpClient(networkInfo, context) { + val httpClientBuilder: OkHttpClient.Builder + get() { + val okHttpClientBuilder = baseHttpClientBuilder + if (BuildConfig.DEBUG) { + with(okHttpClientBuilder) { + addInterceptor(loggingInterceptor()) + addInterceptor( + ChuckerInterceptor.Builder(context) + .collector(ChuckerCollector(context)) + .maxContentLength(250000L) + .redactHeaders(arrayListOf("X-Click-Stream-Data")) + .alwaysReadResponseBody(false) + .build() + ) + } + } + return okHttpClientBuilder + } + + private fun loggingInterceptor() = + HttpLoggingInterceptor().apply { setLevel(HttpLoggingInterceptor.Level.BODY) } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/di/NetworkModule.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/di/NetworkModule.kt new file mode 100644 index 0000000000..10da2ee3e9 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/di/NetworkModule.kt @@ -0,0 +1,135 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.di + +import android.content.Context +import androidx.room.Room +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.navi.base.AppServiceManager +import com.navi.common.model.ModuleName +import com.navi.common.model.ModuleNameV2 +import com.navi.common.model.NetworkInfo +import com.navi.common.network.converter.EmptyBodyHandlingConverterFactory +import com.navi.common.utils.registerUiTronDeSerializers +import com.navi.common.utils.registerUiTronSerializer +import com.navi.moneymanager.common.dataprovider.data.datastore.DataStoreInfoProvider +import com.navi.moneymanager.common.dataprovider.data.datastore.DbDataStoreProvider +import com.navi.moneymanager.common.db.database.MMDatabase +import com.navi.moneymanager.common.db.database.MMEncryptedDatabase +import com.navi.moneymanager.common.network.HttpClient +import com.navi.moneymanager.common.network.service.RetrofitService +import com.navi.moneymanager.common.network.utils.getPassphrase +import com.navi.moneymanager.common.network.utils.loadSqlCipherLibrary +import com.navi.moneymanager.common.utils.Constants.MM_ENCRYPTED_DB_NAME +import com.navi.moneymanager.common.utils.Constants.MM_UNENCRYPTED_DB_NAME +import com.navi.moneymanager.common.utils.DateTimeConverterAdapter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Qualifier +import javax.inject.Singleton +import net.zetetic.database.sqlcipher.SupportOpenHelperFactory +import org.joda.time.DateTime +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +@Module +@InstallIn(SingletonComponent::class) +object MMNetworkModule { + private const val MONEY_MANAGER_NETWORK_INFO_TIMEOUT = 20L + + @Singleton + @Provides + @MoneyManagerNetworkInfo + fun providesNetworkInfo(): NetworkInfo = + NetworkInfo( + baseUrl = AppServiceManager.baseUrl, + appVersionName = AppServiceManager.appVersionName, + appVersionCode = AppServiceManager.appVersionCode, + moduleName = ModuleName.MONEY_MANAGER, + timeoutInSec = MONEY_MANAGER_NETWORK_INFO_TIMEOUT + ) + + @Singleton + @Provides + fun providesMMHttpClient( + @MoneyManagerNetworkInfo networkInfo: NetworkInfo, + @ApplicationContext context: Context + ) = HttpClient(networkInfo = networkInfo, context = context) + + @Singleton + @Provides + @MoneyManagerGsonDeserializer + fun providesDeserializer(): Gson = + GsonBuilder() + .registerTypeAdapter(DateTime::class.java, DateTimeConverterAdapter()) + .registerUiTronDeSerializers() + .create() + + @Singleton + @Provides + @MoneyManagerGsonSerializer + fun providesSerializer(): Gson = GsonBuilder().registerUiTronSerializer().create() + + @Singleton + @Provides + @MoneyManagerRetrofit + fun providesRetrofitClient( + httpClient: HttpClient, + @MoneyManagerGsonDeserializer deserializer: Gson + ): Retrofit = + Retrofit.Builder() + .baseUrl(httpClient.networkInfo.baseUrl) + .client(httpClient.httpClientBuilder.build()) + .addConverterFactory(EmptyBodyHandlingConverterFactory(ModuleNameV2.MONEY_MANAGER.name)) + .addConverterFactory(GsonConverterFactory.create(deserializer)) + .build() + + @Singleton + @Provides + fun providesMMApiService(@MoneyManagerRetrofit retrofit: Retrofit): RetrofitService = + retrofit.create(RetrofitService::class.java) + + @Singleton + @Provides + fun providesMMAppEncryptedDatabase(@ApplicationContext context: Context): MMEncryptedDatabase { + val passphrase = getPassphrase(context) + loadSqlCipherLibrary(context) + + val sqlCipherOpenerFactory = SupportOpenHelperFactory(passphrase, null, true) + return Room.databaseBuilder(context, MMEncryptedDatabase::class.java, MM_ENCRYPTED_DB_NAME) + .openHelperFactory(sqlCipherOpenerFactory) + .build() + } + + @Singleton + @Provides + fun providesMMUnEncryptedDatabase(@ApplicationContext context: Context): MMDatabase { + return Room.databaseBuilder(context, MMDatabase::class.java, MM_UNENCRYPTED_DB_NAME).build() + } + + @Singleton + @Provides + @RoomDataStoreInfoProvider + fun provideDataStoreInfoProvider( + dbDataStoreProvider: DbDataStoreProvider + ): DataStoreInfoProvider = dbDataStoreProvider +} + +@Qualifier @Retention(AnnotationRetention.BINARY) annotation class MoneyManagerRetrofit + +@Qualifier @Retention(AnnotationRetention.BINARY) annotation class MoneyManagerNetworkInfo + +@Qualifier @Retention(AnnotationRetention.BINARY) annotation class MoneyManagerGsonDeserializer + +@Qualifier @Retention(AnnotationRetention.BINARY) annotation class MoneyManagerGsonSerializer + +@Qualifier @Retention(AnnotationRetention.BINARY) annotation class RoomDataStoreInfoProvider diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/AccountData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/AccountData.kt new file mode 100644 index 0000000000..3db8ddfbc3 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/AccountData.kt @@ -0,0 +1,47 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.model + +data class AccountData( + var linkedAccRef: String? = null, + val accountHolderName: String? = null, + val accountHolderEmail: String? = null, + val accountHolderDob: Long? = null, + val accountHolderLandline: String? = null, + val accountHolderAddress: String? = null, + val ckycCompliance: Boolean? = null, + val accountHolderMobileNumber: String? = null, + val accountHolderPan: String? = null, + val accountType: String? = null, + val accountOwnershipType: String? = null, + val nominee: String? = null, + val maskedAccNumber: String? = null, + val accountOpeningDate: String? = null, + val bankName: String? = null, + val bankCode: String? = null, + val bankIconUrl: String? = null, + val bankBranch: String? = null, + val currentBalance: Double? = null, + val accountDrawingLimit: String? = null, + val accountAgeInDays: Long? = null, + val pendingType: String? = null, + val accountStatus: String? = null, + val micrCode: String? = null, + val balanceDateTime: Long? = null, + val currency: String? = null, + val pendingAmount: Long? = null, + val ifscCode: String? = null, + val accountSubType: String? = null, + val facility: String? = null, + val exchangeRate: String? = null, + val refreshedAt: Long? = null, + val updatedAt: Long? = null, + val lastRefreshAttemptedAt: Long? = null, + val fipId: String? = null, + val consentStatus: String? = null, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/AccountDetailResponse.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/AccountDetailResponse.kt new file mode 100644 index 0000000000..3144f93c87 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/AccountDetailResponse.kt @@ -0,0 +1,10 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.model + +data class AccountDetailResponse(val accountDetails: List? = null) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/ConfigResponse.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/ConfigResponse.kt new file mode 100644 index 0000000000..03ecaccec6 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/ConfigResponse.kt @@ -0,0 +1,41 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.model + +data class MMConfigResponse( + val timestampConfig: TimestampConfig? = null, + val dataSyncPollingConfig: PollingConfig? = null, + val accountLinkingPollingConfig: PollingConfig? = null, + val paginationConfig: PaginationConfig? = null, + val onboardingBottomSheetTimeout: Long? = null, + val categoryIcon: String? = null, + val categories: List? = null, + val revokeConsentUrl: String? = null, +) + +data class CategoryItemData( + val categoryId: String? = null, + val categoryName: String? = null, + val categoryIcon: String? = null, + val isRecommended: Boolean? = null, +) + +data class TimestampConfig( + val currentTimestamp: Long? = null, + val twelveMonthsOldTimestamp: Long? = null, + val currentMonthStartTimestamp: Long? = null, + val threshold: Long? = null +) + +data class PollingConfig( + val maxAttempts: Int?, + val initialDelaySeconds: Long?, + val taskIntervalSeconds: Long? +) + +data class PaginationConfig(val pageSize: Int? = null) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/CustomerDetails.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/CustomerDetails.kt new file mode 100644 index 0000000000..18762f6f07 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/CustomerDetails.kt @@ -0,0 +1,14 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.model + +data class CustomerProfileData(val customerDetails: CustomerDetails? = null) { + data class CustomerDetails(val name: ValueField? = null) { + data class ValueField(val value: String? = null) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/EmptyRequestBody.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/EmptyRequestBody.kt new file mode 100644 index 0000000000..eeae63df3e --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/EmptyRequestBody.kt @@ -0,0 +1,10 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.model + +class EmptyRequestBody {} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/FetchConsentUrlResponse.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/FetchConsentUrlResponse.kt new file mode 100644 index 0000000000..302ab2fc34 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/FetchConsentUrlResponse.kt @@ -0,0 +1,10 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.model + +data class FetchConsentUrlResponse(val url: String? = null) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/FinarkeinDataResponse.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/FinarkeinDataResponse.kt new file mode 100644 index 0000000000..1d101009b7 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/FinarkeinDataResponse.kt @@ -0,0 +1,16 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.model + +data class FinarkeinDataResponse( + val requestId: String? = null, + val vendorRequestId: String? = null, + val vendorAuthToken: String? = null, + val vendorRedirectUrl: String? = null, + val message: String? = null, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/OnboardingStatusResponse.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/OnboardingStatusResponse.kt new file mode 100644 index 0000000000..33f5dd6e96 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/OnboardingStatusResponse.kt @@ -0,0 +1,10 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.model + +data class OnboardingStatusResponse(val isOnboarded: Boolean? = null) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/PollingStatusResponse.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/PollingStatusResponse.kt new file mode 100644 index 0000000000..7637a9247c --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/PollingStatusResponse.kt @@ -0,0 +1,14 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.model + +data class PollingStatusResponse( + val accountDetailsStatus: String? = null, + val currMonthTxnsStatus: String? = null, + val oldMonthTxnsStatus: String? = null +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/PostTransactionCategoryResponse.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/PostTransactionCategoryResponse.kt new file mode 100644 index 0000000000..83b95d7e28 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/PostTransactionCategoryResponse.kt @@ -0,0 +1,12 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.model + +import com.navi.moneymanager.common.model.Transaction + +data class PostTransactionCategoryResponse(val transactions: List) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/RefreshDataResponse.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/RefreshDataResponse.kt new file mode 100644 index 0000000000..d2dd9e0e9a --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/RefreshDataResponse.kt @@ -0,0 +1,13 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.model + +data class RefreshDataResponse( + val requestId: String? = null, + val lastRefreshSuccessTimestamp: Long? = null +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/TransactionData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/TransactionData.kt new file mode 100644 index 0000000000..6b6de75c01 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/TransactionData.kt @@ -0,0 +1,25 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.model + +data class TransactionData( + val txnId: String? = null, + val linkedAccRef: String? = null, + val txnReference: String? = null, + val txnTimestamp: Long? = null, + val txnAmount: Double? = null, + val valueDate: Long? = null, + val narration: String? = null, + val mode: String? = null, + val type: String? = null, + val counterPartyName: String? = null, + val txnDate: String?, + val finalCategory: String? = null, + val updatedAt: Long? = null, + val mobileTimestamp: Long? = null +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/TransactionResponse.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/TransactionResponse.kt new file mode 100644 index 0000000000..1fdfe30ad1 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/model/TransactionResponse.kt @@ -0,0 +1,10 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.model + +data class TransactionResponse(val transactions: List? = null) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/service/RetrofitService.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/service/RetrofitService.kt new file mode 100644 index 0000000000..fa6799b9d4 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/service/RetrofitService.kt @@ -0,0 +1,89 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.service + +import com.navi.common.alchemist.model.AlchemistScreenDefinition +import com.navi.common.network.models.GenericResponse +import com.navi.common.network.retry.annotations.RetryPolicy +import com.navi.moneymanager.common.network.model.AccountDetailResponse +import com.navi.moneymanager.common.network.model.CustomerProfileData +import com.navi.moneymanager.common.network.model.EmptyRequestBody +import com.navi.moneymanager.common.network.model.FetchConsentUrlResponse +import com.navi.moneymanager.common.network.model.FinarkeinDataResponse +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.network.model.OnboardingStatusResponse +import com.navi.moneymanager.common.network.model.PollingStatusResponse +import com.navi.moneymanager.common.network.model.RefreshDataResponse +import com.navi.moneymanager.common.network.model.TransactionResponse +import com.navi.moneymanager.common.utils.Constants.API_RETRY_COUNT +import com.navi.moneymanager.postonboard.monthlysummary.model.PostCategoryTransactionData +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query + +interface RetrofitService { + + @RetryPolicy(retryCount = API_RETRY_COUNT) + @GET("/money-manager/core/transactions") + suspend fun fetchTransactions( + @Query("from") from: Long, + @Query("pageNo") pageNo: Int, + @Query("pageSize") pageSize: Int, + @Query("queryBy") queryBy: String + ): Response> + + @RetryPolicy(retryCount = API_RETRY_COUNT) + @GET("/money-manager/core/user-onboarded") + suspend fun fetchUserOnboardingStatus(): Response> + + @RetryPolicy(retryCount = API_RETRY_COUNT) + @GET("/alchemist/inflate/{screenName}") + suspend fun fetchAlchemistScreen( + @Path("screenName") screenName: String + ): Response> + + @RetryPolicy(retryCount = API_RETRY_COUNT) + @GET("/money-manager/core/config") + suspend fun fetchMMConfigResponse(): Response> + + @RetryPolicy(retryCount = API_RETRY_COUNT) + @POST("/money-manager/core/init-onboarding") + suspend fun fetchFinarkeinSDKInitData( + @Body emptyBody: EmptyRequestBody = EmptyRequestBody() + ): Response> + + @RetryPolicy(retryCount = API_RETRY_COUNT) + @GET("/money-manager/core/accounts") + suspend fun fetchUserAccounts(): Response> + + @PUT("/money-manager/core/transactions/receiver-category") + suspend fun postTransactionCategoryData( + @Body requestBody: PostCategoryTransactionData + ): Response> + + @POST("/money-manager/core/refresh-data") + suspend fun refreshData(): Response> + + @GET("/money-manager/core/status") + suspend fun pollSyncStatus( + @Query("requestId") requestId: String + ): Response> + + @GET("/money-manager/core/consent-url") + suspend fun fetchConsentUrl(): Response> + + @GET("/customer-profile/v1/customer/details") + suspend fun fetchCustomerName( + @Header("X-Target") target: String + ): Response> +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/utils/NetworkUtils.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/utils/NetworkUtils.kt new file mode 100644 index 0000000000..5aa8ccc913 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/network/utils/NetworkUtils.kt @@ -0,0 +1,53 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.network.utils + +import android.content.Context +import com.google.android.play.core.splitinstall.SplitInstallHelper +import com.navi.base.sharedpref.PreferenceManager +import com.navi.common.utils.log +import com.navi.moneymanager.common.utils.Constants.AES_ENCRYPTION +import com.navi.moneymanager.common.utils.Constants.KEY_GENERATOR_KEY_SIZE +import com.navi.moneymanager.common.utils.Constants.MM_ENCRYPTED_DB_NAME +import com.navi.moneymanager.common.utils.Constants.MM_KEY_DB_ENCRYPTION +import com.navi.moneymanager.common.utils.Constants.SQL_CIPHER +import javax.crypto.KeyGenerator + +fun getPassphrase(context: Context): ByteArray { + return PreferenceManager.getSecureStringApp(MM_KEY_DB_ENCRYPTION) + ?.toByteArray(Charsets.ISO_8859_1) ?: generateNewPassphraseAndSave(context) +} + +fun generateNewPassphraseAndSave(context: Context): ByteArray { + context.deleteDatabase(MM_ENCRYPTED_DB_NAME) + val newPassphrase = generatePassphrase() + PreferenceManager.saveStringSecurelyApp( + key = MM_KEY_DB_ENCRYPTION, + value = newPassphrase.toString(Charsets.ISO_8859_1) + ) + return newPassphrase +} + +fun loadSqlCipherLibrary(context: Context) { + try { + SplitInstallHelper.loadLibrary(context, SQL_CIPHER) + } catch (e: Exception) { + e.log() + try { + System.loadLibrary(SQL_CIPHER) + } catch (e: Exception) { + e.log() + } + } +} + +fun generatePassphrase(): ByteArray { + val keyGenerator = KeyGenerator.getInstance(AES_ENCRYPTION) + keyGenerator.init(KEY_GENERATOR_KEY_SIZE) + return keyGenerator.generateKey().encoded +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/base/MMBaseActivity.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/base/MMBaseActivity.kt new file mode 100644 index 0000000000..b2f77df73c --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/base/MMBaseActivity.kt @@ -0,0 +1,35 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.base + +import android.os.Bundle +import com.navi.common.navigation.NavHostControlOwnerManager +import com.navi.common.navigation.NavHostControllerOwner +import com.navi.common.ui.activity.BaseActivity +import com.navi.moneymanager.common.navigation.navigator.MMNavigator +import com.navi.moneymanager.common.utils.startEnterAnimation +import com.navi.moneymanager.common.utils.startExitAnimation +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +abstract class MMBaseActivity : + BaseActivity(), NavHostControllerOwner by NavHostControlOwnerManager() { + + @Inject lateinit var mmNavigator: MMNavigator + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + startEnterAnimation() + } + + override fun finish() { + super.finish() + startExitAnimation() + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/ChipListWithSingleSelection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/ChipListWithSingleSelection.kt new file mode 100644 index 0000000000..fb9d7e3e98 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/ChipListWithSingleSelection.kt @@ -0,0 +1,67 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.navi.moneymanager.common.model.SingleChipSelectionData +import com.navi.moneymanager.common.ui.theme.ChipThemeCodes + +/** + * A composable function that displays a list of chips arranged in a flow layout, allowing for + * single selection. + * + * @param modifier Modifier to apply to the layout. + * @param chips A list of chip data to be displayed. If null, no chips will be shown. + * @param selectedChip The ID of the currently selected chip. If null, no chip is selected. + * @param maxItemsInEachRow The maximum number of chips to display in each row. Defaults to + * [Int.MAX_VALUE], allowing as many as needed. + * @param onChipSelected A callback function that is invoked when a chip is selected. It provides + * the ID of the selected chip. + * + * This function utilizes a [FlowRow] to arrange chips in a responsive grid layout that wraps to the + * next line when necessary. The chips are rendered using the `MMChip` composable, which takes care + * of the visual representation and handles the selected state based on the `selectedChip` + * parameter. + */ +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun ChipListWithSingleSelection( + modifier: Modifier = Modifier, + chips: List? = null, + selectedChip: String?, + maxItemsInEachRow: Int = Int.MAX_VALUE, + onChipSelected: (Pair) -> Unit +) { + FlowRow( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + maxItemsInEachRow = maxItemsInEachRow + ) { + chips?.forEach { chip -> + key(chip.id) { + MMChip( + padding = PaddingValues(start = 12.dp, top = 8.dp, end = 16.dp, bottom = 8.dp), + contentFontSize = 14, + contentLineHeight = 22, + chipData = chip.chipData, + isSelected = selectedChip == chip.id, + chipThemeCode = ChipThemeCodes.CATEGORY_THEME, + onChipSelected = { onChipSelected(Pair(chip.id, chip.chipData.content)) } + ) + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/DataLoadingBottomSheet.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/DataLoadingBottomSheet.kt new file mode 100644 index 0000000000..760daef2fc --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/DataLoadingBottomSheet.kt @@ -0,0 +1,110 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable + +import androidx.compose.foundation.background +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.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.model.DataLoadingBottomSheetData +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor + +@Composable +fun DataLoadingBottomSheetContent( + data: DataLoadingBottomSheetData, + onDismiss: () -> Unit, + trackBottomSheetAppears: () -> Unit, + trackBottomSheetDisappears: () -> Unit +) { + LaunchedEffect(Unit) { trackBottomSheetAppears() } + + DisposableEffect(Unit) { onDispose { trackBottomSheetDisappears() } } + + Column( + modifier = + Modifier.fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 32.dp), + horizontalAlignment = Alignment.Start + ) { + Box(modifier = Modifier.fillMaxWidth()) { + if (data.loadingIcon != null) { + Illustration( + modifier = Modifier.size(32.dp), + illustrationType = IllustrationType.Image(data.loadingIcon) + ) + } + if (data.loadingLottie != null) { + Illustration( + modifier = Modifier.size(24.dp), + illustrationType = IllustrationType.Lottie(data.loadingLottie) + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + MMText( + text = data.titleText, + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + letterSpacing = 0.sp, + lineHeight = 24.sp + ) + + Spacer(modifier = Modifier.height(8.dp)) + + MMText( + text = data.subTitleText, + color = MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + letterSpacing = 0.sp, + lineHeight = 22.sp + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Box( + modifier = + Modifier.fillMaxWidth() + .onClickWithDebounce { onDismiss() } + .background(color = MMColor.ctaPrimary, shape = RoundedCornerShape(4.dp)), + contentAlignment = Alignment.Center + ) { + MMText( + text = data.ctaText, + modifier = Modifier.padding(vertical = 12.dp), + color = MMColor.white, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + letterSpacing = 0.sp, + textAlign = TextAlign.Center, + lineHeight = 22.sp + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/MMChip.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/MMChip.kt new file mode 100644 index 0000000000..21df60e465 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/MMChip.kt @@ -0,0 +1,110 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.utils.onClickWithDebounce +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.model.ChipData +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.ChipThemeCodes +import com.navi.moneymanager.common.ui.theme.ChipsStateThemeProvider +import com.navi.moneymanager.common.ui.theme.MMChipTheme +import com.navi.moneymanager.common.ui.theme.color.MMColor + +/** + * A mm chip with an illustration and a selected/unselected state. + * * Illustration is used here to handle the loading states as well. + * + * @param chipData The name to display in the chip. + * @param isSelected Whether a chip is currently selected. + * @param chipThemeCode It determines the chip's theme based on its selected and unselected states. + * @param onChipSelected Callback invoked to select the chip + */ +@Composable +fun MMChip( + padding: PaddingValues = PaddingValues(start = 8.dp, top = 8.dp, end = 8.dp, bottom = 8.dp), + contentFontSize: Int = 12, + contentLineHeight: Int = 16, + chipData: ChipData, + chipThemeCode: ChipThemeCodes = ChipThemeCodes.DEFAULT_THEME, + isSelected: Boolean, + onChipSelected: () -> Unit = {} +) { + val chipThemeProvider = remember { ChipsStateThemeProvider() } + val currentTheme = + remember(key1 = isSelected) { chipThemeProvider.getChipTheme(chipThemeCode, isSelected) } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + modifier = + Modifier.clip(RoundedCornerShape(50)) + .background(currentTheme.backgroundColor) + .shadow( + elevation = if (isSelected) 16.dp else 0.dp, + spotColor = MMColor.shadowSpotColor, + ambientColor = MMColor.ambientColor, + ) + .onClickWithDebounce( + interactionSource = remember { MutableInteractionSource() }, + ) { + if (isSelected.not()) onChipSelected() + } + .then( + if (currentTheme.borderColor != Color.Transparent) { + Modifier.border(1.dp, currentTheme.borderColor, RoundedCornerShape(50)) + } else { + Modifier + } + ) + .padding(padding) + ) { + Illustration( + illustrationType = getIllustrationType(chipData, currentTheme), + modifier = remember { Modifier.size(chipData.illustration.size.dp) } + ) + MMText( + text = chipData.content, + color = currentTheme.contentColor, + fontSize = contentFontSize.sp, + fontWeight = currentTheme.fontWeight, + lineHeight = contentLineHeight.sp + ) + } +} + +private fun getIllustrationType(chipData: ChipData, currentTheme: MMChipTheme): IllustrationType { + return when (chipData.illustration.illustrationType) { + is IllustrationType.Image -> + chipData.illustration.illustrationType.copy( + properties = + chipData.illustration.illustrationType.properties.copy( + colorFilter = currentTheme.colorFilter + ) + ) + else -> chipData.illustration.illustrationType + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/MMProgressBar.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/MMProgressBar.kt new file mode 100644 index 0000000000..bec3811e9d --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/MMProgressBar.kt @@ -0,0 +1,49 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.times + +@Composable +fun MMCategoryProgressBar(progress: Float, color: Color, totalWidth: Dp = 200.dp) { + var animationTriggered by remember { mutableStateOf(false) } + val animatedProgress by + animateFloatAsState( + targetValue = if (animationTriggered) progress else 0f, + animationSpec = tween(durationMillis = 1000), + label = "" + ) + + LaunchedEffect(true) { animationTriggered = true } + + Box( + modifier = + Modifier.height(6.dp) + .width(animatedProgress * totalWidth) + .clip(RoundedCornerShape(5.dp)) + .background(color) + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/MMRolodex.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/MMRolodex.kt new file mode 100644 index 0000000000..293309bfa6 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/MMRolodex.kt @@ -0,0 +1,43 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +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 kotlinx.coroutines.delay + +@Composable +fun MMRolodex( + viewsList: List<@Composable () -> Unit>, + modifier: Modifier = Modifier, + durationBetweenAnimations: Int = 2000 +) { + var currentIndex by remember { mutableStateOf(0) } + + LaunchedEffect(currentIndex) { + delay(durationBetweenAnimations.toLong()) + currentIndex = (currentIndex + 1) % viewsList.size + } + + AnimatedContent( + targetState = currentIndex, + transitionSpec = { slideInVertically { it } togetherWith slideOutVertically { -it } }, + modifier = modifier + ) { + viewsList[it]() + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/MMSearchAndFilter.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/MMSearchAndFilter.kt new file mode 100644 index 0000000000..c71cb2f905 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/MMSearchAndFilter.kt @@ -0,0 +1,104 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +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.unit.dp +import com.navi.base.utils.EMPTY +import com.navi.common.utils.onClickWithDebounce +import com.navi.moneymanager.common.analytics.TransactionHistoryEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.COMMON_CROSS_BLACK +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.COMMON_FILTER +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.COMMON_SEARCH +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.base.MMTextField +import com.navi.moneymanager.common.ui.theme.color.MMColor + +@Composable +fun MMSearchAndFilter( + searchQuery: String, + onQueryChange: (String) -> Unit, + placeholderText: String, + isFilterApplied: Boolean, + onFilterClick: () -> Unit +) { + Row( + modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + MMTextField( + modifier = Modifier.fillMaxWidth().weight(1f), + value = searchQuery, + onValueChange = { + TransactionHistoryEventTrackerImpl.onTransactionHistorySearchQueryChanged(it) + onQueryChange(it) + }, + placeholderText = placeholderText, + prefixIllustration = + IllustrationType.Image( + IllustrationSource.Remote( + url = COMMON_SEARCH, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ), + suffixIllustration = + IllustrationType.Image( + IllustrationSource.Remote( + url = COMMON_CROSS_BLACK, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ), + onSuffixIllustrationClick = { onQueryChange(EMPTY) } + ) + + Spacer(modifier = Modifier.width(width = 8.dp)) + + FilterButton(onFilterClick = onFilterClick, isFilterApplied = isFilterApplied) + } +} + +@Composable +private fun FilterButton(onFilterClick: () -> Unit, isFilterApplied: Boolean) { + Box( + modifier = + Modifier.size(40.dp) + .background( + color = if (isFilterApplied) MMColor.ctaTertiary else MMColor.bgAltColor, + shape = RoundedCornerShape(50.dp), + ) + .clip(RoundedCornerShape(50.dp)) + .onClickWithDebounce { onFilterClick() }, + contentAlignment = Alignment.Center, + ) { + Illustration( + illustrationType = + IllustrationType.Image( + IllustrationSource.Remote( + url = COMMON_FILTER, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ), + modifier = Modifier.size(20.dp) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/MMTopBar.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/MMTopBar.kt new file mode 100644 index 0000000000..6e6448cbbe --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/MMTopBar.kt @@ -0,0 +1,87 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +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.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.R +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.ui.composable.base.MMImage +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MMTopBar( + modifier: Modifier = Modifier, + backgroundColor: Color = MMColor.white, + title: String, + titleColor: Color = MMColor.textPrimary, + titleMaxLines: Int = 1, + navigationIcon: Int = R.drawable.ic_arrow_left_black_v2, + actionIconId: Int = -1, + actionIconText: String? = null, + onActionClick: (() -> Unit)? = null, + onNavigationIconClick: () -> Unit, +) { + CenterAlignedTopAppBar( + title = { + MMText( + text = title, + modifier = Modifier.padding(horizontal = 40.dp), + color = titleColor, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + maxLines = titleMaxLines + ) + }, + navigationIcon = { + IconButton(onClick = { onNavigationIconClick.invoke() }) { + MMImage(iconCode = navigationIcon) + } + }, + actions = { + if (actionIconId > 0) { + MMImage( + iconCode = actionIconId, + modifier = + Modifier.padding(end = 16.dp).onClickWithDebounce { + onActionClick?.invoke() + } + ) + } else if (actionIconText.isNullOrEmpty().not()) { + MMText( + text = actionIconText.orEmpty(), + modifier = + Modifier.padding(horizontal = 13.dp, vertical = 16.dp) + .onClickWithDebounce(indication = null) { onActionClick?.invoke() } + .padding(horizontal = 3.dp), + color = titleColor, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + textAlign = TextAlign.Center + ) + } + }, + modifier = modifier, + colors = TopAppBarDefaults.topAppBarColors(containerColor = backgroundColor) + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/RadioButtonSelectionRow.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/RadioButtonSelectionRow.kt new file mode 100644 index 0000000000..e6609885a7 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/RadioButtonSelectionRow.kt @@ -0,0 +1,108 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor + +@Composable +fun RadioButtonSelectionRow( + title: String, + isSelected: Boolean, + onClick: () -> Unit, + startIcon: IllustrationType? = null, +) { + Column( + modifier = + Modifier.fillMaxWidth() + .clickable( + indication = remember { ripple() }, + interactionSource = remember { MutableInteractionSource() }, + onClick = onClick, + enabled = !isSelected, + ) + .padding(horizontal = 16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(vertical = 20.dp), + ) { + startIcon?.let { + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier.padding(end = 12.dp) + .size(40.dp) + .background(color = MMColor.ctaSecondary, shape = CircleShape) + .clip(CircleShape) + ) { + Illustration(illustrationType = startIcon, modifier = Modifier.size(32.dp)) + } + } + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + MMText( + text = title, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = + if (isSelected) FontWeightEnum.NAVI_BODY_DEMI_BOLD + else FontWeightEnum.NAVI_BODY_REGULAR, + letterSpacing = 0.sp, + lineHeight = 24.sp, + ) + + Box( + modifier = + Modifier.size(24.dp) + .border( + width = if (isSelected) 2.dp else 1.dp, + color = if (isSelected) MMColor.ctaPrimary else MMColor.borderAlt, + shape = CircleShape + ) + .padding(2.dp), + contentAlignment = Alignment.Center + ) { + if (isSelected) { + Box( + modifier = + Modifier.size(12.dp) + .background(MMColor.ctaPrimary, shape = CircleShape) + ) + } + } + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/ScreenInit.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/ScreenInit.kt new file mode 100644 index 0000000000..d49fa91140 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/ScreenInit.kt @@ -0,0 +1,22 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import com.navi.moneymanager.entry.ui.activity.MMActivity + +@Composable +fun ScreenInit(screenName: String, activity: MMActivity) { + LaunchedEffect(Unit) { + activity + .getSharedVM() + .firebaseEventFacade + .setCurrentScreen(activity, screenName = screenName) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/TotalSpendSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/TotalSpendSection.kt new file mode 100644 index 0000000000..08c91c8a96 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/TotalSpendSection.kt @@ -0,0 +1,116 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable + +import androidx.compose.foundation.interaction.MutableInteractionSource +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.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.composable.sectionHeaders.SectionHeader +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.spendanalysis.model.TotalSpendSectionData + +@Composable +fun TotalSpendSectionUI( + data: TotalSpendSectionData, + onMonthChangeClick: (String) -> Unit, + onBankSelectionRequest: (Set) -> Unit, +) { + Column( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Illustration( + illustrationType = IllustrationType.Image(data.iconUrl), + modifier = Modifier.size(48.dp) + ) + Spacer(Modifier.height(8.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth() + ) { + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f).padding(end = 4.dp) + ) { + MMText( + text = data.title, + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 18.sp + ) + } + VerticalDivider( + modifier = Modifier.height(10.dp).width(1.dp), + color = MMColor.textTertiary + ) + Row( + modifier = + Modifier.weight(1f) + .padding(start = 4.dp) + .onClickWithDebounce( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onMonthChangeClick(data.selectedMonth) } + ), + verticalAlignment = Alignment.CenterVertically + ) { + MMText( + text = data.selectedMonth.uppercase(), + color = MMColor.textPrimary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 18.sp + ) + Box(Modifier.padding(bottom = 2.dp, end = 4.dp)) { + Illustration( + illustrationType = IllustrationType.Image(data.actionIcon), + modifier = Modifier.size(16.dp) + ) + } + } + } + Spacer(Modifier.height(8.dp)) + MMText( + text = data.amount, + color = MMColor.textPrimary, + fontSize = 24.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp + ) + Spacer(Modifier.height(32.dp)) + data.spendingTrendSectionData?.let { + SectionHeader( + headerModel = it, + onAction = { onBankSelectionRequest(data.selectedBankReferenceIds) } + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/BarAveragePill.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/BarAveragePill.kt new file mode 100644 index 0000000000..330170a2ff --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/BarAveragePill.kt @@ -0,0 +1,76 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.barGraph + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +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.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.model.YAxisAverageData +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor + +@Composable +fun BarAveragePill( + modifier: Modifier, + data: YAxisAverageData, + onAverageInfoClick: () -> Unit, + isTotalSyncCompleted: Boolean, + icon: IllustrationSource, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = + modifier + .background(color = MMColor.bgAltColor, shape = RoundedCornerShape(50)) + .border( + width = 1.dp, + color = MMColor.transactionDividerColor, + shape = RoundedCornerShape(50) + ) + .clip(shape = RoundedCornerShape(50)) + .onClickWithDebounce { onAverageInfoClick() } + .padding(start = 8.dp, end = 4.dp, top = 4.dp, bottom = 4.dp) + ) { + MMText( + text = data.title, + color = if (isTotalSyncCompleted) MMColor.textPrimary else MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + lineHeight = 16.sp + ) + data.formattedValue?.let { + MMText( + text = it, + color = MMColor.textPrimary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + lineHeight = 16.sp + ) + } + Illustration( + illustrationType = IllustrationType.Image(icon), + modifier = Modifier.size(16.dp) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/BarGraphUtils.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/BarGraphUtils.kt new file mode 100644 index 0000000000..e0468f4c37 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/BarGraphUtils.kt @@ -0,0 +1,128 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.barGraph + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.GraphicsLayerScope +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.navi.moneymanager.common.model.BarGraphElement +import com.navi.moneymanager.common.ui.composable.barGraph.BarGraphLayoutProperties.MAX_BAR_HEIGHT +import com.navi.moneymanager.common.ui.composable.barGraph.BarGraphLayoutProperties.MIN_BAR_HEIGHT +import com.navi.moneymanager.common.ui.theme.color.MMColor + +internal object BarGraphLayoutProperties { + val MIN_BAR_HEIGHT = 8.dp + val MAX_BAR_HEIGHT = 136.dp + val BAR_WIDTH = 24.dp + val X_AXIS_LABEL_HEIGHT = 24.dp + val HORIZONTAL_PADDING = 16.dp + val TOTAL_GRAPH_HEIGHT = 208.dp + val LINE_PADDING_FOR_TOOL_TIP = 4.dp +} + +internal fun getBarHeight(density: Density, spend: Double, maxSpend: Double): Float { + val maxHeight = with(density) { MAX_BAR_HEIGHT.toPx() } + val minHeight = with(density) { MIN_BAR_HEIGHT.toPx() } + val fraction = if (maxSpend.toFloat() == 0f) 0f else spend.toFloat() / maxSpend.toFloat() + return minHeight + fraction * (maxHeight - minHeight) +} + +internal fun GraphicsLayerScope.calculateAveragePillTranslationY( + density: Density, + yAxisValue: Double, + maxValue: Double, + containerHeight: Float, + xAxisLabelHeight: Dp, + yOffset: Float +): Float { + val translationYOffset = (-16).dp.toPx() + val barHeight = getBarHeight(density, yAxisValue, maxValue) + return translationYOffset - xAxisLabelHeight.toPx() - barHeight + containerHeight / 2 - yOffset +} + +internal fun GraphicsLayerScope.calculateAveragePillTranslationX( + containerWidth: Float, + barWidth: Dp, + spaceBetweenBars: Float +): Float { + val translationXOffset = 16.dp.toPx() * 2 + return translationXOffset + barWidth.toPx() * 2.5f + spaceBetweenBars * 2 - containerWidth / 2 +} + +internal fun DrawScope.drawAverageIndicator( + canvasHeight: Float, + averageSpendHeight: Float, + canvasWidth: Float, + barWidth: Dp, + spaceBetweenBars: Float, + yOffset: Float +) { + val startX = 8.dp.toPx() + val endX = canvasWidth - startX - barWidth.toPx() - spaceBetweenBars + val lineY = canvasHeight - averageSpendHeight - yOffset + + drawLine( + start = Offset(startX, lineY), + end = Offset(endX, lineY), + color = MMColor.gray, + strokeWidth = 1.dp.toPx(), + pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f) + ) +} + +internal fun PointerInputScope.handleSelectedBar( + offset: Offset, + barWidth: Float, + data: List, + xAxisLabelHeight: Float, + horizontalPadding: Float, + onBarSelected: (Int, Int) -> Unit +) { + data.forEachIndexed { i, spendData -> + // Adjusted left calculation to include edge padding (if used in the graph drawing) + val spaceBetweenBars = + (size.width - (2 * horizontalPadding) - (barWidth * data.size)) / (data.size - 1) + val left = (spaceBetweenBars * i) + (barWidth * i) + horizontalPadding + + // Clickable area is max possible height of bar graph elements + val clickableBarHeight = MAX_BAR_HEIGHT.toPx() + val top = size.height - clickableBarHeight - xAxisLabelHeight + val right = left + barWidth.times(1.08f) + val bottom = size.height.toFloat() + + if (offset.x in left..right && offset.y in top..bottom) { + onBarSelected(data.size - i - 1, spendData.barId) + return@forEachIndexed // Stop looping once a bar is selected + } + } +} + +internal fun DrawScope.selectedBarIndicatorLine( + left: Float, + barWidth: Dp, + top: Float, + linePadding: Dp, + canvasHeight: Float, + maxValue: Double +) { + drawLine( + color = MMColor.borderColor, + strokeWidth = 1.dp.toPx(), + start = Offset(left + barWidth.toPx() / 2, top - linePadding.toPx()), + end = + Offset( + left + barWidth.toPx() / 2, + canvasHeight - getBarHeight(this, maxValue, maxValue) - linePadding.toPx() + ) + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/BarTooltip.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/BarTooltip.kt new file mode 100644 index 0000000000..e1b18750c9 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/BarTooltip.kt @@ -0,0 +1,119 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.barGraph + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextMeasurer +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.design.font.getFontWeight +import com.navi.design.font.naviFontFamily +import com.navi.moneymanager.common.model.BarGraphElement +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.uitron.model.ui.PeakDirection +import com.navi.uitron.utils.shapes.ToolTipShape + +fun DrawScope.drawBarTooltip( + barData: BarGraphElement, + textMeasurer: TextMeasurer, + left: Float, + barWidthPx: Float, + canvasHeight: Float, + maxValue: Double, + linePadding: Float +) { + val textStyle = + TextStyle( + fontSize = 12.sp, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + fontFamily = naviFontFamily, + lineHeight = 18.sp + ) + + // Measure the text size + val textLayoutResult = + textMeasurer.measure( + text = AnnotatedString(barData.formattedYAxisValue), + style = textStyle, + constraints = Constraints(maxWidth = Int.MAX_VALUE) + ) + + val textWidth = textLayoutResult.size.width.toFloat() + val textHeight = textLayoutResult.size.height.toFloat() + + // Connector coordinate + val connectorCoordinate = + Offset(left + (barWidthPx / 2), canvasHeight - getBarHeight(this, maxValue, maxValue)) + + // Label padding + val labelHorizontalPadding = 6.dp.toPx() + val labelVerticalPadding = 4.dp.toPx() + val toolTipHeight = 6.dp.toPx() + + // Calculate label size + val labelSize = + Size( + textWidth + (2 * labelHorizontalPadding), + textHeight + (2 * labelVerticalPadding) + toolTipHeight + ) + + // Calculate the peak offset to handle overflow + val peakOffset = + when { + connectorCoordinate.x - (labelSize.width / 2) < 0 -> { + labelSize.width / 2 - connectorCoordinate.x + } + connectorCoordinate.x + (labelSize.width / 2) > size.width -> { + size.width - (connectorCoordinate.x + (labelSize.width / 2)) + } + else -> { + 0f + } + } + + // Create the tooltip shape + val toolTipShape = + ToolTipShape( + peakHeightDp = 6.dp, + peakDirection = PeakDirection.BOTTOM, + peakAspectRatio = 2f, + defaultCornerRadiusDp = 4.dp, + peakRadiusDp = 0.dp, + offset = labelSize.width.toDp() / 2 - peakOffset.toDp() + ) + + // Draw the tooltip background + drawContext.canvas.save() + drawContext.canvas.translate( + connectorCoordinate.x - (labelSize.width / 2) + peakOffset, + connectorCoordinate.y - (linePadding.times(2)) - labelSize.height + ) + drawOutline( + toolTipShape.createOutline( + size = labelSize, + layoutDirection = LayoutDirection.Ltr, + density = this + ), + color = MMColor.ctaPrimary + ) + + // Draw the text inside the tooltip + drawContext.canvas.translate(labelHorizontalPadding, labelVerticalPadding) + drawText(textLayoutResult = textLayoutResult, color = MMColor.white) + + drawContext.canvas.restore() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/BarXAxis.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/BarXAxis.kt new file mode 100644 index 0000000000..cbf48c53b4 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/BarXAxis.kt @@ -0,0 +1,62 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.barGraph + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextMeasurer +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.design.font.getFontWeight +import com.navi.design.font.naviFontFamily +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.naviwidgets.extensions.getLetterSpacing + +fun DrawScope.drawXAxisLabel( + isSelected: Boolean, + xAxisLabel: String, + left: Float, + barWidthPx: Float, + canvasHeight: Float, + textMeasurer: TextMeasurer, +) { + val labelTopPadding = 8.dp.toPx() + val labelStyle = + TextStyle( + fontSize = 10.sp, + fontFamily = naviFontFamily, + color = MMColor.textPrimary, + letterSpacing = getLetterSpacing(1.5f), + fontWeight = + if (isSelected) getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD) + else getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR) + ) + val labelLayoutResult = + textMeasurer.measure(text = AnnotatedString(xAxisLabel), style = labelStyle) + drawText( + textLayoutResult = labelLayoutResult, + topLeft = + Offset( + left + (barWidthPx - labelLayoutResult.size.width) / 2 + 0.5.dp.toPx(), + canvasHeight + labelTopPadding + ) + ) +} + +fun DrawScope.drawXAxis(canvasWidth: Float, canvasHeight: Float) { + drawLine( + color = MMColor.transactionDividerColor, + start = Offset(0f, canvasHeight), + end = Offset(canvasWidth, canvasHeight), + strokeWidth = 0.5.dp.toPx() + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/MMBarGraph.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/MMBarGraph.kt new file mode 100644 index 0000000000..897589c022 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/MMBarGraph.kt @@ -0,0 +1,261 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.barGraph + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +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.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.unit.dp +import com.navi.base.utils.orFalse +import com.navi.base.utils.orZero +import com.navi.moneymanager.common.analytics.CategoryDetailsEventTrackerImpl +import com.navi.moneymanager.common.analytics.SpendAnalysisEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.model.BarGraphData +import com.navi.moneymanager.common.model.BarGraphPageData +import com.navi.moneymanager.common.model.SelectedMonth +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.barGraph.BarGraphLayoutProperties.BAR_WIDTH +import com.navi.moneymanager.common.ui.composable.barGraph.BarGraphLayoutProperties.HORIZONTAL_PADDING +import com.navi.moneymanager.common.ui.composable.barGraph.BarGraphLayoutProperties.LINE_PADDING_FOR_TOOL_TIP +import com.navi.moneymanager.common.ui.composable.barGraph.BarGraphLayoutProperties.TOTAL_GRAPH_HEIGHT +import com.navi.moneymanager.common.ui.composable.barGraph.BarGraphLayoutProperties.X_AXIS_LABEL_HEIGHT +import com.navi.naviwidgets.composewidget.widgets.screenWidth +import com.navi.uitron.utils.toPx +import kotlinx.coroutines.launch + +@Composable +fun MMBarGraph( + screenName: String, + barGraphData: BarGraphData, + onAverageInfoClick: (String?) -> Unit, + onBarGraphElementClicked: (SelectedMonth) -> Unit +) { + + LaunchedEffect(Unit) { + when (screenName) { + MMScreen.SPEND_ANALYSIS.screen -> { + SpendAnalysisEventTrackerImpl.onSpendAnalysisScreenGraphViewed() + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl.onCategoryDetailsGraphView() + } + } + } + + var selectedBar by + remember(barGraphData.selectedBar) { mutableStateOf(barGraphData.selectedBar) } + val selectedPage by + remember(selectedBar) { + derivedStateOf { selectedBar.orZero().div(barGraphData.elementsPerPage) } + } + barGraphData.pagedElementList.getOrNull(selectedPage)?.let { pageData -> + pageData.elementList.isNotEmpty().let { + BarGraphUI( + selectedBar = selectedBar, + data = pageData, + onBarSelected = { monthIndex, clickedBar -> + when (screenName) { + MMScreen.SPEND_ANALYSIS.screen -> { + SpendAnalysisEventTrackerImpl.onSpendAnalysisScreenGraphBarClicked( + monthIndex + ) + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl.onCategoryDetailsGraphBarClicked( + monthIndex + ) + } + } + if (barGraphData.isTotalSyncCompleted.orFalse()) { + selectedBar = clickedBar + pageData.elementList + .firstOrNull { element -> element.barId == clickedBar } + ?.let { onBarGraphElementClicked(it.selectedMonth) } + } + }, + onAverageInfoClick = { onAverageInfoClick(pageData.averageData?.formattedValue) }, + isTotalSyncCompleted = barGraphData.isTotalSyncCompleted.orFalse(), + averageIcon = barGraphData.iconUrl + ) + } + } +} + +@Composable +fun BarGraphUI( + selectedBar: Int? = null, + data: BarGraphPageData, + onBarSelected: (Int, Int) -> Unit, + onAverageInfoClick: () -> Unit, + isTotalSyncCompleted: Boolean, + averageIcon: IllustrationSource +) { + val barWidth = BAR_WIDTH + + val xAxisLabelHeight = X_AXIS_LABEL_HEIGHT + + val density = LocalDensity.current + + val textMeasurer = rememberTextMeasurer() + + val averageValue = remember(data) { data.averageData?.value.orZero() } + + var animationPlayed by rememberSaveable(data) { mutableStateOf(false) } + + val animatedHeights = + remember(data.elementList.size) { + MutableList(data.elementList.size) { Animatable(with(density) { 8.dp.toPx() }) } + } + + LaunchedEffect(data) { + if (!animationPlayed) { + data.elementList.forEachIndexed { index, graphElement -> + val targetHeight = getBarHeight(density, graphElement.yAxisValue, data.maxValue) + launch { + animatedHeights[index].animateTo(targetHeight, animationSpec = tween(600, 200)) + } + } + animationPlayed = true + } else { + data.elementList.forEachIndexed { index, graphElement -> + val targetHeight = getBarHeight(density, graphElement.yAxisValue, data.maxValue) + launch { animatedHeights[index].snapTo(targetHeight) } + } + } + } + + Box { + val horizontalPadding = HORIZONTAL_PADDING.toPx() + val canvasWidth = screenWidth.toPx() - (2 * horizontalPadding) + val barAreaWidth = canvasWidth - (2 * horizontalPadding) + val canvasHeight = (TOTAL_GRAPH_HEIGHT - xAxisLabelHeight).toPx() + val spaceBetweenBars = + (barAreaWidth - (barWidth.toPx() * data.elementList.size)) / (data.elementList.size - 1) + + Canvas( + modifier = + Modifier.fillMaxWidth().height(240.dp).padding(16.dp).pointerInput(data) { + detectTapGestures { offset -> + handleSelectedBar( + offset = offset, + barWidth = barWidth.toPx(), + data = data.elementList, + horizontalPadding = 16.dp.toPx(), + xAxisLabelHeight = xAxisLabelHeight.toPx(), + onBarSelected = onBarSelected + ) + } + } + ) { + data.elementList.forEachIndexed { i, graphElement -> + val barHeight = animatedHeights[i].value // Use the animated height + val left = (spaceBetweenBars * i) + (barWidth.toPx() * i) + horizontalPadding + val top = canvasHeight - barHeight + + drawBar( + topLeft = Offset(left, top), + barWidth = barWidth.toPx(), + barHeight = barHeight, + cornerRadius = 2.dp.toPx(), + barColor = + if (selectedBar == graphElement.barId) Color(0xffA8A8A8) + else Color(0xffEBEBEB), + barBorderColor = Color(0xffA8A8A8) + ) + + drawXAxisLabel( + textMeasurer = textMeasurer, + xAxisLabel = graphElement.xAxisValue, + left = left, + barWidthPx = barWidth.toPx(), + canvasHeight = canvasHeight, + isSelected = selectedBar == graphElement.barId + ) + + if (selectedBar == graphElement.barId) { + selectedBarIndicatorLine( + left, + barWidth, + top, + LINE_PADDING_FOR_TOOL_TIP, + canvasHeight, + data.maxValue + ) + drawBarTooltip( + barData = graphElement, + textMeasurer = textMeasurer, + left = left, + barWidthPx = barWidth.toPx(), + canvasHeight = canvasHeight, + maxValue = data.maxValue, + linePadding = LINE_PADDING_FOR_TOOL_TIP.toPx() + ) + } + + drawXAxis(canvasWidth, canvasHeight) + + data.averageData?.let { + val averageValueHeight = getBarHeight(this, averageValue, data.maxValue) + drawAverageIndicator( + canvasHeight, + averageValueHeight, + canvasWidth, + barWidth, + spaceBetweenBars, + if (isTotalSyncCompleted) 0f else 100f + ) + } + } + } + + data.averageData?.let { + BarAveragePill( + modifier = + Modifier.align(Alignment.BottomStart).graphicsLayer { + translationY = + calculateAveragePillTranslationY( + density, + it.value, + data.maxValue, + size.height, + xAxisLabelHeight, + if (isTotalSyncCompleted) 0f else 100f + ) + translationX = + calculateAveragePillTranslationX(size.width, barWidth, spaceBetweenBars) + }, + data = data.averageData, + onAverageInfoClick = onAverageInfoClick, + isTotalSyncCompleted = isTotalSyncCompleted, + icon = averageIcon, + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/RoundedCornerBar.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/RoundedCornerBar.kt new file mode 100644 index 0000000000..1ded1a2284 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/barGraph/RoundedCornerBar.kt @@ -0,0 +1,108 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.barGraph + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.unit.dp + +fun DrawScope.drawBar( + topLeft: Offset, + barWidth: Float, + barHeight: Float, + cornerRadius: Float = 4.dp.toPx(), + barColor: Color = Color.Blue, + barBorderColor: Color = Color.Black +) { + val barPath = + Path().apply { + moveTo(topLeft.x, topLeft.y + cornerRadius) + arcTo( + rect = + Rect( + center = Offset(topLeft.x + cornerRadius, topLeft.y + cornerRadius), + radius = cornerRadius + ), + startAngleDegrees = 180f, + sweepAngleDegrees = 90f, + forceMoveTo = false + ) + lineTo(topLeft.x + barWidth - cornerRadius, topLeft.y) + arcTo( + rect = + Rect( + center = + Offset(topLeft.x + barWidth - cornerRadius, topLeft.y + cornerRadius), + radius = cornerRadius + ), + startAngleDegrees = 270f, + sweepAngleDegrees = 90f, + forceMoveTo = false + ) + lineTo(topLeft.x + barWidth, topLeft.y + barHeight) + lineTo(topLeft.x, topLeft.y + barHeight) + close() + } + + val borderWidth = 0.5.dp.toPx() + val barBack = + Path().apply { + // Start at the bottom-left, offset by borderWidth + moveTo(topLeft.x - borderWidth, topLeft.y + barHeight) + + // Left edge to top-left corner + lineTo(topLeft.x - borderWidth, topLeft.y + cornerRadius) + + // Arc for the top-left corner + arcTo( + rect = + Rect( + center = Offset(topLeft.x + cornerRadius, topLeft.y + cornerRadius), + radius = cornerRadius + borderWidth + ), + startAngleDegrees = 180f, + sweepAngleDegrees = 90f, + forceMoveTo = false + ) + + // Top edge to top-right corner + lineTo( + topLeft.x + barWidth.times(1.08f) - cornerRadius + borderWidth, + topLeft.y - borderWidth + ) + + // Arc for the top-right corner + arcTo( + rect = + Rect( + center = + Offset( + topLeft.x + barWidth.times(1.08f) - cornerRadius, + topLeft.y + cornerRadius + ), + radius = cornerRadius + borderWidth + ), + startAngleDegrees = 270f, + sweepAngleDegrees = 90f, + forceMoveTo = false + ) + + // Right edge back to the bottom-right + lineTo(topLeft.x + barWidth.times(1.08f) + borderWidth, topLeft.y + barHeight) + + // Close the path + close() + } + + drawPath(path = barBack, color = barBorderColor) + + drawPath(path = barPath, color = barColor) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/base/MMCard.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/base/MMCard.kt new file mode 100644 index 0000000000..951ca07d9d --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/base/MMCard.kt @@ -0,0 +1,61 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.base + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CardDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.navi.elex.atoms.ElexCard +import com.navi.moneymanager.common.ui.theme.color.MMColor + +@Composable +fun MMCard( + modifier: Modifier, + cornerRadius: Dp = 4.dp, + ambientColor: Color = MMColor.ambientColor, + spotColor: Color = MMColor.shadowSpotColor, + backgroundColor: Color = MMColor.white, + elevation: Dp = 2.dp, + shadowElevation: Dp = 32.dp, + borderStroke: BorderStroke? = null, + onClick: (() -> Unit)? = null, + enabled: Boolean = true, + content: @Composable () -> Unit, +) { + ElexCard( + modifier = + modifier.shadow( + shape = RoundedCornerShape(cornerRadius), + elevation = shadowElevation, + ambientColor = ambientColor, + spotColor = spotColor, + ), + onClick = onClick ?: {}, + shape = RoundedCornerShape(cornerRadius), + border = borderStroke, + colors = + CardDefaults.cardColors() + .copy( + containerColor = backgroundColor, + disabledContainerColor = backgroundColor, + ), + elevation = + CardDefaults.cardElevation( + defaultElevation = elevation, + ), + enabled = onClick != null && enabled, + ) { + content() + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/base/MMDivider.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/base/MMDivider.kt new file mode 100644 index 0000000000..7b9d357b2a --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/base/MMDivider.kt @@ -0,0 +1,26 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.base + +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun MMDivider( + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f), + thickness: Dp = 1.dp, + startIndent: Dp = 0.dp +) { + Divider(modifier = modifier, color = color, thickness = thickness, startIndent = startIndent) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/base/MMImage.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/base/MMImage.kt new file mode 100644 index 0000000000..e321d5becf --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/base/MMImage.kt @@ -0,0 +1,69 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.base + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.DefaultAlpha +import androidx.compose.ui.graphics.FilterQuality +import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultFilterQuality +import androidx.compose.ui.layout.ContentScale +import coil.compose.AsyncImagePainter.Companion.DefaultTransform +import coil.compose.AsyncImagePainter.State +import com.navi.elex.atoms.ElexAsyncImage +import com.navi.elex.atoms.ElexImage + +@Composable +fun MMImage( + iconCode: Int, + modifier: Modifier = Modifier, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, + contentDescription: String? = null +) { + ElexImage( + iconCode = iconCode, + contentDescription = contentDescription, + modifier = modifier, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter + ) +} + +@Composable +fun MMAsyncImage( + iconUrl: String, + modifier: Modifier = Modifier, + transform: (State) -> State = DefaultTransform, + onState: ((State) -> Unit)? = null, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, + filterQuality: FilterQuality = DefaultFilterQuality, + contentDescription: String? = null, +) { + ElexAsyncImage( + icon = iconUrl, + contentDescription = contentDescription, + modifier = modifier, + transform = transform, + onState = onState, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + filterQuality = filterQuality, + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/base/MMText.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/base/MMText.kt new file mode 100644 index 0000000000..d2bc4c60ca --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/base/MMText.kt @@ -0,0 +1,111 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.base + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.TextUnit +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.elex.atoms.ElexText +import com.navi.moneymanager.common.ui.theme.toElexFontWeight + +@Composable +fun MMText( + text: String, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + color: Color = Color.Black, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeightEnum? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + onTextLayout: ((TextLayoutResult) -> Unit)? = null, + style: TextStyle = TextStyle(platformStyle = PlatformTextStyle(includeFontPadding = true)) +) { + ElexText( + text = text, + modifier = + modifier.then( + if (onClick != null) Modifier.onClickWithDebounce { onClick() } else Modifier + ), + fontSize = fontSize, + color = color, + fontStyle = fontStyle, + fontWeight = fontWeight?.toElexFontWeight(), + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + onTextLayout = onTextLayout, + style = style, + ) +} + +@Composable +fun MMText( + text: AnnotatedString, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + color: Color = Color.Black, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeightEnum? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = TextStyle(platformStyle = PlatformTextStyle(includeFontPadding = true)) +) { + ElexText( + text = text, + modifier = + modifier.then( + if (onClick != null) Modifier.onClickWithDebounce { onClick() } else Modifier + ), + fontSize = fontSize, + color = color, + fontStyle = fontStyle, + fontWeight = fontWeight?.toElexFontWeight(), + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + onTextLayout = onTextLayout, + style = style, + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/base/MMTextField.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/base/MMTextField.kt new file mode 100644 index 0000000000..6296d60dbb --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/base/MMTextField.kt @@ -0,0 +1,128 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.base + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalFocusManager +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.VisualTransformation +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.design.font.getFontWeight +import com.navi.design.font.naviFontFamily +import com.navi.design.utils.NoRippleIndicationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.theme.color.MMColor + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun MMTextField( + modifier: Modifier, + value: String, + onValueChange: (String) -> Unit, + placeholderText: String, + prefixIllustration: IllustrationType, + suffixIllustration: IllustrationType, + onSuffixIllustrationClick: () -> Unit, +) { + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + + val interactionSource = remember { NoRippleIndicationSource() } + val visualTransformation = VisualTransformation.None + val singleLine = true + val enabled = true + + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier.focusRequester(focusRequester), + enabled = enabled, + textStyle = + TextStyle( + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + fontFamily = naviFontFamily, + ), + keyboardOptions = + KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), + singleLine = singleLine, + visualTransformation = visualTransformation, + interactionSource = interactionSource, + ) { + OutlinedTextFieldDefaults.DecorationBox( + value = value, + innerTextField = it, + enabled = enabled, + singleLine = singleLine, + visualTransformation = visualTransformation, + interactionSource = interactionSource, + placeholder = { + MMText( + text = placeholderText, + color = MMColor.gray, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 22.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + prefix = { + Illustration( + illustrationType = prefixIllustration, + modifier = Modifier.padding(end = 8.dp).size(24.dp), + ) + }, + suffix = { + if (value.isNotEmpty()) + Illustration( + illustrationType = suffixIllustration, + modifier = + Modifier.padding(start = 8.dp) + .size(size = 16.dp) + .clickable( + interactionSource = NoRippleIndicationSource(), + indication = null, + onClick = onSuffixIllustrationClick, + ) + ) + }, + colors = + OutlinedTextFieldDefaults.colors( + focusedBorderColor = MMColor.ctaPrimary, + unfocusedBorderColor = MMColor.borderAlt, + cursorColor = MMColor.ctaPrimary, + ), + contentPadding = + OutlinedTextFieldDefaults.contentPadding( + top = 12.dp, + bottom = 12.dp, + ), + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/bottomSheet/AverageInfoBottomSheet.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/bottomSheet/AverageInfoBottomSheet.kt new file mode 100644 index 0000000000..2ed790c775 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/bottomSheet/AverageInfoBottomSheet.kt @@ -0,0 +1,100 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.bottomSheet + +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.material3.ButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.elex.atoms.ElexButton +import com.navi.moneymanager.R +import com.navi.moneymanager.common.analytics.CategoryDetailsEventTrackerImpl +import com.navi.moneymanager.common.analytics.SpendAnalysisEventTrackerImpl +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.fourDpRoundedShape + +@Composable +fun AverageInfoBottomSheetContent(screenName: String, averageValue: String, onDismiss: () -> Unit) { + + LaunchedEffect(Unit) { + when (screenName) { + MMScreen.SPEND_ANALYSIS.screen -> { + SpendAnalysisEventTrackerImpl.onSpendAnalysisScreenAverageInfoBottomSheetAppeared() + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl.onCategoryDetailsAverageInfoBottomSheetAppeared() + } + } + } + + DisposableEffect(Unit) { + onDispose { + when (screenName) { + MMScreen.SPEND_ANALYSIS.screen -> { + SpendAnalysisEventTrackerImpl + .onSpendAnalysisScreenAverageInfoBottomSheetDisappeared() + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl + .onCategoryDetailsAverageInfoBottomSheetDisappeared() + } + } + } + } + + Column( + modifier = + Modifier.fillMaxWidth() + .padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 32.dp), + horizontalAlignment = Alignment.Start + ) { + MMText( + text = stringResource(R.string.average_info_prefix) + averageValue, + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp, + ) + Spacer(modifier = Modifier.height(8.dp)) + MMText( + text = stringResource(R.string.average_info_description), + color = MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 22.sp + ) + Spacer(modifier = Modifier.height(24.dp)) + ElexButton( + onClick = { onDismiss() }, + modifier = Modifier.fillMaxWidth().height(48.dp), + colors = ButtonDefaults.buttonColors(containerColor = MMColor.ctaPrimary), + shape = fourDpRoundedShape + ) { + MMText( + text = stringResource(R.string.okay_got_it_no_comma), + color = MMColor.white, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 22.sp + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/bottomSheet/BottomSheet.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/bottomSheet/BottomSheet.kt new file mode 100644 index 0000000000..ec85655dae --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/bottomSheet/BottomSheet.kt @@ -0,0 +1,36 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.bottomSheet + +import androidx.compose.runtime.Composable +import com.navi.base.utils.orFalse +import com.navi.base.utils.orTrue +import com.navi.common.basemvi.UiEvent +import com.navi.elex.atoms.ElexBottomSheet +import com.navi.moneymanager.common.model.bottomSheet.BottomSheetState +import com.navi.moneymanager.common.model.bottomSheet.ScreenBottomSheetType + +@Composable +fun , uiEvent : UiEvent> BottomSheet( + state: BottomSheetState, + onDismiss: (bottomSheetType, uiEvent) -> Unit, + content: @Composable (bottomSheetType) -> Unit = {} +) { + val bottomSheetConfig = state.type.sheetConfig + ElexBottomSheet( + visible = state.isVisible.orFalse(), + screenHeightOffset = bottomSheetConfig.screenOffset, + scrimColor = bottomSheetConfig.scrimColor, + cancellable = bottomSheetConfig.isCancellable.orTrue(), + onDismissRequest = { bottomSheetConfig.dismissEvent?.let { onDismiss(state.type, it) } }, + shape = bottomSheetConfig.shape, + containerColor = bottomSheetConfig.containerColor + ) { + content(state.type) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/bottomSheet/MonthSelectionBottomSheet.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/bottomSheet/MonthSelectionBottomSheet.kt new file mode 100644 index 0000000000..ed36f62085 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/bottomSheet/MonthSelectionBottomSheet.kt @@ -0,0 +1,200 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.bottomSheet + +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.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.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.shadow +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.analytics.CategoryDetailsEventTrackerImpl +import com.navi.moneymanager.common.analytics.DashboardEventTrackerImpl +import com.navi.moneymanager.common.analytics.SpendAnalysisEventTrackerImpl +import com.navi.moneymanager.common.model.MonthSelectionBottomSheetData +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.RadioButtonSelectionRow +import com.navi.moneymanager.common.ui.composable.base.MMDivider +import com.navi.moneymanager.common.ui.composable.base.MMImage +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.naviwidgets.R as naviWidgetsR + +@Composable +fun MonthSelectionBottomSheetUI( + screenName: String, + data: MonthSelectionBottomSheetData, + onDismiss: () -> Unit, + onMonthSelected: (Pair) -> Unit +) { + LaunchedEffect(Unit) { + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl.onDashboardMonthSelectionBottomSheetAppeared() + } + MMScreen.SPEND_ANALYSIS.screen -> { + SpendAnalysisEventTrackerImpl.onSpendAnalysisMonthSelectionBottomSheetAppeared() + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl.onCategoryDetailsMonthSelectionBottomSheetAppeared() + } + } + } + + DisposableEffect(Unit) { + onDispose { + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl.onDashboardMonthSelectionBottomSheetDisappeared() + } + MMScreen.SPEND_ANALYSIS.screen -> { + SpendAnalysisEventTrackerImpl + .onSpendAnalysisMonthSelectionBottomSheetDisappeared() + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl + .onCategoryDetailsMonthSelectionBottomSheetDisappeared() + } + } + } + } + + var currentSelection by remember { mutableStateOf(data.selectedMonth) } + var currentlySelectedMonthIndex by remember { + mutableIntStateOf(data.monthList.indexOf(currentSelection)) + } + + Column(Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 32.dp)) { + Column() { + Row( + modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + MMText( + text = data.headerText, + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + letterSpacing = 0.sp, + lineHeight = 24.sp, + ) + + MMImage( + iconCode = naviWidgetsR.drawable.ic_cross_black, + modifier = Modifier.size(24.dp).onClickWithDebounce { onDismiss() } + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + MMDivider(thickness = 4.dp, color = MMColor.ctaSecondary) + } + val scrollState = rememberLazyListState() + + LaunchedEffect(data.selectedMonth) { + data.monthList + .indexOf(data.selectedMonth) + .takeIf { it != -1 } + ?.let { selectedIndex -> scrollState.animateScrollToItem(selectedIndex) } + } + + LazyColumn(state = scrollState, modifier = Modifier.fillMaxWidth().weight(1f)) { + val monthList = data.monthList + items(monthList.size) { index -> + val (monthName, year) = monthList[index] + RadioButtonSelectionRow( + title = "$monthName $year", + isSelected = currentSelection == (monthName to year), + onClick = { + currentSelection = monthName to year + currentlySelectedMonthIndex = index + }, + ) + if (index < monthList.size - 1) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { MMDivider() } + } + } + } + Column( + modifier = + Modifier.shadow( + elevation = 16.dp, + ambientColor = MMColor.ambientColor, + spotColor = MMColor.shadowSpotColor + ) + ) { + Spacer(modifier = Modifier.height(32.dp)) + Box( + modifier = + Modifier.fillMaxWidth() + .padding(start = 16.dp, end = 16.dp) + .height(48.dp) + .onClickWithDebounce { + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl.onDashboardMonthSelectionApplied( + currentlySelectedMonthIndex + ) + } + MMScreen.SPEND_ANALYSIS.screen -> { + SpendAnalysisEventTrackerImpl + .onSpendAnalysisScreenMonthSelectionApplied( + currentlySelectedMonthIndex + ) + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl + .onCategoryDetailsMonthSelectionApplied( + currentlySelectedMonthIndex + ) + } + } + onMonthSelected(currentSelection) + onDismiss() + } + .background(color = MMColor.ctaPrimary, shape = RoundedCornerShape(4.dp)), + contentAlignment = Alignment.Center + ) { + MMText( + text = data.ctaText, + modifier = Modifier.padding(vertical = 12.dp), + color = MMColor.white, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + letterSpacing = 0.sp, + textAlign = TextAlign.Center, + lineHeight = 22.sp + ) + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/button/MMButton.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/button/MMButton.kt new file mode 100644 index 0000000000..2086dc6b25 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/button/MMButton.kt @@ -0,0 +1,48 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.button + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor + +@Composable +fun PrimaryButton(onClick: () -> Unit, title: String) { + Row( + horizontalArrangement = Arrangement.Center, + modifier = + Modifier.fillMaxWidth() + .padding(all = 16.dp) + .background(color = MMColor.ctaSecondary, shape = RoundedCornerShape(size = 4.dp)) + .clip(RoundedCornerShape(size = 4.dp)) + .onClickWithDebounce { onClick() } + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + MMText( + text = title, + color = MMColor.ctaPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + textAlign = TextAlign.Center, + lineHeight = 22.sp, + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/sectionHeaders/SectionHeader.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/sectionHeaders/SectionHeader.kt new file mode 100644 index 0000000000..eb35c32ac5 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/sectionHeaders/SectionHeader.kt @@ -0,0 +1,112 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.sectionHeaders + +import androidx.compose.foundation.interaction.MutableInteractionSource +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.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.model.sectionHeader.SectionHeaderData +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor + +@Composable +fun SectionHeader( + headerModel: SectionHeaderData, + modifier: Modifier = Modifier, + isActionVisible: Boolean = true, + onAction: () -> Unit = {}, +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + SectionTitleText( + title = headerModel.title, + modifier = Modifier.weight(1f).padding(end = 16.dp) + ) + if (isActionVisible && headerModel.actionText != null) { + SectionActionContainer( + actionText = headerModel.actionText, + prefixIcon = headerModel.prefixIcon, + suffixIcon = headerModel.suffixIcon, + onAction = onAction + ) + } + } +} + +@Composable +private fun SectionTitleText(title: String, modifier: Modifier = Modifier) { + Box(modifier = modifier) { + MMText( + text = title, + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + } +} + +@Composable +private fun SectionActionContainer( + prefixIcon: IllustrationSource? = null, + suffixIcon: IllustrationSource? = null, + actionText: String, + onAction: () -> Unit +) { + Row( + modifier = + Modifier.onClickWithDebounce( + onClick = { onAction() }, + interactionSource = remember { MutableInteractionSource() }, + indication = null + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + prefixIcon?.let { + Illustration( + illustrationType = IllustrationType.Image(prefixIcon), + modifier = Modifier.size(16.dp) + ) + } + MMText( + text = actionText, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp + ) + suffixIcon?.let { + Illustration( + illustrationType = IllustrationType.Image(suffixIcon), + modifier = Modifier.size(16.dp) + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/spendCategoriztion/CategoryTotalSpendSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/spendCategoriztion/CategoryTotalSpendSection.kt new file mode 100644 index 0000000000..553af51405 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/spendCategoriztion/CategoryTotalSpendSection.kt @@ -0,0 +1,218 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.spendCategoriztion + +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.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.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.design.theme.EBEBEB +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.model.MMUIState +import com.navi.moneymanager.common.model.TotalSpends +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.ui.theme.color.MMColor.lightGray + +@Composable +fun CategoryTotalSpendsSection( + totalSpendsSectionData: TotalSpends?, + mmUIState: MMUIState, + onTotalSpendsCtaClick: (() -> Unit)? = null, +) { + totalSpendsSectionData?.let { data -> + when (mmUIState) { + MMUIState.Empty -> { + TotalSpendsEmptySection(data as TotalSpends.Empty, onTotalSpendsCtaClick) + } + MMUIState.Loaded -> { + TotalSpendsLoadedSection(data as TotalSpends.Loaded, onTotalSpendsCtaClick) + } + MMUIState.Loading -> { + TotalSpendsLoadingSection( + data as TotalSpends.Loading, + ) + } + } + } +} + +@Composable +fun TotalSpendsLoadedSection( + data: TotalSpends.Loaded, + onTotalSpendsCtaClick: (() -> Unit)? = null +) { + + LaunchedEffect(Unit) {} + + Box( + modifier = + Modifier.clip(RoundedCornerShape(size = 4.dp)) + .padding(bottom = 8.dp) + .onClickWithDebounce { onTotalSpendsCtaClick?.invoke() } + ) { + Row( + modifier = + Modifier.padding(top = 24.dp, bottom = 24.dp, start = 16.dp, end = 16.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row { + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier.size(40.dp) + .background(color = lightGray, shape = CircleShape) + .clip(CircleShape) + .border(width = 1.dp, color = EBEBEB, shape = CircleShape) + ) { + Illustration( + illustrationType = IllustrationType.Image(data.iconUrl), + modifier = Modifier.size(32.dp) + ) + } + + Column(modifier = Modifier.padding(start = 16.dp)) { + MMText( + text = data.title, + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 18.sp + ) + MMText( + text = data.subtitle, + modifier = Modifier.padding(top = 4.dp), + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp + ) + } + } + data.actionIcon?.let { + Illustration( + illustrationType = IllustrationType.Image(data.actionIcon), + modifier = Modifier.padding(top = 18.dp).size(24.dp) + ) + } + } + } +} + +@Composable +fun TotalSpendsEmptySection( + data: TotalSpends.Empty, + onTotalSpendsCtaClick: (() -> Unit)? = null, +) { + Box( + modifier = + Modifier.clip(RoundedCornerShape(size = 4.dp)).onClickWithDebounce { + onTotalSpendsCtaClick?.invoke() + } + ) { + Row( + modifier = + Modifier.padding(top = 24.dp, bottom = 32.dp, start = 16.dp, end = 16.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row { + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier.size(40.dp) + .background(color = lightGray, shape = CircleShape) + .clip(CircleShape) + .border(width = 1.dp, color = EBEBEB, shape = CircleShape) + ) { + Illustration( + illustrationType = IllustrationType.Image(data.iconUrl), + modifier = Modifier.size(32.dp) + ) + } + Column(modifier = Modifier.padding(start = 16.dp)) { + MMText( + text = data.title, + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 18.sp + ) + MMText( + text = data.subtitle, + modifier = Modifier.padding(top = 4.dp), + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp + ) + } + } + data.actionIcon?.let { + Illustration( + illustrationType = IllustrationType.Image(data.actionIcon), + modifier = Modifier.padding(top = 18.dp).size(24.dp) + ) + } + } + } +} + +@Composable +fun TotalSpendsLoadingSection(data: TotalSpends.Loading) { + Box(modifier = Modifier.clip(RoundedCornerShape(size = 4.dp))) { + Row( + modifier = + Modifier.padding(top = 24.dp, bottom = 32.dp, start = 16.dp, end = 16.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row { + Illustration( + illustrationType = IllustrationType.Lottie(data.lottieUrl), + modifier = Modifier.size(40.dp) + ) + Column(modifier = Modifier.padding(start = 16.dp)) { + MMText( + text = data.title, + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 18.sp + ) + Spacer(Modifier.height(4.dp)) + Illustration( + illustrationType = IllustrationType.Lottie(data.animationUrl), + modifier = Modifier.height(24.dp).width(40.dp) + ) + } + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/spendCategoriztion/SpendCategorizationSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/spendCategoriztion/SpendCategorizationSection.kt new file mode 100644 index 0000000000..db2dbe3458 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/spendCategoriztion/SpendCategorizationSection.kt @@ -0,0 +1,294 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.spendCategoriztion + +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.ColumnScope +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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.design.font.getFontWeight +import com.navi.design.font.naviFontFamily +import com.navi.moneymanager.common.analytics.DashboardEventTrackerImpl +import com.navi.moneymanager.common.composable.bankselectionbottomsheet.ZeroTransactionView +import com.navi.moneymanager.common.model.MMUIState +import com.navi.moneymanager.common.model.SelectedMonth +import com.navi.moneymanager.common.model.SpendCategorizationAction +import com.navi.moneymanager.common.model.SpendCategorizationState +import com.navi.moneymanager.common.model.sectionHeader.SectionHeaderData +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.composable.sectionHeaders.SectionHeader +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.fourDpRoundedShape + +@Composable +fun SpendCategorizationSection( + screenName: String, + isAddAccountSelected: Boolean, + spendCategorizationState: SpendCategorizationState, + spendCategorizationAction: (SpendCategorizationAction) -> Unit +) { + when (spendCategorizationState) { + is SpendCategorizationState.Loaded -> { + SpendCategorizationLoadedUI( + screenName = screenName, + data = spendCategorizationState, + spendCategorizationAction = spendCategorizationAction + ) + } + is SpendCategorizationState.Loading -> { + SpendCategorizationLoadingUI( + screenName = screenName, + data = spendCategorizationState, + spendCategorizationAction = spendCategorizationAction + ) + } + is SpendCategorizationState.Empty -> { + if (screenName == MMScreen.SPEND_ANALYSIS.screen) { + SpendAnalysisSpendCategorizationEmptyUI(data = spendCategorizationState) + } else { + DashboardSpendCategorizationEmptyUI( + isAddAccountSelected = isAddAccountSelected, + data = spendCategorizationState, + spendCategorizationAction = spendCategorizationAction + ) + } + } + } +} + +@Composable +fun SpendCategorizationLoadingUI( + screenName: String, + spendCategorizationAction: (SpendCategorizationAction) -> Unit, + data: SpendCategorizationState.Loading +) { + SpendCategorizationContentWrapper( + onMonthChangeClick = { + spendCategorizationAction(SpendCategorizationAction.OnMonthChange(it)) + }, + headerData = data.header + ) { + CategoryTotalSpendsSection(data.totalSpends, mmUIState = MMUIState.Loading) + SpendCategoriesHeaderTitle(data.categoryHeaderTitlePrefix, data.categoryHeaderTitleSuffix) + SpendCategoryList(screenName, data.categories, mmUIState = MMUIState.Loading) + } +} + +@Composable +fun DashboardSpendCategorizationEmptyUI( + isAddAccountSelected: Boolean, + spendCategorizationAction: (SpendCategorizationAction) -> Unit, + data: SpendCategorizationState.Empty +) { + SpendCategorizationContentWrapper( + onMonthChangeClick = { + spendCategorizationAction(SpendCategorizationAction.OnMonthChange(it)) + }, + headerData = data.header + ) { + CategoryTotalSpendsSection(data.totalSpends, mmUIState = MMUIState.Empty) { + spendCategorizationAction( + SpendCategorizationAction.ViewTotalSpends( + SelectedMonth(data.selectedMonth, data.selectedYear) + ) + ) + } + SpendCategoriesHeaderTitle(data.categoryHeaderTitlePrefix, data.categoryHeaderTitleSuffix) + SpendCategoryEmptyList(isAddAccountSelected = isAddAccountSelected, data = data) { + isTotalSyncCompleted -> + spendCategorizationAction( + SpendCategorizationAction.AddNewBankAccount(isTotalSyncCompleted) + ) + } + } +} + +@Composable +fun SpendAnalysisSpendCategorizationEmptyUI(data: SpendCategorizationState.Empty) { + SpendCategoriesHeaderTitle(data.categoryHeaderTitlePrefix, data.categoryHeaderTitleSuffix) + ZeroTransactionView( + title = data.title, + illustrationSource = data.iconUrl, + modifier = Modifier.fillMaxWidth().padding(top = 32.dp, bottom = 24.dp) + ) +} + +@Composable +fun SpendCategorizationLoadedUI( + screenName: String, + data: SpendCategorizationState.Loaded, + spendCategorizationAction: (SpendCategorizationAction) -> Unit +) { + Column { + SpendCategorizationContentWrapper( + headerData = data.header, + onMonthChangeClick = { + spendCategorizationAction(SpendCategorizationAction.OnMonthChange(it)) + } + ) { + CategoryTotalSpendsSection( + totalSpendsSectionData = data.totalSpends, + mmUIState = MMUIState.Loaded, + onTotalSpendsCtaClick = { + DashboardEventTrackerImpl.onDashboardTotalSpendClicked() + spendCategorizationAction( + SpendCategorizationAction.ViewTotalSpends( + SelectedMonth(data.selectedMonth, data.selectedYear) + ) + ) + } + ) + SpendCategoriesHeaderTitle( + data.categoryHeaderTitlePrefix, + data.categoryHeaderTitleSuffix + ) + SpendCategoryList( + screenName = screenName, + categories = data.categories, + mmUIState = MMUIState.Loaded, + applyCategoryHorizontalPadding = data.applyCategoryHorizontalPadding, + onCategoryClick = { + spendCategorizationAction( + SpendCategorizationAction.SelectCategory( + it.categoryId, + SelectedMonth(data.selectedMonth, data.selectedYear) + ) + ) + } + ) + ViewMoreCta(data.viewMoreCtaText) { + spendCategorizationAction( + SpendCategorizationAction.ViewMoreCategories( + SelectedMonth(data.selectedMonth, data.selectedYear) + ) + ) + } + } + SelfTransferCategory(data.selfTransferCategory) { + spendCategorizationAction( + SpendCategorizationAction.SelectSelfTransferCategory( + it, + SelectedMonth(data.selectedMonth, data.selectedYear) + ) + ) + } + } +} + +@Composable +fun SpendCategorizationContentWrapper( + onMonthChangeClick: (String) -> Unit, + headerData: SectionHeaderData?, + content: @Composable ColumnScope.() -> Unit +) { + if (headerData == null) { + Column(modifier = Modifier.padding(horizontal = 0.dp).fillMaxWidth(), content = content) + } else { + Spacer(Modifier.height(24.dp)) + SectionHeader( + modifier = Modifier.padding(horizontal = 16.dp), + headerModel = headerData, + onAction = { onMonthChangeClick(headerData.actionText.orEmpty()) }, + ) + Column( + modifier = + Modifier.padding(all = 16.dp) + .fillMaxWidth() + .border(width = 1.dp, color = MMColor.borderColor, shape = fourDpRoundedShape), + content = content + ) + } +} + +@Composable +fun SpendCategoriesHeaderTitle(titlePrefix: String, titleSuffix: String? = null) { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.height(1.dp).weight(1f).background(color = MMColor.dividerColor)) + val annotatedTitle = buildAnnotatedString { + withStyle( + style = + SpanStyle( + color = MMColor.textTertiary, + fontSize = 12.sp, + letterSpacing = 1.5.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD) + ) + ) { + append(titlePrefix) + } + titleSuffix?.let { + withStyle( + style = + SpanStyle( + color = MMColor.textSecondary, + fontSize = 12.sp, + letterSpacing = 1.5.sp, + fontFamily = naviFontFamily, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD) + ) + ) { + append(titleSuffix) + } + } + } + MMText( + text = annotatedTitle, + lineHeight = 20.sp, + modifier = Modifier.padding(horizontal = 8.dp) + ) + Spacer(modifier = Modifier.height(1.dp).weight(1f).background(color = MMColor.dividerColor)) + } +} + +@Composable +fun ViewMoreCta(title: String?, onViewMoreCtaClick: () -> Unit) { + title?.let { titleText -> + Box(Modifier.padding(top = 8.dp, bottom = 16.dp, start = 16.dp, end = 16.dp)) { + Row( + horizontalArrangement = Arrangement.Center, + modifier = + Modifier.fillMaxWidth() + .onClickWithDebounce { onViewMoreCtaClick() } + .background(color = MMColor.ctaSecondary, shape = fourDpRoundedShape) + .padding(horizontal = 16.dp, vertical = 12.dp) + ) { + MMText( + text = titleText, + color = MMColor.ctaPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + textAlign = TextAlign.Center, + lineHeight = 22.sp + ) + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/spendCategoriztion/SpendCategory.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/spendCategoriztion/SpendCategory.kt new file mode 100644 index 0000000000..cfe7af1608 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/spendCategoriztion/SpendCategory.kt @@ -0,0 +1,460 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.spendCategoriztion + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.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.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.analytics.DashboardEventTrackerImpl +import com.navi.moneymanager.common.analytics.SpendAnalysisEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.model.ImageProperties +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.model.MMUIState +import com.navi.moneymanager.common.model.SelfTransferCategoryData +import com.navi.moneymanager.common.model.SpendCategorizationState +import com.navi.moneymanager.common.model.SpendCategoryItemData +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.MMCategoryProgressBar +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.fourDpRoundedShape +import com.navi.uitron.utils.setShimmerEffect + +@Composable +fun SpendCategoryList( + screenName: String? = null, + categories: List, + mmUIState: MMUIState, + applyCategoryHorizontalPadding: Boolean? = true, + onCategoryClick: ((SpendCategoryItemData.Loaded) -> Unit)? = null +) { + Column(Modifier.padding(top = 16.dp)) { + categories.forEachIndexed { index, category -> + when (mmUIState) { + MMUIState.Empty -> {} + MMUIState.Loading -> { + SpendCategoryLoadingItem( + data = category as SpendCategoryItemData.Loading, + ) + } + MMUIState.Loaded -> { + SpendCategoryItem( + data = category as SpendCategoryItemData.Loaded, + onCategoryClick = onCategoryClick, + trackCategoryItemClick = { categoryType -> + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl.onDashboardCategoryClicked( + index + 1, + categoryType + ) + } + MMScreen.SPEND_ANALYSIS.screen -> { + SpendAnalysisEventTrackerImpl + .onSpendAnalysisScreenCategoryClicked( + index + 1, + categoryType + ) + } + } + }, + trackCategoryItemView = { categoryType -> + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl.onDashboardCategoryViewed( + index + 1, + categoryType + ) + } + MMScreen.SPEND_ANALYSIS.screen -> { + SpendAnalysisEventTrackerImpl.onSpendAnalysisCategoryViewed( + index + 1, + categoryType + ) + } + } + }, + ) + } + } + if (index != categories.size - 1) { + Spacer( + modifier = + Modifier.fillMaxWidth() + .padding( + horizontal = + if (applyCategoryHorizontalPadding == true) 0.dp else 16.dp + ) + .height(1.dp) + .background(MMColor.borderColor) + ) + } + } + } +} + +@Composable +fun SpendCategoryItem( + data: SpendCategoryItemData.Loaded, + onCategoryClick: ((SpendCategoryItemData.Loaded) -> Unit)? = null, + trackCategoryItemClick: (String) -> Unit, + trackCategoryItemView: (String) -> Unit +) { + + LaunchedEffect(Unit) { trackCategoryItemView(data.categoryId) } + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = + Modifier.fillMaxWidth() + .onClickWithDebounce { + trackCategoryItemClick(data.categoryId) + onCategoryClick?.invoke(data) + } + .padding(all = 16.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier.size(40.dp) + .background(color = MMColor.ctaSecondary, shape = CircleShape) + .clip(CircleShape) + ) { + Illustration( + illustrationType = IllustrationType.Image(data.iconUrl), + modifier = Modifier.size(32.dp) + ) + } + Column(modifier = Modifier.weight(1f).padding(start = 12.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + MMText( + modifier = Modifier.weight(1f), + text = data.name, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 22.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + Spacer(Modifier.width(8.dp)) + + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + MMText( + text = data.amount, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 22.sp + ) + + Box(Modifier.padding(top = 2.dp)) { + Illustration( + illustrationType = IllustrationType.Image(data.actionIcon), + modifier = Modifier.size(16.dp) + ) + } + } + } + + Spacer(Modifier.height(4.dp)) + + Row( + modifier = Modifier.fillMaxWidth().padding(end = 20.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + MMCategoryProgressBar(progress = data.progress, color = data.progressColor) + } + + Spacer(Modifier.width(8.dp)) + + MMText( + text = data.progressText, + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 16.sp, + ) + } + } + } +} + +@Composable +fun SpendCategoryLoadingItem(data: SpendCategoryItemData.Loading) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp, horizontal = 16.dp) + ) { + Row(modifier = Modifier.weight(1f).padding(end = 8.dp)) { + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier.size(40.dp) + .background(color = MMColor.ctaSecondary, shape = CircleShape) + .clip(CircleShape) + ) { + Illustration( + illustrationType = IllustrationType.Lottie(data.lottieUrl), + modifier = Modifier.size(40.dp) + ) + } + Column( + modifier = Modifier.padding(start = 12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + MMText( + text = data.name, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 22.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + Box( + modifier = + Modifier.width(200.dp) + .height(6.dp) + .clip(RoundedCornerShape(4.dp)) + .setShimmerEffect(true) + ) + } + } + Illustration( + illustrationType = IllustrationType.Lottie(data.actionLottie), + modifier = Modifier.height(24.dp).width(40.dp) + ) + } +} + +@Composable +fun SpendCategoryEmptyList( + isAddAccountSelected: Boolean, + data: SpendCategorizationState.Empty, + addNewBankAccount: (Boolean) -> Unit +) { + Box( + modifier = + Modifier.clip(RoundedCornerShape(12.dp)) + .padding(start = 16.dp, end = 16.dp, top = 32.dp, bottom = 24.dp) + .fillMaxWidth() + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom + ) { + Column { + MMText( + text = data.title, + color = MMColor.textSecondary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + textAlign = TextAlign.Start, + lineHeight = 22.sp + ) + Spacer(modifier = Modifier.height(24.dp)) + + AddAccountButton( + isAddAccountSelected = isAddAccountSelected, + data = data, + addNewBankAccount = addNewBankAccount + ) + } + + Illustration( + illustrationType = IllustrationType.Image(data.iconUrl), + modifier = Modifier.size(80.dp) + ) + } + } +} + +@Composable +fun SelfTransferCategory( + selfTransferCategoryData: SelfTransferCategoryData?, + onSelfTransferCategoryClick: (categoryId: String) -> Unit +) { + selfTransferCategoryData?.let { data -> + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = + Modifier.fillMaxWidth() + .padding(top = 8.dp, start = 16.dp, end = 16.dp, bottom = 16.dp) + .clip(fourDpRoundedShape) + .background(color = MMColor.lightGray, shape = fourDpRoundedShape) + .border(width = 1.dp, shape = fourDpRoundedShape, color = MMColor.borderColor) + .onClickWithDebounce { onSelfTransferCategoryClick(data.categoryName) } + .padding(16.dp) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier.size(40.dp) + .background(color = MMColor.ctaSecondary, shape = CircleShape) + .clip(CircleShape) + ) { + Illustration( + illustrationType = IllustrationType.Image(data.iconUrl), + modifier = Modifier.size(32.dp) + ) + } + Column(modifier = Modifier.padding(start = 12.dp)) { + Row { + MMText( + modifier = Modifier.weight(1f), + text = data.categoryName, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 22.sp, + ) + MMText( + text = data.amount, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 22.sp + ) + Box(Modifier.padding(top = 2.dp, start = 4.dp)) { + Illustration( + illustrationType = IllustrationType.Image(data.actionIcon), + modifier = Modifier.size(16.dp) + ) + } + } + + MMText( + modifier = Modifier.padding(top = 2.dp), + text = data.subtitle, + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 22.sp, + ) + } + } + } +} + +@Composable +fun AddAccountButton( + isAddAccountSelected: Boolean, + data: SpendCategorizationState.Empty, + addNewBankAccount: (Boolean) -> Unit +) { + if (!isAddAccountSelected) { + Row( + modifier = + Modifier.clip(RoundedCornerShape(size = 4.dp)) + .onClickWithDebounce { addNewBankAccount(data.isTotalSyncCompleted) } + .background( + color = + if (data.isTotalSyncCompleted) MMColor.ctaSecondary + else MMColor.ctaPrimaryDisable, + shape = RoundedCornerShape(size = 4.dp) + ) + .padding(start = 12.dp, end = 16.dp, top = 12.dp, bottom = 12.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + data.ctaIconUrl?.let { + Illustration( + illustrationType = + if (data.isTotalSyncCompleted) IllustrationType.Image(it) + else + IllustrationType.Image( + source = data.ctaIconUrl, + properties = + ImageProperties( + colorFilter = ColorFilter.tint(MMColor.ctaPrimaryDisable) + ) + ), + modifier = Modifier.size(16.dp) + ) + } + data.addAccountCtaText?.let { + MMText( + text = it, + color = + if (data.isTotalSyncCompleted) MMColor.ctaPrimary + else MMColor.ctaPrimaryDisable, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + textAlign = TextAlign.Center, + lineHeight = 18.sp + ) + } + } + } else { + Row( + modifier = + Modifier.clip(RoundedCornerShape(size = 4.dp)) + .clickable(enabled = false) {} + .background( + color = MMColor.bgDisabledColor, + shape = RoundedCornerShape(size = 4.dp) + ) + .padding(start = 14.dp, end = 16.dp, top = 8.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Box(contentAlignment = Alignment.Center) { + data.addAccountCtaText?.let { + MMText( + text = it, + fontSize = 12.sp, + lineHeight = 18.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + color = MMColor.ctaPrimary, + textAlign = TextAlign.Center, + modifier = Modifier.alpha(0f).padding(start = 16.dp) + ) + } + data.loadingLottieUrl?.let { + Illustration( + illustrationType = IllustrationType.Lottie(it), + modifier = Modifier.height(24.dp).width(36.dp) + ) + } + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/transaction/Transaction.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/transaction/Transaction.kt new file mode 100644 index 0000000000..a5e31087b7 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/transaction/Transaction.kt @@ -0,0 +1,224 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.transaction + +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.IntrinsicSize +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.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +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.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor + +@Composable +fun Transaction( + transaction: Transaction, + onClick: (String) -> Unit, + onCategoryClick: (Transaction) -> Unit +) { + Row( + modifier = + Modifier.onClickWithDebounce { onClick(transaction.id) } + .padding(all = 16.dp) + .fillMaxWidth() + .height(intrinsicSize = IntrinsicSize.Min), + verticalAlignment = Alignment.Top, + ) { + Avatar( + title = transaction.initials, + backgroundColor = transaction.initialBackgroundColor, + ) + + Spacer(modifier = Modifier.width(width = 12.dp)) + + Column { + Row( + modifier = Modifier.wrapContentHeight(), + verticalAlignment = Alignment.Top, + ) { + Column( + modifier = Modifier.weight(weight = 1f), + ) { + MMText( + text = transaction.title, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + lineHeight = 22.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + Spacer(modifier = Modifier.width(width = 16.dp)) + + Column( + modifier = Modifier.wrapContentHeight(), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.SpaceBetween, + ) { + MMText( + text = transaction.amount, + color = transaction.amountColor, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + textAlign = TextAlign.End, + lineHeight = 22.sp, + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.wrapContentHeight(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(weight = 1f)) { + CategoryPill( + title = transaction.categoryName, + icon = transaction.categoryIcon, + transaction = transaction, + onClick = onCategoryClick + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.wrapContentHeight(), + verticalAlignment = Alignment.Bottom, + ) { + Column( + modifier = Modifier.weight(weight = 1f), + ) { + MMText( + text = transaction.date, + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 18.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + + Spacer(modifier = Modifier.width(width = 24.dp)) + + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.SpaceBetween, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + MMText( + text = transaction.transactionTypeLabel, + color = MMColor.textSecondary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 18.sp, + ) + + Spacer(modifier = Modifier.width(width = 8.dp)) + + Illustration( + illustrationType = IllustrationType.Image(transaction.accountIcon), + modifier = Modifier.size(size = 16.dp), + ) + } + } + } + } + } +} + +@Composable +private fun Avatar(title: String, backgroundColor: Color) { + Box( + modifier = + Modifier.size(40.dp).clip(shape = CircleShape).background(color = backgroundColor), + contentAlignment = Alignment.Center + ) { + MMText( + text = title, + color = MMColor.white, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + lineHeight = 24.sp, + ) + } +} + +@Composable +private fun CategoryPill( + title: String, + icon: IllustrationSource?, + transaction: Transaction, + onClick: (Transaction) -> Unit +) { + Row( + modifier = + Modifier.background( + color = MMColor.ctaSecondary, + shape = RoundedCornerShape(size = 32.dp) + ) + .clip(RoundedCornerShape(size = 32.dp)) + .onClickWithDebounce { onClick(transaction) } + .padding( + start = icon?.let { 8.dp } ?: 12.dp, + top = 4.dp, + end = 12.dp, + bottom = 4.dp + ), + verticalAlignment = Alignment.CenterVertically, + ) { + icon?.let { + Illustration( + illustrationType = IllustrationType.Image(source = icon), + modifier = Modifier.size(size = 16.dp), + ) + + Spacer(modifier = Modifier.width(width = 8.dp)) + } + + MMText( + text = title, + color = icon?.let { MMColor.ctaPrimary } ?: MMColor.textSecondary, + fontSize = 12.sp, + fontWeight = + icon?.let { FontWeightEnum.NAVI_BODY_DEMI_BOLD } + ?: FontWeightEnum.NAVI_HEADLINE_REGULAR, + lineHeight = 16.sp + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/transaction/TransactionDivider.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/transaction/TransactionDivider.kt new file mode 100644 index 0000000000..576328b79e --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/transaction/TransactionDivider.kt @@ -0,0 +1,25 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.transaction + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.navi.moneymanager.common.ui.composable.base.MMDivider +import com.navi.moneymanager.common.ui.theme.color.MMColor + +@Composable +fun TransactionDivider() { + MMDivider( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + thickness = 1.dp, + color = MMColor.transactionDividerColor + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/transaction/TransactionListSectionUI.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/transaction/TransactionListSectionUI.kt new file mode 100644 index 0000000000..d71ad79f7b --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/composable/transaction/TransactionListSectionUI.kt @@ -0,0 +1,52 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.composable.transaction + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.navi.moneymanager.common.analytics.DashboardEventTrackerImpl +import com.navi.moneymanager.common.model.Transaction + +@Composable +fun TransactionListSectionUI( + transactions: List, + onTransactionClick: (String) -> Unit, + onTransactionCategoryClick: (Transaction) -> Unit +) { + Column( + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + transactions.forEachIndexed { index, transaction -> + Transaction( + transaction = transaction, + onClick = { + DashboardEventTrackerImpl.onDashboardTransactionClicked(index + 1) + onTransactionClick(transaction.id) + }, + onCategoryClick = { + DashboardEventTrackerImpl.onDashboardTransactionCategoryClicked( + index + 1, + transaction.categoryId + ) + onTransactionCategoryClick(transaction) + }, + ) + if (index < transactions.lastIndex) { + TransactionDivider() + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/theme/ChipsStateThemeProvider.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/theme/ChipsStateThemeProvider.kt new file mode 100644 index 0000000000..ad883388d0 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/theme/ChipsStateThemeProvider.kt @@ -0,0 +1,157 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.theme + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.ui.theme.color.MMColor + +/** + * Provides theme configurations for chips based on their selection state and specified theme code. + */ +class ChipsStateThemeProvider { + + /** + * Retrieves the appropriate chip theme based on the given theme code and selection state. + * + * @param themeCode The code representing the theme to be applied. Defaults to + * [ChipThemeCodes.DEFAULT_THEME]. + * @param isSelected Indicates whether the chip is currently selected or not. + * @return An instance of [MMChipTheme] representing the theme for the chip. + */ + fun getChipTheme( + themeCode: ChipThemeCodes = ChipThemeCodes.DEFAULT_THEME, + isSelected: Boolean + ): MMChipTheme { + return when (themeCode) { + ChipThemeCodes.ADD_BANK_THEME -> getAddBankTheme(isSelected = isSelected) + ChipThemeCodes.BANK_THEME -> getBankTheme(isSelected = isSelected) + ChipThemeCodes.CATEGORY_THEME -> getCategoryTheme(isSelected = isSelected) + ChipThemeCodes.DEFAULT_THEME -> getDefaultTheme(isSelected = isSelected) + ChipThemeCodes.DISABLED_BANK_THEME -> getDisabledBankTheme() + ChipThemeCodes.ADD_ACCOUNT_DISABLE_THEME -> getAddAccountDisableTheme() + } + } + + private fun getBankTheme(isSelected: Boolean): MMChipTheme { + return if (isSelected) + MMChipTheme( + borderColor = MMColor.ctaPrimary, + backgroundColor = MMColor.white, + contentColor = MMColor.ctaPrimary, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD + ) + else + MMChipTheme( + borderColor = MMColor.transparent, + backgroundColor = MMColor.ctaSecondary, + contentColor = MMColor.ctaPrimary, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR + ) + } + + private fun getCategoryTheme(isSelected: Boolean): MMChipTheme { + return if (isSelected) + MMChipTheme( + borderColor = MMColor.ctaPrimary, + backgroundColor = MMColor.transparent, + contentColor = MMColor.ctaPrimary, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR + ) + else + MMChipTheme( + borderColor = MMColor.ctaSecondary, + backgroundColor = MMColor.ctaSecondary, + contentColor = MMColor.textSecondary, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + colorFilter = ColorFilter.tint(MMColor.textSecondary) + ) + } + + private fun getDefaultTheme(isSelected: Boolean): MMChipTheme { + return if (isSelected) + MMChipTheme( + borderColor = MMColor.transparent, + backgroundColor = MMColor.transparent, + contentColor = MMColor.ctaPrimary, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD + ) + else + MMChipTheme( + borderColor = MMColor.transparent, + backgroundColor = MMColor.transparent, + contentColor = MMColor.ctaPrimary, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD + ) + } + + private fun getAddBankTheme(isSelected: Boolean): MMChipTheme { + return if (isSelected) + MMChipTheme( + borderColor = MMColor.transparent, + backgroundColor = MMColor.bgDisabledColor, + contentColor = MMColor.ctaPrimaryDisable, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD + ) + else + MMChipTheme( + borderColor = MMColor.transparent, + backgroundColor = MMColor.ctaSecondary, + contentColor = MMColor.ctaPrimary, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD + ) + } + + private fun getDisabledBankTheme(): MMChipTheme { + return MMChipTheme( + borderColor = MMColor.transparent, + backgroundColor = MMColor.ctaSecondary, + contentColor = MMColor.ctaPrimaryDisable, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR + ) + } + + private fun getAddAccountDisableTheme(): MMChipTheme { + return MMChipTheme( + borderColor = MMColor.transparent, + backgroundColor = MMColor.ctaSecondary, + contentColor = MMColor.ctaPrimaryDisable, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + colorFilter = ColorFilter.tint(MMColor.ctaPrimaryDisable) + ) + } +} + +/** Enum representing the available theme codes for chips. */ +enum class ChipThemeCodes { + + ADD_BANK_THEME, // Theme for the "Add Bank" chip. + BANK_THEME, // Theme for bank-related chips. + CATEGORY_THEME, // Theme for category-related chips. + DEFAULT_THEME, // Default theme for chips. + DISABLED_BANK_THEME, // Theme for disabled bank-related chips. + ADD_ACCOUNT_DISABLE_THEME, // Theme for disabled disable add account chips. +} + +/** + * Holds the theme properties for a chip UI component. These properties change based on the chip's + * selection state. + * + * @property borderColor The color of the chip's border. + * @property backgroundColor The background color of the chip. + * @property contentColor The color of the chip's text and other content. + * @property fontWeight The font weight for the chip's text. + */ +data class MMChipTheme( + val borderColor: Color, + val backgroundColor: Color, + val contentColor: Color, + val fontWeight: FontWeightEnum, + val colorFilter: ColorFilter? = null +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/theme/MMTheme.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/theme/MMTheme.kt new file mode 100644 index 0000000000..a4a532d277 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/theme/MMTheme.kt @@ -0,0 +1,50 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.Colors +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.navi.design.theme.FF018786 +import com.navi.design.theme.FF1F002A +import com.navi.design.theme.FFFF5732 + +private val LightColors = + Colors( + primary = FF1F002A, + primaryVariant = Color.Black, + secondary = FF1F002A, + secondaryVariant = FF018786, + background = Color.Transparent, + surface = Color.White, + error = FFFF5732, + onPrimary = Color.White, + onSecondary = Color.Gray, + onBackground = Color.Green, + onSurface = Color.Black, + onError = FFFF5732, + isLight = true + ) + +@Composable +fun MoneyManagerMaterialTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val systemUiController = rememberSystemUiController() + systemUiController.setNavigationBarColor( + color = Color.Transparent, + darkIcons = true, + navigationBarContrastEnforced = false + ) + val colorScheme = if (darkTheme) LightColors else LightColors + MaterialTheme(colors = colorScheme, content = content) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/theme/Utils.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/theme/Utils.kt new file mode 100644 index 0000000000..29b266de30 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/theme/Utils.kt @@ -0,0 +1,21 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.theme + +import com.navi.design.font.FontWeightEnum +import com.navi.elex.font.FontWeightEnum as ElexFontWeightEnum + +fun FontWeightEnum.toElexFontWeight(): ElexFontWeightEnum { + return when (this) { + FontWeightEnum.NAVI_HEADLINE_REGULAR -> ElexFontWeightEnum.NAVI_HEADLINE_REGULAR + FontWeightEnum.NAVI_HEADLINE_BOLD -> ElexFontWeightEnum.NAVI_HEADLINE_BOLD + FontWeightEnum.NAVI_BODY_REGULAR -> ElexFontWeightEnum.NAVI_BODY_REGULAR + FontWeightEnum.NAVI_BODY_DEMI_BOLD -> ElexFontWeightEnum.NAVI_BODY_DEMI_BOLD + else -> ElexFontWeightEnum.NAVI_BODY_REGULAR + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/theme/color/MMColor.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/theme/color/MMColor.kt new file mode 100644 index 0000000000..3ea77c1b42 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/ui/theme/color/MMColor.kt @@ -0,0 +1,62 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.ui.theme.color + +import androidx.compose.ui.graphics.Color + +object MMColor { + val textPrimary = Color(0xFF191919) + val textSecondary = Color(0xFF444444) + val textTertiary = Color(0xFF6B6B6B) + val ctaPrimary = Color(0xFF1F002A) + val ctaSecondary = Color(0xFFF5F5F5) + val ctaTertiary = Color(0xFFE8E8E8) + val bgAltColor = Color(0xFFFAFAFA) + val ctaLoaderColor = Color(0xFF584460) + val ctaPrimaryDisable = Color(0xFFB5ACB9) + val borderColor = Color(0xFFEBEBEB) + val bgDisabledColor = Color(0xFFEBE8E8) + val borderAlt = Color(0xFFE3E5E5) + val transparent = Color.Transparent + val white = Color(0xFFFFFFFF) + val black = Color(0xFF000000) + val lightGray = Color(0xFFF9F9FA) + val paleGray = Color(0xFFEFEFEF) + val gray = Color(0xFFA8A8A8) + val whiteSmoke = Color(0xFFF1F1F1) + val dividerColor = Color(0xFFEEEEEE) + val bgBannerColor = Color(0xFF22A940) + val creditAmountColor = Color(0xFF22A940) + val transactionDividerColor = Color(color = 0xFFE3E5E5) + val progressIndicatorColor = Color(0xFF14BC51) + val disabledButtonColor = Color(0xFFB5ACB9) + val bgAltColorSecond = Color(0xFFE9E7F0) + val inputFieldBorderColor = Color(0xFFA8A8A8) + val toggleTrackColor = Color(0xFFE8E8E9) + val shadowSpotColor = Color(0x4DD1D9E6) + val ambientColor = Color(0x4DD1D9E6) + + val categoriesProgressBarColors = + listOf( + Color(0xFFFF6E00), + Color(0xFFFFB400), + Color(0xFF3C7DFF), + Color(0xFF22D081), + Color(0xFFAA3CE6) + ) + + val transactionIconColors = + listOf( + Color(0xFF70388F), + Color(0xFFD47530), + Color(0xFF325396), + Color(0xFF27885C), + Color(0xFFC74444), + Color(0xFFDA950F) + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/Constants.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/Constants.kt new file mode 100644 index 0000000000..490cdad249 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/Constants.kt @@ -0,0 +1,93 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.utils + +object Constants { + const val PRODUCT_HELP_PAGE = "PRODUCT_HELP_PAGE" + const val PRODUCT_HELP_SCREEN_NAME = "SCREEN_NAME" + const val REDIRECTION_CTA = "REDIRECTION_CTA" + const val TRANSITION_DURATION_IN_MILLIS = 400 + const val DASHBOARD_ONBOARDING_TIMEOUT = 40000L + const val MM_KEY_DB_ENCRYPTION = "mmKeyDbEncryption" + const val MM_ENCRYPTED_DB_NAME = "mmEncryptedDb" + const val MM_UNENCRYPTED_DB_NAME = "mmUnEncryptedDb" + const val SQL_CIPHER = "sqlcipher" + const val ACCOUNT_TABLE = "accountTable" + const val TRANSACTION_TABLE = "transactionTable" + const val DATA_STORE_TABLE = "dataStoreTable" + const val AES_ENCRYPTION = "AES" + const val KEY_GENERATOR_KEY_SIZE = 256 + const val ERROR_PAGE_CTA_TEXT = "Try again" + const val MM_IS_USER_ONBOARDED = "MM_IS_USER_ONBOARDED" + const val NO_INTERNET_TEXT = "No internet" + const val SOMETHING_WENT_WRONG = "Something went wrong" + const val PLEASE_CHECK_YOUR_INTERNET_CONNECTION = "Please check your internet connection." + const val PLEASE_TRY_AFTER_SOME_TIME = "Please try after sometime." + const val SUCCESS = "SUCCESS" + const val TRANSACTION_ID = "transactionId" + const val SELF_TRANSFER = "SELF_TRANSFER" + const val SPEND_CATEGORISATION_TOP_CATEGORIES_SIZE = 5 + const val TRANSACTION_HISTORY_PAGE_SIZE = 10 + const val OTHERS_CATEGORY_ID = "OTHERS" + const val API_RETRY_COUNT = 1 + const val ERROR_RED_ALERT_ICON = "ALERT_RED" + const val GREEN_TICK_MARK = "GREEN_TICK_MARK" + const val CREDIT = "CREDIT" + const val DEBIT = "DEBIT" + const val UNCATEGORIZED = "UNCATEGORIZED" + const val PLUS_SPACE = "+ " + const val RECENT_TRANSACTION_COUNT = 3 + const val IS_CONSENT_REVOKED = "IS_CONSENT_REVOKED" + const val UNKNOWN = "Unknown" + const val TXN_TIMESTAMP = "TXN_TIMESTAMP" + const val UPDATED_AT = "UPDATED_AT" + const val PLEASE_TRY_AGAIN = "Please try again" + const val OKAY_GOT_IT = "Okay, got it" + const val SYNC_THRESHOLD_TIME = 600000L + const val PAGE_SIZE = 100 + const val REVOKE_CONSENT_FINARKEIN_URL = "https://revokeconsent.finvu.in/" + const val CPS = "CPS" + + // datastore constants + const val IS_FIRST_MONTH_SYNC_COMPLETED = "IS_FIRST_MONTH_SYNC_COMPLETED" + const val IS_TOTAL_SYNC_COMPLETED = "IS_TOTAL_SYNC_COMPLETED" + + const val NEW_TRANSACTION_COUNT = "NEW_TRANSACTION_COUNT" + + const val LAST_TRANSACTION_SYNCED_TIMESTAMP = "LAST_TRANSACTION_SYNCED_TIMESTAMP" + const val LAST_REFRESH_SUCCESSFUL_TIMESTAMP = "LAST_REFRESH_SUCCESSFUL_TIMESTAMP" + const val USER_NAME = "USER_NAME" +} + +object DbCacheConstants { + const val MONEY_MANAGER_VALUE_PROPOSITION_SCREEN_KEY = + "MONEY_MANAGER_VALUE_PROPOSITION_SCREEN_KEY" + const val MONEY_MANAGER_CONFIG_RESPONSE_KEY = "MONEY_MANAGER_CONFIG_RESPONSE_KEY" +} + +object ScreenNameConstants { + const val MONEY_MANAGER_VALUE_PROPOSITION_SCREEN = "MONEY_MANAGER_VALUE_PROPOSITION_SCREEN" +} + +object MonthConstants { + val monthNames = + listOf( + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/DateTimeConverterAdapter.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/DateTimeConverterAdapter.kt new file mode 100644 index 0000000000..561f74ba73 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/DateTimeConverterAdapter.kt @@ -0,0 +1,49 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.utils + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonPrimitive +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import java.lang.reflect.Type +import org.joda.time.DateTime +import org.joda.time.DateTimeZone + +class DateTimeConverterAdapter : JsonSerializer, JsonDeserializer { + + private fun getDateTimeObjectFromDateTimeString( + dateTime: String?, + timeZone: DateTimeZone = DateTimeZone.getDefault() + ): DateTime? { + dateTime?.let { + if (it.contains("Z") || it.contains("+")) { + return DateTime.parse(it).withZone(timeZone) + } else { + val updatedDateTime = "${it}Z" + return DateTime.parse(updatedDateTime).withZone(timeZone) + } + } ?: return null + } + + override fun deserialize( + json: JsonElement?, + typeOfT: Type?, + context: JsonDeserializationContext? + ): DateTime? = getDateTimeObjectFromDateTimeString(dateTime = json?.asString) + + override fun serialize( + src: DateTime?, + typeOfSrc: Type?, + context: JsonSerializationContext? + ): JsonElement { + return JsonPrimitive(src.toString()) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/MMScreenEventLogger.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/MMScreenEventLogger.kt new file mode 100644 index 0000000000..6700e0dad7 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/MMScreenEventLogger.kt @@ -0,0 +1,34 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner + +@Composable +fun MMScreenEventLogger(onScreenLand: () -> Unit, onScreenExit: () -> Unit) { + val lifecycle = LocalLifecycleOwner.current.lifecycle + DisposableEffect(lifecycle) { + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> { + onScreenLand() + } + Lifecycle.Event.ON_STOP -> { + onScreenExit() + } + else -> Lifecycle.Event.ON_ANY + } + } + lifecycle.addObserver(observer) + onDispose { lifecycle.removeObserver(observer) } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/MockUtils.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/MockUtils.kt new file mode 100644 index 0000000000..c885664184 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/MockUtils.kt @@ -0,0 +1,24 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.utils + +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.navi.base.AppServiceManager +import com.navi.common.network.models.RepoResult +import com.navi.common.viewmodel.getGsonBuilders +import com.navi.moneymanager.R +import java.lang.reflect.Type +import java.nio.charset.StandardCharsets + +fun mockApiResponse(type: Type, jsonKey: String): RepoResult { + val inputStream = AppServiceManager.application.resources.openRawResource(R.raw.mm_mock) + val dataString = String(inputStream.readBytes(), StandardCharsets.UTF_8) + val jsonElement = (JsonParser.parseString(dataString) as? JsonObject)?.get(jsonKey) + return RepoResult(data = getGsonBuilders().fromJson(jsonElement, type)) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/Utils.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/Utils.kt new file mode 100644 index 0000000000..22844bed11 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/Utils.kt @@ -0,0 +1,215 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.utils + +import android.content.Context +import android.icu.util.Calendar +import android.view.Gravity +import android.widget.Toast +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalSavedStateRegistryOwner +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.lifecycle.setViewTreeViewModelStoreOwner +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import com.navi.base.model.CtaData +import com.navi.base.model.LineItem +import com.navi.base.utils.BaseUtils.getDayMonthAndYearFromTimestamp +import com.navi.base.utils.isNotNullAndNotEmpty +import com.navi.common.constants.HELP_CTA_TEXT +import com.navi.moneymanager.R +import com.navi.moneymanager.common.dataprovider.utils.DataProviderConstants.EE_DD_MMM_YYYY +import com.navi.moneymanager.common.dataprovider.utils.DataProviderConstants.MMM +import com.navi.moneymanager.common.dataprovider.utils.DataProviderConstants.YYYY +import com.navi.moneymanager.common.navigation.navigator.MMDeeplinkNavigator.MONEY_MANAGER_ACTIVITY +import com.navi.moneymanager.common.network.model.FinarkeinDataResponse +import com.navi.moneymanager.common.utils.Constants.PRODUCT_HELP_PAGE +import com.navi.moneymanager.common.utils.Constants.PRODUCT_HELP_SCREEN_NAME +import com.navi.moneymanager.common.utils.Constants.REDIRECTION_CTA +import com.navi.moneymanager.common.utils.MonthConstants.monthNames +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlinx.coroutines.delay + +val fourDpRoundedShape = RoundedCornerShape(size = 4.dp) + +fun checkFinarkeinDataValidity(finarkeinDataResponse: FinarkeinDataResponse?): Boolean { + return finarkeinDataResponse?.vendorRequestId.isNotNullAndNotEmpty() && + finarkeinDataResponse?.vendorRedirectUrl.isNotNullAndNotEmpty() && + finarkeinDataResponse?.vendorAuthToken.isNotNullAndNotEmpty() +} + +fun formatDateFromTimestamp(timestampMillis: Long): String { + val date = Date(timestampMillis) + val formatter = SimpleDateFormat(EE_DD_MMM_YYYY, Locale("en", "IN")) + return formatter.format(date) +} + +fun formatMonthFromTimestamp(timestampMillis: Long): String { + val date = Date(timestampMillis) + val formatter = SimpleDateFormat(MMM, Locale("en", "IN")) + return formatter.format(date) +} + +fun formatYearFromTimestamp(timestampMillis: Long): String { + val date = Date(timestampMillis) + val formatter = SimpleDateFormat(YYYY, Locale("en", "IN")) + return formatter.format(date) +} + +fun formatMonthFromInt(month: Int): String { + val formatter = SimpleDateFormat(MMM, Locale("en", "IN")) + return formatter.format(Date(0, month, 1)) +} + +fun Modifier.noIndicationToggleable(value: Boolean, onValueChange: (Boolean) -> Unit): Modifier = + composed { + toggleable( + value = value, + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onValueChange = onValueChange + ) + } + +fun getPastMonthDifferenceFromCurrentWithinOneYear(month: Int): Int { + val currentDateComponents = getDayMonthAndYearFromTimestamp(System.currentTimeMillis()) + val currentMonth = currentDateComponents.second + return (currentMonth - month + 12) % 12 +} + +@Composable +fun ShowCustomToast( + context: Context, + gravity: Int = Gravity.BOTTOM, + xOffset: Int = 0, + yOffset: Int = 100, + toastDuration: ToastDuration, + content: @Composable () -> Unit +) { + val lifecycleOwner = LocalLifecycleOwner.current + val viewModelStoreOwner = LocalViewModelStoreOwner.current + val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current + val toast = remember { Toast(context) } + val toastView = remember { + ComposeView(context).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setViewTreeLifecycleOwner(lifecycleOwner) + setViewTreeViewModelStoreOwner(viewModelStoreOwner) + setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner) + setContent { content() } + } + } + LaunchedEffect(toast) { + toast.apply { + view = toastView + duration = getToastDuration(toastDuration) + setGravity(gravity, xOffset, yOffset) + show() + } + } +} + +fun getToastDuration(duration: ToastDuration): Int = + when (duration) { + ToastDuration.LENGTH_LONG -> Toast.LENGTH_LONG + ToastDuration.LENGTH_SHORT -> Toast.LENGTH_SHORT + } + +enum class ToastDuration { + LENGTH_LONG, + LENGTH_SHORT +} + +fun wrapInSingleQuotes(text: String) = "'$text'" + +fun getTransactionCategorizedMessage(updatedTransactionCount: Int, context: Context): String { + return if (updatedTransactionCount > 1) + "$updatedTransactionCount ${context.getString(R.string.transactions_categorized)}" + else "$updatedTransactionCount ${context.getString(R.string.transaction_categorized)}" +} + +suspend fun MutableState.flipValuePostDelay(initialValue: Boolean, delayInMillis: Long) { + value = initialValue + delay(delayInMillis) + value = !value +} + +fun calculateTimestamps(): Pair { + val now = System.currentTimeMillis() + + // Calculate end timestamp (last millisecond of current month) + val calendarEnd = + Calendar.getInstance().apply { + timeInMillis = now + set(Calendar.DAY_OF_MONTH, getActualMaximum(Calendar.DAY_OF_MONTH)) + set(Calendar.HOUR_OF_DAY, 23) + set(Calendar.MINUTE, 59) + set(Calendar.SECOND, 59) + set(Calendar.MILLISECOND, 999) + } + val endTimestamp = calendarEnd.timeInMillis + + // Calculate start timestamp (first millisecond of month 11 months ago) + val calendarStart = + Calendar.getInstance().apply { + timeInMillis = now + add(Calendar.MONTH, -11) // Go back 11 months + set(Calendar.DAY_OF_MONTH, 1) + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + } + val startTimestamp = calendarStart.timeInMillis + + return Pair(startTimestamp, endTimestamp) +} + +fun getFaqCta(): CtaData { + return CtaData( + url = PRODUCT_HELP_PAGE, + title = HELP_CTA_TEXT, + type = REDIRECTION_CTA, + parameters = + listOf(LineItem(key = PRODUCT_HELP_SCREEN_NAME, value = MONEY_MANAGER_ACTIVITY)) + ) +} + +@Composable +fun ItemsWithDivider( + itemCount: Int, + dividerItem: @Composable () -> Unit, + item: @Composable () -> Unit +) { + repeat(itemCount) { + item() + if (it < itemCount - 1) { + dividerItem() + } + } +} + +fun T.asList(): List { + return listOf(this) +} + +fun getTransactionFilterMonthWithYear(month: Int, year: Int) = "${monthNames[month]} $year" diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/ViewAnimationUtils.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/ViewAnimationUtils.kt new file mode 100644 index 0000000000..f8d08451b1 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/ViewAnimationUtils.kt @@ -0,0 +1,53 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.utils + +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.app.Activity +import android.view.View +import com.navi.common.R as CommonR +import com.navi.common.utils.Constants + +fun Activity.startEnterAnimation() { + this.overridePendingTransition( + CommonR.anim.parallax_slide_in_right, + CommonR.anim.parallax_slide_out_left + ) +} + +fun Activity.startExitAnimation() { + this.overridePendingTransition( + CommonR.anim.parallax_slide_in_left, + CommonR.anim.parallax_slide_out_right + ) +} + +fun moveAnimation(view: View?, width: Float, height: Float, duration: Long) { + // Play animator + val animatorSet = AnimatorSet() + animatorSet.playTogether( + translateX(view = view, width = width, duration = duration), + translateY(view = view, height = height, duration = duration) + ) + animatorSet.start() +} + +fun translateX(view: View?, width: Float, duration: Long): ObjectAnimator { + // x translate animator + val xAnimator = ObjectAnimator.ofFloat(view, Constants.TRANSLATION_X, width) + xAnimator.duration = duration + return xAnimator +} + +fun translateY(view: View?, height: Float, duration: Long): ObjectAnimator { + // y translate animator + val yAnimator = ObjectAnimator.ofFloat(view, Constants.TRANSLATION_Y, height) + yAnimator.duration = duration + return yAnimator +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/flowTransformerUtils.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/flowTransformerUtils.kt new file mode 100644 index 0000000000..8cc560ffca --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/common/utils/flowTransformerUtils.kt @@ -0,0 +1,38 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.common.utils + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow + +@OptIn(ExperimentalCoroutinesApi::class) +fun combineWithFlatMapLatest( + flow1: Flow, + flow2: Flow, + combineTransform: (T1, T2) -> TransformationResult, + flatMapLatestTransform: suspend FlowCollector.(TransformationResult) -> Unit +): Flow { + return combine(flow1, flow2) { t1, t2 -> combineTransform(t1, t2) } + .flatMapLatest { combinedResult -> flow { flatMapLatestTransform(combinedResult) } } +} + +@OptIn(ExperimentalCoroutinesApi::class) +fun combineWithFlatMapLatest( + flow1: Flow, + flow2: Flow, + flow3: Flow, + combineTransform: (T1, T2, T3) -> TransformationResult, + flatMapLatestTransform: suspend FlowCollector.(TransformationResult) -> Unit +): Flow { + return combine(flow1, flow2, flow3) { t1, t2, t3 -> combineTransform(t1, t2, t3) } + .flatMapLatest { combinedResult -> flow { flatMapLatestTransform(combinedResult) } } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/entry/repo/MMSharedRepository.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/entry/repo/MMSharedRepository.kt new file mode 100644 index 0000000000..25c2e75955 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/entry/repo/MMSharedRepository.kt @@ -0,0 +1,29 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.entry.repo + +import com.navi.common.network.models.RepoResult +import com.navi.moneymanager.common.dataprovider.domain.LauncherDataProvider +import com.navi.moneymanager.common.dataprovider.domain.RemoteDataProvider +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.network.model.OnboardingStatusResponse +import javax.inject.Inject + +class MMSharedRepository +@Inject +constructor( + private val remoteDataProvider: RemoteDataProvider, + private val launcherDataProvider: LauncherDataProvider +) { + + suspend fun fetchUserOnboardingStatus(): RepoResult = + remoteDataProvider.fetchUserOnboardingStatus( + screenName = MMScreen.DASHBOARD.screen, + naeApplicable = false, + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/entry/ui/NavHostEngine.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/entry/ui/NavHostEngine.kt new file mode 100644 index 0000000000..17e081c77d --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/entry/ui/NavHostEngine.kt @@ -0,0 +1,51 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.entry.ui + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.runtime.Composable +import com.navi.moneymanager.common.utils.Constants.TRANSITION_DURATION_IN_MILLIS +import com.ramcosta.composedestinations.animations.defaults.RootNavGraphDefaultAnimations +import com.ramcosta.composedestinations.animations.rememberAnimatedNavHostEngine +import com.ramcosta.composedestinations.spec.NavHostEngine + +@OptIn(ExperimentalAnimationApi::class) +@Composable +fun naviHostEngine(): NavHostEngine { + return rememberAnimatedNavHostEngine( + rootDefaultAnimations = + RootNavGraphDefaultAnimations( + enterTransition = { + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Left, + animationSpec = tween(TRANSITION_DURATION_IN_MILLIS) + ) + }, + exitTransition = { + slideOutOfContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Left, + animationSpec = tween(TRANSITION_DURATION_IN_MILLIS) + ) + }, + popEnterTransition = { + slideIntoContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Right, + animationSpec = tween(TRANSITION_DURATION_IN_MILLIS) + ) + }, + popExitTransition = { + slideOutOfContainer( + towards = AnimatedContentTransitionScope.SlideDirection.Companion.Right, + animationSpec = tween(TRANSITION_DURATION_IN_MILLIS) + ) + } + ) + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/entry/ui/activity/MMActivity.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/entry/ui/activity/MMActivity.kt new file mode 100644 index 0000000000..898e70bbd8 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/entry/ui/activity/MMActivity.kt @@ -0,0 +1,137 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.entry.ui.activity + +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import androidx.activity.SystemBarStyle +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.compose.rememberNavController +import com.navi.base.model.CtaData +import com.navi.base.utils.EMPTY +import com.navi.common.model.ModuleNameV2 +import com.navi.common.navigation.NavigationAction +import com.navi.common.utils.safeLaunch +import com.navi.moneymanager.NavGraphs +import com.navi.moneymanager.common.analytics.FinarkeinEventTrackerImpl +import com.navi.moneymanager.common.navigation.navigator.MMDeeplinkNavigator.MONEY_MANAGER_ACTIVITY +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.base.MMBaseActivity +import com.navi.moneymanager.common.ui.theme.MoneyManagerMaterialTheme +import com.navi.moneymanager.destinations.DashboardScreenDestination +import com.navi.moneymanager.destinations.LauncherScreenDestination +import com.navi.moneymanager.entry.ui.naviHostEngine +import com.navi.moneymanager.entry.viewmodel.MMSharedViewModel +import com.ramcosta.composedestinations.DestinationsNavHost +import com.ramcosta.composedestinations.navigation.dependency +import dagger.hilt.android.AndroidEntryPoint +import io.finarkein.anubhav.Finarkein +import io.finarkein.anubhav.OpenFinarkeinAnubhav +import io.finarkein.anubhav.configuration.AnubhavConfiguration +import io.finarkein.anubhav.configuration.Auth +import io.finarkein.anubhav.result.AnubhavExit +import io.finarkein.anubhav.result.AnubhavSuccess +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class MMActivity : MMBaseActivity(), Auth { + + private val mmSharedVM by viewModels() + + private var finarkeinAccessToken = EMPTY + private val anubhav = + registerForActivityResult(OpenFinarkeinAnubhav()) { anubhavResult -> + when (anubhavResult) { + is AnubhavSuccess -> { + FinarkeinEventTrackerImpl.finarkeinSuccess(anubhavResult) + mmNavigator.navigateTo( + activity = this, + ctaData = CtaData(url = MMScreen.ACCOUNT_LINKING_SUCCESS.screen), + navigationAction = NavigationAction.Default + ) + } + is AnubhavExit -> mmSharedVM.handleFinarkeinError(anubhavResult) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT), + navigationBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT) + ) + val isUserOnBoarded = mmSharedVM.getUserOnboardingStatus() + mmSharedVM.syncLocalDataWithRemote( + isUserOnBoarded, + navigateToScreen = { screen, bundle -> + lifecycleScope.safeLaunch(Dispatchers.Main) { + mmNavigator.navigateTo( + activity = this@MMActivity, + ctaData = CtaData(url = screen), + bundle = bundle, + navigationAction = NavigationAction.ClearStackAndNext + ) + } + } + ) + initComposableContent(isUserOnBoarded) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + mmSharedVM.navigateToDashboardWithClearStack(mmNavigator) + } + + private fun initComposableContent(isUserOnBoarded: Boolean) { + setContent { + navController = rememberNavController() + MoneyManagerMaterialTheme { + DestinationsNavHost( + startRoute = + if (isUserOnBoarded) DashboardScreenDestination + else LauncherScreenDestination, + navGraph = NavGraphs.root, + engine = naviHostEngine(), + navController = navController, + dependenciesContainerBuilder = { dependency(this@MMActivity) } + ) + } + } + } + + fun launchFinarkeinSdk( + accessToken: String, + requestId: String, + redirectUrl: String, + ) { + finarkeinAccessToken = accessToken + val inputs = + AnubhavConfiguration.withWebViewUrl(redirectUrl).requestId(requestId).auth(this).build() + Finarkein.setEventListener { event -> + FinarkeinEventTrackerImpl.finarkeinJourneyEvent(event) + } + anubhav.launch(inputs) + } + + fun getSharedVM(): MMSharedViewModel { + return mmSharedVM + } + + override suspend fun token() = finarkeinAccessToken + + override val screenName: String + get() = MONEY_MANAGER_ACTIVITY + + override val moduleName: ModuleNameV2 + get() = ModuleNameV2.MONEY_MANAGER +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/entry/viewmodel/MMSharedViewModel.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/entry/viewmodel/MMSharedViewModel.kt new file mode 100644 index 0000000000..50fd18e81b --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/entry/viewmodel/MMSharedViewModel.kt @@ -0,0 +1,107 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.entry.viewmodel + +import android.os.Bundle +import androidx.lifecycle.viewModelScope +import com.navi.base.model.CtaData +import com.navi.base.sharedpref.PreferenceManager +import com.navi.common.alchemist.model.AlchemistScreenDefinition +import com.navi.common.navigation.NavigationAction +import com.navi.common.network.models.isSuccessWithData +import com.navi.common.utils.FirebaseEventFacade +import com.navi.common.viewmodel.BaseVM +import com.navi.moneymanager.common.analytics.FinarkeinEventTrackerImpl +import com.navi.moneymanager.common.navigation.navigator.MMNavigator +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.utils.Constants.IS_CONSENT_REVOKED +import com.navi.moneymanager.common.utils.Constants.MM_IS_USER_ONBOARDED +import com.navi.moneymanager.entry.repo.MMSharedRepository +import com.navi.moneymanager.entry.ui.activity.MMActivity +import dagger.hilt.android.lifecycle.HiltViewModel +import io.finarkein.anubhav.constants.AnubhavConstants +import io.finarkein.anubhav.result.AnubhavExit +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +@HiltViewModel +class MMSharedViewModel +@Inject +constructor( + private val sharedRepository: MMSharedRepository, + val firebaseEventFacade: FirebaseEventFacade +) : BaseVM() { + var valuePropScreenDefinition: AlchemistScreenDefinition? = null + private set + + private val _showFinarkeinErrorBottomSheet = MutableSharedFlow(replay = 1) + val showFinarkeinErrorBottomSheet = _showFinarkeinErrorBottomSheet.asSharedFlow() + + private fun fetchUserOnboardingStatus( + navigateToScreen: (screen: String, bundle: Bundle?) -> Unit + ) { + viewModelScope.safeLaunch(Dispatchers.IO) { + val response = sharedRepository.fetchUserOnboardingStatus() + if (response.isSuccessWithData() && response.data?.isOnboarded == false) { + navigateToScreen( + MMScreen.LAUNCHER.screen, + Bundle().apply { putBoolean(IS_CONSENT_REVOKED, true) } + ) + } + } + } + + fun setValuePropScreenDefinition(newDefinition: AlchemistScreenDefinition) { + valuePropScreenDefinition = newDefinition + } + + fun getUserOnboardingStatus(): Boolean { + return PreferenceManager.getBooleanPreference(key = MM_IS_USER_ONBOARDED) + } + + fun syncLocalDataWithRemote( + isUserOnBoarded: Boolean, + navigateToScreen: (screen: String, bundle: Bundle?) -> Unit + ) { + if (isUserOnBoarded) { + fetchUserOnboardingStatus(navigateToScreen = navigateToScreen) + } + } + + fun handleFinarkeinError(anubhavExit: AnubhavExit) { + val ignoredErrorCodes = + setOf( + AnubhavConstants.ERROR_CODE_JOURNEY_CANCELLED, + AnubhavConstants.ERROR_CODE_ALTERNATIVE_CHOSEN, + AnubhavConstants.ERROR_CODE_CONSENT_REJECTED + ) + if (anubhavExit.error?.errorCode in ignoredErrorCodes) { + FinarkeinEventTrackerImpl.finarkeinUserExit(anubhavExit) + } else { + FinarkeinEventTrackerImpl.finarkeinError(anubhavExit) + updateFinarkeinErrorBottomSheetState(show = true) + } + } + + fun updateFinarkeinErrorBottomSheetState(show: Boolean) { + viewModelScope.safeLaunch { _showFinarkeinErrorBottomSheet.emit(show) } + } + + context(MMActivity) + fun navigateToDashboardWithClearStack(mmNavigator: MMNavigator) { + if (getUserOnboardingStatus()) { + mmNavigator.navigateTo( + activity = this@MMActivity, + ctaData = CtaData(url = MMScreen.DASHBOARD.screen), + navigationAction = NavigationAction.ClearStackAndNext + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategoryDetailsScreenData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategoryDetailsScreenData.kt new file mode 100644 index 0000000000..24e1887a67 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategoryDetailsScreenData.kt @@ -0,0 +1,45 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.categorydetails.model + +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.model.BarGraphData +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.common.model.ZeroTransactionData +import com.navi.moneymanager.common.model.sectionHeader.SectionHeaderData + +data class CategoryDetailsScreenData( + val totalSpendSection: CategoryTotalSpendSectionData? = null, + val barGraphData: BarGraphData? = null, + val sortOption: SortOption = SortOption.RECENT_FIRST, + val transactions: List? = null, + val zeroTransactionData: ZeroTransactionData? = null, +) + +data class CategoryTotalSpendSectionData( + val iconUrl: IllustrationSource, + val title: String, + val selectedMonth: String, + val actionIcon: IllustrationSource, + val amount: String, + val selectedBankReferenceIds: Set, + val spendingTrendSectionData: SectionHeaderData? = null, + val category: CategoryTotalSpendSectionCategoryDataData, +) + +data class CategoryTotalSpendSectionCategoryDataData( + val categoryId: String, + val categoryName: String, + val infoIconUrl: IllustrationSource? = null, +) + +enum class SortOption { + HIGHEST_FIRST, + LOWEST_FIRST, + RECENT_FIRST +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategoryDetailsScreenNavigationData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategoryDetailsScreenNavigationData.kt new file mode 100644 index 0000000000..57032dd5e7 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategoryDetailsScreenNavigationData.kt @@ -0,0 +1,22 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.categorydetails.model + +import android.os.Parcelable +import androidx.compose.runtime.Immutable +import com.navi.common.navigation.NavigationScreenData +import kotlinx.parcelize.Parcelize + +@Parcelize +@Immutable +data class CategoryDetailsScreenNavigationData( + val month: Int, + val year: Int, + val selectedCategory: String, + val selectedBanks: Set? = null, +) : Parcelable, NavigationScreenData diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategoryDetailsScreenUiEffect.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategoryDetailsScreenUiEffect.kt new file mode 100644 index 0000000000..4670eea762 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategoryDetailsScreenUiEffect.kt @@ -0,0 +1,35 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.categorydetails.model + +import androidx.compose.runtime.Immutable +import com.navi.base.model.CtaData +import com.navi.common.basemvi.UiEffect +import com.navi.moneymanager.common.model.Transaction + +@Immutable +sealed class CategoryDetailsScreenUiEffect : UiEffect { + + @Immutable + sealed class Navigation : CategoryDetailsScreenUiEffect() { + data object Back : Navigation() + + data object Help : Navigation() + + data class TransactionDetails(val transactionId: String) : Navigation() + + @Immutable data class NavigateToCta(val ctaData: CtaData) : Navigation() + } + + data class OpenCategoryBottomSheet(val transaction: Transaction) : + CategoryDetailsScreenUiEffect() + + data class SortTransactions(val sortOption: SortOption) : CategoryDetailsScreenUiEffect() + + data object FetchConsentUrl : CategoryDetailsScreenUiEffect() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategoryDetailsScreenUiEvent.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategoryDetailsScreenUiEvent.kt new file mode 100644 index 0000000000..8673298412 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategoryDetailsScreenUiEvent.kt @@ -0,0 +1,38 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.categorydetails.model + +import androidx.compose.runtime.Immutable +import com.navi.common.basemvi.UiEvent +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.common.model.bottomSheet.CategoryDetailsScreenBottomSheets +import com.navi.moneymanager.postonboard.help.model.HelpBottomSheetState + +@Immutable +sealed interface CategoryDetailsScreenUiEvent : UiEvent { + @Immutable + data class RenderUI(val data: CategoryDetailsScreenData) : CategoryDetailsScreenUiEvent + + data class ShowBottomSheet(val type: CategoryDetailsScreenBottomSheets) : + CategoryDetailsScreenUiEvent + + data class DismissBottomSheet( + val type: Class = + CategoryDetailsScreenBottomSheets.NoBottomSheet::class.java + ) : CategoryDetailsScreenUiEvent + + data class UpdateTransactionData(val transaction: Transaction) : CategoryDetailsScreenUiEvent + + data class UpdateTransactionsSortingOrder( + val sortOption: SortOption, + val transactions: List + ) : CategoryDetailsScreenUiEvent + + data class UpdateHelpBottomSheetState(val state: HelpBottomSheetState) : + CategoryDetailsScreenUiEvent +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategoryDetailsScreenUiState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategoryDetailsScreenUiState.kt new file mode 100644 index 0000000000..635e827ac4 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategoryDetailsScreenUiState.kt @@ -0,0 +1,30 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.categorydetails.model + +import androidx.compose.runtime.Immutable +import com.navi.common.basemvi.UiState +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.common.model.bottomSheet.BottomSheetState +import com.navi.moneymanager.common.model.bottomSheet.CategoryDetailsScreenBottomSheets + +@Immutable +data class CategoryDetailsScreenUiState( + val screenData: CategoryDetailsScreenData? = null, + val bottomSheetState: + BottomSheetState, + val transaction: Transaction? = null +) : UiState { + companion object { + val initialState = + CategoryDetailsScreenUiState( + bottomSheetState = + BottomSheetState(type = CategoryDetailsScreenBottomSheets.NoBottomSheet) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategorySelectionBottomSheetData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategorySelectionBottomSheetData.kt new file mode 100644 index 0000000000..bdb6d80696 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/CategorySelectionBottomSheetData.kt @@ -0,0 +1,22 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.categorydetails.model + +data class CategorySelectionBottomSheetData( + val selectedCategory: String, + val categoryList: List, + val ctaText: String, + val headerTitle: String, +) + +data class CategorySelectionBottomSheetCategoryData( + val categoryId: String, + val iconUrl: String, + val categoryName: String, + val isSelected: Boolean +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/UncategorizedInfoBottomSheetData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/UncategorizedInfoBottomSheetData.kt new file mode 100644 index 0000000000..827c3a70da --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/model/UncategorizedInfoBottomSheetData.kt @@ -0,0 +1,17 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.categorydetails.model + +import com.navi.moneymanager.common.illustration.model.IllustrationSource + +data class UncategorizedInfoBottomSheetData( + val title: String, + val description: String, + val ctaText: String, + val describeIllustration: IllustrationSource, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/reducer/CategoryDetailsScreenReducer.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/reducer/CategoryDetailsScreenReducer.kt new file mode 100644 index 0000000000..dec85d0702 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/reducer/CategoryDetailsScreenReducer.kt @@ -0,0 +1,69 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.categorydetails.reducer + +import com.navi.common.basemvi.BaseReducer +import com.navi.moneymanager.common.model.bottomSheet.BottomSheetState +import com.navi.moneymanager.common.model.bottomSheet.CategoryDetailsScreenBottomSheets +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenUiEvent +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenUiState + +class CategoryDetailsScreenReducer : + BaseReducer { + + override fun reduce( + previousState: CategoryDetailsScreenUiState, + event: CategoryDetailsScreenUiEvent + ): CategoryDetailsScreenUiState { + return when (event) { + is CategoryDetailsScreenUiEvent.RenderUI -> { + previousState.copy(screenData = event.data) + } + is CategoryDetailsScreenUiEvent.DismissBottomSheet -> { + val currentBottomSheet = previousState.bottomSheetState.type + if ( + event.type == CategoryDetailsScreenBottomSheets.NoBottomSheet::class.java || + event.type.isInstance(currentBottomSheet) + ) { + previousState.copy( + bottomSheetState = previousState.bottomSheetState.copy(isVisible = false) + ) + } else previousState + } + is CategoryDetailsScreenUiEvent.ShowBottomSheet -> { + previousState.copy( + bottomSheetState = BottomSheetState(isVisible = true, type = event.type) + ) + } + is CategoryDetailsScreenUiEvent.UpdateTransactionData -> { + previousState.copy(transaction = event.transaction) + } + is CategoryDetailsScreenUiEvent.UpdateTransactionsSortingOrder -> { + previousState.copy( + screenData = + previousState.screenData?.copy( + sortOption = event.sortOption, + transactions = event.transactions + ) + ) + } + is CategoryDetailsScreenUiEvent.UpdateHelpBottomSheetState -> { + val currentBottomSheetType = previousState.bottomSheetState.type + if (currentBottomSheetType is CategoryDetailsScreenBottomSheets.HelpBottomSheet) { + previousState.copy( + bottomSheetState = + previousState.bottomSheetState.copy( + type = + CategoryDetailsScreenBottomSheets.HelpBottomSheet(event.state) + ) + ) + } else previousState + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/repo/CategoryDetailsRepository.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/repo/CategoryDetailsRepository.kt new file mode 100644 index 0000000000..ea9da09f03 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/repo/CategoryDetailsRepository.kt @@ -0,0 +1,50 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.categorydetails.repo + +import com.navi.moneymanager.common.dataprovider.domain.CategoryDetailsLocalDataProvider +import com.navi.moneymanager.postonboard.categorydetails.model.SortOption +import javax.inject.Inject + +class CategoryDetailsRepository +@Inject +constructor(private val categoryDetailsLocalDataProvider: CategoryDetailsLocalDataProvider) { + + suspend fun getSortedTransactions(sort: SortOption) = + categoryDetailsLocalDataProvider.getSortedTransactions(sortOption = sort) + + suspend fun getCategoryDetailsScreenData( + month: Int?, + year: Int?, + sort: SortOption, + selectedBankReferenceIds: Set, + category: String, + ) = + categoryDetailsLocalDataProvider.getCategoryDetailsScreenData( + month, + year, + sort, + selectedBankReferenceIds, + category, + ) + + suspend fun getMonthSelectionBottomSheetData(displayMonthText: String) = + categoryDetailsLocalDataProvider.getMonthSelectionBottomSheetData(displayMonthText) + + suspend fun getBankSelectionBottomSheetData(selectedBankReferenceIds: Set) = + categoryDetailsLocalDataProvider.getBankSelectionBottomSheetData(selectedBankReferenceIds) + + suspend fun getPastMonthDataLoadingBottomSheetData() = + categoryDetailsLocalDataProvider.getPastMonthDataLoadingBottomSheetData() + + suspend fun getCategorySelectionBottomSheetData(selectedCategory: String) = + categoryDetailsLocalDataProvider.getCategorySelectionBottomSheetData(selectedCategory) + + fun getUncategorizedInfoBottomSheetData() = + categoryDetailsLocalDataProvider.getUncategorizedInfoBottomSheetData() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/ui/CategoryDetailsScaffoldRenderer.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/ui/CategoryDetailsScaffoldRenderer.kt new file mode 100644 index 0000000000..9dca7b5c70 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/ui/CategoryDetailsScaffoldRenderer.kt @@ -0,0 +1,346 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.categorydetails.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +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.fillMaxHeight +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.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Scaffold +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +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.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.base.utils.EMPTY +import com.navi.common.R as CommonR +import com.navi.common.constants.HELP_CTA_TEXT +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.R +import com.navi.moneymanager.common.analytics.CategoryDetailsEventTrackerImpl +import com.navi.moneymanager.common.composable.bankselectionbottomsheet.ZeroTransactionView +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.SORT_ICON +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.model.SelectedMonth +import com.navi.moneymanager.common.model.sectionHeader.SectionHeaderData +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.MMTopBar +import com.navi.moneymanager.common.ui.composable.barGraph.MMBarGraph +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.composable.sectionHeaders.SectionHeader +import com.navi.moneymanager.common.ui.composable.transaction.Transaction +import com.navi.moneymanager.common.ui.composable.transaction.TransactionDivider +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenUiEffect +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenUiState +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryTotalSpendSectionData +import com.navi.moneymanager.postonboard.categorydetails.model.SortOption +import com.navi.moneymanager.postonboard.categorydetails.viewmodel.CategoryDetailsVM + +@Composable +fun CategoryDetailsScaffoldRenderer( + modifier: Modifier = Modifier, + categoryDetailsScreenUiState: () -> CategoryDetailsScreenUiState, + getViewModel: () -> CategoryDetailsVM, + onMonthChangeClick: (String) -> Unit, + onBankSelectionRequest: (Set) -> Unit, + onAverageInfoClick: (String?) -> Unit, + onCategoryClick: (String) -> Unit, + onInfoButtonClick: () -> Unit, + onSortTransactionsRequest: () -> Unit, + onBarGraphElementClicked: (SelectedMonth) -> Unit +) { + Scaffold( + modifier = modifier, + containerColor = MMColor.white, + topBar = { + MMTopBar( + title = EMPTY, + titleColor = MMColor.ctaPrimary, + navigationIcon = CommonR.drawable.ic_arrow_left_black_v2, + actionIconText = HELP_CTA_TEXT, + onActionClick = { + CategoryDetailsEventTrackerImpl.onCategoryDetailsHelpClicked() + getViewModel().setEffect { CategoryDetailsScreenUiEffect.Navigation.Help } + }, + onNavigationIconClick = { + getViewModel().setEffect { CategoryDetailsScreenUiEffect.Navigation.Back } + } + ) + }, + ) { + LazyColumn( + modifier = Modifier.background(Color.White).padding(it).fillMaxHeight(), + state = rememberLazyListState(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + categoryDetailsScreenUiState().screenData?.totalSpendSection?.let { totalSpendSection -> + item { + CategoryTotalSpendSection( + totalSpendSection, + onMonthChangeClick, + onBankSelectionRequest, + onCategoryClick, + onInfoButtonClick = onInfoButtonClick + ) + } + } + categoryDetailsScreenUiState().screenData?.barGraphData?.let { barGraphData -> + item { + MMBarGraph( + screenName = MMScreen.CATEGORY_DETAILS.screen, + barGraphData = barGraphData, + onAverageInfoClick = onAverageInfoClick, + onBarGraphElementClicked = onBarGraphElementClicked + ) + } + } + item { Spacer(modifier = Modifier.height(16.dp)) } + val transactions = categoryDetailsScreenUiState().screenData?.transactions + if (transactions.isNullOrEmpty()) { + categoryDetailsScreenUiState().screenData?.zeroTransactionData?.let { + zeroTransactionData -> + item { + SectionHeader( + modifier = Modifier.padding(horizontal = 16.dp), + headerModel = + SectionHeaderData( + title = "Category transactions", + ), + isActionVisible = false, + ) + ZeroTransactionView( + title = zeroTransactionData.title, + illustrationSource = zeroTransactionData.illustrationSource, + modifier = Modifier.fillMaxWidth().padding(vertical = 32.dp), + ) + } + } + } else { + categoryDetailsScreenUiState().screenData?.sortOption?.let { currentSortOption -> + item { + SectionHeader( + modifier = Modifier.padding(horizontal = 16.dp), + headerModel = + SectionHeaderData( + title = "Category transactions", + actionText = + stringResource( + when (currentSortOption) { + SortOption.HIGHEST_FIRST -> R.string.highest_first + SortOption.LOWEST_FIRST -> R.string.lowest_first + SortOption.RECENT_FIRST -> R.string.recent_first + } + ), + suffixIcon = + IllustrationSource.Remote( + url = SORT_ICON, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ), + onAction = onSortTransactionsRequest, + isActionVisible = true, + ) + } + } + + itemsIndexed(transactions) { index, transaction -> + Transaction( + transaction = transaction, + onClick = { transactionId -> + CategoryDetailsEventTrackerImpl.onCategoryDetailsTransactionClicked( + transactionRank = index + 1 + ) + getViewModel().setEffect { + CategoryDetailsScreenUiEffect.Navigation.TransactionDetails( + transactionId = transactionId + ) + } + }, + onCategoryClick = { txn -> + CategoryDetailsEventTrackerImpl + .onCategoryDetailsTransactionCategoryClicked( + transactionRank = index + 1, + category = txn.categoryId + ) + getViewModel().setEffect { + CategoryDetailsScreenUiEffect.OpenCategoryBottomSheet(txn) + } + }, + ) + + if (index < transactions.lastIndex) { + TransactionDivider() + } + } + } + } + } +} + +@Composable +private fun CategoryTotalSpendSection( + data: CategoryTotalSpendSectionData, + onMonthChangeClick: (String) -> Unit, + onBankSelectionRequest: (Set) -> Unit, + onCategoryClick: (String) -> Unit, + onInfoButtonClick: () -> Unit, +) { + Column( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = + Modifier.size(48.dp).background(color = MMColor.ctaSecondary, shape = CircleShape), + contentAlignment = Alignment.Center + ) { + Illustration( + illustrationType = IllustrationType.Image(data.iconUrl), + modifier = Modifier.size(32.dp) + ) + } + CategoryRow( + data, + onCategoryClick, + onInfoButtonClick, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth() + ) { + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f).padding(end = 4.dp) + ) { + MMText( + text = data.title, + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 18.sp + ) + } + VerticalDivider( + modifier = Modifier.height(10.dp).width(1.dp), + color = MMColor.textTertiary + ) + Row( + modifier = + Modifier.weight(1f) + .padding(start = 4.dp) + .onClickWithDebounce( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { onMonthChangeClick(data.selectedMonth) } + ), + verticalAlignment = Alignment.CenterVertically + ) { + MMText( + text = data.selectedMonth.uppercase(), + color = MMColor.textPrimary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 18.sp + ) + Box(Modifier.padding(bottom = 2.dp, end = 4.dp)) { + Illustration( + illustrationType = IllustrationType.Image(data.actionIcon), + modifier = Modifier.size(16.dp) + ) + } + } + } + Spacer(Modifier.height(8.dp)) + MMText( + text = data.amount, + color = MMColor.textPrimary, + fontSize = 24.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp + ) + Spacer(Modifier.height(40.dp)) + + data.spendingTrendSectionData?.let { + SectionHeader( + headerModel = it, + onAction = { onBankSelectionRequest(data.selectedBankReferenceIds) } + ) + } + } +} + +@Composable +private fun CategoryRow( + data: CategoryTotalSpendSectionData, + onCategoryClick: (String) -> Unit, + onInfoButtonClick: () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 16.dp, top = 8.dp) + ) { + Row( + modifier = + Modifier.background(color = MMColor.ctaSecondary, shape = RoundedCornerShape(32.dp)) + .clip(RoundedCornerShape(32.dp)) + .onClickWithDebounce { onCategoryClick(data.category.categoryId) } + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + MMText( + text = data.category.categoryName, + modifier = Modifier.padding(horizontal = 4.dp), + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD + ) + Illustration( + illustrationType = IllustrationType.Image(data.actionIcon), + modifier = Modifier.size(16.dp) + ) + } + data.category.infoIconUrl?.let { + Spacer(Modifier.width(8.dp)) + Illustration( + illustrationType = IllustrationType.Image(it), + modifier = + Modifier.size(16.dp).onClickWithDebounce( + interactionSource = remember { MutableInteractionSource() }, + ) { + onInfoButtonClick() + } + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/ui/CategoryDetailsScreen.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/ui/CategoryDetailsScreen.kt new file mode 100644 index 0000000000..e9ccff2552 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/ui/CategoryDetailsScreen.kt @@ -0,0 +1,364 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.categorydetails.ui + +import android.os.Bundle +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.navi.base.model.CtaData +import com.navi.base.model.LineItem +import com.navi.base.utils.BaseUtils.getDayMonthAndYearFromTimestamp +import com.navi.base.utils.orZero +import com.navi.common.utils.Constants.WEB_URL +import com.navi.moneymanager.common.analytics.CategoryDetailsEventTrackerImpl +import com.navi.moneymanager.common.composable.bankselectionbottomsheet.BankSelectionBottomSheetContent +import com.navi.moneymanager.common.dataprovider.data.datastore.DataStoreInfoProvider +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.model.bottomSheet.CategoryDetailsScreenBottomSheets +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.navigation.utils.navigateToPreviousScreen +import com.navi.moneymanager.common.ui.composable.DataLoadingBottomSheetContent +import com.navi.moneymanager.common.ui.composable.ScreenInit +import com.navi.moneymanager.common.ui.composable.bottomSheet.AverageInfoBottomSheetContent +import com.navi.moneymanager.common.ui.composable.bottomSheet.BottomSheet +import com.navi.moneymanager.common.ui.composable.bottomSheet.MonthSelectionBottomSheetUI +import com.navi.moneymanager.common.utils.Constants.GREEN_TICK_MARK +import com.navi.moneymanager.common.utils.Constants.IS_TOTAL_SYNC_COMPLETED +import com.navi.moneymanager.common.utils.Constants.TRANSACTION_ID +import com.navi.moneymanager.common.utils.MMScreenEventLogger +import com.navi.moneymanager.common.utils.MonthConstants +import com.navi.moneymanager.common.utils.ShowCustomToast +import com.navi.moneymanager.common.utils.ToastDuration +import com.navi.moneymanager.common.utils.getFaqCta +import com.navi.moneymanager.common.utils.getPastMonthDifferenceFromCurrentWithinOneYear +import com.navi.moneymanager.common.utils.getTransactionCategorizedMessage +import com.navi.moneymanager.entry.ui.activity.MMActivity +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenNavigationData +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenUiEffect +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenUiEvent +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenUiState +import com.navi.moneymanager.postonboard.categorydetails.viewmodel.CategoryDetailsVM +import com.navi.moneymanager.postonboard.help.ui.HelpBottomSheetContent +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetTransactionData +import com.navi.moneymanager.postonboard.monthlysummary.ui.bottomsheet.CategoryCommonBottomSheet +import com.navi.moneymanager.postonboard.monthlysummary.ui.bottomsheet.CustomToastView +import com.navi.naviwidgets.utils.URL +import com.ramcosta.composedestinations.annotation.Destination +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach + +@Destination +@Composable +fun CategoryDetailsScreen( + activity: MMActivity, + data: CategoryDetailsScreenNavigationData?, + viewModel: CategoryDetailsVM = hiltViewModel(), +) { + val screenData = rememberSaveable { mutableStateOf(data) } + val state by viewModel.state.collectAsStateWithLifecycle() + val showBottomSheet = remember { mutableStateOf(false) } + var showTransactionUpdatedToast by remember { mutableStateOf(false) } + var updatedTransactionCount by remember { mutableIntStateOf(0) } + MMScreenEventLogger( + onScreenLand = { + val selectedMonthIndex = + getPastMonthDifferenceFromCurrentWithinOneYear( + screenData.value?.month + ?: getDayMonthAndYearFromTimestamp(System.currentTimeMillis()).second + ) + CategoryDetailsEventTrackerImpl.onCategoryDetailsScreenLanded( + category = screenData.value?.selectedCategory.orEmpty(), + monthIndex = selectedMonthIndex, + numberOfBanks = screenData.value?.selectedBanks?.size.orZero() + ) + }, + onScreenExit = { CategoryDetailsEventTrackerImpl.onCategoryDetailsScreenExit() } + ) + + ScreenInit(screenName = MMScreen.CATEGORY_DETAILS.screen, activity = activity) + + LaunchedEffect(Unit) { + viewModel.effect + .onEach { effect -> + when (effect) { + is CategoryDetailsScreenUiEffect.Navigation.Back -> { + navigateToPreviousScreen(activity) + } + is CategoryDetailsScreenUiEffect.Navigation.NavigateToCta -> { + activity.mmNavigator.navigateTo( + activity = activity, + ctaData = effect.ctaData + ) + } + CategoryDetailsScreenUiEffect.Navigation.Help -> { + viewModel.sendEvent( + CategoryDetailsScreenUiEvent.ShowBottomSheet( + type = CategoryDetailsScreenBottomSheets.HelpBottomSheet() + ) + ) + } + is CategoryDetailsScreenUiEffect.Navigation.TransactionDetails -> { + activity.mmNavigator.navigateTo( + activity = activity, + ctaData = CtaData(url = MMScreen.TRANSACTION_DETAILS.screen), + bundle = + Bundle().apply { putString(TRANSACTION_ID, effect.transactionId) } + ) + } + is CategoryDetailsScreenUiEffect.OpenCategoryBottomSheet -> { + viewModel.sendEvent( + CategoryDetailsScreenUiEvent.UpdateTransactionData(effect.transaction) + ) + showBottomSheet.value = true + } + is CategoryDetailsScreenUiEffect.SortTransactions -> { + viewModel.sortTransactions(sortOption = effect.sortOption) + } + CategoryDetailsScreenUiEffect.FetchConsentUrl -> { + viewModel.fetchConsentUrl() + } + } + } + .collect() + } + + LaunchedEffect(screenData.value) { + viewModel.updateScreenParams( + screenData.value?.month, + screenData.value?.year, + screenData.value?.selectedBanks, + screenData.value?.selectedCategory, + ) + } + + BackHandler { viewModel.setEffect { CategoryDetailsScreenUiEffect.Navigation.Back } } + CategoryDetailsScaffoldRenderer( + modifier = Modifier.fillMaxSize(), + categoryDetailsScreenUiState = { state }, + getViewModel = { viewModel }, + onMonthChangeClick = { + CategoryDetailsEventTrackerImpl.onCategoryDetailsMonthSelectionRequested() + viewModel.handleMonthSelectBottomSheet(it) + }, + onBankSelectionRequest = { + CategoryDetailsEventTrackerImpl.onCategoryDetailsBankSelectionRequested() + viewModel.handleBankSelectBottomSheet(it) + }, + onAverageInfoClick = { averageValue -> + CategoryDetailsEventTrackerImpl.onCategoryDetailsGraphAverageInfoClicked() + viewModel.handleAverageInfoBottomSheet(averageValue) + }, + onCategoryClick = { + CategoryDetailsEventTrackerImpl.onCategoryDetailsCategorySelectionRequested() + viewModel.handleCategorySelectionBottomSheet(it) + }, + onInfoButtonClick = { + CategoryDetailsEventTrackerImpl.onCategoryDetailsUncategorizedInfoClicked() + viewModel.handleUncategorizedInfoBottomSheet() + }, + onSortTransactionsRequest = { + CategoryDetailsEventTrackerImpl.onCategoryDetailsSortTransactionsRequested() + viewModel.handleSortTransactionsBottomSheet() + }, + onBarGraphElementClicked = { + screenData.value = + screenData.value?.copy( + month = it.month.orZero(), + year = it.year.orZero(), + ) + } + ) + BottomSheet( + state = state.bottomSheetState, + onDismiss = { type, uiEvent -> viewModel.sendEvent(uiEvent) } + ) { bottomSheet -> + CategoryDetailsBottomSheetContentHandler( + state = state, + dataStoreInfoProvider = viewModel.dbDataStoreProvider, + bottomSheet = bottomSheet, + onEvent = { viewModel.sendEvent(it) }, + screenData = screenData, + onEffect = { viewModel.setEffect { it } } + ) + } + + CategoryCommonBottomSheet( + screenName = MMScreen.CATEGORY_DETAILS.screen, + modifier = Modifier, + showBottomSheet = showBottomSheet, + categoryTransactionData = + CategoryBottomSheetTransactionData( + transactionId = state.transaction?.id.orEmpty(), + categoryId = state.transaction?.categoryId.orEmpty(), + counterPartyName = state.transaction?.counterPartyName.orEmpty(), + categoryName = state.transaction?.categoryName.orEmpty(), + transactionType = state.transaction?.type.orEmpty() + ), + onDismiss = { showBottomSheet.value = false }, + onSuccessFullyUpdatedCategory = { + showTransactionUpdatedToast = true + updatedTransactionCount = it + } + ) + if (showTransactionUpdatedToast) { + ShowCustomToast( + context = activity, + toastDuration = ToastDuration.LENGTH_SHORT, + content = { + CustomToastView( + message = getTransactionCategorizedMessage(updatedTransactionCount, activity), + illustrationType = + IllustrationType.Image(IllustrationSource.Resource(resId = GREEN_TICK_MARK)) + ) + } + ) + showTransactionUpdatedToast = false + } +} + +@Composable +private fun CategoryDetailsBottomSheetContentHandler( + state: CategoryDetailsScreenUiState, + bottomSheet: CategoryDetailsScreenBottomSheets, + onEvent: (CategoryDetailsScreenUiEvent) -> Unit, + dataStoreInfoProvider: DataStoreInfoProvider, + screenData: MutableState, + onEffect: (CategoryDetailsScreenUiEffect) -> Unit +) { + if (bottomSheet.sheetData == null) return + val onDismissAction = bottomSheet.createDismissAction(onEvent) + when (bottomSheet) { + is CategoryDetailsScreenBottomSheets.BankSelection -> { + BankSelectionBottomSheetContent( + bottomSheetData = bottomSheet.data, + onBackIconClicked = { onDismissAction() }, + onBankSelectionApplied = { selectedBankReferenceIds -> + CategoryDetailsEventTrackerImpl.onCategoryDetailsBankSelectionApplied( + numberOfSelectedBankAccounts = selectedBankReferenceIds.size + ) + screenData.value = + screenData.value?.copy(selectedBanks = selectedBankReferenceIds) + onDismissAction() + } + ) + } + is CategoryDetailsScreenBottomSheets.MonthSelection -> { + MonthSelectionBottomSheetUI( + screenName = MMScreen.CATEGORY_DETAILS.screen, + data = bottomSheet.data!!, + onDismiss = { onDismissAction() }, + onMonthSelected = { month -> + val monthList = MonthConstants.monthNames + screenData.value = + screenData.value?.copy( + month = monthList.indexOf(month.first), + year = month.second + ) + } + ) + } + is CategoryDetailsScreenBottomSheets.AverageInfo -> { + AverageInfoBottomSheetContent( + screenName = MMScreen.CATEGORY_DETAILS.screen, + averageValue = bottomSheet.averageValue.orEmpty(), + onDismiss = { + CategoryDetailsEventTrackerImpl + .onCategoryDetailsGraphAverageInfoBottomSheetAcknowledged() + onDismissAction() + } + ) + } + is CategoryDetailsScreenBottomSheets.PastMonthDataLoading -> { + LaunchedEffect(Unit) { + dataStoreInfoProvider.getBooleanData(key = IS_TOTAL_SYNC_COMPLETED).collect { + pastMonthData -> + if (pastMonthData) { + onDismissAction() + } + } + } + DataLoadingBottomSheetContent( + data = bottomSheet.data!!, + onDismiss = { onDismissAction() }, + trackBottomSheetAppears = { + CategoryDetailsEventTrackerImpl + .onCategoryDetailsPastMonthDataLoadingBottomSheetAppeared() + }, + trackBottomSheetDisappears = { + CategoryDetailsEventTrackerImpl + .onCategoryDetailsPastMonthDataLoadingBottomSheetDisappeared() + } + ) + } + is CategoryDetailsScreenBottomSheets.CategorySelection -> { + CategorySelectionBottomSheetContent( + data = bottomSheet.data!!, + onDismiss = onDismissAction, + onCategorySelection = { + screenData.value = screenData.value?.copy(selectedCategory = it) + } + ) + } + is CategoryDetailsScreenBottomSheets.UncategorizedInfo -> { + UncategorizedInfoBottomSheetContent( + data = bottomSheet.data!!, + onDismiss = { onDismissAction() } + ) + } + CategoryDetailsScreenBottomSheets.SortTransactions -> { + state.screenData?.sortOption?.let { currentSortOption -> + SortTransactionsBottomSheetContent( + onDismiss = { onDismissAction() }, + currentSortOption = currentSortOption, + onSortApplied = { sortOption -> + CategoryDetailsEventTrackerImpl.onCategoryDetailsSortTransactionsApplied( + sortOption.name + ) + onEffect(CategoryDetailsScreenUiEffect.SortTransactions(sortOption)) + onDismissAction() + } + ) + } + } + is CategoryDetailsScreenBottomSheets.HelpBottomSheet -> { + HelpBottomSheetContent( + screenName = MMScreen.CATEGORY_DETAILS.screen, + state = bottomSheet.state, + onFaqClicked = { + onEffect(CategoryDetailsScreenUiEffect.Navigation.NavigateToCta(getFaqCta())) + }, + manageConsent = { onEffect(CategoryDetailsScreenUiEffect.FetchConsentUrl) }, + navigateToUrl = { url -> + onEffect( + CategoryDetailsScreenUiEffect.Navigation.NavigateToCta( + CtaData( + url = WEB_URL, + parameters = listOf(LineItem(key = URL, value = url)) + ) + ) + ) + }, + onDismiss = onDismissAction + ) + } + CategoryDetailsScreenBottomSheets.NoBottomSheet -> {} + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/ui/CategorySelectionBottomSheetContent.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/ui/CategorySelectionBottomSheetContent.kt new file mode 100644 index 0000000000..c68084ece6 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/ui/CategorySelectionBottomSheetContent.kt @@ -0,0 +1,142 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.categorydetails.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ButtonDefaults +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.elex.font.FontWeightEnum as ElexFontWeightEnum +import com.navi.elex.molecules.ElexButtonWithText +import com.navi.moneymanager.common.analytics.CategoryDetailsEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.ui.composable.RadioButtonSelectionRow +import com.navi.moneymanager.common.ui.composable.base.MMDivider +import com.navi.moneymanager.common.ui.composable.base.MMImage +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.categorydetails.model.CategorySelectionBottomSheetData +import com.navi.naviwidgets.R as NaviWidgetsR + +@Composable +fun CategorySelectionBottomSheetContent( + data: CategorySelectionBottomSheetData, + onDismiss: () -> Unit, + onCategorySelection: (String) -> Unit +) { + var currentSelection by remember { mutableStateOf(data.selectedCategory) } + var currentlySelectedCategoryIndex by remember { + mutableStateOf(data.categoryList.indexOfFirst { it.categoryId == data.selectedCategory }) + } + Column(Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 32.dp)) { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + MMText( + text = data.headerTitle, + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp, + ) + + MMImage( + iconCode = NaviWidgetsR.drawable.ic_cross_black, + modifier = Modifier.size(24.dp).onClickWithDebounce { onDismiss() } + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + MMDivider(thickness = 4.dp, color = MMColor.ctaSecondary) + + val scrollState = rememberLazyListState() + + LaunchedEffect(data.selectedCategory) { + val selectedIndex = + data.categoryList.indexOfFirst { it.categoryId == data.selectedCategory } + if (selectedIndex != -1) { + scrollState.animateScrollToItem(selectedIndex) + } + } + + LazyColumn(state = scrollState, modifier = Modifier.fillMaxWidth().weight(1f)) { + val categoryList = data.categoryList + items(categoryList.size) { index -> + data.categoryList[index].let { + RadioButtonSelectionRow( + title = it.categoryName, + isSelected = it.categoryId == currentSelection, + onClick = { + currentSelection = it.categoryId + currentlySelectedCategoryIndex = index + }, + startIcon = + IllustrationType.Image( + IllustrationSource.Remote( + url = it.iconUrl, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ), + ) + if (index < categoryList.size - 1) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { MMDivider() } + } + } + } + } + + ElexButtonWithText( + modifier = + Modifier.fillMaxWidth() + .padding(top = 32.dp, start = 16.dp, end = 16.dp) + .height(48.dp), + text = data.ctaText, + textColor = MMColor.white, + fontWeight = ElexFontWeightEnum.NAVI_BODY_DEMI_BOLD, + fontSize = 14.sp, + onClick = { + com.navi.moneymanager.common.analytics.CategoryDetailsEventTrackerImpl + .onCategoryDetailsCategorySelectionApplied( + currentlySelectedCategoryIndex + 1, + currentSelection + ) + onCategorySelection(currentSelection) + onDismiss() + }, + colors = + ButtonDefaults.buttonColors() + .copy( + containerColor = MMColor.ctaPrimary, + ), + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/ui/SortTransactionsBottomSheetContent.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/ui/SortTransactionsBottomSheetContent.kt new file mode 100644 index 0000000000..1ff756e93a --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/ui/SortTransactionsBottomSheetContent.kt @@ -0,0 +1,174 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.categorydetails.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +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.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ripple +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.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.elex.atoms.ElexButton +import com.navi.moneymanager.R +import com.navi.moneymanager.common.ui.composable.base.MMDivider +import com.navi.moneymanager.common.ui.composable.base.MMImage +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.fourDpRoundedShape +import com.navi.moneymanager.postonboard.categorydetails.model.SortOption + +@Composable +fun SortTransactionsBottomSheetContent( + onDismiss: () -> Unit, + currentSortOption: SortOption, + onSortApplied: (SortOption) -> Unit +) { + var stagedSortOption by remember { mutableStateOf(currentSortOption) } + Column( + modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 32.dp), + horizontalAlignment = Alignment.Start + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + MMText( + text = stringResource(R.string.sort_transactions), + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + letterSpacing = 0.sp, + lineHeight = 24.sp, + ) + MMImage( + iconCode = com.navi.naviwidgets.R.drawable.ic_cross_black, + modifier = Modifier.size(24.dp).clickable { onDismiss() } + ) + } + Spacer(modifier = Modifier.height(4.dp)) + SortOptions( + currentSortOption = currentSortOption, + stagedSortOption = stagedSortOption, + onSortOptionClick = { stagedSortOption = it } + ) + Spacer(modifier = Modifier.height(32.dp)) + ElexButton( + onClick = { onSortApplied(stagedSortOption) }, + modifier = Modifier.fillMaxWidth().height(48.dp).padding(horizontal = 16.dp), + colors = ButtonDefaults.buttonColors(containerColor = MMColor.ctaPrimary), + shape = fourDpRoundedShape + ) { + MMText( + text = stringResource(R.string.apply_pascal_case), + color = MMColor.white, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 22.sp + ) + } + } +} + +@Composable +fun SortOptions( + currentSortOption: SortOption, + stagedSortOption: SortOption = currentSortOption, + onSortOptionClick: (SortOption) -> Unit +) { + Column { + SortOption.entries.forEachIndexed { index, sortOption -> + SortOptionItem( + sortOption = sortOption, + onSortOptionClick = { onSortOptionClick(it) }, + isSelected = sortOption == stagedSortOption + ) + if (index < SortOption.entries.size - 1) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { MMDivider() } + } + } + } +} + +@Composable +fun SortOptionItem( + sortOption: SortOption, + onSortOptionClick: (SortOption) -> Unit, + isSelected: Boolean +) { + Row( + modifier = + Modifier.fillMaxWidth() + .clickable( + onClick = { onSortOptionClick(sortOption) }, + indication = remember { ripple() }, + interactionSource = remember { MutableInteractionSource() } + ) + .padding(horizontal = 16.dp, vertical = 20.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + MMText( + text = + stringResource( + when (sortOption) { + SortOption.HIGHEST_FIRST -> R.string.highest_first + SortOption.LOWEST_FIRST -> R.string.lowest_first + SortOption.RECENT_FIRST -> R.string.recent_first + } + ), + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = + if (isSelected) FontWeightEnum.NAVI_BODY_DEMI_BOLD + else FontWeightEnum.NAVI_BODY_REGULAR, + letterSpacing = 0.sp, + lineHeight = 24.sp, + ) + + Box( + modifier = + Modifier.size(24.dp) + .border( + width = 2.dp, + color = if (isSelected) MMColor.ctaPrimary else MMColor.borderAlt, + shape = CircleShape + ) + .padding(2.dp), + contentAlignment = Alignment.Center + ) { + if (isSelected) { + Box( + modifier = + Modifier.size(12.dp).background(MMColor.ctaPrimary, shape = CircleShape) + ) + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/ui/UncategorizedInfoBottomSheetContent.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/ui/UncategorizedInfoBottomSheetContent.kt new file mode 100644 index 0000000000..654b43ab7a --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/ui/UncategorizedInfoBottomSheetContent.kt @@ -0,0 +1,90 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.categorydetails.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.elex.font.FontWeightEnum as ElexFontWeightEnum +import com.navi.elex.molecules.ElexButtonWithText +import com.navi.moneymanager.common.analytics.CategoryDetailsEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.model.ImageProperties +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.categorydetails.model.UncategorizedInfoBottomSheetData + +@Composable +fun UncategorizedInfoBottomSheetContent( + data: UncategorizedInfoBottomSheetData, + onDismiss: () -> Unit +) { + Column( + modifier = + Modifier.fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 32.dp), + ) { + MMText( + text = data.title, + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + ) + Spacer(modifier = Modifier.height(8.dp)) + MMText( + text = data.description, + color = MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 22.sp, + ) + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp), + horizontalArrangement = Arrangement.Center + ) { + Illustration( + modifier = Modifier.fillMaxWidth().heightIn(max = 79.dp), + illustrationType = + IllustrationType.Image( + data.describeIllustration, + ImageProperties(contentScale = ContentScale.FillBounds) + ), + ) + } + ElexButtonWithText( + modifier = Modifier.fillMaxWidth().height(48.dp), + text = data.ctaText, + textColor = MMColor.white, + fontWeight = ElexFontWeightEnum.NAVI_BODY_DEMI_BOLD, + fontSize = 14.sp, + onClick = { + CategoryDetailsEventTrackerImpl + .onCategoryDetailsUncategorizedInfoBottomSheetAcknowledged() + onDismiss() + }, + colors = + ButtonDefaults.buttonColors() + .copy( + containerColor = MMColor.ctaPrimary, + ), + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/viewmodel/CategoryDetailsVM.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/viewmodel/CategoryDetailsVM.kt new file mode 100644 index 0000000000..208d658783 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/categorydetails/viewmodel/CategoryDetailsVM.kt @@ -0,0 +1,248 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.categorydetails.viewmodel + +import androidx.lifecycle.viewModelScope +import com.navi.base.utils.orZero +import com.navi.moneymanager.base.viewmodel.MMBaseViewModel +import com.navi.moneymanager.common.analytics.DataProviderEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.data.datastore.DataStoreInfoProvider +import com.navi.moneymanager.common.model.CategoryDetailsScreenInputParams +import com.navi.moneymanager.common.model.bottomSheet.CategoryDetailsScreenBottomSheets +import com.navi.moneymanager.common.network.di.RoomDataStoreInfoProvider +import com.navi.moneymanager.common.utils.Constants.IS_TOTAL_SYNC_COMPLETED +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenData +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenUiEffect +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenUiEvent +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenUiState +import com.navi.moneymanager.postonboard.categorydetails.model.SortOption +import com.navi.moneymanager.postonboard.categorydetails.reducer.CategoryDetailsScreenReducer +import com.navi.moneymanager.postonboard.categorydetails.repo.CategoryDetailsRepository +import com.navi.moneymanager.postonboard.help.model.HelpBottomSheetState +import com.navi.moneymanager.postonboard.help.usecase.ManageConsentUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import timber.log.Timber + +@HiltViewModel +class CategoryDetailsVM +@Inject +constructor( + @RoomDataStoreInfoProvider val dbDataStoreProvider: DataStoreInfoProvider, + private val repository: CategoryDetailsRepository, + private val manageConsentUseCase: ManageConsentUseCase +) : + MMBaseViewModel< + CategoryDetailsScreenUiState, CategoryDetailsScreenUiEvent, CategoryDetailsScreenUiEffect + >( + initialState = CategoryDetailsScreenUiState.initialState, + reducer = CategoryDetailsScreenReducer() + ) { + + private val screenParams = MutableStateFlow(null) + + @OptIn(ExperimentalCoroutinesApi::class) + private val categoryDetailsScreenData: StateFlow = + screenParams + .filterNotNull() + .distinctUntilChanged() + .flatMapLatest { params -> + DataProviderEventTrackerImpl.categoryDetailsVmCollect( + month = params.month.toString(), + year = params.year.toString(), + selectedBanksListSize = params.selectedBankReferenceIds?.size.orZero(), + selectedCategory = params.selectedCategory.orEmpty(), + ) + repository.getCategoryDetailsScreenData( + month = params.month, + year = params.year, + selectedBankReferenceIds = params.selectedBankReferenceIds.orEmpty(), + category = params.selectedCategory.orEmpty(), + sort = state.value.screenData?.sortOption ?: SortOption.RECENT_FIRST + ) + } + .flowOn(Dispatchers.IO) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = CategoryDetailsScreenData() + ) + + init { + handleCategoryDetailsScreenData() + } + + private fun handleCategoryDetailsScreenData() { + viewModelScope.safeLaunch(Dispatchers.Default) { + categoryDetailsScreenData.filterNotNull().collect { screenData -> + Timber.tag("money_manager") + .d("CategoryDetailsVM ${::handleCategoryDetailsScreenData.name}") + sendEvent(CategoryDetailsScreenUiEvent.RenderUI(screenData)) + } + } + } + + fun updateScreenParams( + month: Int?, + year: Int?, + selectedBankReferenceIds: Set? = null, + selectedCategory: String?, + ) { + viewModelScope.safeLaunch(Dispatchers.IO) { + DataProviderEventTrackerImpl.categoryDetailsVmParamsUpdate( + month = month.toString(), + year = year.toString(), + selectedBanksListSize = selectedBankReferenceIds?.size.orZero(), + selectedCategory = selectedCategory.orEmpty(), + ) + screenParams.update { + CategoryDetailsScreenInputParams( + month = month, + year = year, + selectedBankReferenceIds = selectedBankReferenceIds, + selectedCategory = selectedCategory.orEmpty(), + ) + } + } + } + + fun handleMonthSelectBottomSheet(displayMonthText: String) { + viewModelScope.safeLaunch(Dispatchers.IO) { + val isTotalSyncCompleted = + dbDataStoreProvider.getBooleanData(key = IS_TOTAL_SYNC_COMPLETED).first() + if (isTotalSyncCompleted) { + val data = repository.getMonthSelectionBottomSheetData(displayMonthText) + sendEvent( + CategoryDetailsScreenUiEvent.ShowBottomSheet( + type = CategoryDetailsScreenBottomSheets.MonthSelection(data) + ) + ) + } else { + val data = repository.getPastMonthDataLoadingBottomSheetData() + sendEvent( + CategoryDetailsScreenUiEvent.ShowBottomSheet( + type = CategoryDetailsScreenBottomSheets.PastMonthDataLoading(data) + ) + ) + } + } + } + + fun handleBankSelectBottomSheet(selectedBankReferenceIds: Set) { + viewModelScope.safeLaunch(Dispatchers.IO) { + val data = repository.getBankSelectionBottomSheetData(selectedBankReferenceIds) + sendEvent( + CategoryDetailsScreenUiEvent.ShowBottomSheet( + type = CategoryDetailsScreenBottomSheets.BankSelection(data) + ) + ) + } + } + + fun handleAverageInfoBottomSheet(averageValue: String?) { + viewModelScope.safeLaunch(Dispatchers.IO) { + val isTotalSyncCompleted = + dbDataStoreProvider.getBooleanData(key = IS_TOTAL_SYNC_COMPLETED).first() + if (isTotalSyncCompleted) { + sendEvent( + CategoryDetailsScreenUiEvent.ShowBottomSheet( + type = CategoryDetailsScreenBottomSheets.AverageInfo(averageValue) + ) + ) + } else { + val data = repository.getPastMonthDataLoadingBottomSheetData() + sendEvent( + CategoryDetailsScreenUiEvent.ShowBottomSheet( + type = CategoryDetailsScreenBottomSheets.PastMonthDataLoading(data) + ) + ) + } + } + } + + fun handleCategorySelectionBottomSheet(category: String) { + viewModelScope.safeLaunch(Dispatchers.IO) { + val data = repository.getCategorySelectionBottomSheetData(category) + sendEvent( + CategoryDetailsScreenUiEvent.ShowBottomSheet( + type = CategoryDetailsScreenBottomSheets.CategorySelection(data) + ) + ) + } + } + + fun handleUncategorizedInfoBottomSheet() { + sendEvent( + CategoryDetailsScreenUiEvent.ShowBottomSheet( + CategoryDetailsScreenBottomSheets.UncategorizedInfo( + data = repository.getUncategorizedInfoBottomSheetData() + ) + ) + ) + } + + fun sortTransactions(sortOption: SortOption) { + viewModelScope.safeLaunch(Dispatchers.IO) { + val transactions = repository.getSortedTransactions(sortOption) + sendEvent( + CategoryDetailsScreenUiEvent.UpdateTransactionsSortingOrder( + sortOption, + transactions + ) + ) + } + } + + fun handleSortTransactionsBottomSheet() { + sendEvent( + CategoryDetailsScreenUiEvent.ShowBottomSheet( + type = CategoryDetailsScreenBottomSheets.SortTransactions + ) + ) + } + + fun fetchConsentUrl() { + viewModelScope.safeLaunch(Dispatchers.IO) { + manageConsentUseCase.execute( + onLoading = { + sendEvent( + CategoryDetailsScreenUiEvent.UpdateHelpBottomSheetState( + HelpBottomSheetState.Loading + ) + ) + }, + onSuccess = { url -> + sendEvent( + CategoryDetailsScreenUiEvent.UpdateHelpBottomSheetState( + HelpBottomSheetState.Success(url) + ) + ) + }, + onFailure = { + sendEvent( + CategoryDetailsScreenUiEvent.UpdateHelpBottomSheetState( + HelpBottomSheetState.Error + ) + ) + } + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/DashboardData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/DashboardData.kt new file mode 100644 index 0000000000..6e33330988 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/DashboardData.kt @@ -0,0 +1,147 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.model + +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.model.SpendCategorizationState +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.common.model.ZeroTransactionData + +data class DashboardData( + val topNavBar: NavBarData = NavBarData(), + val header: UserHeaderState = UserHeaderState.Loading, + val bankSectionData: BankSectionData = BankSectionData(), + val spendCategorizationState: SpendCategorizationState? = null, + val recentTransactionsData: RecentTransactionsSectionData = RecentTransactionsSectionData(), +) + +data class NavBarData(val actionLabel: String? = null) + +sealed class UserHeaderState(val data: UserHeaderInfo) { + + data object Loading : UserHeaderState(UserHeaderInfo(isLoading = true)) + + data class Loaded(val greetingPrompt: String? = null, val expensePrompt: String? = null) : + UserHeaderState( + UserHeaderInfo( + isLoading = false, + greetingPrompt = greetingPrompt, + expensePrompt = expensePrompt + ) + ) +} + +data class UserHeaderInfo( + val isLoading: Boolean? = null, + val greetingPrompt: String? = null, + val expensePrompt: String? = null +) + +data class BankSectionData( + val selectedBank: String? = null, + val isBankListExpanded: Boolean = false, + val state: BankAccountsState = BankAccountsState.None +) + +sealed interface BankAccountsState { + data class Loading(val data: BankAccountsLoadingState) : BankAccountsState + + data class Loaded(val data: BankAccountsData) : BankAccountsState + + data object None : BankAccountsState +} + +data class BankAccountsLoadingState( + val chipText: String, + val chipLottie: IllustrationSource, + val balanceLottie: IllustrationSource, + val balanceSectionLottie: IllustrationSource, + val balanceSectionTitle: String, + val lastUpdated: String +) + +data class BankAccountsData( + val accounts: List, + val aggregate: BankAccount? = null, + val addBankChipInfo: AddBankChipInfo +) + +data class BankAccountFooterSection( + val referenceId: String, + val lastUpdatedPrefixText: String, + val lastUpdatedSuffixText: String +) + +data class BankAccount( + val isBalanceFetched: Boolean, + val balanceSectionTitlePrefix: String, + val referenceId: String, + val balanceSectionTitleSuffix: String, + val bankChipContent: String, + val bankChipImage: IllustrationSource, + val bankBalanceSectionImage: IllustrationSource, + val bankLoadingLottie: IllustrationSource, + val bankBalance: String, + val lastUpdatedPrefixText: String, + val lastUpdatedSuffixText: String +) + +data class BankAccountUpdateData(val referenceId: String, val lastUpdated: Long) + +data class AddBankChipInfo( + val addBankText: String, + val addBankIcon: IllustrationSource, + val addBankLoaderLottie: IllustrationSource, + val isTotalSyncCompleted: Boolean = false +) + +data class RecentTransactionsSectionData( + val selectedBank: String? = null, + val autoSelectedBank: String? = null, + val isBankListExpanded: Boolean = false, + val state: RecentTransactionsState = RecentTransactionsState.None +) + +sealed interface RecentTransactionsState { + data class Loaded( + val sectionTitle: String, + val primaryCtaTitle: String, + val zeroTransactionData: ZeroTransactionData, + val accountTransactions: List, + val aggregateTransactions: BankTransactionsData? = null + ) : RecentTransactionsState + + data class Loading( + val sectionTitle: String, + val transactions: List, + ) : RecentTransactionsState + + data object None : RecentTransactionsState +} + +data class BankTransactionsData(val bankReferenceId: String, val transactions: List) + +data class RecentTransactionsLoadingItemData( + val name: String, + val categoryText: String, + val dateText: String, + val lottieUrl: IllustrationSource, + val actionLottie: IllustrationSource, +) + +sealed interface AddAccountState { + data object UnSelected : AddAccountState + + sealed interface Selected : AddAccountState { + data object BankSection : Selected + + data object SpentCategorizationSection : Selected + + data object RecentTransactionSection : Selected + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/DashboardScreenUiEffect.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/DashboardScreenUiEffect.kt new file mode 100644 index 0000000000..b9d3a63853 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/DashboardScreenUiEffect.kt @@ -0,0 +1,43 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.model + +import com.navi.base.model.CtaData +import com.navi.common.basemvi.UiEffect +import com.navi.moneymanager.common.model.SelectedMonth +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenNavigationData +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionFilterData + +sealed class DashboardScreenUiEffect : UiEffect { + sealed class Navigation : DashboardScreenUiEffect() { + data object Back : Navigation() + + data object Help : Navigation() + + data object FetchFinarkeinData : Navigation() + + data object DismissFinarkeinBottomSheet : Navigation() + + data class TransactionDetails(val transactionId: String) : Navigation() + + data class SpendAnalysisScreen(val selectedMonth: SelectedMonth) : Navigation() + + data class CategoryDetailsScreen(val data: CategoryDetailsScreenNavigationData) : + Navigation() + + data class TransactionHistory(val filterList: List? = null) : + Navigation() + + data class NavigateToCta(val ctaData: CtaData) : Navigation() + } + + data class OpenCategoryBottomSheet(val transaction: Transaction) : DashboardScreenUiEffect() + + data object FetchConsentUrl : DashboardScreenUiEffect() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/DashboardScreenUiEvent.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/DashboardScreenUiEvent.kt new file mode 100644 index 0000000000..6780104db5 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/DashboardScreenUiEvent.kt @@ -0,0 +1,64 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.model + +import com.navi.common.basemvi.UiEvent +import com.navi.moneymanager.common.model.SpendCategorizationState +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.common.model.bottomSheet.DashboardScreenBottomSheets +import com.navi.moneymanager.postonboard.help.model.HelpBottomSheetState + +sealed class DashboardScreenUiEvent : UiEvent { + data class UpdateDashboardTopNavBarData(val data: NavBarData) : DashboardScreenUiEvent() + + data class UpdateDashboardHeaderState(val state: UserHeaderState) : DashboardScreenUiEvent() + + data class UpdateDashboardBankSectionState( + val state: BankAccountsState, + val isFirstMonthSyncCompleted: Boolean + ) : DashboardScreenUiEvent() + + data class UpdateBankSectionLastUpdatedTime(val data: List) : + DashboardScreenUiEvent() + + data class UpdateSpendCategorizationSectionState(val state: SpendCategorizationState) : + DashboardScreenUiEvent() + + data class UpdateRecentTransactionsSectionState(val state: RecentTransactionsState) : + DashboardScreenUiEvent() + + data object ExpandRecentTransactionsSectionBankList : DashboardScreenUiEvent() + + data object ExpandBankBalanceSectionBankList : DashboardScreenUiEvent() + + data class UpdateSelectedBankForBalance(val bankId: String) : DashboardScreenUiEvent() + + data class UpdateSelectedBankForTransactions(val bankId: String) : DashboardScreenUiEvent() + + data class DismissBottomSheet( + val type: Class = + DashboardScreenBottomSheets.NoBottomSheet::class.java + ) : DashboardScreenUiEvent() + + data class ShowBottomSheet(val type: DashboardScreenBottomSheets) : DashboardScreenUiEvent() + + data class UpdateOnBoardingBottomSheetStatus(val status: OnboardingStatus) : + DashboardScreenUiEvent() + + data class UpdateFinarkeinBottomSheetType( + val type: FinarkeinSheetStatus, + ) : DashboardScreenUiEvent() + + data class HandleAddAccountState(val addAccountState: AddAccountState) : + DashboardScreenUiEvent() + + data class UpdateTransactionData(val transaction: Transaction) : DashboardScreenUiEvent() + + data class UpdateHelpBottomSheetState(val state: HelpBottomSheetState) : + DashboardScreenUiEvent() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/DashboardScreenUiState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/DashboardScreenUiState.kt new file mode 100644 index 0000000000..b0d46113a7 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/DashboardScreenUiState.kt @@ -0,0 +1,34 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.model + +import androidx.compose.runtime.Stable +import com.navi.common.basemvi.UiState +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.common.model.bottomSheet.BottomSheetState +import com.navi.moneymanager.common.model.bottomSheet.DashboardScreenBottomSheets +import com.navi.moneymanager.common.model.bottomSheet.DashboardScreenBottomSheets.NoBottomSheet + +@Stable +data class DashboardScreenUiState( + val isLoading: Boolean, + val screenData: DashboardData, + val bottomSheetState: BottomSheetState, + val addAccountState: AddAccountState, + val transaction: Transaction? = null +) : UiState { + companion object { + val initialState = + DashboardScreenUiState( + isLoading = true, + screenData = DashboardData(), + addAccountState = AddAccountState.UnSelected, + bottomSheetState = BottomSheetState(type = NoBottomSheet) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/FinarkeinErrorBottomSheetState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/FinarkeinErrorBottomSheetState.kt new file mode 100644 index 0000000000..55b0c3a8e2 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/FinarkeinErrorBottomSheetState.kt @@ -0,0 +1,26 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.model + +import com.navi.moneymanager.common.illustration.model.IllustrationSource + +data class FinarkeinErrorBottomSheetData( + val sheetStatus: FinarkeinSheetStatus = FinarkeinSheetStatus.Error, + val headerIcon: IllustrationSource, + val titleText: String, + val subTitleText: String, + val leftCtaText: String, + val rightCtaData: RightCtaData +) + +data class RightCtaData(val lottieUrl: String, val title: String) + +enum class FinarkeinSheetStatus { + Error, + Retry +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/OnboardingStatus.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/OnboardingStatus.kt new file mode 100644 index 0000000000..4d8e53ab11 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/model/OnboardingStatus.kt @@ -0,0 +1,16 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.model + +sealed interface OnboardingStatus { + data object Loading : OnboardingStatus + + data object Success : OnboardingStatus + + data object TakingLonger : OnboardingStatus +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/reducer/DashboardScreenReducer.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/reducer/DashboardScreenReducer.kt new file mode 100644 index 0000000000..44f022ad4e --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/reducer/DashboardScreenReducer.kt @@ -0,0 +1,363 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.reducer + +import com.navi.common.basemvi.BaseReducer +import com.navi.moneymanager.common.model.bottomSheet.BottomSheetState +import com.navi.moneymanager.common.model.bottomSheet.DashboardScreenBottomSheets +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountsState +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiEvent +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiState +import com.navi.moneymanager.postonboard.dashboard.model.OnboardingStatus +import com.navi.moneymanager.postonboard.dashboard.model.RecentTransactionsState + +class DashboardScreenReducer : BaseReducer { + + override fun reduce( + previousState: DashboardScreenUiState, + event: DashboardScreenUiEvent + ): DashboardScreenUiState { + return when (event) { + is DashboardScreenUiEvent.UpdateDashboardTopNavBarData -> { + previousState.copy( + screenData = previousState.screenData.copy(topNavBar = event.data) + ) + } + is DashboardScreenUiEvent.UpdateDashboardHeaderState -> { + previousState.copy(screenData = previousState.screenData.copy(header = event.state)) + } + is DashboardScreenUiEvent.UpdateDashboardBankSectionState -> { + val previousBankSection = previousState.screenData.bankSectionData.state + val bankSection = event.state + + val updatedState = + if (bankSection is BankAccountsState.Loaded) { + val previousAggregate = + (previousBankSection as? BankAccountsState.Loaded)?.data?.aggregate + val previousAccounts = + (previousBankSection as? BankAccountsState.Loaded) + ?.data + ?.accounts + ?.associateBy { it.referenceId } + + val updatedAggregate = + bankSection.data.aggregate?.let { newAggregate -> + val shouldUsePreviousAggregate = + previousAggregate?.lastUpdatedPrefixText?.isNotEmpty() ?: false + newAggregate.copy( + lastUpdatedPrefixText = + if (shouldUsePreviousAggregate) { + previousAggregate?.lastUpdatedPrefixText.orEmpty() + } else { + newAggregate.lastUpdatedPrefixText + }, + lastUpdatedSuffixText = + if (shouldUsePreviousAggregate) { + previousAggregate?.lastUpdatedSuffixText.orEmpty() + } else { + newAggregate.lastUpdatedSuffixText + } + ) + } + + val updatedAccounts = + bankSection.data.accounts.map { newAccount -> + val previousAccount = previousAccounts?.get(newAccount.referenceId) + val shouldUsePreviousAccount = + previousAggregate?.lastUpdatedPrefixText?.isNotEmpty() ?: false + + newAccount.copy( + lastUpdatedPrefixText = + if (shouldUsePreviousAccount) { + previousAccount?.lastUpdatedPrefixText.orEmpty() + } else { + newAccount.lastUpdatedPrefixText + }, + lastUpdatedSuffixText = + if (shouldUsePreviousAccount) { + previousAccount?.lastUpdatedSuffixText.orEmpty() + } else { + newAccount.lastUpdatedSuffixText + } + ) + } + + bankSection.copy( + data = + bankSection.data.copy( + aggregate = updatedAggregate, + accounts = updatedAccounts + ) + ) + } else { + bankSection + } + + val selectedBank = + when (updatedState) { + is BankAccountsState.Loading, + BankAccountsState.None -> null + is BankAccountsState.Loaded -> { + val previousSelectedBank = + previousState.screenData.bankSectionData.selectedBank + val updatedAccountsRefId = + updatedState.data.accounts.map { it.referenceId } + val aggregateRefId = updatedState.data.aggregate?.referenceId + + when { + previousSelectedBank != null && + (previousSelectedBank in updatedAccountsRefId || + previousSelectedBank == aggregateRefId) -> { + previousSelectedBank + } + updatedAccountsRefId.size > 1 -> aggregateRefId + updatedAccountsRefId.size == 1 -> updatedAccountsRefId.firstOrNull() + else -> null + } + } + } + + previousState.copy( + isLoading = + updatedState is BankAccountsState.None && + event.isFirstMonthSyncCompleted.not(), + screenData = + previousState.screenData.copy( + bankSectionData = + previousState.screenData.bankSectionData.copy( + state = updatedState, + selectedBank = selectedBank + ) + ) + ) + } + is DashboardScreenUiEvent.UpdateBankSectionLastUpdatedTime -> { + if (previousState.screenData.bankSectionData.state !is BankAccountsState.Loaded) { + return previousState + } + val bankSection = previousState.screenData.bankSectionData.state + val aggregateData = bankSection.data.aggregate + val aggregateUpdated = + event.data.firstOrNull { + it.referenceId == aggregateData?.referenceId.orEmpty() + } + + val updatedAggregate = + aggregateData?.copy( + lastUpdatedPrefixText = aggregateUpdated?.lastUpdatedPrefixText.orEmpty(), + lastUpdatedSuffixText = aggregateUpdated?.lastUpdatedSuffixText.orEmpty() + ) + + val updatedAccounts = + bankSection.data.accounts.map { account -> + val lastUpdated = + event.data.firstOrNull { it.referenceId == account.referenceId } + account.copy( + lastUpdatedPrefixText = lastUpdated?.lastUpdatedPrefixText.orEmpty(), + lastUpdatedSuffixText = lastUpdated?.lastUpdatedSuffixText.orEmpty() + ) + } + + previousState.copy( + screenData = + previousState.screenData.copy( + bankSectionData = + previousState.screenData.bankSectionData.copy( + state = + bankSection.copy( + data = + bankSection.data.copy( + aggregate = updatedAggregate, + accounts = updatedAccounts + ) + ) + ) + ) + ) + } + is DashboardScreenUiEvent.UpdateSpendCategorizationSectionState -> { + previousState.copy( + screenData = + previousState.screenData.copy(spendCategorizationState = event.state) + ) + } + is DashboardScreenUiEvent.UpdateRecentTransactionsSectionState -> { + + var selectedBank: String? = null + var autoSelectedBank: String? = null + + when (val recentTransactionsState = event.state) { + is RecentTransactionsState.Loading, + RecentTransactionsState.None -> { + autoSelectedBank = null + selectedBank = null + } + is RecentTransactionsState.Loaded -> { + val previousSelectedBank = + previousState.screenData.bankSectionData.selectedBank + val updatedAccountsRefId = + recentTransactionsState.accountTransactions.map { it.bankReferenceId } + val aggregateRefId = + recentTransactionsState.aggregateTransactions?.bankReferenceId + + selectedBank = + when { + previousSelectedBank in updatedAccountsRefId || + previousSelectedBank == aggregateRefId -> previousSelectedBank + updatedAccountsRefId.size > 1 -> aggregateRefId + updatedAccountsRefId.size == 1 -> updatedAccountsRefId.firstOrNull() + else -> null + } + + autoSelectedBank = + previousState.screenData.recentTransactionsData.selectedBank + ?: (recentTransactionsState.aggregateTransactions?.bankReferenceId + ?: recentTransactionsState.accountTransactions + .firstOrNull() + ?.bankReferenceId) + } + } + + previousState.copy( + screenData = + previousState.screenData.copy( + recentTransactionsData = + previousState.screenData.recentTransactionsData.copy( + state = event.state, + autoSelectedBank = autoSelectedBank, + selectedBank = selectedBank + ) + ) + ) + } + DashboardScreenUiEvent.ExpandBankBalanceSectionBankList -> { + previousState.copy( + screenData = + previousState.screenData.copy( + bankSectionData = + previousState.screenData.bankSectionData.copy( + isBankListExpanded = true + ) + ) + ) + } + DashboardScreenUiEvent.ExpandRecentTransactionsSectionBankList -> { + previousState.copy( + screenData = + previousState.screenData.copy( + recentTransactionsData = + previousState.screenData.recentTransactionsData.copy( + isBankListExpanded = true + ) + ) + ) + } + is DashboardScreenUiEvent.UpdateSelectedBankForBalance -> { + previousState.copy( + screenData = + previousState.screenData.copy( + bankSectionData = + previousState.screenData.bankSectionData.copy( + selectedBank = event.bankId + ) + ) + ) + } + is DashboardScreenUiEvent.UpdateSelectedBankForTransactions -> { + previousState.copy( + screenData = + previousState.screenData.copy( + recentTransactionsData = + previousState.screenData.recentTransactionsData.copy( + selectedBank = event.bankId + ) + ) + ) + } + is DashboardScreenUiEvent.ShowBottomSheet -> { + previousState.copy( + bottomSheetState = BottomSheetState(isVisible = true, type = event.type) + ) + } + is DashboardScreenUiEvent.DismissBottomSheet -> { + val currentBottomSheet = previousState.bottomSheetState.type + if ( + event.type == DashboardScreenBottomSheets.NoBottomSheet::class.java || + event.type.isInstance(currentBottomSheet) + ) { + previousState.copy( + bottomSheetState = previousState.bottomSheetState.copy(isVisible = false) + ) + } else previousState + } + is DashboardScreenUiEvent.UpdateOnBoardingBottomSheetStatus -> { + val currentBottomSheetType = previousState.bottomSheetState.type + if (currentBottomSheetType !is DashboardScreenBottomSheets.OnBoarding) { + return if (event.status == OnboardingStatus.Success) { + previousState + } else { + previousState.copy( + bottomSheetState = + BottomSheetState( + isVisible = true, + type = DashboardScreenBottomSheets.OnBoarding(event.status) + ) + ) + } + } + + val newState = + when { + event.status == OnboardingStatus.Success && + currentBottomSheetType.state !is OnboardingStatus.Success -> + OnboardingStatus.Success + else -> event.status + } + + previousState.copy( + bottomSheetState = + BottomSheetState( + isVisible = true, + type = DashboardScreenBottomSheets.OnBoarding(newState) + ) + ) + } + is DashboardScreenUiEvent.HandleAddAccountState -> { + previousState.copy(addAccountState = event.addAccountState) + } + is DashboardScreenUiEvent.UpdateTransactionData -> { + previousState.copy(transaction = event.transaction) + } + is DashboardScreenUiEvent.UpdateFinarkeinBottomSheetType -> { + if ( + previousState.bottomSheetState.type + is DashboardScreenBottomSheets.FinarkeinError + ) { + val updatedBottomSheetData = + previousState.bottomSheetState.type.data?.copy(sheetStatus = event.type) + val updatedBottomSheetType = + previousState.bottomSheetState.type.copy(data = updatedBottomSheetData) + val updatedBottomSheetState = + previousState.bottomSheetState.copy(type = updatedBottomSheetType) + previousState.copy(bottomSheetState = updatedBottomSheetState) + } else previousState + } + is DashboardScreenUiEvent.UpdateHelpBottomSheetState -> { + val currentBottomSheetType = previousState.bottomSheetState.type + if (currentBottomSheetType is DashboardScreenBottomSheets.HelpBottomSheet) { + previousState.copy( + bottomSheetState = + previousState.bottomSheetState.copy( + type = DashboardScreenBottomSheets.HelpBottomSheet(event.state) + ) + ) + } else previousState + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/repo/DashboardScreenRepository.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/repo/DashboardScreenRepository.kt new file mode 100644 index 0000000000..f904191b1d --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/repo/DashboardScreenRepository.kt @@ -0,0 +1,105 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.repo + +import androidx.compose.runtime.MutableState +import com.navi.base.utils.EMPTY +import com.navi.base.utils.orZero +import com.navi.moneymanager.common.dataprovider.data.datastore.DataStoreInfoProvider +import com.navi.moneymanager.common.dataprovider.domain.DashboardDataProvider +import com.navi.moneymanager.common.dataprovider.domain.RemoteDataProvider +import com.navi.moneymanager.common.db.database.MMEncryptedDatabase +import com.navi.moneymanager.common.model.SelectedMonth +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.network.di.RoomDataStoreInfoProvider +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.utils.Constants.USER_NAME +import com.navi.moneymanager.postonboard.dashboard.model.UserHeaderState +import dagger.Lazy +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow + +class DashboardScreenRepository +@Inject +constructor( + private val remoteDataProvider: RemoteDataProvider, + private val dashboardDataProvider: DashboardDataProvider, + private val encryptedDatabase: Lazy, + @RoomDataStoreInfoProvider private val dbDataStoreProvider: DataStoreInfoProvider, +) { + suspend fun fetchAndSaveConfigResponse(): MMConfigResponse? { + val networkResponse = + remoteDataProvider.fetchMMConfigResponse(screenName = MMScreen.DASHBOARD.screen) + + return networkResponse.data?.let { + encryptedDatabase + .get() + .transactionsDao() + .deleteTransactionsOlderThan(it.timestampConfig?.twelveMonthsOldTimestamp.orZero()) + dashboardDataProvider.saveMMConfigToDB(it) + dashboardDataProvider.getMMConfig() + } ?: run { null } + } + + suspend fun getTopNavBarData() = dashboardDataProvider.getTopNavBarData() + + fun getHeaderStateFlow(): Flow = flow { + val storedName = dbDataStoreProvider.getStringData(USER_NAME, EMPTY).first() + if (dbDataStoreProvider.containsKey(USER_NAME)) { + emit(dashboardDataProvider.getUserHeader(storedName)) + } + + val oneProfileData = remoteDataProvider.getCustomerProfileData() + val remoteName = oneProfileData.data?.customerDetails?.name?.value + + val userHeaderInfo = + if (storedName.isNotEmpty() && storedName == remoteName) { + dashboardDataProvider.getUserHeader(storedName) + } else { + remoteName?.let { + dbDataStoreProvider.saveStringData(USER_NAME, it) + dashboardDataProvider.getUserHeader(it) + } ?: dashboardDataProvider.getUserHeader(storedName.ifEmpty { null }) + } + emit(userHeaderInfo) + } + + suspend fun getBankSectionStateFlow() = dashboardDataProvider.getBankSectionStateFlow() + + suspend fun updateBankSectionRefreshingText(refreshing: Boolean) = + dashboardDataProvider.updateBankSectionRefreshingText(refreshing) + + suspend fun fetchFinarkeinData() = + remoteDataProvider.fetchFinarkeinData(screenName = MMScreen.DASHBOARD.screen) + + suspend fun getSpendCategorizationSectionStateFlow( + screenParams: MutableStateFlow + ) = dashboardDataProvider.getSpendCategorizationSectionStateFlow(screenParams) + + suspend fun getRecentTransactionsSectionStateFlow( + currentMonthTag: MutableState, + ) = dashboardDataProvider.getRecentTransactionsSectionStateFlow(currentMonthTag) + + suspend fun getMonthSelectionBottomSheetData(displayMonthText: String) = + dashboardDataProvider.getMonthSelectionBottomSheetData(displayMonthText) + + suspend fun getPastMonthDataLoadingBottomSheetData() = + dashboardDataProvider.getPastMonthDataLoadingBottomSheetData() + + suspend fun getTransactionsFetchingBottomSheetData() = + dashboardDataProvider.getTransactionsFetchingBottomSheetData() + + suspend fun getFinarkeinErrorBottomSheetData() = + dashboardDataProvider.getFinarkeinErrorBottomSheetData() + + suspend fun getAddAccountLoadingBottomSheetData() = + dashboardDataProvider.getAddAccountLoadingBottomSheetData() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashBoardFinarkeinErrorBottomSheet.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashBoardFinarkeinErrorBottomSheet.kt new file mode 100644 index 0000000000..7c15114697 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashBoardFinarkeinErrorBottomSheet.kt @@ -0,0 +1,171 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.ui + +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +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.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.navi.design.font.FontWeightEnum +import com.navi.elex.molecules.ElexButtonWithLoader +import com.navi.elex.theme.typography.TextTypography +import com.navi.moneymanager.common.analytics.DashboardEventTrackerImpl +import com.navi.moneymanager.common.analytics.ValuePropEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.dashboard.model.FinarkeinErrorBottomSheetData +import com.navi.moneymanager.postonboard.dashboard.model.FinarkeinSheetStatus + +@Composable +fun FinarkeinErrorBottomSheetContent( + screenName: String, + onRetry: () -> Unit, + onDismiss: () -> Unit, + data: FinarkeinErrorBottomSheetData +) { + + LaunchedEffect(Unit) { + when (screenName) { + MMScreen.VALUE_PROP_SCREEN.screen -> { + ValuePropEventTrackerImpl.onValuePropFinarkeinErrorBottomSheetAppeared() + } + MMScreen.VALUE_PROP_SCREEN.screen -> { + DashboardEventTrackerImpl.onDashboardFinarkeinErrorBottomSheetAppeared() + } + } + } + + DisposableEffect(Unit) { + onDispose { + when (screenName) { + MMScreen.VALUE_PROP_SCREEN.screen -> { + ValuePropEventTrackerImpl.onValuePropFinarkeinErrorBottomSheetDisappeared() + } + MMScreen.VALUE_PROP_SCREEN.screen -> { + DashboardEventTrackerImpl.onDashboardFinarkeinErrorBottomSheetDisappeared() + } + } + } + } + + Column( + modifier = + Modifier.fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 32.dp) + .navigationBarsPadding(), + horizontalAlignment = Alignment.Start + ) { + Box(modifier = Modifier.fillMaxWidth()) { + Illustration( + modifier = Modifier.size(24.dp), + illustrationType = IllustrationType.Image(data.headerIcon) + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + MMText( + text = data.titleText, + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + letterSpacing = 0.sp, + lineHeight = 24.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + + Spacer(modifier = Modifier.height(8.dp)) + + MMText( + text = data.subTitleText, + color = MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + letterSpacing = 0.sp, + lineHeight = 22.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Row(modifier = Modifier.fillMaxWidth()) { + Box( + modifier = + Modifier.weight(1f) + .clickable(enabled = (data.sheetStatus == FinarkeinSheetStatus.Error)) { + onDismiss() + } + .background(color = MMColor.ctaSecondary, shape = RoundedCornerShape(4.dp)), + contentAlignment = Alignment.Center + ) { + MMText( + text = data.leftCtaText, + modifier = Modifier.padding(horizontal = 40.dp, vertical = 12.dp), + color = MMColor.ctaPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + letterSpacing = 0.sp, + textAlign = TextAlign.Center, + lineHeight = 22.sp + ) + } + + Spacer(modifier = Modifier.width(16.dp)) + + ElexButtonWithLoader( + modifier = Modifier.weight(1f).height(44.dp), + shape = RoundedCornerShape(4.dp), + colors = + ButtonDefaults.buttonColors( + disabledContainerColor = MMColor.ctaLoaderColor, + containerColor = MMColor.ctaPrimary + ), + textColor = MMColor.white, + enabled = (data.sheetStatus == FinarkeinSheetStatus.Error), + text = + if (data.sheetStatus == FinarkeinSheetStatus.Error) data.rightCtaData.title + else "", + loading = (data.sheetStatus == FinarkeinSheetStatus.Retry), + textTypography = TextTypography.bodyMediumSemiBold, + lottieSpec = + LottieCompositionSpec.Url( + url = + if (data.sheetStatus == FinarkeinSheetStatus.Retry) + data.rightCtaData.lottieUrl + else "" + ), + onClick = { onRetry() }, + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardFooterSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardFooterSection.kt new file mode 100644 index 0000000000..e2569c0388 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardFooterSection.kt @@ -0,0 +1,37 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.ui + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.model.ImageProperties +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.DASHBOARD_FOOTER +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_PLACEHOLDER_XX_LARGE +import com.navi.moneymanager.common.illustration.ui.Illustration + +@Composable +fun DashboardFooterSection() { + Illustration( + illustrationType = + IllustrationType.Image( + source = + IllustrationSource.Remote( + url = DASHBOARD_FOOTER, + placeholder = IMAGE_PLACEHOLDER_XX_LARGE + ), + properties = ImageProperties(contentScale = ContentScale.FillWidth) + ), + modifier = Modifier.fillMaxWidth().padding(top = 24.dp), + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardHeaderSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardHeaderSection.kt new file mode 100644 index 0000000000..1a196b18bf --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardHeaderSection.kt @@ -0,0 +1,80 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.ui + +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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.design.utils.shimmerEffect +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.dashboard.model.UserHeaderState + +@Composable +fun DashboardHeaderSection(header: UserHeaderState, onHeaderHeightMeasured: (Int) -> Unit) { + Column( + modifier = + Modifier.fillMaxWidth().padding(top = 8.dp).onSizeChanged { + onHeaderHeightMeasured(it.height) + }, + horizontalAlignment = Alignment.Start + ) { + when (header) { + is UserHeaderState.Loaded -> { + MMText( + text = header.greetingPrompt.orEmpty(), + color = MMColor.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontSize = 20.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 28.sp, + ) + MMText( + text = header.expensePrompt.orEmpty(), + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 18.sp, + ) + } + UserHeaderState.Loading -> { + Box( + Modifier.fillMaxWidth(0.3f) + .clip(RoundedCornerShape(2.dp)) + .height(28.dp) + .shimmerEffect( + listOf(MMColor.ctaSecondary, MMColor.white, MMColor.ctaSecondary) + ) + ) + Spacer(Modifier.height(2.dp)) + Box( + Modifier.fillMaxWidth(0.7f) + .clip(RoundedCornerShape(2.dp)) + .height(18.dp) + .shimmerEffect( + listOf(MMColor.ctaSecondary, MMColor.white, MMColor.ctaSecondary) + ) + ) + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardOnboardingBottomSheetContent.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardOnboardingBottomSheetContent.kt new file mode 100644 index 0000000000..ca63b20a7e --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardOnboardingBottomSheetContent.kt @@ -0,0 +1,339 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +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.layout.size +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.ProgressIndicatorDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +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.res.stringResource +import androidx.compose.ui.text.style.TextOverflow.Companion.Ellipsis +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.base.utils.EMPTY +import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.R +import com.navi.moneymanager.common.analytics.DashboardEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.DASHBOARD_BOTTOMSHEET_LOADING +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.LOADING_DONE_TICK +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.MMRolodex +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.Constants.DASHBOARD_ONBOARDING_TIMEOUT +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiEffect +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiEvent +import com.navi.moneymanager.postonboard.dashboard.model.OnboardingStatus +import kotlinx.coroutines.delay + +@Composable +fun DashboardOnboardingBottomSheetContent( + onBoardingBottomSheetType: OnboardingStatus, + onEvent: (DashboardScreenUiEvent) -> Unit, + onEffect: (DashboardScreenUiEffect) -> Unit +) { + var currentProgress by remember { mutableFloatStateOf(0f) } + var progressIndicatorVisibility by remember { mutableStateOf(true) } + LaunchedEffect(Unit) { + loadProgress( + updateProgress = { + if (currentProgress < 1f) { + currentProgress = it + } + }, + onEvent = onEvent + ) + } + LaunchedEffect(onBoardingBottomSheetType) { + if (onBoardingBottomSheetType == OnboardingStatus.Success) { + currentProgress = 1f + } + } + BackHandler { onEffect(DashboardScreenUiEffect.Navigation.Back) } + Column(modifier = Modifier.fillMaxWidth().padding(bottom = 32.dp)) { + SpendCategorizationLinearProgressIndicator(currentProgress = currentProgress) { + progressIndicatorVisibility = false + } + AnimatedContent( + targetState = onBoardingBottomSheetType, + transitionSpec = { + fadeIn(animationSpec = tween(220, delayMillis = 90)) + .togetherWith(fadeOut(animationSpec = tween(90))) + }, + label = "OnBoardingBottomSheet content animation" + ) { + when (it) { + is OnboardingStatus.Loading -> { + DashboardLoadingBottomSheetContent() + } + is OnboardingStatus.TakingLonger -> { + DashboardTakingLongerBottomSheetContent(onEffect = onEffect) + } + is OnboardingStatus.Success -> { + DashboardSuccessBottomSheetContent() + } + } + } + } +} + +@Composable +fun DashboardLoadingBottomSheetContent() { + + LaunchedEffect(Unit) { + DashboardEventTrackerImpl.onDashboardOnboardingLoadingBottomSheetAppeared() + } + + DisposableEffect(Unit) { + onDispose { DashboardEventTrackerImpl.onDashboardOnboardingLoadingBottomSheetDisappeared() } + } + Column(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = 16.dp), + horizontalAlignment = Alignment.Start + ) { + Illustration( + illustrationType = + IllustrationType.Image( + source = + IllustrationSource.Remote( + url = DASHBOARD_BOTTOMSHEET_LOADING, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ) + ), + modifier = Modifier.size(32.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + MMRolodex( + viewsList = + List(3) { index -> + { + MMText( + text = + stringResource( + when (index) { + 0 -> R.string.categorising_spends + 1 -> R.string.connecting_with_your_banks + else -> R.string.tracking_recent_transactions + } + ), + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp, + overflow = Ellipsis, + maxLines = 1 + ) + } + }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + MMText( + text = stringResource(R.string.do_not_press_back), + color = MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 22.sp + ) + } + } +} + +@Composable +fun DashboardTakingLongerBottomSheetContent(onEffect: (DashboardScreenUiEffect) -> Unit) { + + LaunchedEffect(Unit) { + DashboardEventTrackerImpl.onDashboardOnboardingTakingLongBottomSheetAppeared() + } + + DisposableEffect(Unit) { + onDispose { + DashboardEventTrackerImpl.onDashboardOnboardingTakingLongBottomSheetDisappeared() + } + } + + Column(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = 16.dp), + horizontalAlignment = Alignment.Start + ) { + Illustration( + illustrationType = + IllustrationType.Image( + source = + IllustrationSource.Remote( + url = DASHBOARD_BOTTOMSHEET_LOADING, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ) + ), + modifier = Modifier.size(32.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + MMText( + text = stringResource(R.string.taking_longer_than_usual), + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp, + overflow = Ellipsis, + maxLines = 1 + ) + Spacer(modifier = Modifier.height(8.dp)) + MMText( + text = stringResource(R.string.bank_taking_longer), + color = MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 22.sp + ) + Spacer(modifier = Modifier.height(24.dp)) + Box( + modifier = + Modifier.fillMaxWidth().background(MMColor.ctaSecondary).onClickWithDebounce { + onEffect(DashboardScreenUiEffect.Navigation.Back) + }, + contentAlignment = Alignment.Center + ) { + MMText( + text = stringResource(R.string.mm_notify_me), + modifier = Modifier.padding(vertical = 16.dp, horizontal = 16.dp), + color = MMColor.ctaPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 22.sp + ) + } + } + } +} + +@Composable +fun DashboardSuccessBottomSheetContent() { + + LaunchedEffect(Unit) { + DashboardEventTrackerImpl.onDashboardOnboardingSuccessBottomSheetAppeared() + } + + DisposableEffect(Unit) { + onDispose { DashboardEventTrackerImpl.onDashboardOnboardingSuccessBottomSheetDisappeared() } + } + + Column( + modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = 16.dp), + horizontalAlignment = Alignment.Start + ) { + Illustration( + modifier = Modifier.size(32.dp), + illustrationType = + IllustrationType.Image( + source = + IllustrationSource.Remote( + url = LOADING_DONE_TICK, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ) + ) + ) + Spacer(modifier = Modifier.height(8.dp)) + MMText( + text = stringResource(R.string.done), + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp + ) + Spacer(modifier = Modifier.height(8.dp)) + MMText( + text = stringResource(R.string.do_not_press_back), + color = MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 22.sp + ) + } +} + +@Composable +fun SpendCategorizationLinearProgressIndicator( + currentProgress: Float, + hideProgressBar: () -> Unit +) { + val animatedProgress by + animateFloatAsState( + targetValue = currentProgress, + animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec, + label = EMPTY + ) { + if (it == 1f) { + hideProgressBar() + } + } + LinearProgressIndicator( + progress = { animatedProgress }, + modifier = Modifier.fillMaxWidth().height(4.dp), + color = MMColor.progressIndicatorColor, + trackColor = MMColor.ctaSecondary, + drawStopIndicator = {} + ) +} + +suspend fun loadProgress( + updateProgress: (Float) -> Unit, + onEvent: (DashboardScreenUiEvent) -> Unit +) { + val totalLoadingDuration = + FirebaseRemoteConfigHelper.getLong( + key = FirebaseRemoteConfigHelper.MONEY_MANAGER_DASHBOARD_SPEND_CATEGORIZATION_TIMEOUT, + defaultValue = DASHBOARD_ONBOARDING_TIMEOUT + ) + val updateInterval = 50L + val intervalsCount = totalLoadingDuration / updateInterval + + for (interval in 1..intervalsCount) { + updateProgress(interval.toFloat() / intervalsCount) + delay(updateInterval) + + // Trigger the "TakingLonger" event when 80% of the duration has passed + if (interval >= intervalsCount * 0.8) { + onEvent( + DashboardScreenUiEvent.UpdateOnBoardingBottomSheetStatus( + status = OnboardingStatus.TakingLonger + ) + ) + break + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardRecentTransactionsSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardRecentTransactionsSection.kt new file mode 100644 index 0000000000..42ad59eb67 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardRecentTransactionsSection.kt @@ -0,0 +1,299 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.alfred.utils.orFalse +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.composable.bankselectionbottomsheet.ZeroTransactionView +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.common.model.ZeroTransactionData +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.composable.button.PrimaryButton +import com.navi.moneymanager.common.ui.composable.transaction.TransactionDivider +import com.navi.moneymanager.common.ui.composable.transaction.TransactionListSectionUI +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.dashboard.model.AddAccountState +import com.navi.moneymanager.postonboard.dashboard.model.BankTransactionsData +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiEvent +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiState +import com.navi.moneymanager.postonboard.dashboard.model.RecentTransactionsLoadingItemData +import com.navi.moneymanager.postonboard.dashboard.model.RecentTransactionsState +import com.navi.moneymanager.postonboard.dashboard.ui.bankSection.BankChipCarousel + +@Composable +internal fun DashboardRecentTransactionsSection( + onBankClickWhileLoading: () -> Unit, + onAddAccountChipClick: (Boolean) -> Unit, + dashBoardState: () -> DashboardScreenUiState, + onEvent: (event: DashboardScreenUiEvent) -> Unit, + onTransactionClick: (String) -> Unit, + onTransactionCategoryClick: (Transaction) -> Unit, + onViewAllTransactionsClick: () -> Unit, + trackDashboardBankChipCarouselView: (Int) -> Unit, + trackDashboardBankChipClick: (Int) -> Unit, + trackDashboardAddAccountPillClick: () -> Unit +) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center, + ) { + when (val data = dashBoardState().screenData.recentTransactionsData.state) { + is RecentTransactionsState.Loaded -> { + RecentTransactionsLoadedUI( + data = data, + onBankClickWhileLoading = onBankClickWhileLoading, + onAddAccountChipClick = onAddAccountChipClick, + dashBoardState = dashBoardState, + onEvent = onEvent, + onTransactionClick = onTransactionClick, + onTransactionCategoryClick = onTransactionCategoryClick, + onViewAllTransactionsClick = onViewAllTransactionsClick, + trackDashboardBankChipCarouselView = trackDashboardBankChipCarouselView, + trackDashboardBankChipClick = trackDashboardBankChipClick, + trackDashboardAddAccountPillClick = trackDashboardAddAccountPillClick + ) + } + is RecentTransactionsState.Loading -> { + RecentTransactionsLoadingUI( + data = data, + onBankClickWhileLoading = onBankClickWhileLoading, + onAddAccountChipClick = onAddAccountChipClick, + dashBoardState = dashBoardState, + onEvent = onEvent + ) + } + RecentTransactionsState.None -> {} + } + } +} + +@Composable +private fun RecentTransactionsLoadedUI( + data: RecentTransactionsState.Loaded, + onBankClickWhileLoading: () -> Unit, + onAddAccountChipClick: (Boolean) -> Unit, + dashBoardState: () -> DashboardScreenUiState, + onEvent: (event: DashboardScreenUiEvent) -> Unit, + onTransactionClick: (String) -> Unit, + onTransactionCategoryClick: (Transaction) -> Unit, + onViewAllTransactionsClick: () -> Unit, + trackDashboardBankChipCarouselView: (Int) -> Unit, + trackDashboardBankChipClick: (Int) -> Unit, + trackDashboardAddAccountPillClick: () -> Unit +) { + SectionTitle(title = data.sectionTitle) + + BankChipCarousel( + onBankClickWhileLoading = onBankClickWhileLoading, + onAddAccountChipClick = onAddAccountChipClick, + dashBoardState = dashBoardState, + onEvent = onEvent, + trackDashboardBankChipCarouselView = trackDashboardBankChipCarouselView, + trackDashboardBankChipClick = trackDashboardBankChipClick, + trackDashboardAddAccountPillClick = trackDashboardAddAccountPillClick + ) + + TransactionWrapper( + selectedAccount = + dashBoardState().screenData.recentTransactionsData.selectedBank + ?: dashBoardState().screenData.recentTransactionsData.autoSelectedBank, + zeroTransactionData = data.zeroTransactionData, + aggregateTransactions = data.aggregateTransactions, + accountTransactions = data.accountTransactions, + onTransactionClick = onTransactionClick, + onTransactionCategoryClick = onTransactionCategoryClick + ) + + PrimaryButton( + title = data.primaryCtaTitle, + onClick = onViewAllTransactionsClick, + ) +} + +@Composable +private fun RecentTransactionsLoadingUI( + data: RecentTransactionsState.Loading, + onBankClickWhileLoading: () -> Unit, + onAddAccountChipClick: (Boolean) -> Unit, + dashBoardState: () -> DashboardScreenUiState, + onEvent: (event: DashboardScreenUiEvent) -> Unit +) { + SectionTitle(title = data.sectionTitle) + + BankChipCarousel( + onBankClickWhileLoading = onBankClickWhileLoading, + onAddAccountChipClick = onAddAccountChipClick, + dashBoardState = dashBoardState, + onEvent = onEvent + ) + + Column( + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + data.transactions.forEachIndexed { index, transaction -> + RecentTransactionsLoadingItem( + transaction = transaction, + ) + + if (index < data.transactions.lastIndex) { + TransactionDivider() + } + } + } +} + +@Composable +private fun SectionTitle(title: String) { + MMText( + text = title, + modifier = Modifier.padding(all = 16.dp), + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp, + ) +} + +@Composable +private fun BankChipCarousel( + onBankClickWhileLoading: () -> Unit, + onAddAccountChipClick: (Boolean) -> Unit, + dashBoardState: () -> DashboardScreenUiState, + onEvent: (DashboardScreenUiEvent) -> Unit, + trackDashboardBankChipCarouselView: ((Int) -> Unit)? = null, + trackDashboardBankChipClick: ((Int) -> Unit)? = null, + trackDashboardAddAccountPillClick: (() -> Unit)? = null +) { + val recentTransactionsData = dashBoardState().screenData.recentTransactionsData + BankChipCarousel( + onBankClickWhileLoading = onBankClickWhileLoading, + onAddAccountChipClick = onAddAccountChipClick, + isAddAccountChipSelected = + dashBoardState().addAccountState is AddAccountState.Selected.RecentTransactionSection, + selectedBank = + recentTransactionsData.selectedBank + ?: recentTransactionsData.autoSelectedBank.orEmpty(), + bankListExpandedState = recentTransactionsData.isBankListExpanded.orFalse(), + bankAccountsState = dashBoardState().screenData.bankSectionData.state, + onBankSelected = { bankId -> + onEvent(DashboardScreenUiEvent.UpdateSelectedBankForTransactions(bankId = bankId)) + }, + onExpandList = { onEvent(DashboardScreenUiEvent.ExpandRecentTransactionsSectionBankList) }, + trackDashboardBankChipCarouselView = trackDashboardBankChipCarouselView, + trackDashboardBankPillClick = trackDashboardBankChipClick, + trackDashboardAddAccountPillClick = trackDashboardAddAccountPillClick + ) +} + +@Composable +private fun TransactionWrapper( + selectedAccount: String? = null, + zeroTransactionData: ZeroTransactionData, + aggregateTransactions: BankTransactionsData?, + accountTransactions: List, + onTransactionClick: (String) -> Unit, + onTransactionCategoryClick: (Transaction) -> Unit, +) { + val transactions = + if (selectedAccount == aggregateTransactions?.bankReferenceId) { + aggregateTransactions + } else { + accountTransactions.firstOrNull { it.bankReferenceId == selectedAccount } + } + if (transactions == null || transactions.transactions.isEmpty()) { + ZeroTransactionView( + title = zeroTransactionData.title, + illustrationSource = zeroTransactionData.illustrationSource, + modifier = Modifier.fillMaxWidth().padding(top = 72.dp, bottom = 64.dp) + ) + } else { + TransactionListSectionUI( + transactions = transactions.transactions, + onTransactionClick = onTransactionClick, + onTransactionCategoryClick = onTransactionCategoryClick + ) + } +} + +@Composable +private fun RecentTransactionsLoadingItem(transaction: RecentTransactionsLoadingItemData) { + Row( + modifier = + Modifier.padding(all = 16.dp).fillMaxWidth().height(intrinsicSize = IntrinsicSize.Min), + verticalAlignment = Alignment.Top, + ) { + Illustration( + illustrationType = IllustrationType.Lottie(source = transaction.lottieUrl), + modifier = Modifier.size(size = 40.dp), + ) + + Spacer(modifier = Modifier.width(width = 12.dp)) + + Column( + modifier = Modifier.weight(weight = 1f), + verticalArrangement = Arrangement.spacedBy(space = 8.dp), + ) { + MMText( + text = transaction.name, + color = MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + lineHeight = 22.sp, + ) + + MMText( + text = transaction.categoryText, + color = MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 22.sp, + ) + + MMText( + text = transaction.dateText, + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 18.sp, + ) + } + + Spacer(modifier = Modifier.width(width = 24.dp)) + + Column( + modifier = Modifier.fillMaxHeight(), + horizontalAlignment = Alignment.End, + ) { + Illustration( + illustrationType = IllustrationType.Lottie(source = transaction.actionLottie), + modifier = Modifier.size(size = 30.dp), + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardScaffoldRenderer.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardScaffoldRenderer.kt new file mode 100644 index 0000000000..b53119afed --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardScaffoldRenderer.kt @@ -0,0 +1,455 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +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.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.shadow +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.base.model.CtaData +import com.navi.base.utils.EMPTY +import com.navi.base.utils.orZero +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.R +import com.navi.moneymanager.common.analytics.DashboardEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.COMMON_ARROW_RIGHT_WHITE +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.NEW_TRANSACTION +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.model.FilterAttribute +import com.navi.moneymanager.common.model.SpendCategorizationAction +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.MMTopBar +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.composable.spendCategoriztion.SpendCategorizationSection +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.Constants.NEW_TRANSACTION_COUNT +import com.navi.moneymanager.common.utils.asList +import com.navi.moneymanager.common.utils.flipValuePostDelay +import com.navi.moneymanager.common.utils.getTransactionFilterMonthWithYear +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenNavigationData +import com.navi.moneymanager.postonboard.dashboard.model.AddAccountState +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountsState +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiEffect +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiEvent +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiState +import com.navi.moneymanager.postonboard.dashboard.ui.bankSection.BankSection +import com.navi.moneymanager.postonboard.dashboard.viewmodel.DashboardViewModel +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionFilterData +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionFilterItem +import com.navi.moneymanager.preonboard.launcher.ui.LauncherLoadingShimmer +import com.navi.naviwidgets.R as NaviWidgetsR +import kotlinx.coroutines.flow.distinctUntilChanged + +@Composable +fun DashboardScaffoldRenderer( + modifier: Modifier, + dashboardState: () -> DashboardScreenUiState, + onEvent: (DashboardScreenUiEvent) -> Unit, + onEffect: (DashboardScreenUiEffect) -> Unit, + getViewModel: () -> DashboardViewModel +) { + + val scrollState = rememberScrollState() + val density = LocalDensity.current + var headerSectionHeight by remember { mutableIntStateOf(0) } + val headerScrolledToTop by remember { + derivedStateOf { scrollState.value > headerSectionHeight + with(density) { 24.dp.toPx() } } + } + + if (dashboardState().isLoading) { + LauncherLoadingShimmer() + return + } + + val bankSectionData = dashboardState().screenData.bankSectionData + val backgroundColor by + animateColorAsState( + targetValue = + if (bankSectionData.state is BankAccountsState.Loaded) { + MMColor.ctaSecondary + } else { + MMColor.white + }, + animationSpec = tween(200), + label = "HeaderBgColor" + ) + Scaffold( + modifier = modifier, + topBar = { + MMTopBar( + title = EMPTY, + titleColor = MMColor.ctaPrimary, + backgroundColor = if (headerScrolledToTop) MMColor.white else backgroundColor, + navigationIcon = NaviWidgetsR.drawable.ic_cross_black, + actionIconText = dashboardState().screenData.topNavBar.actionLabel, + onActionClick = { + DashboardEventTrackerImpl.onDashboardHelpClicked() + onEffect(DashboardScreenUiEffect.Navigation.Help) + }, + onNavigationIconClick = { onEffect(DashboardScreenUiEffect.Navigation.Back) }, + modifier = + Modifier.apply { + if (headerScrolledToTop) { + shadow( + elevation = 1.dp, + ambientColor = MMColor.ctaSecondary, + spotColor = MMColor.ctaSecondary + ) + } + } + ) + }, + ) { + Column( + modifier = + Modifier.fillMaxSize() + .background(MMColor.white) + .padding(it) + .verticalScroll(state = scrollState), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween + ) { + Column { + Column( + modifier = Modifier.background(backgroundColor).padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + DashboardHeaderSection(header = dashboardState().screenData.header) { height -> + headerSectionHeight = height + } + BankSection( + bankSectionData = bankSectionData, + isAddAccountChipSelected = + dashboardState().addAccountState + is AddAccountState.Selected.BankSection, + onBankClickWhileLoading = { getViewModel().handleBankClickWhileLoading() }, + onAddAccountChipClick = { isTotalSyncCompleted -> + if (isTotalSyncCompleted) { + onEvent( + DashboardScreenUiEvent.HandleAddAccountState( + AddAccountState.Selected.BankSection + ) + ) + onEffect(DashboardScreenUiEffect.Navigation.FetchFinarkeinData) + } else { + getViewModel().showAddAccountLoadingBottomSheet() + } + }, + onEvent = onEvent, + trackDashboardBankChipCarouselView = { pillCount -> + DashboardEventTrackerImpl.onDashboardBankAccountsPillsViewed( + "bank_section", + pillCount + ) + }, + trackDashboardBankChipClick = { pillRank -> + DashboardEventTrackerImpl.onDashboardBankAccountsPillsClicked( + "bank_section", + pillRank + ) + }, + trackDashboardAddAccountPillClick = { + DashboardEventTrackerImpl.onDashboardAddAccountPillClicked( + "bank_section" + ) + } + ) + } + dashboardState().screenData.spendCategorizationState?.let { spendCategorizationState + -> + SpendCategorizationSection( + screenName = MMScreen.DASHBOARD.screen, + isAddAccountSelected = + (dashboardState().addAccountState + is AddAccountState.Selected.SpentCategorizationSection), + spendCategorizationState = spendCategorizationState, + spendCategorizationAction = { spendCategorizationAction -> + when (spendCategorizationAction) { + is SpendCategorizationAction.AddNewBankAccount -> { + if (spendCategorizationAction.isTotalSyncCompleted) { + onEvent( + DashboardScreenUiEvent.HandleAddAccountState( + AddAccountState.Selected.SpentCategorizationSection + ) + ) + onEffect( + DashboardScreenUiEffect.Navigation.FetchFinarkeinData + ) + } else { + getViewModel().showAddAccountLoadingBottomSheet() + } + } + is SpendCategorizationAction.SelectCategory -> { + onEffect( + DashboardScreenUiEffect.Navigation.CategoryDetailsScreen( + data = + CategoryDetailsScreenNavigationData( + month = + spendCategorizationAction.selectedMonth + .month + .orZero(), + year = + spendCategorizationAction.selectedMonth.year + .orZero(), + selectedCategory = + spendCategorizationAction.categoryId + ), + ) + ) + } + is SpendCategorizationAction.SelectSelfTransferCategory -> { + val transactionFilterList = + mutableListOf() + + transactionFilterList.add( + TransactionFilterData( + attributeKey = FilterAttribute.CATEGORY.value, + itemList = + TransactionFilterItem( + valueName = + spendCategorizationAction.categoryName, + isSelected = true + ) + .asList() + ) + ) + + if ( + spendCategorizationAction.selectedMonth.month != null && + spendCategorizationAction.selectedMonth.year != null + ) { + transactionFilterList.add( + TransactionFilterData( + attributeKey = FilterAttribute.MONTH.value, + itemList = + TransactionFilterItem( + valueName = + getTransactionFilterMonthWithYear( + spendCategorizationAction + .selectedMonth + .month, + spendCategorizationAction + .selectedMonth + .year + ), + isSelected = true + ) + .asList() + ) + ) + } + DashboardEventTrackerImpl + .onDashboardSelfTransferCategoryClicked() + onEffect( + DashboardScreenUiEffect.Navigation.TransactionHistory( + filterList = transactionFilterList + ) + ) + } + is SpendCategorizationAction.OnMonthChange -> { + DashboardEventTrackerImpl.onDashboardMonthSelectionRequested() + getViewModel() + .handleMonthlySummaryMonthChangeClick( + spendCategorizationAction.displayMonthText + ) + } + is SpendCategorizationAction.ViewMoreCategories -> { + DashboardEventTrackerImpl + .onDashboardCategorySectionViewMoreClicked() + onEffect( + DashboardScreenUiEffect.Navigation.SpendAnalysisScreen( + selectedMonth = spendCategorizationAction.selectedMonth + ) + ) + } + is SpendCategorizationAction.ViewTotalSpends -> { + onEffect( + DashboardScreenUiEffect.Navigation.SpendAnalysisScreen( + selectedMonth = spendCategorizationAction.selectedMonth + ) + ) + } + } + } + ) + } + DashboardRecentTransactionsSection( + onBankClickWhileLoading = { getViewModel().handleBankClickWhileLoading() }, + onAddAccountChipClick = { isTotalSyncCompleted -> + if (isTotalSyncCompleted) { + onEvent( + DashboardScreenUiEvent.HandleAddAccountState( + AddAccountState.Selected.RecentTransactionSection + ) + ) + onEffect(DashboardScreenUiEffect.Navigation.FetchFinarkeinData) + } else { + getViewModel().showAddAccountLoadingBottomSheet() + } + }, + dashBoardState = dashboardState, + onEvent = { event -> onEvent(event) }, + onTransactionClick = { transactionId -> + onEffect( + DashboardScreenUiEffect.Navigation.TransactionDetails( + transactionId = transactionId, + ) + ) + }, + onTransactionCategoryClick = { transaction -> + onEffect(DashboardScreenUiEffect.OpenCategoryBottomSheet(transaction)) + }, + onViewAllTransactionsClick = { + DashboardEventTrackerImpl.onDashboardViewAllTransactionsClicked() + onEffect(DashboardScreenUiEffect.Navigation.TransactionHistory()) + }, + trackDashboardBankChipCarouselView = { pillCount -> + DashboardEventTrackerImpl.onDashboardBankAccountsPillsViewed( + "recent_transactions_section", + pillCount + ) + }, + trackDashboardBankChipClick = { pillRank -> + DashboardEventTrackerImpl.onDashboardBankAccountsPillsClicked( + "recent_transactions_section", + pillRank + ) + }, + trackDashboardAddAccountPillClick = { + DashboardEventTrackerImpl.onDashboardAddAccountPillClicked( + "recent_transactions_section" + ) + } + ) + } + DashboardFooterSection() + } + } + + if (dashboardState().addAccountState is AddAccountState.Selected) { + Box(modifier = Modifier.fillMaxSize().clickable(enabled = false) {}) + } + FloatingNewTransactionButton(viewModel = getViewModel) +} + +@Composable +fun FloatingNewTransactionButton(viewModel: () -> DashboardViewModel) { + + val context = LocalContext.current + + val isVisible = remember { mutableStateOf(false) } + + var newTransactionCount by remember { mutableIntStateOf(0) } + + LaunchedEffect(Unit) { + viewModel().initNewTransactionObserver().distinctUntilChanged().collect { + if (it <= 0) return@collect + newTransactionCount = it + viewModel().dbDataStoreProvider.saveIntData(NEW_TRANSACTION_COUNT, -1) + isVisible.flipValuePostDelay(true, 6000) + } + } + + AnimatedVisibility( + modifier = Modifier.navigationBarsPadding(), + visible = newTransactionCount > 0 && isVisible.value, + enter = fadeIn() + slideInVertically(initialOffsetY = { 500 }), + exit = fadeOut() + slideOutVertically(targetOffsetY = { 500 }) + ) { + Box(modifier = Modifier.fillMaxSize().padding(bottom = 32.dp)) { + Row( + modifier = + Modifier.align(Alignment.BottomCenter) + .wrapContentSize() + .background(color = MMColor.ctaPrimary, shape = RoundedCornerShape(4.dp)) + .padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 8.dp) + .onClickWithDebounce { + viewModel().setEffect { + DashboardScreenUiEffect.Navigation.NavigateToCta( + CtaData(url = MMScreen.TRANSACTION_HISTORY.screen) + ) + } + } + ) { + Illustration( + modifier = Modifier.padding(bottom = 1.dp).size(22.dp), + illustrationType = + IllustrationType.Image( + IllustrationSource.Remote( + url = NEW_TRANSACTION, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ) + ) + MMText( + text = + if (newTransactionCount > 1) { + "$newTransactionCount ${context.getString(R.string.new_transactions)}" + } else { + "$newTransactionCount ${context.getString(R.string.new_transactions)}" + }, + modifier = Modifier.padding(start = 8.dp, end = 8.dp), + color = MMColor.white, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + letterSpacing = 0.sp, + textAlign = TextAlign.Center, + lineHeight = 22.sp + ) + Illustration( + modifier = Modifier.padding(bottom = 1.dp).size(20.dp), + illustrationType = + IllustrationType.Image( + IllustrationSource.Remote( + url = COMMON_ARROW_RIGHT_WHITE, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ) + ) + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardScreen.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardScreen.kt new file mode 100644 index 0000000000..6bdabfab09 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/DashboardScreen.kt @@ -0,0 +1,384 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.ui + +import android.os.Bundle +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.navi.base.model.CtaData +import com.navi.base.model.LineItem +import com.navi.base.utils.orZero +import com.navi.common.constants.MONTH +import com.navi.common.constants.YEAR +import com.navi.common.navigation.NavigationAction +import com.navi.common.utils.Constants.WEB_URL +import com.navi.moneymanager.common.analytics.DashboardEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.data.datastore.DataStoreInfoProvider +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.model.bottomSheet.DashboardScreenBottomSheets +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.DataLoadingBottomSheetContent +import com.navi.moneymanager.common.ui.composable.ScreenInit +import com.navi.moneymanager.common.ui.composable.bottomSheet.BottomSheet +import com.navi.moneymanager.common.ui.composable.bottomSheet.MonthSelectionBottomSheetUI +import com.navi.moneymanager.common.utils.Constants.GREEN_TICK_MARK +import com.navi.moneymanager.common.utils.Constants.IS_TOTAL_SYNC_COMPLETED +import com.navi.moneymanager.common.utils.Constants.OKAY_GOT_IT +import com.navi.moneymanager.common.utils.Constants.PLEASE_TRY_AGAIN +import com.navi.moneymanager.common.utils.Constants.SOMETHING_WENT_WRONG +import com.navi.moneymanager.common.utils.Constants.TRANSACTION_ID +import com.navi.moneymanager.common.utils.MMScreenEventLogger +import com.navi.moneymanager.common.utils.ShowCustomToast +import com.navi.moneymanager.common.utils.ToastDuration +import com.navi.moneymanager.common.utils.getFaqCta +import com.navi.moneymanager.common.utils.getTransactionCategorizedMessage +import com.navi.moneymanager.entry.ui.activity.MMActivity +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiEffect +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiEvent +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiEvent.UpdateFinarkeinBottomSheetType +import com.navi.moneymanager.postonboard.dashboard.model.FinarkeinSheetStatus +import com.navi.moneymanager.postonboard.dashboard.viewmodel.DashboardViewModel +import com.navi.moneymanager.postonboard.help.ui.HelpBottomSheetContent +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetTransactionData +import com.navi.moneymanager.postonboard.monthlysummary.ui.bottomsheet.CategoryCommonBottomSheet +import com.navi.moneymanager.postonboard.monthlysummary.ui.bottomsheet.CustomToastView +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenNavigationData +import com.navi.moneymanager.preonboard.finarkein.model.FinarkeinDataState +import com.navi.moneymanager.preonboard.launcher.model.GenericErrorBottomSheetData +import com.navi.moneymanager.preonboard.launcher.ui.GenericErrorBottomSheet +import com.navi.naviwidgets.utils.URL +import com.ramcosta.composedestinations.annotation.Destination +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach + +@Destination +@Composable +fun DashboardScreen( + activity: MMActivity, + bundle: Bundle? = null, + viewModel: DashboardViewModel = hiltViewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val sharedVM = activity.getSharedVM() + + val showBottomSheet = remember { mutableStateOf(false) } + var showTransactionUpdatedToast by remember { mutableStateOf(false) } + var updatedTransactionCount by remember { mutableIntStateOf(0) } + + ScreenInit(screenName = MMScreen.DASHBOARD.screen, activity = activity) + + MMScreenEventLogger( + onScreenLand = { DashboardEventTrackerImpl.onDashboardScreenLanded() }, + onScreenExit = { DashboardEventTrackerImpl.onDashboardScreenExit() } + ) + + LaunchedEffect(Unit) { + sharedVM.showFinarkeinErrorBottomSheet.collect { show -> + if (show) { + viewModel.showFinarkeinErrorBottomSheet() + } + } + } + + LaunchedEffect(Unit) { + viewModel.effect + .onEach { effect -> + when (effect) { + DashboardScreenUiEffect.Navigation.Back -> { + activity.finish() + } + DashboardScreenUiEffect.Navigation.Help -> { + viewModel.sendEvent( + DashboardScreenUiEvent.ShowBottomSheet( + type = DashboardScreenBottomSheets.HelpBottomSheet() + ) + ) + } + DashboardScreenUiEffect.Navigation.FetchFinarkeinData -> { + viewModel.fetchFinarkeinData { + when (it) { + FinarkeinDataState.Failure -> { + viewModel.sendEvent( + DashboardScreenUiEvent.ShowBottomSheet( + type = + DashboardScreenBottomSheets.GenericErrorBottomSheet( + data = + GenericErrorBottomSheetData( + titleText = SOMETHING_WENT_WRONG, + subTitleText = PLEASE_TRY_AGAIN, + ctaText = OKAY_GOT_IT + ) + ) + ) + ) + } + is FinarkeinDataState.Success -> { + activity.launchFinarkeinSdk( + accessToken = it.data.vendorAuthToken.orEmpty(), + requestId = it.data.vendorRequestId.orEmpty(), + redirectUrl = it.data.vendorRedirectUrl.orEmpty(), + ) + } + } + } + } + DashboardScreenUiEffect.Navigation.DismissFinarkeinBottomSheet -> { + sharedVM.updateFinarkeinErrorBottomSheetState(show = false) + } + is DashboardScreenUiEffect.Navigation.TransactionDetails -> { + activity.mmNavigator.navigateTo( + activity = activity, + ctaData = CtaData(url = MMScreen.TRANSACTION_DETAILS.screen), + bundle = + Bundle().apply { putString(TRANSACTION_ID, effect.transactionId) }, + navigationAction = NavigationAction.Default, + ) + } + is DashboardScreenUiEffect.Navigation.TransactionHistory -> { + val screenData = + effect.filterList?.let { + TransactionHistoryScreenNavigationData(filterList = it) + } + activity.mmNavigator.navigateTo( + activity = activity, + ctaData = CtaData(url = MMScreen.TRANSACTION_HISTORY.screen), + navigationAction = NavigationAction.Default, + screenData = screenData, + ) + } + is DashboardScreenUiEffect.Navigation.NavigateToCta -> { + activity.mmNavigator.navigateTo( + activity = activity, + ctaData = effect.ctaData + ) + } + is DashboardScreenUiEffect.Navigation.SpendAnalysisScreen -> { + activity.mmNavigator.navigateTo( + activity = activity, + ctaData = CtaData(url = MMScreen.SPEND_ANALYSIS.screen), + bundle = + Bundle().apply { + putInt(MONTH, effect.selectedMonth.month.orZero()) + putInt(YEAR, effect.selectedMonth.year.orZero()) + }, + navigationAction = NavigationAction.Default, + ) + } + is DashboardScreenUiEffect.Navigation.CategoryDetailsScreen -> { + activity.mmNavigator.navigateTo( + activity = activity, + ctaData = CtaData(url = MMScreen.CATEGORY_DETAILS.screen), + screenData = effect.data, + navigationAction = NavigationAction.Default, + ) + } + is DashboardScreenUiEffect.OpenCategoryBottomSheet -> { + viewModel.sendEvent( + DashboardScreenUiEvent.UpdateTransactionData(effect.transaction) + ) + showBottomSheet.value = true + } + DashboardScreenUiEffect.FetchConsentUrl -> { + viewModel.fetchConsentUrl() + } + } + } + .collect() + } + + LaunchedEffect(Unit) { viewModel.updateSpendAnalysisSection() } + + BackHandler { viewModel.setEffect { DashboardScreenUiEffect.Navigation.Back } } + + DashboardScaffoldRenderer( + modifier = Modifier, + dashboardState = { state }, + onEvent = { event -> viewModel.sendEvent(event) }, + onEffect = { effect -> viewModel.setEffect { effect } }, + getViewModel = { viewModel } + ) + + BottomSheet( + state = state.bottomSheetState, + onDismiss = { _, uiEvent -> viewModel.sendEvent(uiEvent) } + ) { bottomSheet -> + DashboardBottomSheetContentHandler( + bottomSheet = bottomSheet, + dataStoreInfoProvider = viewModel.dbDataStoreProvider, + onEvent = { viewModel.sendEvent(it) }, + onEffect = { viewModel.setEffect { it } }, + onMonthSelected = { month -> + viewModel.updateSpendCategorizationSectionData(month.first, month.second) + } + ) + } + + CategoryCommonBottomSheet( + screenName = MMScreen.DASHBOARD.screen, + modifier = Modifier, + showBottomSheet = showBottomSheet, + categoryTransactionData = + CategoryBottomSheetTransactionData( + transactionId = state.transaction?.id.orEmpty(), + categoryId = state.transaction?.categoryId.orEmpty(), + counterPartyName = state.transaction?.counterPartyName.orEmpty(), + categoryName = state.transaction?.categoryName.orEmpty(), + transactionType = state.transaction?.type.orEmpty() + ), + onDismiss = { showBottomSheet.value = false }, + onSuccessFullyUpdatedCategory = { + showTransactionUpdatedToast = true + updatedTransactionCount = it + } + ) + if (showTransactionUpdatedToast) { + ShowCustomToast( + context = activity, + toastDuration = ToastDuration.LENGTH_SHORT, + content = { + CustomToastView( + message = getTransactionCategorizedMessage(updatedTransactionCount, activity), + illustrationType = + IllustrationType.Image(IllustrationSource.Resource(resId = GREEN_TICK_MARK)) + ) + } + ) + showTransactionUpdatedToast = false + } +} + +@Composable +private fun DashboardBottomSheetContentHandler( + bottomSheet: DashboardScreenBottomSheets, + dataStoreInfoProvider: DataStoreInfoProvider, + onEvent: (DashboardScreenUiEvent) -> Unit, + onEffect: (DashboardScreenUiEffect) -> Unit, + onMonthSelected: (Pair) -> Unit = {} +) { + if (bottomSheet.sheetData == null) return + val onDismissAction = bottomSheet.createDismissAction(onEvent) + + when (bottomSheet) { + is DashboardScreenBottomSheets.FetchingTransactions -> { + DataLoadingBottomSheetContent( + data = bottomSheet.data!!, + onDismiss = { onDismissAction() }, + trackBottomSheetAppears = { + DashboardEventTrackerImpl.onDashboardFetchingTransactionsBottomSheetAppeared() + }, + trackBottomSheetDisappears = { + DashboardEventTrackerImpl + .onDashboardFetchingTransactionsBottomSheetDisappeared() + } + ) + } + is DashboardScreenBottomSheets.MonthSelection -> { + MonthSelectionBottomSheetUI( + screenName = MMScreen.DASHBOARD.screen, + data = bottomSheet.data!!, + onDismiss = { onDismissAction() }, + onMonthSelected = onMonthSelected + ) + } + is DashboardScreenBottomSheets.OnBoarding -> { + DashboardOnboardingBottomSheetContent( + onBoardingBottomSheetType = bottomSheet.state, + onEvent = onEvent, + onEffect = onEffect + ) + } + is DashboardScreenBottomSheets.PastMonthDataLoading -> { + LaunchedEffect(Unit) { + dataStoreInfoProvider.getBooleanData(key = IS_TOTAL_SYNC_COMPLETED).collect { + pastMonthData -> + if (pastMonthData) { + onDismissAction() + } + } + } + DataLoadingBottomSheetContent( + data = bottomSheet.data!!, + onDismiss = { onDismissAction() }, + trackBottomSheetAppears = { + DashboardEventTrackerImpl + .onDashboardPastMonthCategoryRefreshInProgressBottomsheetAppeared() + }, + trackBottomSheetDisappears = { + DashboardEventTrackerImpl + .onDashboardPastMonthCategoryRefreshInProgressBottomsheetDisappeared() + } + ) + } + is DashboardScreenBottomSheets.FinarkeinError -> { + FinarkeinErrorBottomSheetContent( + screenName = MMScreen.DASHBOARD.screen, + data = bottomSheet.data!!, + onRetry = { + onEvent(UpdateFinarkeinBottomSheetType(FinarkeinSheetStatus.Retry)) + onEffect(DashboardScreenUiEffect.Navigation.DismissFinarkeinBottomSheet) + onEffect(DashboardScreenUiEffect.Navigation.FetchFinarkeinData) + }, + onDismiss = { + onEffect(DashboardScreenUiEffect.Navigation.DismissFinarkeinBottomSheet) + onDismissAction() + } + ) + } + is DashboardScreenBottomSheets.HelpBottomSheet -> { + HelpBottomSheetContent( + screenName = MMScreen.DASHBOARD.screen, + state = bottomSheet.state, + onFaqClicked = { + onEffect(DashboardScreenUiEffect.Navigation.NavigateToCta(getFaqCta())) + }, + manageConsent = { onEffect(DashboardScreenUiEffect.FetchConsentUrl) }, + navigateToUrl = { url -> + onEffect( + DashboardScreenUiEffect.Navigation.NavigateToCta( + CtaData( + url = WEB_URL, + parameters = listOf(LineItem(key = URL, value = url)) + ) + ) + ) + }, + onDismiss = onDismissAction + ) + } + is DashboardScreenBottomSheets.GenericErrorBottomSheet -> { + GenericErrorBottomSheet( + data = bottomSheet.data!!, + onDismiss = { onDismissAction() }, + screenName = MMScreen.DASHBOARD.name + ) + } + is DashboardScreenBottomSheets.AddAccountLoading -> { + DataLoadingBottomSheetContent( + data = bottomSheet.data!!, + onDismiss = { onDismissAction() }, + trackBottomSheetAppears = { + DashboardEventTrackerImpl.onDashboardAddAccountLoadingBottomSheetAppeared() + }, + trackBottomSheetDisappears = { + DashboardEventTrackerImpl.onDashboardAddAccountLoadingBottomSheetDisappeared() + } + ) + } + DashboardScreenBottomSheets.NoBottomSheet -> {} + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/bankSection/BankBalanceSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/bankSection/BankBalanceSection.kt new file mode 100644 index 0000000000..61e4ea4922 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/bankSection/BankBalanceSection.kt @@ -0,0 +1,230 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.ui.bankSection + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.size +import androidx.compose.foundation.layout.widthIn +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.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountsData +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountsState + +/** + * Displays the bank balance section, displaying an illustration and balance details based on the + * current bank summary state and selected bank. + * + * @param selectedBank The reference ID of the currently selected bank. + * @param bankAccountsState The state of the bank summary data. + */ +@Composable +fun BankBalanceSection(selectedBank: String, bankAccountsState: BankAccountsState) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(16.dp) + ) { + BankBalanceSectionIllustration( + bankAccountsState = bankAccountsState, + selectedBank = selectedBank + ) + + BankBalanceSectionContent( + bankAccountsState = bankAccountsState, + selectedBank = selectedBank, + ) + } +} + +/** + * Displays an illustration based on the `bankSummaryState`. It displays a Lottie animation when + * loading and an image when loaded, with styling dependent on whether the aggregate bank is + * selected. + * + * @param bankAccountsState The state of the bank summary. + * @param selectedBank The reference ID of the currently selected bank. + */ +@Composable +private fun BankBalanceSectionIllustration( + bankAccountsState: BankAccountsState, + selectedBank: String +) { + when (bankAccountsState) { + is BankAccountsState.Loading -> { + bankAccountsState.data.balanceSectionLottie.let { lottie -> + Illustration( + illustrationType = IllustrationType.Lottie(lottie), + modifier = Modifier.size(40.dp) + ) + } + } + is BankAccountsState.Loaded -> { + val data = bankAccountsState.data + val isAggregateSelected = selectedBank == data.aggregate?.referenceId + val image = + if (isAggregateSelected) { + data.aggregate?.bankBalanceSectionImage + } else { + data.accounts.find { it.referenceId == selectedBank }?.bankBalanceSectionImage + } + Box( + modifier = + Modifier.clip(CircleShape) + .size(40.dp) + .background(if (isAggregateSelected) MMColor.lightGray else MMColor.white) + .border(width = 1.dp, color = MMColor.borderColor, shape = CircleShape), + contentAlignment = Alignment.Center + ) { + image?.let { + Illustration( + illustrationType = IllustrationType.Image(image), + modifier = Modifier.size(24.dp) + ) + } + } + } + BankAccountsState.None -> {} + } +} + +/** + * Displays the content of the bank balance section based on the current `bankSummaryState`. + * + * In the loading state, it shows a placeholder message and a Lottie animation. In the loaded state, + * it displays the selected bank's name and balance, or the aggregate bank details if selected + * + * @param bankAccountsState The state of the bank summary, determining the content to display. + * @param selectedBank The reference ID of the currently selected bank. + */ +@Composable +private fun BankBalanceSectionContent(bankAccountsState: BankAccountsState, selectedBank: String) { + Column(modifier = Modifier.padding(start = 16.dp)) { + when (bankAccountsState) { + is BankAccountsState.Loading -> { + bankAccountsState.data.balanceLottie.let { lottie -> + MMText( + text = bankAccountsState.data.balanceSectionTitle, + modifier = Modifier.widthIn(max = 200.dp), + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 18.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + Spacer(Modifier.height(2.dp)) + Illustration( + illustrationType = IllustrationType.Lottie(lottie), + modifier = Modifier.height(26.dp) + ) + } + } + is BankAccountsState.Loaded -> { + val isAggregateSelected = + selectedBank == bankAccountsState.data.aggregate?.referenceId + when { + // When the aggregate bank is selected or only one bank is available. + (isAggregateSelected && bankAccountsState.data.aggregate != null) -> { + MMText( + text = bankAccountsState.data.aggregate.balanceSectionTitlePrefix, + modifier = Modifier.widthIn(max = 200.dp), + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 18.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + } + else -> { + BalanceSectionTitle( + data = bankAccountsState.data, + selectedBank = selectedBank + ) + } + } + Spacer(Modifier.height(2.dp)) + // Displays the balance of the selected bank or the aggregate bank + MMText( + text = + when { + isAggregateSelected -> + bankAccountsState.data.aggregate?.bankBalance.orEmpty() + else -> + bankAccountsState.data.accounts + .find { it.referenceId == selectedBank } + ?.bankBalance + .orEmpty() + }, + modifier = Modifier.widthIn(max = 200.dp), + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + } + BankAccountsState.None -> {} + } + } +} + +/** + * Displays the title for the balance section of an individual bank account. + * + * It combines the bank name and the last 4 digits of the account number. The bank name may be + * ellipsized to accommodate the screen width and ensure the account number is visible. + * + * @param data The loaded bank accounts data containing information for all accounts. + * @param selectedBank The reference ID of the currently selected bank. + */ +@Composable +private fun BalanceSectionTitle(data: BankAccountsData, selectedBank: String) { + val bank = data.accounts.find { it.referenceId == selectedBank } + Row(Modifier.fillMaxWidth()) { + MMText( + text = bank?.balanceSectionTitlePrefix.orEmpty(), + modifier = Modifier.weight(1f, fill = false), + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 18.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + MMText( + text = bank?.balanceSectionTitleSuffix.orEmpty(), + modifier = Modifier.weight(1f, fill = false), + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 18.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/bankSection/BankChipCarousel.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/bankSection/BankChipCarousel.kt new file mode 100644 index 0000000000..c12122765c --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/bankSection/BankChipCarousel.kt @@ -0,0 +1,351 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.ui.bankSection + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.key +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.base.utils.EMPTY +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.model.ChipData +import com.navi.moneymanager.common.model.ChipIllustration +import com.navi.moneymanager.common.ui.composable.MMChip +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.ChipThemeCodes +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.dashboard.model.AddBankChipInfo +import com.navi.moneymanager.postonboard.dashboard.model.BankAccount +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountsData +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountsLoadingState +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountsState + +@Composable +fun BankChipCarousel( + modifier: Modifier = Modifier, + bankAccountsState: BankAccountsState, + selectedBank: String, + bankListExpandedState: Boolean, + isAddAccountChipSelected: Boolean, + onBankClickWhileLoading: () -> Unit, + onAddAccountChipClick: (Boolean) -> Unit, + onExpandList: () -> Unit, + onBankSelected: (String) -> Unit, + trackDashboardBankChipCarouselView: ((Int) -> Unit)? = null, + trackDashboardBankPillClick: ((Int) -> Unit)? = null, + trackDashboardAddAccountPillClick: (() -> Unit)? = null +) { + LaunchedEffect(bankAccountsState is BankAccountsState.Loaded) { + if (bankAccountsState is BankAccountsState.Loaded) { + val pillCount = bankAccountsState.data.accounts.size + 1 + trackDashboardBankChipCarouselView?.invoke(pillCount) + } + } + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier.fillMaxWidth().horizontalScroll(rememberScrollState()) + ) { + // Spacer to add the start spacing + Spacer(Modifier.width(8.dp)) + + when (bankAccountsState) { + is BankAccountsState.Loading -> LoadingBankChip(data = bankAccountsState.data) + is BankAccountsState.Loaded -> { + val data = bankAccountsState.data + // Displays the All bank chip if there is more than 1 banks + if (data.aggregate != null) { + AggregateBankChip( + selectedBank = selectedBank, + aggregateDetails = data.aggregate, + onAggregatedBankSelected = onBankSelected, + trackDashboardBankPillClick = trackDashboardBankPillClick + ) + } + // Displays the list of banks + BankChips( + onBankClickWhileLoading = onBankClickWhileLoading, + bankList = data.accounts.take(2), + selectedBank = selectedBank, + onBankSelected = onBankSelected, + startRank = 2, + trackDashboardBankPillClick = trackDashboardBankPillClick + ) + // Displays the remaining banks if there are more than 2 + ExpandableBankChips( + onBankClickWhileLoading = onBankClickWhileLoading, + data = data, + bankListExpandedState = bankListExpandedState, + selectedBank = selectedBank, + onBankSelected = onBankSelected, + onExpandList = onExpandList, + trackDashboardBankPillClick = trackDashboardBankPillClick + ) + // Displays a chip to add a new bank + AddBankChip( + onAddAccountChipClicked = onAddAccountChipClick, + isAddAccountChipSelected = isAddAccountChipSelected, + addBankChipInfo = data.addBankChipInfo, + trackDashboardAddAccountPillClick = trackDashboardAddAccountPillClick + ) + // Spacer to add the end spacing + Spacer(Modifier.width(8.dp)) + } + BankAccountsState.None -> {} + } + } +} + +@Composable +private fun LoadingBankChip(data: BankAccountsLoadingState) { + MMChip( + chipData = + ChipData( + illustration = + ChipIllustration( + illustrationType = IllustrationType.Lottie(data.chipLottie), + size = 16 + ), + content = data.chipText + ), + chipThemeCode = ChipThemeCodes.BANK_THEME, + isSelected = true, + padding = PaddingValues(start = 8.dp, end = 12.dp, top = 8.dp, bottom = 8.dp) + ) +} + +@Composable +private fun ExpandableBankChips( + data: BankAccountsData, + bankListExpandedState: Boolean, + selectedBank: String, + onBankSelected: (String) -> Unit, + onExpandList: () -> Unit, + onBankClickWhileLoading: () -> Unit, + trackDashboardBankPillClick: ((Int) -> Unit)? = null +) { + if (data.accounts.size > 2) { + AnimatedContent( + targetState = bankListExpandedState, + transitionSpec = { + fadeIn(animationSpec = tween(300)) + + expandHorizontally(animationSpec = tween(300)) togetherWith + fadeOut(animationSpec = tween(300)) + + shrinkHorizontally(animationSpec = tween(300)) + }, + label = EMPTY + ) { isExpanded -> + if (isExpanded) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + BankChips( + onBankClickWhileLoading = onBankClickWhileLoading, + bankList = data.accounts.drop(2), + selectedBank = selectedBank, + onBankSelected = onBankSelected, + startRank = 4, + trackDashboardBankPillClick = trackDashboardBankPillClick + ) + } + } else { + ExpandBankListButton( + shouldShow = data.accounts.size > 2, + remainingListCount = data.accounts.size - 2, + onExpandList = onExpandList, + trackDashboardBankPillClick = trackDashboardBankPillClick + ) + } + } + } +} + +@Composable +private fun ExpandBankListButton( + shouldShow: Boolean, + remainingListCount: Int, + onExpandList: () -> Unit, + trackDashboardBankPillClick: ((Int) -> Unit)? = null +) { + if (shouldShow) { + Box( + modifier = + Modifier.size(32.dp) + .clip(CircleShape) + .background(MMColor.ctaSecondary) + .onClickWithDebounce( + interactionSource = remember { MutableInteractionSource() }, + onClick = { + trackDashboardBankPillClick?.invoke(-1) + onExpandList() + } + ), + contentAlignment = Alignment.Center + ) { + MMText( + text = "+$remainingListCount", + color = MMColor.ctaPrimary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 16.sp, + ) + } + } +} + +@Composable +private fun AggregateBankChip( + selectedBank: String, + aggregateDetails: BankAccount, + onAggregatedBankSelected: (String) -> Unit, + trackDashboardBankPillClick: ((Int) -> Unit)? = null +) { + MMChip( + chipData = + ChipData( + illustration = + ChipIllustration( + illustrationType = IllustrationType.Image(aggregateDetails.bankChipImage), + size = 16 + ), + content = aggregateDetails.bankChipContent + ), + chipThemeCode = ChipThemeCodes.BANK_THEME, + isSelected = selectedBank == aggregateDetails.referenceId, + onChipSelected = { + trackDashboardBankPillClick?.invoke(1) + onAggregatedBankSelected(aggregateDetails.referenceId) + }, + padding = PaddingValues(start = 8.dp, end = 12.dp, top = 8.dp, bottom = 8.dp) + ) +} + +@Composable +private fun BankChips( + bankList: List, + selectedBank: String, + onBankClickWhileLoading: () -> Unit, + onBankSelected: (String) -> Unit, + startRank: Int, + trackDashboardBankPillClick: ((Int) -> Unit)? = null +) { + bankList.forEachIndexed { index, bank -> + key(bank.referenceId) { + if (bank.isBalanceFetched) { + MMChip( + chipData = + ChipData( + illustration = + ChipIllustration( + illustrationType = IllustrationType.Image(bank.bankChipImage), + size = 16 + ), + content = bank.bankChipContent, + ), + chipThemeCode = ChipThemeCodes.BANK_THEME, + isSelected = selectedBank == bank.referenceId, + onChipSelected = { + trackDashboardBankPillClick?.invoke(startRank + index) + onBankSelected(bank.referenceId) + }, + padding = PaddingValues(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp) + ) + } else { + MMChip( + chipData = + ChipData( + illustration = + ChipIllustration( + illustrationType = + IllustrationType.Lottie(bank.bankLoadingLottie), + size = 16 + ), + content = bank.bankChipContent, + ), + chipThemeCode = ChipThemeCodes.DISABLED_BANK_THEME, + isSelected = false, + onChipSelected = { onBankClickWhileLoading() }, + padding = PaddingValues(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 8.dp) + ) + } + } + } +} + +@Composable +private fun AddBankChip( + addBankChipInfo: AddBankChipInfo, + isAddAccountChipSelected: Boolean, + onAddAccountChipClicked: (Boolean) -> Unit, + trackDashboardAddAccountPillClick: (() -> Unit)? = null, +) { + if (addBankChipInfo.isTotalSyncCompleted) { + MMChip( + chipData = + ChipData( + illustration = + ChipIllustration( + illustrationType = + if (isAddAccountChipSelected) + IllustrationType.Lottie(addBankChipInfo.addBankLoaderLottie) + else IllustrationType.Image(addBankChipInfo.addBankIcon), + size = 16 + ), + content = addBankChipInfo.addBankText, + ), + chipThemeCode = ChipThemeCodes.ADD_BANK_THEME, + isSelected = isAddAccountChipSelected, + onChipSelected = { + trackDashboardAddAccountPillClick?.invoke() + onAddAccountChipClicked(true) + }, + padding = PaddingValues(start = 8.dp, end = 12.dp, top = 8.dp, bottom = 8.dp) + ) + } else { + MMChip( + chipData = + ChipData( + illustration = + ChipIllustration( + illustrationType = + IllustrationType.Image(source = addBankChipInfo.addBankIcon), + size = 16 + ), + content = addBankChipInfo.addBankText, + ), + chipThemeCode = ChipThemeCodes.ADD_ACCOUNT_DISABLE_THEME, + isSelected = false, + onChipSelected = { onAddAccountChipClicked(false) }, + padding = PaddingValues(start = 8.dp, end = 12.dp, top = 8.dp, bottom = 8.dp) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/bankSection/BankSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/bankSection/BankSection.kt new file mode 100644 index 0000000000..1d3e07fb5e --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/bankSection/BankSection.kt @@ -0,0 +1,90 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.ui.bankSection + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.unit.dp +import com.navi.base.utils.orFalse +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountsState +import com.navi.moneymanager.postonboard.dashboard.model.BankSectionData +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiEvent + +@Composable +fun BankSection( + isAddAccountChipSelected: Boolean, + bankSectionData: BankSectionData, + onAddAccountChipClick: (Boolean) -> Unit, + onBankClickWhileLoading: () -> Unit, + onEvent: (event: DashboardScreenUiEvent) -> Unit, + trackDashboardBankChipCarouselView: (Int) -> Unit, + trackDashboardBankChipClick: (Int) -> Unit, + trackDashboardAddAccountPillClick: () -> Unit +) { + if (bankSectionData.state is BankAccountsState.None) return + + val borderColor by + remember(bankSectionData.state) { + derivedStateOf { + if (bankSectionData.state is BankAccountsState.Loading) MMColor.ctaSecondary + else MMColor.white + } + } + + Column( + modifier = + Modifier.fillMaxWidth() + .padding(top = 16.dp, bottom = 24.dp) + .shadow( + elevation = 32.dp, + shape = RoundedCornerShape(size = 4.dp), + ambientColor = MMColor.ambientColor, + spotColor = MMColor.shadowSpotColor + ) + .background(color = MMColor.white, shape = RoundedCornerShape(size = 4.dp)) + .border(width = 1.dp, color = borderColor, shape = RoundedCornerShape(size = 4.dp)) + ) { + BankChipCarousel( + onBankClickWhileLoading = onBankClickWhileLoading, + onAddAccountChipClick = onAddAccountChipClick, + isAddAccountChipSelected = isAddAccountChipSelected, + selectedBank = bankSectionData.selectedBank.orEmpty(), + bankListExpandedState = bankSectionData.isBankListExpanded.orFalse(), + bankAccountsState = bankSectionData.state, + modifier = Modifier.padding(vertical = 16.dp), + onBankSelected = { bankId -> + onEvent(DashboardScreenUiEvent.UpdateSelectedBankForBalance(bankId = bankId)) + }, + onExpandList = { onEvent(DashboardScreenUiEvent.ExpandBankBalanceSectionBankList) }, + trackDashboardBankChipCarouselView = trackDashboardBankChipCarouselView, + trackDashboardBankPillClick = trackDashboardBankChipClick, + trackDashboardAddAccountPillClick = trackDashboardAddAccountPillClick + ) + HorizontalDivider(color = MMColor.ctaSecondary, thickness = 1.dp) + BankBalanceSection( + selectedBank = bankSectionData.selectedBank.orEmpty(), + bankAccountsState = bankSectionData.state + ) + BankStatusSection( + selectedBank = bankSectionData.selectedBank.orEmpty(), + bankAccountsState = bankSectionData.state + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/bankSection/BankStatusSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/bankSection/BankStatusSection.kt new file mode 100644 index 0000000000..596dddfb05 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/ui/bankSection/BankStatusSection.kt @@ -0,0 +1,87 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.ui.bankSection + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.base.utils.EMPTY +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.analytics.DashboardEventTrackerImpl +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.dashboard.model.BankAccountsState + +/** + * Displays the status of the bank account summary, indicating whether it's fetching data or showing + * the last updated timestamp of the selected bank's balance + * + * @param bankAccountsState The state of the bank summary data, used to determine the displayed + * status. + * @param selectedBank The reference ID of the currently selected bank. + */ +@Composable +fun BankStatusSection( + bankAccountsState: BankAccountsState, + selectedBank: String, +) { + + LaunchedEffect(bankAccountsState is BankAccountsState.Loaded) { + if (bankAccountsState is BankAccountsState.Loaded) { + DashboardEventTrackerImpl.onDashboardLastRefreshViewed() + } + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth().background(color = MMColor.bgAltColor) + ) { + val (lastUpdatedPrefix, lastUpdatedSuffix) = + when (bankAccountsState) { + is BankAccountsState.Loading -> Pair(bankAccountsState.data.lastUpdated, EMPTY) + is BankAccountsState.Loaded -> { + val data = bankAccountsState.data + if (data.aggregate != null && selectedBank == data.aggregate.bankChipContent) { + Pair( + data.aggregate.lastUpdatedPrefixText, + data.aggregate.lastUpdatedSuffixText + ) + } else { + val account = data.accounts.find { it.referenceId == selectedBank } + Pair(account?.lastUpdatedPrefixText, account?.lastUpdatedSuffixText) + } + } + BankAccountsState.None -> Pair(EMPTY, EMPTY) + } + MMText( + text = lastUpdatedPrefix.orEmpty(), + modifier = Modifier.padding(vertical = 8.dp), + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 16.sp + ) + MMText( + text = lastUpdatedSuffix.orEmpty(), + modifier = Modifier.padding(vertical = 8.dp), + color = MMColor.textPrimary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 16.sp + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/usecase/RefreshAndSyncDataUseCase.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/usecase/RefreshAndSyncDataUseCase.kt new file mode 100644 index 0000000000..73365e5f43 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/usecase/RefreshAndSyncDataUseCase.kt @@ -0,0 +1,174 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.usecase + +import com.navi.common.scheduler.TaskRepeater +import com.navi.common.utils.safeLaunch +import com.navi.moneymanager.common.analytics.DataSyncEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.data.datastore.DataStoreInfoProvider +import com.navi.moneymanager.common.dataprovider.domain.RemoteDataProvider +import com.navi.moneymanager.common.datasync.DBSyncExecutor +import com.navi.moneymanager.common.datasync.SyncStatus +import com.navi.moneymanager.common.datasync.model.DBSyncConfig +import com.navi.moneymanager.common.datasync.model.DataSyncState +import com.navi.moneymanager.common.network.di.RoomDataStoreInfoProvider +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.network.model.PollingStatusResponse +import com.navi.moneymanager.common.utils.Constants.LAST_REFRESH_SUCCESSFUL_TIMESTAMP +import dagger.hilt.android.scopes.ViewModelScoped +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +@ViewModelScoped +class RefreshAndSyncDataUseCase +@Inject +constructor( + private val remoteDataProvider: RemoteDataProvider, + @RoomDataStoreInfoProvider private val dbDataStoreProvider: DataStoreInfoProvider, + private val dBSyncExecutor: DBSyncExecutor +) { + private var taskRepeater: TaskRepeater? = null + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing = _isRefreshing.asStateFlow() + + suspend fun execute(scope: CoroutineScope, configResponse: MMConfigResponse) { + val response = remoteDataProvider.refreshData() + response.data?.requestId?.let { + DataSyncEventTrackerImpl.refreshStarted( + requestId = it, + lastRefreshTimestamp = response.data?.lastRefreshSuccessTimestamp ?: 0L + ) + updateDataRefreshingState(true) + saveRefreshTimestamp(response.data?.lastRefreshSuccessTimestamp) + observeDataSyncStatus(scope) + startPolling( + scope = scope, + requestId = it, + configResponse = configResponse, + ) + } + } + + private suspend fun startPolling( + scope: CoroutineScope, + requestId: String, + configResponse: MMConfigResponse, + ) { + if (taskRepeater != null) { + return + } + taskRepeater = + getTaskRepeater( + scope = scope, + requestId = requestId, + configResponse = configResponse, + ) + DataSyncEventTrackerImpl.periodicTaskSchedulerCreated() + DataSyncEventTrackerImpl.pollingStarted() + taskRepeater?.startTask() + } + + private fun getTaskRepeater( + scope: CoroutineScope, + requestId: String, + configResponse: MMConfigResponse + ): TaskRepeater { + return TaskRepeater( + maxAttempts = configResponse.dataSyncPollingConfig?.maxAttempts ?: 60, + initialDelaySeconds = configResponse.dataSyncPollingConfig?.initialDelaySeconds ?: 0, + taskIntervalSeconds = configResponse.dataSyncPollingConfig?.taskIntervalSeconds ?: 5, + onTimeout = { + updateDataRefreshingState(false) + DataSyncEventTrackerImpl.pollingTimeout() + }, + task = { pollSyncStatus(requestId = requestId, configResponse) } + ) + } + + private suspend fun pollSyncStatus( + requestId: String, + configResponse: MMConfigResponse, + ) { + val pollingStatusData = remoteDataProvider.pollSyncStatus(requestId).data + if (pollingStatusData != null) { + val dbSyncConfig = + DBSyncConfig( + pollingStatusResponse = pollingStatusData, + timestampConfig = configResponse.timestampConfig, + paginationConfig = configResponse.paginationConfig + ) + + if (isAccountSyncCompleted(pollingStatusData)) { + dBSyncExecutor.execute(dbSyncConfig) + + if ( + areAllTxnsSyncCompleted(pollingStatusData) || + areAllTxnsSyncFailed(pollingStatusData) + ) { + stopPolling() + } + } else if (isAccountSyncFailed(pollingStatusData)) { + stopPolling() + } + } + } + + private fun observeDataSyncStatus(scope: CoroutineScope) { + dBSyncExecutor.initDBSyncExecutor(scope) + scope.safeLaunch(Dispatchers.IO) { + dBSyncExecutor.dataSyncStatus.collect { status -> + when { + status.allMonthsSyncStatus is DataSyncState.Completed || + status.allMonthsSyncStatus is DataSyncState.Failed || + status.currentMonthSyncStatus is DataSyncState.Completed -> { + updateDataRefreshingState(false) + } + } + } + } + } + + private fun stopPolling() { + taskRepeater?.stopTask() + taskRepeater = null + DataSyncEventTrackerImpl.pollingStopped() + } + + private fun isAccountSyncCompleted(pollingStatusData: PollingStatusResponse): Boolean { + return pollingStatusData.accountDetailsStatus == SyncStatus.COMPLETED.name + } + + private fun isAccountSyncFailed(pollingStatusData: PollingStatusResponse): Boolean { + return pollingStatusData.accountDetailsStatus == SyncStatus.FAILED.name + } + + private fun areAllTxnsSyncCompleted(pollingStatusData: PollingStatusResponse): Boolean { + return pollingStatusData.oldMonthTxnsStatus == SyncStatus.COMPLETED.name && + pollingStatusData.currMonthTxnsStatus == SyncStatus.COMPLETED.name + } + + private fun areAllTxnsSyncFailed(pollingStatusData: PollingStatusResponse): Boolean { + return pollingStatusData.oldMonthTxnsStatus == SyncStatus.FAILED.name && + pollingStatusData.currMonthTxnsStatus == SyncStatus.FAILED.name + } + + private fun updateDataRefreshingState(state: Boolean) { + _isRefreshing.update { state } + } + + private suspend fun saveRefreshTimestamp(lastRefreshSuccessTimestamp: Long?) { + dbDataStoreProvider.saveLongData( + key = LAST_REFRESH_SUCCESSFUL_TIMESTAMP, + value = lastRefreshSuccessTimestamp ?: 0L + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/usecase/SyncPreviousDataUseCase.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/usecase/SyncPreviousDataUseCase.kt new file mode 100644 index 0000000000..2fb5cf3644 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/usecase/SyncPreviousDataUseCase.kt @@ -0,0 +1,62 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.usecase + +import com.navi.common.utils.safeLaunch +import com.navi.moneymanager.common.datasync.DBSyncExecutor +import com.navi.moneymanager.common.datasync.SyncStatus +import com.navi.moneymanager.common.datasync.model.DBSyncConfig +import com.navi.moneymanager.common.datasync.model.DataSyncState +import com.navi.moneymanager.common.network.model.MMConfigResponse +import com.navi.moneymanager.common.network.model.PollingStatusResponse +import dagger.hilt.android.scopes.ViewModelScoped +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.firstOrNull + +@ViewModelScoped +class SyncPreviousDataUseCase +@Inject +constructor( + private val dBSyncExecutor: DBSyncExecutor, + private val refreshAndSyncDataUseCase: RefreshAndSyncDataUseCase, +) { + suspend fun execute(scope: CoroutineScope, configResponse: MMConfigResponse) { + val pollingStatusData = + PollingStatusResponse( + accountDetailsStatus = SyncStatus.COMPLETED.name, + currMonthTxnsStatus = SyncStatus.COMPLETED.name, + oldMonthTxnsStatus = SyncStatus.COMPLETED.name + ) + val dbSyncConfig = + DBSyncConfig( + pollingStatusResponse = pollingStatusData, + timestampConfig = configResponse.timestampConfig, + paginationConfig = configResponse.paginationConfig + ) + observeDataSyncStatus(scope, configResponse) + dBSyncExecutor.execute(dbSyncConfig) + } + + private fun observeDataSyncStatus(scope: CoroutineScope, configResponse: MMConfigResponse) { + dBSyncExecutor.initDBSyncExecutor(scope) + scope.safeLaunch(Dispatchers.IO) { + dBSyncExecutor.dataSyncStatus + .filter { status -> status.allMonthsSyncStatus is DataSyncState.Completed } + .firstOrNull() + ?.let { + refreshAndSyncDataUseCase.execute( + scope = scope, + configResponse = configResponse, + ) + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/viewmodel/DashboardViewModel.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/viewmodel/DashboardViewModel.kt new file mode 100644 index 0000000000..14868f6c55 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/dashboard/viewmodel/DashboardViewModel.kt @@ -0,0 +1,358 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.dashboard.viewmodel + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.viewModelScope +import com.navi.base.sharedpref.PreferenceManager +import com.navi.common.network.models.isSuccessWithData +import com.navi.moneymanager.base.viewmodel.MMBaseViewModel +import com.navi.moneymanager.common.analytics.DataProviderEventTrackerImpl +import com.navi.moneymanager.common.analytics.DataSyncEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.data.datastore.DataStoreInfoProvider +import com.navi.moneymanager.common.model.SelectedMonth +import com.navi.moneymanager.common.model.bottomSheet.DashboardScreenBottomSheets +import com.navi.moneymanager.common.network.di.RoomDataStoreInfoProvider +import com.navi.moneymanager.common.utils.Constants.IS_FIRST_MONTH_SYNC_COMPLETED +import com.navi.moneymanager.common.utils.Constants.IS_TOTAL_SYNC_COMPLETED +import com.navi.moneymanager.common.utils.Constants.LAST_REFRESH_SUCCESSFUL_TIMESTAMP +import com.navi.moneymanager.common.utils.Constants.MM_IS_USER_ONBOARDED +import com.navi.moneymanager.common.utils.Constants.NEW_TRANSACTION_COUNT +import com.navi.moneymanager.common.utils.Constants.SYNC_THRESHOLD_TIME +import com.navi.moneymanager.common.utils.MonthConstants +import com.navi.moneymanager.common.utils.checkFinarkeinDataValidity +import com.navi.moneymanager.postonboard.dashboard.model.AddAccountState +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiEffect +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiEvent +import com.navi.moneymanager.postonboard.dashboard.model.DashboardScreenUiState +import com.navi.moneymanager.postonboard.dashboard.model.OnboardingStatus +import com.navi.moneymanager.postonboard.dashboard.reducer.DashboardScreenReducer +import com.navi.moneymanager.postonboard.dashboard.repo.DashboardScreenRepository +import com.navi.moneymanager.postonboard.dashboard.usecase.RefreshAndSyncDataUseCase +import com.navi.moneymanager.postonboard.dashboard.usecase.SyncPreviousDataUseCase +import com.navi.moneymanager.postonboard.help.model.HelpBottomSheetState +import com.navi.moneymanager.postonboard.help.usecase.ManageConsentUseCase +import com.navi.moneymanager.preonboard.finarkein.model.FinarkeinDataState +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.update + +@HiltViewModel +class DashboardViewModel +@Inject +constructor( + @RoomDataStoreInfoProvider val dbDataStoreProvider: DataStoreInfoProvider, + val repository: DashboardScreenRepository, + private val refreshAndSyncDataUseCase: RefreshAndSyncDataUseCase, + private val syncPreviousDataUseCase: SyncPreviousDataUseCase, + private val manageConsentUseCase: ManageConsentUseCase +) : + MMBaseViewModel( + initialState = DashboardScreenUiState.initialState, + reducer = DashboardScreenReducer() + ) { + + private val screenParams = MutableStateFlow(null) + + init { + PreferenceManager.setBooleanPreference(MM_IS_USER_ONBOARDED, true) + loadDashboardScreenData() + observeRefreshDataAndUpdateBankSectionLastUpdatedTime() + viewModelScope.safeLaunch { + dbDataStoreProvider + .getBooleanData(IS_FIRST_MONTH_SYNC_COMPLETED) + .distinctUntilChanged() + .collect { isTransactionsDataSynced -> + if (isTransactionsDataSynced) { + sendEvent( + DashboardScreenUiEvent.UpdateOnBoardingBottomSheetStatus( + status = OnboardingStatus.Success + ) + ) + delay(1000) + sendEvent( + DashboardScreenUiEvent.DismissBottomSheet( + DashboardScreenBottomSheets.OnBoarding::class.java + ) + ) + } else { + sendEvent( + DashboardScreenUiEvent.UpdateOnBoardingBottomSheetStatus( + status = OnboardingStatus.Loading + ) + ) + } + } + } + syncDashboardData() + } + + private fun loadDashboardScreenData() { + loadTopNavBar() + loadUserHeader() + loadBankSection() + loadSpendAnalysisSection() + loadRecentTransactionsSection() + } + + private fun loadTopNavBar() { + viewModelScope.safeLaunch(Dispatchers.IO) { + val topNavData = repository.getTopNavBarData() + sendEvent(DashboardScreenUiEvent.UpdateDashboardTopNavBarData(topNavData)) + } + } + + private fun loadUserHeader() { + viewModelScope.safeLaunch(Dispatchers.IO) { + repository.getHeaderStateFlow().collect { userHeaderInfo -> + sendEvent(DashboardScreenUiEvent.UpdateDashboardHeaderState(userHeaderInfo)) + } + } + } + + private fun loadBankSection() { + viewModelScope.safeLaunch(Dispatchers.IO) { + repository.getBankSectionStateFlow().collect { (state, isCurrentMonthSynced) -> + DataProviderEventTrackerImpl.dashboardVmBankSectionCollect( + isFirstMonthSyncCompleted = isCurrentMonthSynced, + bankSection = state::class.simpleName.orEmpty() + ) + sendEvent( + DashboardScreenUiEvent.UpdateDashboardBankSectionState( + state = state, + isFirstMonthSyncCompleted = isCurrentMonthSynced + ) + ) + } + } + } + + private fun loadSpendAnalysisSection() { + viewModelScope.safeLaunch(Dispatchers.IO) { + repository + .getSpendCategorizationSectionStateFlow(screenParams = screenParams) + .collect { state -> + DataProviderEventTrackerImpl.dashboardVmSpendAnalysisSectionCollect( + stateName = state::class.simpleName.orEmpty() + ) + sendEvent(DashboardScreenUiEvent.UpdateSpendCategorizationSectionState(state)) + } + } + } + + private fun loadRecentTransactionsSection() { + val currentMonthTag = mutableStateOf(null) + viewModelScope.safeLaunch(Dispatchers.IO) { + repository + .getRecentTransactionsSectionStateFlow(currentMonthTag = currentMonthTag) + .collect { state -> + DataProviderEventTrackerImpl.dashboardVmRecentTransactionSectionCollect( + stateName = state::class.simpleName.orEmpty() + ) + sendEvent(DashboardScreenUiEvent.UpdateRecentTransactionsSectionState(state)) + } + } + } + + fun updateSpendAnalysisSection() { + updateSpendAnalysisSectionParams( + state.value.screenData.spendCategorizationState?.selectedMonth, + state.value.screenData.spendCategorizationState?.selectedYear + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun observeRefreshDataAndUpdateBankSectionLastUpdatedTime() { + viewModelScope.safeLaunch(Dispatchers.IO) { + refreshAndSyncDataUseCase.isRefreshing + .flatMapLatest { refreshing -> + repository.updateBankSectionRefreshingText(refreshing) + } + .collect { sendEvent(DashboardScreenUiEvent.UpdateBankSectionLastUpdatedTime(it)) } + } + } + + private fun updateSpendAnalysisSectionParams(month: Int? = null, year: Int? = null) { + viewModelScope.safeLaunch(Dispatchers.IO) { + DataProviderEventTrackerImpl.dashboardVmSpendAnalysisParamsUpdate( + month.toString(), + year.toString() + ) + screenParams.update { SelectedMonth(month = month, year = year) } + } + } + + fun fetchFinarkeinData(callback: (FinarkeinDataState) -> Unit) { + viewModelScope.safeLaunch(Dispatchers.IO) { + val response = repository.fetchFinarkeinData() + sendEvent( + DashboardScreenUiEvent.HandleAddAccountState( + addAccountState = AddAccountState.UnSelected + ) + ) + sendEvent( + DashboardScreenUiEvent.DismissBottomSheet( + DashboardScreenBottomSheets.FinarkeinError::class.java + ) + ) + if (response.isSuccessWithData() && checkFinarkeinDataValidity(response.data)) { + callback(FinarkeinDataState.Success(response.data!!)) + } else { + callback(FinarkeinDataState.Failure) + } + } + } + + fun handleMonthlySummaryMonthChangeClick(displayMonthText: String) { + viewModelScope.safeLaunch(Dispatchers.IO) { + val pastMonthData = + dbDataStoreProvider.getBooleanData(key = IS_TOTAL_SYNC_COMPLETED).first() + if (!pastMonthData) { + val data = repository.getPastMonthDataLoadingBottomSheetData() + sendEvent( + DashboardScreenUiEvent.ShowBottomSheet( + type = DashboardScreenBottomSheets.PastMonthDataLoading(data) + ) + ) + } else { + val data = repository.getMonthSelectionBottomSheetData(displayMonthText) + sendEvent( + DashboardScreenUiEvent.ShowBottomSheet( + type = DashboardScreenBottomSheets.MonthSelection(data) + ) + ) + } + } + } + + fun updateSpendCategorizationSectionData(monthName: String, year: Int) { + val monthList = MonthConstants.monthNames + val month = monthList.indexOf(monthName) + updateSpendAnalysisSectionParams(month, year) + } + + fun handleBankClickWhileLoading() { + viewModelScope.safeLaunch(Dispatchers.IO) { + val data = repository.getTransactionsFetchingBottomSheetData() + sendEvent( + DashboardScreenUiEvent.ShowBottomSheet( + type = DashboardScreenBottomSheets.FetchingTransactions(data) + ) + ) + } + } + + fun showFinarkeinErrorBottomSheet() { + viewModelScope.safeLaunch(Dispatchers.IO) { + val data = repository.getFinarkeinErrorBottomSheetData() + sendEvent( + DashboardScreenUiEvent.ShowBottomSheet( + type = DashboardScreenBottomSheets.FinarkeinError(data) + ) + ) + } + } + + private fun syncDashboardData() { + viewModelScope.safeLaunch(Dispatchers.IO) { + val configData = repository.fetchAndSaveConfigResponse() + if (configData != null) { + val lastRefreshSuccessfulTimestamp = + dbDataStoreProvider.getLongData(LAST_REFRESH_SUCCESSFUL_TIMESTAMP, 0L).first() + if ( + shouldRefresh( + currentTimestamp = configData.timestampConfig?.currentTimestamp, + threshold = configData.timestampConfig?.threshold ?: SYNC_THRESHOLD_TIME, + lastRefreshTimestamp = lastRefreshSuccessfulTimestamp, + ) + ) { + DataSyncEventTrackerImpl.rsFlowTriggered( + currentTimestamp = configData.timestampConfig?.currentTimestamp.toString(), + threshold = configData.timestampConfig?.threshold ?: SYNC_THRESHOLD_TIME, + lastRefreshTimestamp = lastRefreshSuccessfulTimestamp, + ) + refreshAndSyncDataUseCase.execute( + scope = viewModelScope, + configResponse = configData, + ) + } else { + DataSyncEventTrackerImpl.sFlowTriggered( + currentTimestamp = configData.timestampConfig?.currentTimestamp.toString(), + threshold = configData.timestampConfig?.threshold ?: SYNC_THRESHOLD_TIME, + lastRefreshTimestamp = lastRefreshSuccessfulTimestamp, + ) + syncPreviousDataUseCase.execute( + scope = viewModelScope, + configResponse = configData + ) + } + } + } + } + + private fun shouldRefresh( + currentTimestamp: Long?, + threshold: Long, + lastRefreshTimestamp: Long + ): Boolean { + return currentTimestamp?.let { + lastRefreshTimestamp == 0L || (it - lastRefreshTimestamp <= threshold) + } ?: false + } + + suspend fun initNewTransactionObserver(): Flow { + return dbDataStoreProvider.getIntData(NEW_TRANSACTION_COUNT, -1) + } + + fun fetchConsentUrl() { + viewModelScope.safeLaunch(Dispatchers.IO) { + manageConsentUseCase.execute( + onLoading = { + sendEvent( + DashboardScreenUiEvent.UpdateHelpBottomSheetState( + HelpBottomSheetState.Loading + ) + ) + }, + onSuccess = { url -> + sendEvent( + DashboardScreenUiEvent.UpdateHelpBottomSheetState( + HelpBottomSheetState.Success(url) + ) + ) + }, + onFailure = { + sendEvent( + DashboardScreenUiEvent.UpdateHelpBottomSheetState( + HelpBottomSheetState.Error + ) + ) + } + ) + } + } + + fun showAddAccountLoadingBottomSheet() { + viewModelScope.safeLaunch(Dispatchers.IO) { + val data = repository.getAddAccountLoadingBottomSheetData() + sendEvent( + DashboardScreenUiEvent.ShowBottomSheet( + type = DashboardScreenBottomSheets.AddAccountLoading(data) + ) + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/help/model/HelpBottomSheetState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/help/model/HelpBottomSheetState.kt new file mode 100644 index 0000000000..c3d159139d --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/help/model/HelpBottomSheetState.kt @@ -0,0 +1,18 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.help.model + +sealed interface HelpBottomSheetState { + data object Initial : HelpBottomSheetState + + data object Loading : HelpBottomSheetState + + data class Success(val url: String?) : HelpBottomSheetState + + data object Error : HelpBottomSheetState +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/help/ui/HelpBottomSheet.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/help/ui/HelpBottomSheet.kt new file mode 100644 index 0000000000..e0994c1afb --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/help/ui/HelpBottomSheet.kt @@ -0,0 +1,256 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.help.ui + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.R +import com.navi.moneymanager.common.analytics.CategoryDetailsEventTrackerImpl +import com.navi.moneymanager.common.analytics.DashboardEventTrackerImpl +import com.navi.moneymanager.common.analytics.SpendAnalysisEventTrackerImpl +import com.navi.moneymanager.common.analytics.TransactionDetailsEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.CHEVRON_PURPLE_RIGHT_ICON +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.CONSENT_ICON +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.FAQ_ICON +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.base.MMDivider +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.Constants.ERROR_RED_ALERT_ICON +import com.navi.moneymanager.common.utils.ShowCustomToast +import com.navi.moneymanager.common.utils.ToastDuration +import com.navi.moneymanager.postonboard.help.model.HelpBottomSheetState +import com.navi.moneymanager.postonboard.monthlysummary.ui.bottomsheet.CustomToastView +import com.navi.naviwidgets.utils.NaviWidgetLottieUtils.TUK_TUK_LOTTIE + +@Composable +fun HelpBottomSheetContent( + screenName: String, + state: HelpBottomSheetState, + onFaqClicked: () -> Unit, + manageConsent: () -> Unit, + navigateToUrl: (url: String?) -> Unit, + onDismiss: () -> Unit +) { + + LaunchedEffect(Unit) { + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl.onDashboardHelpBottomSheetAppeared() + } + MMScreen.TRANSACTION_DETAILS.screen -> { + TransactionDetailsEventTrackerImpl.onTransactionDetailsHelpBottomSheetAppeared() + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl.onCategoryDetailsHelpBottomSheetAppeared() + } + MMScreen.SPEND_ANALYSIS.screen -> { + SpendAnalysisEventTrackerImpl.onSpendAnalysisHelpBottomSheetAppeared() + } + } + } + + DisposableEffect(Unit) { + onDispose { + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl.onDashboardHelpBottomSheetDisappeared() + } + MMScreen.TRANSACTION_DETAILS.screen -> { + TransactionDetailsEventTrackerImpl + .onTransactionDetailsHelpBottomSheetDisappeared() + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl.onCategoryDetailsHelpBottomSheetDisappeared() + } + MMScreen.SPEND_ANALYSIS.screen -> { + SpendAnalysisEventTrackerImpl.onSpendAnalysisHelpBottomSheetDisappeared() + } + } + } + } + + LaunchedEffect(state) { + if (state is HelpBottomSheetState.Success) { + onDismiss() + navigateToUrl(state.url) + } + } + Column(modifier = Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 32.dp)) { + MMText( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + text = stringResource(R.string.get_help), + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + Spacer(modifier = Modifier.height(12.dp)) + HelpBottomSheetItem( + text = stringResource(R.string.frequently_asked_questions), + iconUrl = + IllustrationSource.Remote( + url = FAQ_ICON, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ) { + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl.onDashboardHelpBottomSheetFaqClicked() + } + MMScreen.TRANSACTION_DETAILS.screen -> { + TransactionDetailsEventTrackerImpl + .onTransactionDetailsHelpBottomSheetFaqClicked() + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl.onCategoryDetailsHelpBottomSheetFaqClicked() + } + MMScreen.SPEND_ANALYSIS.screen -> { + SpendAnalysisEventTrackerImpl.onSpendAnalysisHelpBottomSheetFaqClicked() + } + } + onDismiss() + onFaqClicked() + } + MMDivider( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + thickness = 1.dp, + color = MMColor.transactionDividerColor + ) + HelpBottomSheetItem( + state = state, + text = stringResource(R.string.manage_consent), + iconUrl = + IllustrationSource.Remote( + url = CONSENT_ICON, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ) { + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl.onDashboardHelpBottomSheetManageConsentClicked() + } + MMScreen.TRANSACTION_DETAILS.screen -> { + TransactionDetailsEventTrackerImpl + .onTransactionDetailsHelpBottomSheetManageConsentClicked() + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl + .onCategoryDetailsHelpBottomSheetManageConsentClicked() + } + MMScreen.SPEND_ANALYSIS.screen -> { + SpendAnalysisEventTrackerImpl + .onSpendAnalysisHelpBottomSheetManageConsentClicked() + } + } + manageConsent() + } + } +} + +@Composable +fun HelpBottomSheetItem( + state: HelpBottomSheetState? = null, + text: String, + iconUrl: IllustrationSource, + onClick: () -> Unit +) { + val context = LocalContext.current + Row( + modifier = + Modifier.fillMaxWidth() + .onClickWithDebounce(interactionSource = remember { MutableInteractionSource() }) { + onClick() + } + .padding(horizontal = 16.dp, vertical = 20.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier.wrapContentWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Illustration( + modifier = Modifier.size(24.dp), + illustrationType = IllustrationType.Image(iconUrl) + ) + Spacer(modifier = Modifier.size(12.dp)) + MMText( + text = text, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + lineHeight = 22.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + } + if (state == HelpBottomSheetState.Loading) { + Illustration( + illustrationType = + IllustrationType.Lottie(IllustrationSource.Resource(TUK_TUK_LOTTIE)), + modifier = Modifier.size(24.dp) + ) + } else { + Illustration( + modifier = Modifier.size(24.dp), + illustrationType = + IllustrationType.Image( + IllustrationSource.Remote( + url = CHEVRON_PURPLE_RIGHT_ICON, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ) + ) + } + if (state == HelpBottomSheetState.Error) { + ShowCustomToast( + context = context, + toastDuration = ToastDuration.LENGTH_LONG, + content = { + CustomToastView( + modifier = Modifier, + message = + context.resources.getString(R.string.something_went_wrong_try_again), + illustrationType = + IllustrationType.Image( + IllustrationSource.Resource(resId = ERROR_RED_ALERT_ICON) + ) + ) + } + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/help/usecase/ManageConsentUseCase.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/help/usecase/ManageConsentUseCase.kt new file mode 100644 index 0000000000..7665d54315 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/help/usecase/ManageConsentUseCase.kt @@ -0,0 +1,40 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.help.usecase + +import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.MMConfigResponseHelper +import com.navi.moneymanager.common.dataprovider.domain.RemoteDataProvider +import com.navi.moneymanager.common.utils.Constants.REVOKE_CONSENT_FINARKEIN_URL +import dagger.hilt.android.scopes.ViewModelScoped +import javax.inject.Inject + +@ViewModelScoped +class ManageConsentUseCase +@Inject +constructor( + private val mmConfigResponseHelper: MMConfigResponseHelper, + private val remoteDataProvider: RemoteDataProvider +) { + + suspend fun execute( + onLoading: () -> Unit, + onSuccess: (url: String?) -> Unit, + onFailure: () -> Unit + ) { + onLoading() + val response = + mmConfigResponseHelper.getMMConfig()?.revokeConsentUrl ?: REVOKE_CONSENT_FINARKEIN_URL + onSuccess(response) + // val response = remoteDataProvider.fetchConsentUrl() + // if (response.isSuccessWithData()) { + // onSuccess(response.data?.url) + // } else { + // onFailure() + // } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/AddCategoryBottomSheetData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/AddCategoryBottomSheetData.kt new file mode 100644 index 0000000000..13891df0e3 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/AddCategoryBottomSheetData.kt @@ -0,0 +1,68 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.model + +import androidx.compose.ui.graphics.Color +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.model.SingleChipSelectionData + +data class AddCategoryBottomSheetData( + val headerData: AddCategoryHeaderData, + val contentData: AddCategoryContentData, + val footerData: AddCategoryFooterData?, + val hasOnlyOneSimilarTransaction: Boolean? = null +) + +data class AddCategoryContentData( + val transactionDetails: TransactionDetails, + val chipsContainerData: ChipsContainerData +) + +data class TransactionDetails( + val transactionDate: String, + val transactionDay: String, + val paymentMethod: String, + val accountInfo: AccountInfo +) + +data class AccountInfo(val account: String, val endIconUrl: IllustrationSource) + +data class AddCategoryFooterData( + val buttonLabel: String, + val lottieUrl: String, + val isEnabled: Boolean = false, + val isLoading: Boolean = false +) + +data class AddCategoryHeaderData( + val transactionAmount: String, + val name: String, + val startIcon: StartHeaderIcon, + val endIcon: IllustrationSource +) + +data class StartHeaderIcon( + val iconText: String? = null, + val iconBackgroundColor: Color, + val iconUrl: String? = null +) + +data class ChipsContainerData( + val categoriesData: RecommendedCategoriesData, + val moreCategoriesData: MoreCategoriesData?, +) + +data class RecommendedCategoriesData( + val categoriesTitle: String, + val categoriesList: List? = null, +) + +data class MoreCategoriesData( + val moreCategoriesTitle: String, + val moreCategoriesList: List? = null +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/CategoryBottomSheetTransactionData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/CategoryBottomSheetTransactionData.kt new file mode 100644 index 0000000000..745074bf23 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/CategoryBottomSheetTransactionData.kt @@ -0,0 +1,16 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.model + +data class CategoryBottomSheetTransactionData( + val transactionId: String, + val categoryId: String, + val counterPartyName: String, + val categoryName: String, + val transactionType: String, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/CategoryBottomSheetUiEffect.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/CategoryBottomSheetUiEffect.kt new file mode 100644 index 0000000000..1b535357db --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/CategoryBottomSheetUiEffect.kt @@ -0,0 +1,16 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.model + +import androidx.compose.runtime.Immutable +import com.navi.common.basemvi.UiEffect + +@Immutable +sealed class CategoryBottomSheetUiEffect : UiEffect { + @Immutable sealed class Navigation : CategoryBottomSheetUiEffect() {} +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/CategoryBottomSheetUiEvent.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/CategoryBottomSheetUiEvent.kt new file mode 100644 index 0000000000..4961ee2369 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/CategoryBottomSheetUiEvent.kt @@ -0,0 +1,40 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.model + +import androidx.compose.runtime.Immutable +import com.navi.common.basemvi.UiEvent + +@Immutable +sealed class CategoryBottomSheetUiEvent : UiEvent { + + @Immutable + data class UpdateAddCategoryBottomSheetData(val data: AddCategoryBottomSheetData) : + CategoryBottomSheetUiEvent() + + @Immutable + data class UpdateSimilarTransactionBottomSheetData( + val data: SimilarTransactionBottomSheetData + ) : CategoryBottomSheetUiEvent() + + @Immutable + data class UpdatePostCategoryTransactionState(val data: PostTransactionCategory) : + CategoryBottomSheetUiEvent() + + @Immutable + data class ToggleButtonLoader( + val isLoading: Boolean, + val buttonType: CategoryBottomSheetButtonType + ) : CategoryBottomSheetUiEvent() + + @Immutable + data class ToggleButtonState( + val isEnabled: Boolean, + val buttonType: CategoryBottomSheetButtonType + ) : CategoryBottomSheetUiEvent() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/CategoryBottomSheetUiState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/CategoryBottomSheetUiState.kt new file mode 100644 index 0000000000..4b7a4d9c2d --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/CategoryBottomSheetUiState.kt @@ -0,0 +1,23 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.model + +import androidx.compose.runtime.Immutable +import com.navi.common.basemvi.UiState + +@Immutable +data class CategoryBottomSheetUiState( + val addCategoryBottomSheetData: AddCategoryBottomSheetData? = null, + val similarTransactionBottomSheetData: SimilarTransactionBottomSheetData? = null, + val postTransactionCategory: PostTransactionCategory? = null, + val isBottomSheetVisible: Boolean? = null, +) : UiState { + companion object { + val initialState = CategoryBottomSheetUiState() + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/PostCategoryTransactionData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/PostCategoryTransactionData.kt new file mode 100644 index 0000000000..593247167e --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/PostCategoryTransactionData.kt @@ -0,0 +1,23 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.model + +data class PostCategoryTransactionData( + val overrideTransactions: List? = null, + val counterPartyPreferences: List? = null, +) + +data class OverriddenTransaction( + val txnId: String, + val overriddenCategory: String, +) + +data class CounterPartyPreference( + val counterPartyName: String, + val selectedCategory: String, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/SimilarTransactionBottomSheetData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/SimilarTransactionBottomSheetData.kt new file mode 100644 index 0000000000..d1306ec34d --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/model/SimilarTransactionBottomSheetData.kt @@ -0,0 +1,73 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.model + +import com.navi.common.network.models.ErrorMessage +import com.navi.moneymanager.common.illustration.model.IllustrationSource + +data class SimilarTransactionBottomSheetData( + val header: CategoryTransactionHeaderData, + val content: CategoryTransactionContentData, + val footer: CategoryTransactionFooterData, +) + +class CategoryTransactionHeaderData( + val topIconUrl: IllustrationSource, + val title: String, + val subTitle: String, + val description: String, + val annotatedDescription: String +) + +data class CategoryTransactionContentData(val transactionItems: List) + +data class TransactionItemData( + val id: String, + val name: String, + val date: String, + val amount: String, + val type: String, + val transactionPaidType: TransactionPaidType +) + +data class TransactionPaidType(val label: String, val iconUrl: String) + +data class CategoryTransactionFooterData( + val footerInfoData: FooterInfoData, + val proceedButtonData: FooterButtonData, + val skipButtonData: FooterButtonData +) + +data class FooterInfoData(val infoText: String, val annotatedInfoText: String) + +data class FooterButtonData( + val isEnabled: Boolean = false, + val isLoading: Boolean = false, + val buttonText: String, + val lottieUrl: String +) + +data class PostTransactionCategory( + val state: PostTransactionCategoryState, + val buttonType: CategoryBottomSheetButtonType +) + +sealed class PostTransactionCategoryState { + data object Loading : PostTransactionCategoryState() + + data class Success(val data: PostCategoryTransactionData? = null) : + PostTransactionCategoryState() + + data class Error(val error: ErrorMessage? = null) : PostTransactionCategoryState() +} + +enum class CategoryBottomSheetButtonType { + ADD_CATEGORY_CONFIRM, + SIMILAR_TRANSACTION_PROCEED, + SIMILAR_TRANSACTION_SKIP +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/reducer/CategoryBottomSheetReducer.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/reducer/CategoryBottomSheetReducer.kt new file mode 100644 index 0000000000..a98c8b0812 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/reducer/CategoryBottomSheetReducer.kt @@ -0,0 +1,125 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.reducer + +import com.navi.common.basemvi.BaseReducer +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetButtonType +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetUiEvent +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetUiState +import com.navi.moneymanager.postonboard.monthlysummary.model.PostTransactionCategory + +class CategoryBottomSheetReducer : + BaseReducer { + + override fun reduce( + previousState: CategoryBottomSheetUiState, + event: CategoryBottomSheetUiEvent + ): CategoryBottomSheetUiState { + return when (event) { + is CategoryBottomSheetUiEvent.UpdateAddCategoryBottomSheetData -> { + (previousState.copy(addCategoryBottomSheetData = event.data)) + } + is CategoryBottomSheetUiEvent.UpdateSimilarTransactionBottomSheetData -> { + (previousState.copy(similarTransactionBottomSheetData = event.data)) + } + is CategoryBottomSheetUiEvent.ToggleButtonLoader -> { + when (event.buttonType) { + CategoryBottomSheetButtonType.ADD_CATEGORY_CONFIRM -> + (previousState.copy( + addCategoryBottomSheetData = + previousState.addCategoryBottomSheetData?.copy( + footerData = + previousState.addCategoryBottomSheetData.footerData?.copy( + isLoading = event.isLoading + ) + ) + )) + CategoryBottomSheetButtonType.SIMILAR_TRANSACTION_PROCEED -> + (previousState.copy( + similarTransactionBottomSheetData = + previousState.similarTransactionBottomSheetData?.copy( + footer = + previousState.similarTransactionBottomSheetData.footer.copy( + proceedButtonData = + previousState.similarTransactionBottomSheetData + .footer + .proceedButtonData + .copy(isLoading = event.isLoading) + ) + ) + )) + CategoryBottomSheetButtonType.SIMILAR_TRANSACTION_SKIP -> + (previousState.copy( + similarTransactionBottomSheetData = + previousState.similarTransactionBottomSheetData?.copy( + footer = + previousState.similarTransactionBottomSheetData.footer.copy( + skipButtonData = + previousState.similarTransactionBottomSheetData + .footer + .skipButtonData + .copy(isLoading = event.isLoading) + ) + ) + )) + } + } + is CategoryBottomSheetUiEvent.UpdatePostCategoryTransactionState -> { + (previousState.copy( + postTransactionCategory = + PostTransactionCategory( + state = event.data.state, + buttonType = event.data.buttonType + ) + )) + } + is CategoryBottomSheetUiEvent.ToggleButtonState -> { + when (event.buttonType) { + CategoryBottomSheetButtonType.ADD_CATEGORY_CONFIRM -> + (previousState.copy( + addCategoryBottomSheetData = + previousState.addCategoryBottomSheetData?.copy( + footerData = + previousState.addCategoryBottomSheetData.footerData?.copy( + isEnabled = event.isEnabled + ) + ) + )) + CategoryBottomSheetButtonType.SIMILAR_TRANSACTION_PROCEED -> + (previousState.copy( + similarTransactionBottomSheetData = + previousState.similarTransactionBottomSheetData?.copy( + footer = + previousState.similarTransactionBottomSheetData.footer.copy( + proceedButtonData = + previousState.similarTransactionBottomSheetData + .footer + .proceedButtonData + .copy(isEnabled = event.isEnabled) + ) + ) + )) + CategoryBottomSheetButtonType.SIMILAR_TRANSACTION_SKIP -> + (previousState.copy( + similarTransactionBottomSheetData = + previousState.similarTransactionBottomSheetData?.copy( + footer = + previousState.similarTransactionBottomSheetData.footer.copy( + skipButtonData = + previousState.similarTransactionBottomSheetData + .footer + .skipButtonData + .copy(isEnabled = event.isEnabled) + ) + ) + )) + } + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/repo/CategoryDetailsRepository.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/repo/CategoryDetailsRepository.kt new file mode 100644 index 0000000000..9bdae92a5c --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/repo/CategoryDetailsRepository.kt @@ -0,0 +1,52 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.repo + +import com.navi.common.network.models.RepoResult +import com.navi.moneymanager.common.dataprovider.domain.AddCategoryDataProvider +import com.navi.moneymanager.common.dataprovider.domain.RemoteDataProvider +import com.navi.moneymanager.postonboard.monthlysummary.model.PostCategoryTransactionData +import javax.inject.Inject + +class CategoryDetailsRepository +@Inject +constructor( + private val remoteDataProvider: RemoteDataProvider, + private val addCategoryDataProvider: AddCategoryDataProvider +) { + + suspend fun fetchAddCategoryBottomSheetData(transactionId: String) = + addCategoryDataProvider.fetchAddCategoryBottomSheetData(transactionId) + + suspend fun getCategoryTransactionBottomSheetData( + transactionId: String, + categoryId: String, + transactionType: String + ) = + addCategoryDataProvider.fetchSimilarTransactionBottomSheetData( + transactionId, + categoryId, + transactionType + ) + + suspend fun postCategoryTypeForTransaction( + screenName: String, + postCategoryTransactionData: PostCategoryTransactionData + ): RepoResult { + return remoteDataProvider.postTransactionCategoryData( + screenName = screenName, + requestBody = postCategoryTransactionData, + ) + } + + suspend fun updateTransactionEntity(transactionId: String, categoryId: String) = + addCategoryDataProvider.updateTransactionEntity( + transactionId = transactionId, + categoryId = categoryId + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/bottomsheet/CategoryCommonBottomSheet.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/bottomsheet/CategoryCommonBottomSheet.kt new file mode 100644 index 0000000000..987e05c4ad --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/bottomsheet/CategoryCommonBottomSheet.kt @@ -0,0 +1,811 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.ui.bottomsheet + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +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.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.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +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.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.navi.base.utils.orFalse +import com.navi.base.utils.orZero +import com.navi.common.utils.EMPTY +import com.navi.design.font.FontWeightEnum +import com.navi.elex.atoms.ElexBottomSheet +import com.navi.moneymanager.R +import com.navi.moneymanager.common.analytics.AddCategoryEventTrackerImpl +import com.navi.moneymanager.common.analytics.CategoryDetailsEventTrackerImpl +import com.navi.moneymanager.common.analytics.DashboardEventTrackerImpl +import com.navi.moneymanager.common.analytics.TransactionDetailsEventTrackerImpl +import com.navi.moneymanager.common.analytics.TransactionHistoryEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.Constants +import com.navi.moneymanager.common.utils.Constants.ERROR_RED_ALERT_ICON +import com.navi.moneymanager.common.utils.ShowCustomToast +import com.navi.moneymanager.common.utils.ToastDuration +import com.navi.moneymanager.postonboard.monthlysummary.model.AddCategoryBottomSheetData +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetButtonType +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetTransactionData +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetUiEvent +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetUiState +import com.navi.moneymanager.postonboard.monthlysummary.model.CounterPartyPreference +import com.navi.moneymanager.postonboard.monthlysummary.model.OverriddenTransaction +import com.navi.moneymanager.postonboard.monthlysummary.model.PostCategoryTransactionData +import com.navi.moneymanager.postonboard.monthlysummary.model.PostTransactionCategoryState +import com.navi.moneymanager.postonboard.monthlysummary.model.SimilarTransactionBottomSheetData +import com.navi.moneymanager.postonboard.monthlysummary.model.TransactionItemData +import com.navi.moneymanager.postonboard.monthlysummary.ui.composable.AddCategoryBottomSheetContentSection +import com.navi.moneymanager.postonboard.monthlysummary.ui.composable.AddCategoryBottomSheetFooterSection +import com.navi.moneymanager.postonboard.monthlysummary.ui.composable.AddCategoryBottomSheetHeaderSection +import com.navi.moneymanager.postonboard.monthlysummary.ui.composable.TransactionBottomSheetContentSection +import com.navi.moneymanager.postonboard.monthlysummary.ui.composable.TransactionBottomSheetFooterSection +import com.navi.moneymanager.postonboard.monthlysummary.ui.composable.TransactionBottomSheetHeaderSection +import com.navi.moneymanager.postonboard.monthlysummary.viewmodel.CategoryBottomSheetViewModel + +@Composable +fun CategoryCommonBottomSheet( + screenName: String, + modifier: Modifier = Modifier, + showBottomSheet: MutableState, + categoryTransactionData: CategoryBottomSheetTransactionData, + onSuccessFullyUpdatedCategory: (Int) -> Unit = {}, + onDismiss: () -> Unit +) { + val viewModel = + hiltViewModel( + creationCallback = { factory -> factory.create(screenName) } + ) + + val context = LocalContext.current + val currentContentType = remember { + mutableStateOf(BottomSheetContentType.AddCategoryBottomSheetContent) + } + val selectedCategory = remember { + mutableStateOf( + Pair(categoryTransactionData.categoryId, categoryTransactionData.categoryName) + ) + } + + var showErrorToast by remember { mutableStateOf(false) } + + LaunchedEffect(showBottomSheet.value) { + if (showBottomSheet.value) { + currentContentType.value = BottomSheetContentType.AddCategoryBottomSheetContent + selectedCategory.value = + Pair(categoryTransactionData.categoryId, categoryTransactionData.categoryName) + } + } + + val bottomSheetUiState = viewModel.state.collectAsStateWithLifecycle().value + + LaunchedEffect(key1 = bottomSheetUiState.postTransactionCategory) { + if (showBottomSheet.value) { + bottomSheetUiState.postTransactionCategory?.let { + when (it.state) { + is PostTransactionCategoryState.Loading -> { + AddCategoryEventTrackerImpl.postTransactionCategoryLoading() + viewModel.sendEvent( + CategoryBottomSheetUiEvent.ToggleButtonLoader( + isLoading = true, + buttonType = it.buttonType + ) + ) + } + is PostTransactionCategoryState.Error -> { + AddCategoryEventTrackerImpl.postTransactionCategoryError( + errorCode = it.state.error?.statusCode.toString(), + errorMessage = it.state.error?.message.toString() + ) + viewModel.sendEvent( + CategoryBottomSheetUiEvent.ToggleButtonLoader( + isLoading = false, + buttonType = it.buttonType + ) + ) + showErrorToast = true + } + is PostTransactionCategoryState.Success -> { + AddCategoryEventTrackerImpl.postTransactionCategorySuccess() + viewModel.sendEvent( + CategoryBottomSheetUiEvent.ToggleButtonLoader( + isLoading = false, + buttonType = it.buttonType + ) + ) + showBottomSheet.value = false + onSuccessFullyUpdatedCategory( + it.state.data?.overrideTransactions?.size.orZero() + ) + } + } + } + } + } + + if (showErrorToast) { + ShowCustomToast( + context = context, + toastDuration = ToastDuration.LENGTH_LONG, + content = { + CustomToastView( + modifier = Modifier, + message = context.resources.getString(R.string.something_went_wrong_try_again), + illustrationType = + IllustrationType.Image( + IllustrationSource.Resource(resId = ERROR_RED_ALERT_ICON) + ) + ) + } + ) + showErrorToast = false + } + + LaunchedEffect(showBottomSheet.value) { + if (showBottomSheet.value) { + viewModel.fetchAddCategoryData(transactionId = categoryTransactionData.transactionId) + if ( + categoryTransactionData.counterPartyName + .equals(Constants.UNKNOWN, ignoreCase = true) + .not() + ) { + viewModel.fetchSimilarTransactionData( + transactionId = categoryTransactionData.transactionId, + categoryId = selectedCategory.value.first, + transactionType = categoryTransactionData.transactionType + ) + } + } + } + + ElexBottomSheet( + visible = showBottomSheet.value, + onDismissRequest = onDismiss, + screenHeightOffset = 80, + scrimColor = Color(0XA322223D) + ) { + AnimatedContent( + targetState = currentContentType.value, + label = EMPTY, + transitionSpec = { createSlideTransition(targetState) } + ) { contentType -> + when (contentType) { + BottomSheetContentType.AddCategoryBottomSheetContent -> { + bottomSheetUiState.addCategoryBottomSheetData?.let { + AddCategoryBottomSheetContent( + screenName = screenName, + modifier = modifier, + categoryTransactionData = categoryTransactionData, + showBottomSheet = showBottomSheet, + data = it, + viewModel = viewModel, + selectedCategory = selectedCategory.value, + onCategorySelect = { chip -> selectedCategory.value = chip }, + changeContentType = { contentType -> + currentContentType.value = contentType + } + ) + } + } + BottomSheetContentType.SimilarTransactionsBottomSheetContent -> { + bottomSheetUiState.similarTransactionBottomSheetData?.let { + SimilarTransactionBottomSheetContent( + screenName = screenName, + modifier = modifier, + data = it, + viewModel = viewModel, + categoryTransactionData = categoryTransactionData, + selectedChip = selectedCategory.value, + changeContentType = { contentType -> + currentContentType.value = contentType + } + ) + } + } + } + HandleBottomSheetBackNavigation( + currentContentType = currentContentType, + showBottomSheet = showBottomSheet, + bottomSheetUiState = bottomSheetUiState + ) + } + } +} + +@Composable +fun AddCategoryBottomSheetContent( + screenName: String, + modifier: Modifier = Modifier, + showBottomSheet: MutableState, + categoryTransactionData: CategoryBottomSheetTransactionData, + data: AddCategoryBottomSheetData, + selectedCategory: Pair, + onCategorySelect: (Pair) -> Unit, + changeContentType: (BottomSheetContentType) -> Unit, + viewModel: CategoryBottomSheetViewModel +) { + + LaunchedEffect(Unit) { + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl.onDashboardCategorySelectionBottomSheetAppeared() + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl + .onCategoryDetailsTransactionCategorySelectionBottomSheetAppeared() + } + MMScreen.TRANSACTION_HISTORY.screen -> { + TransactionHistoryEventTrackerImpl + .onTransactionHistoryCategorySelectionBottomSheetAppeared() + } + MMScreen.TRANSACTION_DETAILS.screen -> { + TransactionDetailsEventTrackerImpl + .onTransactionDetailsCategorySelectionBottomSheetAppeared() + } + } + } + + DisposableEffect(Unit) { + onDispose { + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl.onDashboardCategorySelectionBottomSheetDisappeared() + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl + .onCategoryDetailsTransactionCategorySelectionBottomSheetDisappeared() + } + MMScreen.TRANSACTION_HISTORY.screen -> { + TransactionHistoryEventTrackerImpl + .onTransactionHistoryCategorySelectionBottomSheetDisappeared() + } + MMScreen.TRANSACTION_DETAILS.screen -> { + TransactionDetailsEventTrackerImpl + .onTransactionDetailsCategorySelectionBottomSheetDisappeared() + } + } + } + } + + var height by remember { mutableStateOf(0.dp) } + val density = LocalDensity.current + + var isFooterButtonEnabled by remember { mutableStateOf(data.footerData?.isEnabled.orFalse()) } + + var previousChip by remember { mutableStateOf(selectedCategory) } + + Column( + modifier = + Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 32.dp).onGloballyPositioned { + layoutCoordinates -> + val sheetHeight = layoutCoordinates.size.height + height = with(density) { sheetHeight.toDp() } + } + ) { + AddCategoryBottomSheetHeaderSection( + addCategoryHeaderData = data.headerData, + onHeaderBackButton = { showBottomSheet.value = false } + ) + Spacer(modifier = modifier.height(16.dp)) + Column(modifier = modifier.padding(start = 16.dp, end = 16.dp).weight(1f, false)) { + AddCategoryBottomSheetContentSection( + addCategoryContentData = data.contentData, + selectedCategoryId = selectedCategory.first, + onCategorySelect = { chipId -> + previousChip = selectedCategory + onCategorySelect(chipId) + isFooterButtonEnabled = chipId.first != categoryTransactionData.categoryId + } + ) + } + + AddCategoryBottomSheetFooterSection( + isEnabled = isFooterButtonEnabled, + footerData = data.footerData, + onClick = { + onConfirmFooterClicked( + screenName, + data, + viewModel, + categoryTransactionData, + selectedCategory, + changeContentType, + previousChip + ) + } + ) + } + if (data.footerData?.isLoading.orFalse()) { + Box( + modifier = + Modifier.fillMaxWidth() + .height(height) + .background(color = Color.Transparent) + .clickable(enabled = false) {} + ) + } +} + +private fun onConfirmFooterClicked( + screenName: String, + data: AddCategoryBottomSheetData, + viewModel: CategoryBottomSheetViewModel, + categoryTransactionData: CategoryBottomSheetTransactionData, + selectedChip: Pair, + changeContentType: (BottomSheetContentType) -> Unit, + previousChip: Pair +) { + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl.onDashboardTransactionCategorySelectionApplied( + previousCategory = previousChip.first, + newCategory = selectedChip.first + ) + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl.onCategoryDetailsTransactionCategorySelectionApplied( + previousCategory = previousChip.first, + newCategory = selectedChip.first + ) + } + MMScreen.TRANSACTION_HISTORY.screen -> { + TransactionHistoryEventTrackerImpl + .onTransactionHistoryTransactionCategorySelectionApplied( + previousCategory = previousChip.first, + newCategory = selectedChip.first + ) + } + MMScreen.TRANSACTION_DETAILS.screen -> { + TransactionDetailsEventTrackerImpl + .onTransactionDetailsTransactionCategorySelectionApplied( + previousCategory = previousChip.first, + newCategory = selectedChip.first + ) + } + } + + AddCategoryEventTrackerImpl.addCategoryConfirmFooterButtonClicked(selectedChip.first) + if (data.hasOnlyOneSimilarTransaction.orFalse()) { + viewModel.postCategoryTypeForTransaction( + callerType = CategoryBottomSheetButtonType.ADD_CATEGORY_CONFIRM, + postCategoryTransactionData = + PostCategoryTransactionData( + overrideTransactions = + listOf( + OverriddenTransaction( + txnId = categoryTransactionData.transactionId, + overriddenCategory = selectedChip.first + ) + ), + counterPartyPreferences = + listOf( + CounterPartyPreference( + counterPartyName = categoryTransactionData.counterPartyName, + selectedCategory = selectedChip.first + ) + ) + ) + ) + } else { + changeContentType(BottomSheetContentType.SimilarTransactionsBottomSheetContent) + } +} + +@Composable +fun SimilarTransactionBottomSheetContent( + screenName: String, + modifier: Modifier = Modifier, + data: SimilarTransactionBottomSheetData, + viewModel: CategoryBottomSheetViewModel, + categoryTransactionData: CategoryBottomSheetTransactionData, + selectedChip: Pair, + changeContentType: (BottomSheetContentType) -> Unit, +) { + + LaunchedEffect(Unit) { + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl + .onDashboardCategoriseSimilarTransactionsBottomSheetAppeared() + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl + .onCategoryDetailsCategoriseSimilarTransactionBottomSheetAppeared() + } + MMScreen.TRANSACTION_HISTORY.screen -> { + TransactionHistoryEventTrackerImpl + .onTransactionHistoryCategoriseSimilarTransactionBottomSheetAppeared() + } + MMScreen.TRANSACTION_DETAILS.screen -> { + TransactionDetailsEventTrackerImpl + .onTransactionDetailsCategoriseSimilarTransactionBottomSheetAppeared() + } + } + } + + DisposableEffect(Unit) { + onDispose { + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl + .onDashboardCategoriseSimilarTransactionsBottomSheetDisappeared() + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl + .onCategoryDetailsCategoriseSimilarTransactionBottomSheetDisappeared() + } + MMScreen.TRANSACTION_HISTORY.screen -> { + TransactionHistoryEventTrackerImpl + .onTransactionHistoryCategoriseSimilarTransactionBottomSheetDisappeared() + } + MMScreen.TRANSACTION_DETAILS.screen -> { + TransactionDetailsEventTrackerImpl + .onTransactionDetailsCategoriseSimilarTransactionBottomSheetDisappeared() + } + } + } + } + + var height by remember { mutableStateOf(0.dp) } + val density = LocalDensity.current + val isProceedFooterButtonEnabled = remember { + mutableStateOf(data.footer.proceedButtonData.isEnabled) + } + var isAutoCategoriseChecked by remember { mutableStateOf(true) } + val selectedItems = remember { + mutableStateListOf().apply { + addAll(data.content.transactionItems.map { it }) + } + } + Column( + modifier = + Modifier.fillMaxWidth().padding(top = 16.dp, bottom = 32.dp).onGloballyPositioned { + layoutCoordinates -> + val sheetHeight = layoutCoordinates.size.height + height = with(density) { sheetHeight.toDp() } + } + ) { + TransactionBottomSheetHeaderSection( + data = data.header, + selectedCategoryName = selectedChip.second, + onHeaderBackButton = { + viewModel.sendEvent( + CategoryBottomSheetUiEvent.ToggleButtonState( + isEnabled = true, + buttonType = CategoryBottomSheetButtonType.ADD_CATEGORY_CONFIRM + ) + ) + onHeaderBackButtonClicked(changeContentType) + } + ) + Spacer(modifier = modifier.height(16.dp)) + Column(modifier = modifier.weight(1f, false)) { + TransactionBottomSheetContentSection( + data = data.content, + selectedTransactionsItems = selectedItems + ) + } + isProceedFooterButtonEnabled.value = selectedItems.isNotEmpty() + + TransactionBottomSheetFooterSection( + footerData = data.footer, + selectedCategory = selectedChip.second, + isProceedButtonEnabled = isProceedFooterButtonEnabled.value, + autoCategoriseChecked = isAutoCategoriseChecked, + onAutoCategoriseChecked = { isAutoCategoriseChecked = it }, + onProceedFooterButtonClick = + onProceedFooterButtonClick( + screenName = screenName, + isAutoCategoriseChecked = isAutoCategoriseChecked, + areAllItemsSelected = selectedItems.size == data.content.transactionItems.size, + selectedItems = selectedItems, + viewModel = viewModel, + selectedChip = selectedChip.first, + categoryTransactionData = categoryTransactionData + ), + onSkipFooterButtonClick = + onSkipFooterButtonClicked( + screenName = screenName, + isAutoCategoriseChecked = isAutoCategoriseChecked, + viewModel = viewModel, + selectedCategoryId = selectedChip.first, + categoryTransactionData = categoryTransactionData + ) + ) + } + val isLoading = data.footer.proceedButtonData.isLoading || data.footer.skipButtonData.isLoading + if (isLoading) { + Box( + modifier = + Modifier.fillMaxWidth() + .height(height) + .background(color = Color.Transparent) + .clickable(enabled = false) {} + ) + } +} + +private fun onSkipFooterButtonClicked( + screenName: String, + isAutoCategoriseChecked: Boolean, + viewModel: CategoryBottomSheetViewModel, + selectedCategoryId: String, + categoryTransactionData: CategoryBottomSheetTransactionData, +): () -> Unit = { + AddCategoryEventTrackerImpl.proceedFooterButtonClicked( + isAutoCategoriseChecked = isAutoCategoriseChecked.toString(), + buttonType = CategoryBottomSheetButtonType.SIMILAR_TRANSACTION_SKIP.toString() + ) + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl.onDashboardCategoriseSimilarTransactionSkipped( + category = selectedCategoryId + ) + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl.onCategoryDetailsCategoriseSimilarTransactionSkipped( + category = selectedCategoryId + ) + } + MMScreen.TRANSACTION_HISTORY.screen -> { + TransactionHistoryEventTrackerImpl + .onTransactionHistoryCategoriseSimilarTransactionSkipped( + category = selectedCategoryId + ) + } + MMScreen.TRANSACTION_DETAILS.screen -> { + TransactionDetailsEventTrackerImpl + .onTransactionDetailsCategoriseSimilarTransactionSkipped( + category = selectedCategoryId + ) + } + } + val overriddenTransaction = + listOf( + OverriddenTransaction( + txnId = categoryTransactionData.transactionId, + overriddenCategory = selectedCategoryId + ) + ) + val counterPartyPreferences = + if (isAutoCategoriseChecked) { + listOf( + CounterPartyPreference( + counterPartyName = categoryTransactionData.counterPartyName, + selectedCategory = selectedCategoryId + ) + ) + } else { + null + } + + viewModel.postCategoryTypeForTransaction( + callerType = CategoryBottomSheetButtonType.SIMILAR_TRANSACTION_SKIP, + postCategoryTransactionData = + PostCategoryTransactionData( + overrideTransactions = overriddenTransaction, + counterPartyPreferences = counterPartyPreferences + ) + ) +} + +private fun onProceedFooterButtonClick( + screenName: String, + isAutoCategoriseChecked: Boolean, + selectedItems: List, + areAllItemsSelected: Boolean, + selectedChip: String, + viewModel: CategoryBottomSheetViewModel, + categoryTransactionData: CategoryBottomSheetTransactionData +): () -> Unit = { + AddCategoryEventTrackerImpl.proceedFooterButtonClicked( + isAutoCategoriseChecked = isAutoCategoriseChecked.toString(), + buttonType = CategoryBottomSheetButtonType.SIMILAR_TRANSACTION_PROCEED.toString() + ) + val postCategoryTransactionData = + PostCategoryTransactionData( + overrideTransactions = + getTransactionList( + selectedItems = selectedItems, + selectedCategoryId = selectedChip, + createTransaction = { item, category -> + OverriddenTransaction(txnId = item.id, overriddenCategory = category) + } + ) + + OverriddenTransaction( + txnId = categoryTransactionData.transactionId, + overriddenCategory = selectedChip + ), + counterPartyPreferences = + if (isAutoCategoriseChecked) { + getTransactionList( + selectedItems = selectedItems, + selectedCategoryId = selectedChip, + createTransaction = { item, category -> + CounterPartyPreference( + counterPartyName = item.name, + selectedCategory = category + ) + } + ) + } else { + null + } + ) + when (screenName) { + MMScreen.DASHBOARD.screen -> { + DashboardEventTrackerImpl.onDashboardCategoriseSimilarTransactionConfirmed( + isAutoCategoriseChecked, + areAllItemsSelected, + selectedItems.size, + selectedChip + ) + } + MMScreen.CATEGORY_DETAILS.screen -> { + CategoryDetailsEventTrackerImpl.onCategoryDetailsCategoriseSimilarTransactionConfirmed( + isAutoCategoriseChecked, + areAllItemsSelected, + selectedItems.size, + selectedChip + ) + } + MMScreen.TRANSACTION_HISTORY.screen -> { + TransactionHistoryEventTrackerImpl + .onTransactionHistoryCategoriseSimilarTransactionConfirmed( + isAutoCategoriseChecked, + areAllItemsSelected, + selectedItems.size, + selectedChip + ) + } + MMScreen.TRANSACTION_DETAILS.screen -> { + TransactionDetailsEventTrackerImpl + .onTransactionDetailsCategoriseSimilarTransactionConfirmed( + isAutoCategoriseChecked, + areAllItemsSelected, + selectedItems.size, + selectedChip + ) + } + } + viewModel.postCategoryTypeForTransaction( + postCategoryTransactionData = postCategoryTransactionData, + callerType = CategoryBottomSheetButtonType.SIMILAR_TRANSACTION_PROCEED + ) +} + +private fun getTransactionList( + selectedItems: List, + selectedCategoryId: String, + createTransaction: (TransactionItemData, String) -> T +): List { + return selectedItems.map { createTransaction(it, selectedCategoryId) } +} + +private fun onHeaderBackButtonClicked(changeContentType: (BottomSheetContentType) -> Unit) { + AddCategoryEventTrackerImpl.similarTxnHeaderBackButtonClicked() + changeContentType(BottomSheetContentType.AddCategoryBottomSheetContent) +} + +@Composable +private fun HandleBottomSheetBackNavigation( + currentContentType: MutableState, + showBottomSheet: MutableState, + bottomSheetUiState: CategoryBottomSheetUiState, +) { + + AddCategoryEventTrackerImpl.systemBackButtonClicked(currentContentType.value.toString()) + if (isLoading(bottomSheetUiState).not()) { + BackHandler { + if ( + currentContentType.value == + BottomSheetContentType.SimilarTransactionsBottomSheetContent + ) { + currentContentType.value = BottomSheetContentType.AddCategoryBottomSheetContent + } else { + showBottomSheet.value = false + } + } + } +} + +fun isLoading(bottomSheetData: CategoryBottomSheetUiState): Boolean { + return bottomSheetData.addCategoryBottomSheetData?.footerData?.isLoading.orFalse() || + bottomSheetData.similarTransactionBottomSheetData + ?.footer + ?.proceedButtonData + ?.isLoading + .orFalse() || + bottomSheetData.similarTransactionBottomSheetData + ?.footer + ?.skipButtonData + ?.isLoading + .orFalse() +} + +@Composable +fun CustomToastView( + modifier: Modifier = Modifier, + message: String, + illustrationType: IllustrationType +) { + Row( + modifier = + Modifier.background(color = MMColor.textSecondary, shape = RoundedCornerShape(4.dp)) + .wrapContentSize() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Illustration(modifier = modifier.size(16.dp), illustrationType = illustrationType) + Spacer(modifier = modifier.width(8.dp)) + MMText( + text = message, + modifier = Modifier.wrapContentSize(), + color = MMColor.white, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + ) + } +} + +fun createSlideTransition(targetState: BottomSheetContentType): ContentTransform { + val isAddingCategory = targetState == BottomSheetContentType.AddCategoryBottomSheetContent + + val initialOffsetX = { fullWidth: Int -> if (isAddingCategory) -fullWidth else fullWidth } + val targetOffsetX = { fullWidth: Int -> if (isAddingCategory) fullWidth else -fullWidth } + + return slideInHorizontally( + animationSpec = tween(200), + initialOffsetX = initialOffsetX + ) togetherWith slideOutHorizontally(animationSpec = tween(200), targetOffsetX = targetOffsetX) +} + +enum class BottomSheetContentType { + AddCategoryBottomSheetContent, + SimilarTransactionsBottomSheetContent +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/AddCategoryBottomSheetContentSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/AddCategoryBottomSheetContentSection.kt new file mode 100644 index 0000000000..68e49a9be1 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/AddCategoryBottomSheetContentSection.kt @@ -0,0 +1,186 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.ui.composable + +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.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.ChipListWithSingleSelection +import com.navi.moneymanager.common.ui.composable.base.MMDivider +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.monthlysummary.model.AddCategoryContentData +import com.navi.moneymanager.postonboard.monthlysummary.model.ChipsContainerData +import com.navi.moneymanager.postonboard.monthlysummary.model.TransactionDetails + +@Composable +fun AddCategoryBottomSheetContentSection( + modifier: Modifier = Modifier, + addCategoryContentData: AddCategoryContentData, + selectedCategoryId: String?, + onCategorySelect: (Pair) -> Unit +) { + TransactionDetailsSection(transactionDetails = addCategoryContentData.transactionDetails) + AddCategoryChipsContainer( + containerData = addCategoryContentData.chipsContainerData, + selectedChip = selectedCategoryId, + onChipSelected = onCategorySelect + ) +} + +@Composable +fun TransactionDetailsSection( + modifier: Modifier = Modifier, + transactionDetails: TransactionDetails +) { + Column { + MMDivider(modifier = modifier.fillMaxWidth(), thickness = 1.dp, color = MMColor.borderColor) + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column { + MMText( + text = transactionDetails.transactionDay, + color = MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR + ) + Spacer(modifier = modifier.height(8.dp)) + MMText( + text = transactionDetails.transactionDate, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR + ) + } + Spacer(modifier = modifier.weight(1f)) + Column { + MMText( + text = transactionDetails.paymentMethod, + color = MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR + ) + Spacer(modifier = modifier.height(8.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Illustration( + modifier = modifier.size(16.dp), + illustrationType = + IllustrationType.Image(transactionDetails.accountInfo.endIconUrl) + ) + Spacer(modifier = modifier.width(8.dp)) + MMText( + text = transactionDetails.accountInfo.account, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR + ) + } + } + Spacer(modifier = modifier.width(55.dp)) + } + MMDivider(modifier = modifier.fillMaxWidth(), thickness = 1.dp, color = MMColor.borderColor) + } +} + +/** + * A composable function that displays a container for chips, including titles and dividers, + * allowing for the selection of chips from two separate categories. + * + * @param modifier Modifier to apply to the overall layout of the container. + * @param containerData The data object containing information about the categories and their + * respective chip lists. + * + * The function consists of: + * - A title for the first category of chips. + * - A list of chips for the first category, with single selection support. + * - A divider and a title for more categories. + * - A list of chips for the more categories, also with single selection support. + */ +@Composable +fun AddCategoryChipsContainer( + modifier: Modifier = Modifier, + containerData: ChipsContainerData, + selectedChip: String?, + onChipSelected: (Pair) -> Unit +) { + + val scrollState = rememberScrollState() + Column(modifier = modifier.verticalScroll(scrollState)) { + Spacer(modifier.height(24.dp)) + MMText( + text = containerData.categoriesData.categoriesTitle, + modifier = modifier.fillMaxWidth(), + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + textAlign = TextAlign.Start + ) + Spacer(modifier.height(16.dp)) + ChipListWithSingleSelection( + chips = containerData.categoriesData.categoriesList, + selectedChip = selectedChip, + onChipSelected = onChipSelected + ) + + containerData.moreCategoriesData?.let { + Spacer(modifier.height(32.dp)) + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + MMDivider( + modifier = modifier.wrapContentSize().weight(1f).padding(vertical = 8.dp), + thickness = 1.dp, + color = MMColor.borderColor + ) + MMText( + text = containerData.moreCategoriesData.moreCategoriesTitle, + modifier = modifier.padding(horizontal = 8.dp).wrapContentSize(), + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + letterSpacing = 1.5.sp, + textAlign = TextAlign.Center + ) + MMDivider( + modifier = modifier.wrapContentSize().weight(1f).padding(vertical = 8.dp), + thickness = 1.dp, + color = MMColor.borderColor + ) + } + + Spacer(modifier.height(32.dp)) + + ChipListWithSingleSelection( + chips = containerData.moreCategoriesData.moreCategoriesList, + selectedChip = selectedChip, + onChipSelected = onChipSelected + ) + } + Spacer(modifier = modifier.height(16.dp)) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/AddCategoryBottomSheetFooterSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/AddCategoryBottomSheetFooterSection.kt new file mode 100644 index 0000000000..199eb28256 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/AddCategoryBottomSheetFooterSection.kt @@ -0,0 +1,69 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.ui.composable + +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.material3.ButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.base.utils.orFalse +import com.navi.elex.font.FontWeightEnum as ElexFontWeightEnum +import com.navi.elex.molecules.ElexButtonWithLoader +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.utils.lottieCompositionSpecBasedOnSource +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.monthlysummary.model.AddCategoryFooterData +import com.navi.naviwidgets.utils.NaviWidgetLottieUtils.TUK_TUK_WHITE_LOTTIE + +@Composable +fun AddCategoryBottomSheetFooterSection( + isEnabled: Boolean, + footerData: AddCategoryFooterData?, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Column( + modifier = + Modifier.shadow( + elevation = 16.dp, + ambientColor = MMColor.ambientColor, + spotColor = MMColor.shadowSpotColor + ) + ) { + Spacer(modifier = Modifier.height(32.dp)) + ElexButtonWithLoader( + modifier = modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp).height(50.dp), + shape = RoundedCornerShape(4.dp), + colors = + ButtonDefaults.buttonColors( + disabledContainerColor = Color(0XFFB5ACB9), + containerColor = MMColor.ctaPrimary + ), + textColor = MMColor.white, + enabled = isEnabled, + text = footerData?.buttonLabel.orEmpty(), + loading = footerData?.isLoading.orFalse(), + fontSize = 14.sp, + fontWeight = ElexFontWeightEnum.NAVI_BODY_DEMI_BOLD, + lottieSpec = + lottieCompositionSpecBasedOnSource( + lottie = IllustrationSource.Resource(resId = TUK_TUK_WHITE_LOTTIE) + ), + onClick = onClick + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/AddCategoryBottomSheetHeaderSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/AddCategoryBottomSheetHeaderSection.kt new file mode 100644 index 0000000000..bb37e4c17b --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/AddCategoryBottomSheetHeaderSection.kt @@ -0,0 +1,106 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.ui.composable + +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.monthlysummary.model.AddCategoryHeaderData +import com.navi.moneymanager.postonboard.monthlysummary.model.StartHeaderIcon + +@Composable +fun AddCategoryBottomSheetHeaderSection( + modifier: Modifier = Modifier, + addCategoryHeaderData: AddCategoryHeaderData, + onHeaderBackButton: () -> Unit +) { + Row( + modifier = modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column { + RenderIcon(startHeaderIcon = addCategoryHeaderData.startIcon) + Spacer(modifier = modifier.height(12.dp)) + MMText( + text = addCategoryHeaderData.transactionAmount, + color = MMColor.black, + fontSize = 20.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD + ) + Spacer(modifier = modifier.height(4.dp)) + MMText( + text = addCategoryHeaderData.name, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR + ) + } + Spacer(modifier = modifier.weight(1f)) + IconButton( + modifier = modifier.size(24.dp).align(Alignment.Top), + onClick = { onHeaderBackButton() } + ) { + Illustration( + modifier = modifier, + illustrationType = IllustrationType.Image(addCategoryHeaderData.endIcon) + ) + } + } +} + +@Composable +private fun RenderIcon(modifier: Modifier = Modifier, startHeaderIcon: StartHeaderIcon) { + startHeaderIcon.iconUrl?.let { + Illustration( + modifier = modifier.size(24.dp), + illustrationType = + IllustrationType.Image( + IllustrationSource.Remote( + url = it, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ) + ) + } + ?: run { + Box( + contentAlignment = Alignment.Center, + modifier = + modifier + .size(40.dp) + .background(startHeaderIcon.iconBackgroundColor, CircleShape) + ) { + MMText( + text = startHeaderIcon.iconText.orEmpty(), + color = MMColor.white, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/TransactionBottomSheetContentSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/TransactionBottomSheetContentSection.kt new file mode 100644 index 0000000000..66f3673831 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/TransactionBottomSheetContentSection.kt @@ -0,0 +1,201 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.ui.composable + +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.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.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.CheckboxDefaults +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.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.elex.atoms.ElexCheckbox +import com.navi.moneymanager.R +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.base.MMDivider +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.noIndicationToggleable +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryTransactionContentData +import com.navi.moneymanager.postonboard.monthlysummary.model.TransactionItemData + +@Composable +fun TransactionBottomSheetContentSection( + modifier: Modifier = Modifier, + data: CategoryTransactionContentData, + selectedTransactionsItems: MutableList, +) { + Row( + modifier = + Modifier.fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 16.dp) + .noIndicationToggleable( + value = (selectedTransactionsItems.size == data.transactionItems.size) + ) { isChecked -> + if (isChecked) { + selectedTransactionsItems.clear() + selectedTransactionsItems.addAll(data.transactionItems) + } else { + selectedTransactionsItems.clear() + } + }, + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.Start + ) { + ElexCheckbox( + modifier = Modifier.size(16.dp).padding(start = 4.dp, top = 8.dp), + checked = (selectedTransactionsItems.size == data.transactionItems.size), + colors = + CheckboxDefaults.colors( + checkedColor = MMColor.ctaPrimary, + uncheckedColor = MMColor.gray + ) + ) + Spacer(modifier = Modifier.width(12.dp)) + MMText( + text = stringResource(id = R.string.select_all), + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR + ) + } + Spacer(modifier = Modifier.height(16.dp)) + MMDivider(modifier = modifier.fillMaxWidth(), thickness = 4.dp, color = MMColor.ctaSecondary) + TransactionList(modifier, data.transactionItems, selectedTransactionsItems) +} + +@Composable +fun TransactionList( + modifier: Modifier = Modifier, + transactions: List, + selectedTransactionsItems: MutableList, +) { + LazyColumn(modifier = Modifier.padding(horizontal = 16.dp)) { + itemsIndexed(transactions) { index, transaction -> + TransactionItem(transaction, selectedTransactionsItems, transaction) + if (index < transactions.lastIndex) { + MMDivider( + modifier = modifier.fillMaxWidth(), + thickness = 0.5.dp, + color = MMColor.borderColor + ) + } + } + } +} + +@Composable +fun TransactionItem( + transactionData: TransactionItemData, + selectedTransactionsItems: MutableList, + item: TransactionItemData +) { + Column( + modifier = + Modifier.noIndicationToggleable(value = selectedTransactionsItems.contains(item)) { + isChecked -> + if (isChecked) { + selectedTransactionsItems.add(item) + } else { + selectedTransactionsItems.remove(item) + } + } + ) { + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.Start + ) { + ElexCheckbox( + modifier = Modifier.size(16.dp).padding(start = 4.dp, top = 8.dp), + checked = selectedTransactionsItems.contains(item), + colors = + CheckboxDefaults.colors( + checkedColor = MMColor.ctaPrimary, + uncheckedColor = MMColor.gray + ) + ) + Spacer(modifier = Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + MMText( + text = transactionData.name, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR + ) + Spacer(modifier = Modifier.height(4.dp)) + MMText( + text = transactionData.date, + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR + ) + } + + Column(horizontalAlignment = Alignment.End) { + MMText( + text = transactionData.amount, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR + ) + Spacer(modifier = Modifier.height(4.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + MMText( + text = transactionData.transactionPaidType.label, + color = MMColor.textSecondary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR + ) + Spacer(modifier = Modifier.width(4.dp)) + Box( + modifier = + Modifier.size(24.dp) + .clip(shape = CircleShape) + .background(color = MMColor.bgAltColor), + contentAlignment = Alignment.Center + ) { + Illustration( + modifier = Modifier.size(16.dp), + illustrationType = + IllustrationType.Image( + IllustrationSource.Remote( + url = transactionData.transactionPaidType.iconUrl, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ) + ) + } + } + } + } + Spacer(modifier = Modifier.height(16.dp)) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/TransactionBottomSheetFooterSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/TransactionBottomSheetFooterSection.kt new file mode 100644 index 0000000000..29c1f80409 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/TransactionBottomSheetFooterSection.kt @@ -0,0 +1,170 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.runtime.Composable +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.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.design.font.getFontWeight +import com.navi.design.font.naviFontFamily +import com.navi.elex.font.FontWeightEnum as ElexFontWeightEnum +import com.navi.elex.molecules.ElexButtonWithLoader +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.utils.lottieCompositionSpecBasedOnSource +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.wrapInSingleQuotes +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryTransactionFooterData +import com.navi.naviwidgets.utils.NaviWidgetLottieUtils.TUK_TUK_LOTTIE +import com.navi.naviwidgets.utils.NaviWidgetLottieUtils.TUK_TUK_WHITE_LOTTIE + +@Composable +fun TransactionBottomSheetFooterSection( + modifier: Modifier = Modifier, + footerData: CategoryTransactionFooterData, + selectedCategory: String, + isProceedButtonEnabled: Boolean, + autoCategoriseChecked: Boolean, + onAutoCategoriseChecked: (Boolean) -> Unit, + onProceedFooterButtonClick: () -> Unit, + onSkipFooterButtonClick: () -> Unit, +) { + Column( + modifier = + Modifier.shadow( + elevation = 16.dp, + ambientColor = MMColor.ambientColor, + spotColor = MMColor.shadowSpotColor + ) + .padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.height(24.dp)) + + Row( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.SpaceBetween + ) { + val footerInfoText = getAnnotatedFooterText(footerData, selectedCategory) + MMText( + text = footerInfoText, + modifier = Modifier.weight(1f).padding(end = 16.dp), + fontSize = 14.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + + Switch( + modifier = Modifier, + checked = autoCategoriseChecked, + onCheckedChange = { isChecked -> onAutoCategoriseChecked(isChecked) }, + colors = + SwitchDefaults.colors( + checkedTrackColor = MMColor.ctaPrimary, + uncheckedTrackColor = MMColor.toggleTrackColor, + uncheckedBorderColor = MMColor.transparent, + uncheckedThumbColor = MMColor.white + ) + ) + } + Spacer(modifier = Modifier.height(16.dp)) + ElexButtonWithLoader( + modifier = modifier.fillMaxWidth().height(50.dp), + shape = RoundedCornerShape(4.dp), + colors = + ButtonDefaults.buttonColors( + disabledContainerColor = Color(0XFFB5ACB9), + containerColor = MMColor.ctaPrimary + ), + textColor = MMColor.white, + enabled = isProceedButtonEnabled, + text = footerData.proceedButtonData.buttonText, + loading = footerData.proceedButtonData.isLoading, + fontSize = 14.sp, + fontWeight = ElexFontWeightEnum.NAVI_BODY_DEMI_BOLD, + lottieSpec = + lottieCompositionSpecBasedOnSource( + lottie = IllustrationSource.Resource(resId = TUK_TUK_WHITE_LOTTIE) + ), + onClick = { onProceedFooterButtonClick() }, + ) + Spacer(modifier = Modifier.height(16.dp)) + ElexButtonWithLoader( + modifier = modifier.fillMaxWidth().height(50.dp), + shape = RoundedCornerShape(4.dp), + colors = + ButtonDefaults.buttonColors( + disabledContainerColor = MMColor.borderColor, + containerColor = MMColor.ctaSecondary + ), + textColor = MMColor.ctaPrimary, + enabled = true, + text = footerData.skipButtonData.buttonText, + loading = footerData.skipButtonData.isLoading, + fontSize = 14.sp, + fontWeight = ElexFontWeightEnum.NAVI_BODY_DEMI_BOLD, + lottieSpec = + lottieCompositionSpecBasedOnSource( + lottie = IllustrationSource.Resource(resId = TUK_TUK_LOTTIE) + ), + onClick = { onSkipFooterButtonClick() }, + ) + } +} + +@Composable +private fun getAnnotatedFooterText( + data: CategoryTransactionFooterData, + selectedCategory: String +): AnnotatedString { + return buildAnnotatedString { + withStyle( + style = + SpanStyle( + color = MMColor.textSecondary, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + fontFamily = naviFontFamily + ) + ) { + append(data.footerInfoData.infoText) + } + + withStyle( + style = + SpanStyle( + color = MMColor.textPrimary, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + fontFamily = naviFontFamily + ) + ) { + append(wrapInSingleQuotes(selectedCategory)) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/TransactionBottomSheetHeaderSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/TransactionBottomSheetHeaderSection.kt new file mode 100644 index 0000000000..34ad876640 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/ui/composable/TransactionBottomSheetHeaderSection.kt @@ -0,0 +1,87 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.ui.composable + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.design.font.getFontWeight +import com.navi.design.font.naviFontFamily +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.wrapInSingleQuotes +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryTransactionHeaderData + +@Composable +fun TransactionBottomSheetHeaderSection( + modifier: Modifier = Modifier, + data: CategoryTransactionHeaderData, + selectedCategoryName: String, + onHeaderBackButton: () -> Unit +) { + Column(modifier = modifier.padding(horizontal = 16.dp)) { + IconButton(modifier = modifier.size(24.dp), onClick = { onHeaderBackButton() }) { + Illustration( + modifier = modifier, + illustrationType = IllustrationType.Image(data.topIconUrl) + ) + } + Spacer(modifier = modifier.height(16.dp)) + MMText( + text = data.title, + color = MMColor.black, + fontSize = 20.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD + ) + Spacer(modifier = modifier.height(4.dp)) + MMText( + text = data.subTitle, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR + ) + Spacer(modifier = modifier.height(16.dp)) + val description = buildAnnotatedString { + withStyle( + style = + SpanStyle( + color = MMColor.textSecondary, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_REGULAR), + fontFamily = naviFontFamily + ) + ) { + append(data.description) + } + + withStyle( + style = + SpanStyle( + color = MMColor.textPrimary, + fontWeight = getFontWeight(FontWeightEnum.NAVI_BODY_DEMI_BOLD), + fontFamily = naviFontFamily + ) + ) { + append(wrapInSingleQuotes(selectedCategoryName)) + } + } + MMText(text = description, fontSize = 14.sp) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/viewmodel/CategoryBottomSheetViewModel.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/viewmodel/CategoryBottomSheetViewModel.kt new file mode 100644 index 0000000000..80e87a825c --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/monthlysummary/viewmodel/CategoryBottomSheetViewModel.kt @@ -0,0 +1,120 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.monthlysummary.viewmodel + +import androidx.lifecycle.viewModelScope +import com.navi.common.utils.isValidResponse +import com.navi.moneymanager.base.viewmodel.MMBaseViewModel +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetButtonType +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetUiEffect +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetUiEvent +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetUiState +import com.navi.moneymanager.postonboard.monthlysummary.model.PostCategoryTransactionData +import com.navi.moneymanager.postonboard.monthlysummary.model.PostTransactionCategory +import com.navi.moneymanager.postonboard.monthlysummary.model.PostTransactionCategoryState +import com.navi.moneymanager.postonboard.monthlysummary.reducer.CategoryBottomSheetReducer +import com.navi.moneymanager.postonboard.monthlysummary.repo.CategoryDetailsRepository +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers + +@HiltViewModel(assistedFactory = CategoryBottomSheetViewModel.Factory::class) +class CategoryBottomSheetViewModel +@AssistedInject +constructor( + @Assisted private val screenName: String, + private val repository: CategoryDetailsRepository, +) : + MMBaseViewModel< + CategoryBottomSheetUiState, CategoryBottomSheetUiEvent, CategoryBottomSheetUiEffect + >( + initialState = CategoryBottomSheetUiState.initialState, + reducer = CategoryBottomSheetReducer() + ) { + + @AssistedFactory + interface Factory { + fun create(screenName: String): CategoryBottomSheetViewModel + } + + fun fetchAddCategoryData(transactionId: String) { + viewModelScope.safeLaunch(Dispatchers.IO) { + repository.fetchAddCategoryBottomSheetData(transactionId).collect { + sendEvent(event = CategoryBottomSheetUiEvent.UpdateAddCategoryBottomSheetData(it)) + } + } + } + + fun fetchSimilarTransactionData( + transactionId: String, + categoryId: String, + transactionType: String + ) { + viewModelScope.safeLaunch(Dispatchers.IO) { + repository + .getCategoryTransactionBottomSheetData(transactionId, categoryId, transactionType) + .collect { + sendEvent( + event = + CategoryBottomSheetUiEvent.UpdateSimilarTransactionBottomSheetData(it) + ) + } + } + } + + fun postCategoryTypeForTransaction( + postCategoryTransactionData: PostCategoryTransactionData, + callerType: CategoryBottomSheetButtonType + ) { + viewModelScope.safeLaunch(Dispatchers.IO) { + sendEvent( + CategoryBottomSheetUiEvent.UpdatePostCategoryTransactionState( + data = + PostTransactionCategory( + state = PostTransactionCategoryState.Loading, + buttonType = callerType + ) + ) + ) + val response = + repository.postCategoryTypeForTransaction( + screenName = screenName, + postCategoryTransactionData = postCategoryTransactionData, + ) + if (response.isValidResponse()) { + response.data?.overrideTransactions?.forEach { + repository.updateTransactionEntity( + categoryId = it.overriddenCategory, + transactionId = it.txnId + ) + } + sendEvent( + CategoryBottomSheetUiEvent.UpdatePostCategoryTransactionState( + data = + PostTransactionCategory( + state = PostTransactionCategoryState.Success(data = response.data), + buttonType = callerType + ) + ) + ) + } else { + sendEvent( + CategoryBottomSheetUiEvent.UpdatePostCategoryTransactionState( + data = + PostTransactionCategory( + state = PostTransactionCategoryState.Error(error = response.error), + buttonType = callerType + ) + ) + ) + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/model/OtherCategoriesBottomSheetState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/model/OtherCategoriesBottomSheetState.kt new file mode 100644 index 0000000000..9d88d1b357 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/model/OtherCategoriesBottomSheetState.kt @@ -0,0 +1,22 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.spendanalysis.model + +import com.navi.moneymanager.common.model.SpendCategoryItemData + +data class OtherCategoriesBottomSheetData( + val headerData: OtherCategoriesBottomSheetHeaderData, + val categories: List, + val ctaText: String +) + +data class OtherCategoriesBottomSheetHeaderData( + val iconUrl: String, + val title: String, + val subTitle: String +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/model/SpendAnalysisScreenData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/model/SpendAnalysisScreenData.kt new file mode 100644 index 0000000000..d640305939 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/model/SpendAnalysisScreenData.kt @@ -0,0 +1,32 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.spendanalysis.model + +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.model.BarGraphData +import com.navi.moneymanager.common.model.SpendCategorizationState +import com.navi.moneymanager.common.model.sectionHeader.SectionHeaderData +import com.navi.moneymanager.postonboard.dashboard.model.NavBarData + +data class SpendAnalysisScreenData( + val topNavBar: NavBarData? = null, + val totalSpendSection: TotalSpendSectionData? = null, + val spendCategorizationState: SpendCategorizationState, + val barGraphData: BarGraphData? = null, + val viewTransactionHistoryTitle: String? = null, +) + +data class TotalSpendSectionData( + val iconUrl: IllustrationSource, + val title: String, + val selectedMonth: String, + val actionIcon: IllustrationSource, + val amount: String, + val selectedBankReferenceIds: Set, + val spendingTrendSectionData: SectionHeaderData? = null +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/model/SpendAnalysisScreenUiEffect.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/model/SpendAnalysisScreenUiEffect.kt new file mode 100644 index 0000000000..ebd17517aa --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/model/SpendAnalysisScreenUiEffect.kt @@ -0,0 +1,38 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.spendanalysis.model + +import androidx.compose.runtime.Immutable +import com.navi.base.model.CtaData +import com.navi.common.basemvi.UiEffect +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenNavigationData + +@Immutable +sealed class SpendAnalysisScreenUiEffect : UiEffect { + + @Immutable + sealed class Navigation : SpendAnalysisScreenUiEffect() { + data object Back : Navigation() + + data object Help : Navigation() + + data object TransactionHistory : Navigation() + + @Immutable data class NavigateToCta(val ctaData: CtaData) : Navigation() + + @Immutable + data class CategoryDetailsScreen(val data: CategoryDetailsScreenNavigationData) : + Navigation() + } + + @Immutable + data class UpdateSelectedBankReferenceIds(val selectedBankReferenceIds: Set) : + SpendAnalysisScreenUiEffect() + + data object FetchConsentUrl : SpendAnalysisScreenUiEffect() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/model/SpendAnalysisScreenUiEvent.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/model/SpendAnalysisScreenUiEvent.kt new file mode 100644 index 0000000000..dd6a0fddf7 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/model/SpendAnalysisScreenUiEvent.kt @@ -0,0 +1,29 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.spendanalysis.model + +import androidx.compose.runtime.Immutable +import com.navi.common.basemvi.UiEvent +import com.navi.moneymanager.common.model.bottomSheet.SpendAnalysisScreenBottomSheets +import com.navi.moneymanager.postonboard.help.model.HelpBottomSheetState + +@Immutable +sealed interface SpendAnalysisScreenUiEvent : UiEvent { + @Immutable data class RenderUI(val data: SpendAnalysisScreenData) : SpendAnalysisScreenUiEvent + + data class ShowBottomSheet(val type: SpendAnalysisScreenBottomSheets) : + SpendAnalysisScreenUiEvent + + data class DismissBottomSheet( + val type: Class = + SpendAnalysisScreenBottomSheets.NoBottomSheet::class.java + ) : SpendAnalysisScreenUiEvent + + data class UpdateHelpBottomSheetState(val state: HelpBottomSheetState) : + SpendAnalysisScreenUiEvent +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/model/SpendAnalysisScreenUiState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/model/SpendAnalysisScreenUiState.kt new file mode 100644 index 0000000000..a641b68a87 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/model/SpendAnalysisScreenUiState.kt @@ -0,0 +1,26 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.spendanalysis.model + +import androidx.compose.runtime.Immutable +import com.navi.common.basemvi.UiState +import com.navi.moneymanager.common.model.bottomSheet.BottomSheetState +import com.navi.moneymanager.common.model.bottomSheet.SpendAnalysisScreenBottomSheets +import com.navi.moneymanager.common.model.bottomSheet.SpendAnalysisScreenBottomSheets.NoBottomSheet + +@Immutable +data class SpendAnalysisScreenUiState( + val screenData: SpendAnalysisScreenData? = null, + val bottomSheetState: + BottomSheetState +) : UiState { + companion object { + val initialState = + SpendAnalysisScreenUiState(bottomSheetState = BottomSheetState(type = NoBottomSheet)) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/reducer/SpendAnalysisScreenReducer.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/reducer/SpendAnalysisScreenReducer.kt new file mode 100644 index 0000000000..3d43109977 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/reducer/SpendAnalysisScreenReducer.kt @@ -0,0 +1,56 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.spendanalysis.reducer + +import com.navi.common.basemvi.BaseReducer +import com.navi.moneymanager.common.model.bottomSheet.BottomSheetState +import com.navi.moneymanager.common.model.bottomSheet.SpendAnalysisScreenBottomSheets +import com.navi.moneymanager.postonboard.spendanalysis.model.SpendAnalysisScreenUiEvent +import com.navi.moneymanager.postonboard.spendanalysis.model.SpendAnalysisScreenUiState + +class SpendAnalysisScreenReducer : + BaseReducer { + + override fun reduce( + previousState: SpendAnalysisScreenUiState, + event: SpendAnalysisScreenUiEvent + ): SpendAnalysisScreenUiState { + return when (event) { + is SpendAnalysisScreenUiEvent.RenderUI -> { + previousState.copy(screenData = event.data) + } + is SpendAnalysisScreenUiEvent.DismissBottomSheet -> { + val currentBottomSheet = previousState.bottomSheetState.type + if ( + event.type == SpendAnalysisScreenBottomSheets.NoBottomSheet::class.java || + event.type.isInstance(currentBottomSheet) + ) { + previousState.copy( + bottomSheetState = previousState.bottomSheetState.copy(isVisible = false) + ) + } else previousState + } + is SpendAnalysisScreenUiEvent.ShowBottomSheet -> { + previousState.copy( + bottomSheetState = BottomSheetState(isVisible = true, type = event.type) + ) + } + is SpendAnalysisScreenUiEvent.UpdateHelpBottomSheetState -> { + val currentBottomSheetType = previousState.bottomSheetState.type + if (currentBottomSheetType is SpendAnalysisScreenBottomSheets.HelpBottomSheet) { + previousState.copy( + bottomSheetState = + previousState.bottomSheetState.copy( + type = SpendAnalysisScreenBottomSheets.HelpBottomSheet(event.state) + ) + ) + } else previousState + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/repo/SpendAnalysisRepository.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/repo/SpendAnalysisRepository.kt new file mode 100644 index 0000000000..9d0cc25093 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/repo/SpendAnalysisRepository.kt @@ -0,0 +1,42 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.spendanalysis.repo + +import com.navi.moneymanager.common.dataprovider.domain.SpendAnalysisLocalDataProvider +import javax.inject.Inject + +class SpendAnalysisRepository +@Inject +constructor(private val spendAnalysisDataProvider: SpendAnalysisLocalDataProvider) { + + suspend fun getSpendAnalysisScreenData( + month: Int?, + year: Int?, + selectedBankReferenceIds: Set? + ) = spendAnalysisDataProvider.getSpendAnalysisScreenData(month, year, selectedBankReferenceIds) + + suspend fun getMonthSelectionBottomSheetData(displayMonthText: String) = + spendAnalysisDataProvider.getMonthSelectionBottomSheetData(displayMonthText) + + suspend fun getBankSelectionBottomSheetData(selectedBankReferenceIds: Set) = + spendAnalysisDataProvider.getBankSelectionBottomSheetData(selectedBankReferenceIds) + + suspend fun getOtherCategoriesBottomSheetData( + month: Int?, + year: Int?, + selectedBankReferenceIds: Set? + ) = + spendAnalysisDataProvider.getOtherCategoriesBottomSheetData( + month, + year, + selectedBankReferenceIds + ) + + suspend fun getPastMonthDataLoadingBottomSheetData() = + spendAnalysisDataProvider.getPastMonthDataLoadingBottomSheetData() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/ui/OtherCategoriesBottomSheet.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/ui/OtherCategoriesBottomSheet.kt new file mode 100644 index 0000000000..e7623a0d37 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/ui/OtherCategoriesBottomSheet.kt @@ -0,0 +1,176 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.spendanalysis.ui + +import androidx.compose.foundation.background +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.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Divider +import androidx.compose.material3.ButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.elex.atoms.ElexButton +import com.navi.moneymanager.common.analytics.SpendAnalysisEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.model.SpendCategoryItemData +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.composable.spendCategoriztion.SpendCategoryItem +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.fourDpRoundedShape +import com.navi.moneymanager.postonboard.spendanalysis.model.OtherCategoriesBottomSheetData +import com.navi.moneymanager.postonboard.spendanalysis.model.OtherCategoriesBottomSheetHeaderData + +@Composable +fun OtherCategoriesBottomSheetContent( + bottomSheetData: OtherCategoriesBottomSheetData, + onCategoryClick: (String) -> Unit, + onConfirm: () -> Unit, + trackCategoryItemClick: (Int, String) -> Unit, + trackCategoryItemView: (Int, String) -> Unit +) { + + LaunchedEffect(Unit) { + SpendAnalysisEventTrackerImpl.onSpendAnalysisScreenOtherCategoriesBottomSheetAppeared() + } + + DisposableEffect(Unit) { + onDispose { + SpendAnalysisEventTrackerImpl + .onSpendAnalysisScreenOtherCategoriesBottomSheetDisappeared() + } + } + + Column(modifier = Modifier.fillMaxWidth().padding(bottom = 32.dp)) { + OtherCategoriesBottomSheetHeaderSection(headerData = bottomSheetData.headerData) + Divider(color = MMColor.ctaSecondary, thickness = 4.dp) + OtherCategoriesBottomSheetCategoriesSection( + categories = bottomSheetData.categories, + onCategoryClick = onCategoryClick, + trackCategoryItemClick = trackCategoryItemClick, + trackCategoryItemView = trackCategoryItemView, + modifier = Modifier.weight(1f, false) + ) + OtherCategoriesBottomSheetFooterSection( + ctaText = bottomSheetData.ctaText, + onConfirm = onConfirm + ) + } +} + +@Composable +fun OtherCategoriesBottomSheetHeaderSection( + headerData: OtherCategoriesBottomSheetHeaderData, +) { + Column( + modifier = Modifier.fillMaxWidth().padding(16.dp), + horizontalAlignment = Alignment.Start + ) { + Box( + modifier = Modifier.size(40.dp).clip(CircleShape).background(MMColor.ctaSecondary), + contentAlignment = Alignment.Center + ) { + Illustration( + modifier = Modifier.size(32.dp), + illustrationType = + IllustrationType.Image( + IllustrationSource.Remote( + url = headerData.iconUrl, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM + ) + ) + ) + } + Spacer(modifier = Modifier.height(8.dp)) + MMText( + text = headerData.title, + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 24.sp + ) + Spacer(modifier = Modifier.height(8.dp)) + MMText( + text = headerData.subTitle, + color = MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 22.sp + ) + } +} + +@Composable +fun OtherCategoriesBottomSheetCategoriesSection( + categories: List, + onCategoryClick: (String) -> Unit, + trackCategoryItemClick: (Int, String) -> Unit, + trackCategoryItemView: (Int, String) -> Unit, + modifier: Modifier = Modifier +) { + Column(modifier = modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { + Spacer(modifier = Modifier.height(16.dp)) + categories.forEachIndexed { index, categoryItemData -> + SpendCategoryItem( + data = categoryItemData, + onCategoryClick = { onCategoryClick(it.categoryId) }, + trackCategoryItemClick = { trackCategoryItemClick(index + 1, it) }, + trackCategoryItemView = { trackCategoryItemView(index + 1, it) } + ) + if (index != categories.size - 1) { + Divider( + modifier = Modifier.padding(horizontal = 16.dp), + color = MMColor.paleGray, + thickness = 1.dp + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Composable +fun OtherCategoriesBottomSheetFooterSection( + ctaText: String, + onConfirm: () -> Unit, + modifier: Modifier = Modifier +) { + Column(modifier = Modifier.fillMaxWidth().padding(top = 16.dp, start = 16.dp, end = 16.dp)) { + ElexButton( + onClick = { onConfirm() }, + modifier = Modifier.fillMaxWidth().height(48.dp), + colors = ButtonDefaults.buttonColors(containerColor = MMColor.ctaPrimary), + shape = fourDpRoundedShape + ) { + MMText( + text = ctaText, + color = MMColor.white, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 22.sp + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/ui/SpendAnalysisScaffoldRenderer.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/ui/SpendAnalysisScaffoldRenderer.kt new file mode 100644 index 0000000000..60b1164253 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/ui/SpendAnalysisScaffoldRenderer.kt @@ -0,0 +1,153 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.spendanalysis.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +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.Scaffold +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.layout.ContentScale +import androidx.compose.ui.unit.dp +import com.navi.base.utils.EMPTY +import com.navi.common.R as CommonR +import com.navi.common.constants.HELP_CTA_TEXT +import com.navi.moneymanager.common.analytics.SpendAnalysisEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.model.ImageProperties +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.DASHBOARD_FOOTER +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_PLACEHOLDER_XX_LARGE +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.model.SelectedMonth +import com.navi.moneymanager.common.model.SpendCategorizationAction +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.MMTopBar +import com.navi.moneymanager.common.ui.composable.TotalSpendSectionUI +import com.navi.moneymanager.common.ui.composable.barGraph.MMBarGraph +import com.navi.moneymanager.common.ui.composable.button.PrimaryButton +import com.navi.moneymanager.common.ui.composable.spendCategoriztion.SpendCategorizationSection +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.spendanalysis.model.SpendAnalysisScreenUiEffect +import com.navi.moneymanager.postonboard.spendanalysis.model.SpendAnalysisScreenUiState +import com.navi.moneymanager.postonboard.spendanalysis.viewmodel.SpendAnalysisVM + +@Composable +fun SpendAnalysisScaffoldRenderer( + modifier: Modifier = Modifier, + spendAnalysisScreenUiState: () -> SpendAnalysisScreenUiState, + getViewModel: () -> SpendAnalysisVM, + onMonthChangeClick: (String) -> Unit, + onBankSelectionRequest: (Set) -> Unit, + onCategoryClick: (String) -> Unit, + onAverageInfoClick: (String?) -> Unit, + onViewAllTransactionsClick: () -> Unit, + onBarGraphElementClicked: (SelectedMonth) -> Unit +) { + Scaffold( + modifier = modifier, + containerColor = MMColor.white, + topBar = { + MMTopBar( + title = EMPTY, + titleColor = MMColor.ctaPrimary, + navigationIcon = CommonR.drawable.ic_arrow_left_black_v2, + actionIconText = HELP_CTA_TEXT, + onActionClick = { + SpendAnalysisEventTrackerImpl.onSpendAnalysisScreenHelpClicked() + getViewModel().setEffect { SpendAnalysisScreenUiEffect.Navigation.Help } + }, + onNavigationIconClick = { + getViewModel().setEffect { SpendAnalysisScreenUiEffect.Navigation.Back } + } + ) + }, + ) { + Column( + modifier = + Modifier.background(Color.White) + .padding(it) + .fillMaxHeight() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween + ) { + Column { + spendAnalysisScreenUiState().screenData?.totalSpendSection?.let { totalSpendSection + -> + TotalSpendSectionUI( + totalSpendSection, + onMonthChangeClick, + onBankSelectionRequest + ) + } + spendAnalysisScreenUiState().screenData?.barGraphData?.let { barGraphData -> + MMBarGraph( + MMScreen.SPEND_ANALYSIS.screen, + barGraphData, + onAverageInfoClick, + onBarGraphElementClicked = { selectedMonth -> + onBarGraphElementClicked(selectedMonth) + } + ) + } + spendAnalysisScreenUiState().screenData?.spendCategorizationState?.let { + spendCategorizationState -> + Spacer(modifier = Modifier.height(16.dp)) + SpendCategorizationSection( + screenName = MMScreen.SPEND_ANALYSIS.screen, + isAddAccountSelected = false, + spendCategorizationState = spendCategorizationState, + spendCategorizationAction = { action -> + when (action) { + is SpendCategorizationAction.SelectCategory -> { + onCategoryClick(action.categoryId) + } + else -> {} + } + } + ) + } + + spendAnalysisScreenUiState().screenData?.viewTransactionHistoryTitle?.let { title -> + PrimaryButton( + title = title, + onClick = onViewAllTransactionsClick, + ) + } + } + SpendAnalysisFooterSection() + } + } +} + +@Composable +fun SpendAnalysisFooterSection() { + Illustration( + illustrationType = + IllustrationType.Image( + source = + IllustrationSource.Remote( + url = DASHBOARD_FOOTER, + placeholder = IMAGE_PLACEHOLDER_XX_LARGE + ), + properties = ImageProperties(contentScale = ContentScale.FillWidth) + ), + modifier = Modifier.fillMaxWidth().padding(top = 24.dp), + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/ui/SpendAnalysisScreen.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/ui/SpendAnalysisScreen.kt new file mode 100644 index 0000000000..6f70b2da7e --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/ui/SpendAnalysisScreen.kt @@ -0,0 +1,375 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.spendanalysis.ui + +import android.os.Bundle +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.navi.base.model.CtaData +import com.navi.base.model.LineItem +import com.navi.base.utils.BaseUtils.getDayMonthAndYearFromTimestamp +import com.navi.base.utils.orZero +import com.navi.common.constants.MONTH +import com.navi.common.constants.YEAR +import com.navi.common.navigation.NavigationAction +import com.navi.common.utils.Constants.WEB_URL +import com.navi.moneymanager.common.analytics.SpendAnalysisEventTrackerImpl +import com.navi.moneymanager.common.composable.bankselectionbottomsheet.BankSelectionBottomSheetContent +import com.navi.moneymanager.common.dataprovider.data.datastore.DataStoreInfoProvider +import com.navi.moneymanager.common.model.FilterAttribute +import com.navi.moneymanager.common.model.SelectedMonth +import com.navi.moneymanager.common.model.bottomSheet.SpendAnalysisScreenBottomSheets +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.navigation.utils.navigateToPreviousScreen +import com.navi.moneymanager.common.ui.composable.DataLoadingBottomSheetContent +import com.navi.moneymanager.common.ui.composable.ScreenInit +import com.navi.moneymanager.common.ui.composable.bottomSheet.AverageInfoBottomSheetContent +import com.navi.moneymanager.common.ui.composable.bottomSheet.BottomSheet +import com.navi.moneymanager.common.ui.composable.bottomSheet.MonthSelectionBottomSheetUI +import com.navi.moneymanager.common.utils.Constants +import com.navi.moneymanager.common.utils.Constants.IS_TOTAL_SYNC_COMPLETED +import com.navi.moneymanager.common.utils.MMScreenEventLogger +import com.navi.moneymanager.common.utils.MonthConstants +import com.navi.moneymanager.common.utils.MonthConstants.monthNames +import com.navi.moneymanager.common.utils.asList +import com.navi.moneymanager.common.utils.getFaqCta +import com.navi.moneymanager.common.utils.getPastMonthDifferenceFromCurrentWithinOneYear +import com.navi.moneymanager.common.utils.getTransactionFilterMonthWithYear +import com.navi.moneymanager.entry.ui.activity.MMActivity +import com.navi.moneymanager.postonboard.categorydetails.model.CategoryDetailsScreenNavigationData +import com.navi.moneymanager.postonboard.help.ui.HelpBottomSheetContent +import com.navi.moneymanager.postonboard.spendanalysis.model.SpendAnalysisScreenUiEffect +import com.navi.moneymanager.postonboard.spendanalysis.model.SpendAnalysisScreenUiEvent +import com.navi.moneymanager.postonboard.spendanalysis.model.SpendAnalysisScreenUiState +import com.navi.moneymanager.postonboard.spendanalysis.viewmodel.SpendAnalysisVM +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionFilterData +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionFilterItem +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenNavigationData +import com.navi.naviwidgets.utils.URL +import com.ramcosta.composedestinations.annotation.Destination +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach + +@Destination +@Composable +fun SpendAnalysisScreen( + activity: MMActivity, + bundle: Bundle? = null, + viewModel: SpendAnalysisVM = hiltViewModel() +) { + + val state by viewModel.state.collectAsStateWithLifecycle() + val selectedMonth = rememberSaveable { + mutableStateOf(SelectedMonth(bundle?.getInt(MONTH), bundle?.getInt(YEAR))) + } + MMScreenEventLogger( + onScreenLand = { + val selectedMonthIndex = + getPastMonthDifferenceFromCurrentWithinOneYear( + selectedMonth.value.month + ?: getDayMonthAndYearFromTimestamp(System.currentTimeMillis()).second + ) + SpendAnalysisEventTrackerImpl.onSpendAnalysisScreenLanded( + monthIndex = selectedMonthIndex, + numberOfBanks = + state.screenData?.totalSpendSection?.selectedBankReferenceIds?.size.orZero() + ) + }, + onScreenExit = { SpendAnalysisEventTrackerImpl.onSpendAnalysisScreenExit() } + ) + + ScreenInit(screenName = MMScreen.SPEND_ANALYSIS.screen, activity = activity) + + LaunchedEffect(Unit) { + viewModel.effect + .onEach { effect -> + when (effect) { + is SpendAnalysisScreenUiEffect.Navigation.Back -> { + navigateToPreviousScreen(activity) + } + is SpendAnalysisScreenUiEffect.Navigation.NavigateToCta -> { + activity.mmNavigator.navigateTo( + activity = activity, + ctaData = effect.ctaData + ) + } + SpendAnalysisScreenUiEffect.Navigation.Help -> { + viewModel.sendEvent( + SpendAnalysisScreenUiEvent.ShowBottomSheet( + type = SpendAnalysisScreenBottomSheets.HelpBottomSheet() + ) + ) + } + is SpendAnalysisScreenUiEffect.Navigation.TransactionHistory -> { + val transactionHistoryScreenData = + selectedMonth.value + .takeIf { it.month != null && it.year != null } + ?.let { + TransactionHistoryScreenNavigationData( + TransactionFilterData( + attributeKey = FilterAttribute.MONTH.value, + itemList = + TransactionFilterItem( + valueName = + getTransactionFilterMonthWithYear( + it.month!!, + it.year!! + ), + isSelected = true + ) + .asList() + ) + .asList() + ) + } + activity.mmNavigator.navigateTo( + activity = activity, + ctaData = CtaData(url = MMScreen.TRANSACTION_HISTORY.screen), + navigationAction = NavigationAction.Default, + screenData = transactionHistoryScreenData + ) + } + is SpendAnalysisScreenUiEffect.Navigation.CategoryDetailsScreen -> { + activity.mmNavigator.navigateTo( + activity = activity, + ctaData = CtaData(url = MMScreen.CATEGORY_DETAILS.screen), + navigationAction = NavigationAction.Default, + screenData = effect.data + ) + } + is SpendAnalysisScreenUiEffect.UpdateSelectedBankReferenceIds -> { + viewModel.updateScreenParams( + selectedMonth = selectedMonth.value, + selectedBankReferenceIds = effect.selectedBankReferenceIds + ) + } + SpendAnalysisScreenUiEffect.FetchConsentUrl -> { + viewModel.fetchConsentUrl() + } + } + } + .collect() + } + + LaunchedEffect(selectedMonth.value) { + viewModel.updateScreenParams( + selectedMonth = selectedMonth.value, + selectedBankReferenceIds = state.screenData?.totalSpendSection?.selectedBankReferenceIds + ) + } + + BackHandler { viewModel.setEffect { SpendAnalysisScreenUiEffect.Navigation.Back } } + + SpendAnalysisScaffoldRenderer( + modifier = Modifier.fillMaxSize(), + spendAnalysisScreenUiState = { state }, + getViewModel = { viewModel }, + onMonthChangeClick = { + SpendAnalysisEventTrackerImpl.onSpendAnalysisScreenMonthSelectionRequested() + viewModel.handleMonthSelectBottomSheet(it) + }, + onBankSelectionRequest = { + SpendAnalysisEventTrackerImpl.onSpendAnalysisScreenBankSelectionRequested() + viewModel.handleBankSelectBottomSheet(it) + }, + onCategoryClick = { categoryId -> + if (categoryId == Constants.OTHERS_CATEGORY_ID) { + SpendAnalysisEventTrackerImpl.onSpendAnalysisScreenOtherCategoriesClicked() + viewModel.handleOtherCategoriesBottomSheet(selectedMonth.value) + } else { + viewModel.setEffect { + SpendAnalysisScreenUiEffect.Navigation.CategoryDetailsScreen( + CategoryDetailsScreenNavigationData( + month = selectedMonth.value.month.orZero(), + year = selectedMonth.value.year.orZero(), + selectedCategory = categoryId, + selectedBanks = + state.screenData?.totalSpendSection?.selectedBankReferenceIds, + ) + ) + } + } + }, + onAverageInfoClick = { averageValue -> + SpendAnalysisEventTrackerImpl.onSpendAnalysisScreenGraphAverageInfoClicked() + viewModel.handleAverageInfoBottomSheet(averageValue) + }, + onViewAllTransactionsClick = { + SpendAnalysisEventTrackerImpl.onSpendAnalysisScreenViewTransactionHistoryClicked() + viewModel.setEffect { SpendAnalysisScreenUiEffect.Navigation.TransactionHistory } + }, + onBarGraphElementClicked = { selectedMonth.value = it } + ) + + BottomSheet( + state = state.bottomSheetState, + onDismiss = { _, uiEvent -> viewModel.sendEvent(uiEvent) } + ) { bottomSheet -> + SpendAnalysisBottomSheetContentHandler( + state = state, + dataStoreInfoProvider = viewModel.dbDataStoreProvider, + bottomSheet = bottomSheet, + onEvent = { viewModel.sendEvent(it) }, + onEffect = { viewModel.setEffect { it } }, + selectedMonth = selectedMonth + ) + } +} + +@Composable +private fun SpendAnalysisBottomSheetContentHandler( + state: SpendAnalysisScreenUiState, + bottomSheet: SpendAnalysisScreenBottomSheets, + onEvent: (SpendAnalysisScreenUiEvent) -> Unit, + dataStoreInfoProvider: DataStoreInfoProvider, + onEffect: (SpendAnalysisScreenUiEffect) -> Unit, + selectedMonth: MutableState +) { + if (bottomSheet.sheetData == null) return + val onDismissAction = bottomSheet.createDismissAction(onEvent) + when (bottomSheet) { + is SpendAnalysisScreenBottomSheets.BankSelection -> { + BankSelectionBottomSheetContent( + bottomSheetData = bottomSheet.data, + onBackIconClicked = { onDismissAction() }, + onBankSelectionApplied = { selectedBankReferenceIds -> + SpendAnalysisEventTrackerImpl.onSpendAnalysisScreenBankSelectionApplied( + selectedBankReferenceIds.size + ) + onEffect( + SpendAnalysisScreenUiEffect.UpdateSelectedBankReferenceIds( + selectedBankReferenceIds + ) + ) + onDismissAction() + } + ) + } + is SpendAnalysisScreenBottomSheets.MonthSelection -> { + MonthSelectionBottomSheetUI( + screenName = MMScreen.SPEND_ANALYSIS.screen, + data = bottomSheet.data!!, + onDismiss = { onDismissAction() }, + onMonthSelected = { monthYear -> + val monthList = MonthConstants.monthNames + val month = monthList.indexOf(monthYear.first) + selectedMonth.value = SelectedMonth(month, monthYear.second) + } + ) + } + is SpendAnalysisScreenBottomSheets.OtherCategories -> { + OtherCategoriesBottomSheetContent( + bottomSheetData = bottomSheet.data!!, + onCategoryClick = { + onEvent( + SpendAnalysisScreenUiEvent.DismissBottomSheet( + SpendAnalysisScreenBottomSheets.OtherCategories::class.java + ) + ) + onEffect( + SpendAnalysisScreenUiEffect.Navigation.CategoryDetailsScreen( + data = + CategoryDetailsScreenNavigationData( + month = selectedMonth.value.month.orZero(), + year = selectedMonth.value.year.orZero(), + selectedCategory = it, + selectedBanks = + state.screenData + ?.totalSpendSection + ?.selectedBankReferenceIds + ) + ) + ) + }, + onConfirm = { + SpendAnalysisEventTrackerImpl + .onSpendAnalysisScreenOtherCategoriesBottomSheetAcknowledged() + onDismissAction() + }, + trackCategoryItemClick = { categoryRank, categoryType -> + SpendAnalysisEventTrackerImpl + .onSpendAnalysisScreenOtherCategoriesBottomSheetCategoryClicked( + categoryRank, + categoryType + ) + }, + trackCategoryItemView = { categoryRank, categoryType -> + SpendAnalysisEventTrackerImpl + .onSpendAnalysisScreenOtherCategoriesBottomSheetCategoryViewed( + categoryRank, + categoryType + ) + } + ) + } + is SpendAnalysisScreenBottomSheets.AverageInfo -> { + AverageInfoBottomSheetContent( + screenName = MMScreen.SPEND_ANALYSIS.screen, + averageValue = bottomSheet.averageValue.orEmpty(), + onDismiss = { + SpendAnalysisEventTrackerImpl + .onSpendAnalysisScreenAverageInfoBottomSheetAcknowledged() + onDismissAction() + } + ) + } + is SpendAnalysisScreenBottomSheets.PastMonthDataLoading -> { + LaunchedEffect(Unit) { + dataStoreInfoProvider.getBooleanData(key = IS_TOTAL_SYNC_COMPLETED).collect { + pastMonthData -> + if (pastMonthData) { + onDismissAction() + } + } + } + DataLoadingBottomSheetContent( + data = bottomSheet.data!!, + onDismiss = { onDismissAction() }, + trackBottomSheetAppears = { + SpendAnalysisEventTrackerImpl + .onSpendAnalysisPastMonthDataLoadingBottomSheetAppeared() + }, + trackBottomSheetDisappears = { + SpendAnalysisEventTrackerImpl + .onSpendAnalysisPastMonthDataLoadingBottomSheetDisappeared() + } + ) + } + is SpendAnalysisScreenBottomSheets.HelpBottomSheet -> { + HelpBottomSheetContent( + screenName = MMScreen.SPEND_ANALYSIS.screen, + state = bottomSheet.state, + onFaqClicked = { + onEffect(SpendAnalysisScreenUiEffect.Navigation.NavigateToCta(getFaqCta())) + }, + manageConsent = { onEffect(SpendAnalysisScreenUiEffect.FetchConsentUrl) }, + navigateToUrl = { url -> + onEffect( + SpendAnalysisScreenUiEffect.Navigation.NavigateToCta( + CtaData( + url = WEB_URL, + parameters = listOf(LineItem(key = URL, value = url)) + ) + ) + ) + }, + onDismiss = onDismissAction + ) + } + SpendAnalysisScreenBottomSheets.NoBottomSheet -> {} + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/viewmodel/SpendAnalysisVM.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/viewmodel/SpendAnalysisVM.kt new file mode 100644 index 0000000000..6008cf89a2 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/spendanalysis/viewmodel/SpendAnalysisVM.kt @@ -0,0 +1,214 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.spendanalysis.viewmodel + +import androidx.lifecycle.viewModelScope +import com.navi.moneymanager.base.viewmodel.MMBaseViewModel +import com.navi.moneymanager.common.analytics.DataProviderEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.data.datastore.DataStoreInfoProvider +import com.navi.moneymanager.common.model.SelectedMonth +import com.navi.moneymanager.common.model.SpendAnalysisScreenInputParams +import com.navi.moneymanager.common.model.bottomSheet.SpendAnalysisScreenBottomSheets +import com.navi.moneymanager.common.network.di.RoomDataStoreInfoProvider +import com.navi.moneymanager.common.utils.Constants.IS_TOTAL_SYNC_COMPLETED +import com.navi.moneymanager.postonboard.help.model.HelpBottomSheetState +import com.navi.moneymanager.postonboard.help.usecase.ManageConsentUseCase +import com.navi.moneymanager.postonboard.spendanalysis.model.SpendAnalysisScreenUiEffect +import com.navi.moneymanager.postonboard.spendanalysis.model.SpendAnalysisScreenUiEvent +import com.navi.moneymanager.postonboard.spendanalysis.model.SpendAnalysisScreenUiState +import com.navi.moneymanager.postonboard.spendanalysis.reducer.SpendAnalysisScreenReducer +import com.navi.moneymanager.postonboard.spendanalysis.repo.SpendAnalysisRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import timber.log.Timber + +@HiltViewModel +class SpendAnalysisVM +@Inject +constructor( + @RoomDataStoreInfoProvider val dbDataStoreProvider: DataStoreInfoProvider, + private val repository: SpendAnalysisRepository, + private val manageConsentUseCase: ManageConsentUseCase +) : + MMBaseViewModel< + SpendAnalysisScreenUiState, SpendAnalysisScreenUiEvent, SpendAnalysisScreenUiEffect + >( + initialState = SpendAnalysisScreenUiState.initialState, + reducer = SpendAnalysisScreenReducer() + ) { + + private val screenParams = MutableStateFlow(null) + + @OptIn(ExperimentalCoroutinesApi::class) + private val spendAnalysisScreenData = + screenParams + .filterNotNull() + .distinctUntilChanged() + .flatMapLatest { params -> + DataProviderEventTrackerImpl.spendAnalysisVmCollect( + month = params.month.toString(), + year = params.year.toString(), + selectedBanksListSize = params.selectedBankReferenceIds?.size.toString() + ) + repository.getSpendAnalysisScreenData( + month = params.month, + year = params.year, + selectedBankReferenceIds = params.selectedBankReferenceIds + ) + } + .flowOn(Dispatchers.IO) + .stateIn( + viewModelScope, + initialValue = null, + started = SharingStarted.WhileSubscribed() + ) + + init { + handleSpendAnalysisScreenData() + } + + private fun handleSpendAnalysisScreenData() { + viewModelScope.safeLaunch(Dispatchers.IO) { + spendAnalysisScreenData.filterNotNull().collect { + Timber.tag("money_manager") + .d("SpendAnalysisVM ${::handleSpendAnalysisScreenData.name}") + sendEvent(SpendAnalysisScreenUiEvent.RenderUI(it)) + } + } + } + + fun updateScreenParams( + selectedMonth: SelectedMonth, + selectedBankReferenceIds: Set? = null, + ) { + viewModelScope.safeLaunch(Dispatchers.IO) { + DataProviderEventTrackerImpl.spendAnalysisVmUpdate( + month = selectedMonth.month.toString(), + year = selectedMonth.year.toString(), + selectedBanksListSize = selectedBankReferenceIds?.size.toString() + ) + screenParams.update { + SpendAnalysisScreenInputParams( + month = selectedMonth.month, + year = selectedMonth.year, + selectedBankReferenceIds = selectedBankReferenceIds + ) + } + } + } + + fun handleMonthSelectBottomSheet(displayMonthText: String) { + viewModelScope.safeLaunch(Dispatchers.IO) { + val isTotalSyncCompleted = + dbDataStoreProvider.getBooleanData(key = IS_TOTAL_SYNC_COMPLETED).first() + if (isTotalSyncCompleted) { + val data = repository.getMonthSelectionBottomSheetData(displayMonthText) + sendEvent( + SpendAnalysisScreenUiEvent.ShowBottomSheet( + type = SpendAnalysisScreenBottomSheets.MonthSelection(data) + ) + ) + } else { + val data = repository.getPastMonthDataLoadingBottomSheetData() + sendEvent( + SpendAnalysisScreenUiEvent.ShowBottomSheet( + type = SpendAnalysisScreenBottomSheets.PastMonthDataLoading(data) + ) + ) + } + } + } + + fun handleBankSelectBottomSheet(selectedBankReferenceIds: Set) { + viewModelScope.safeLaunch(Dispatchers.IO) { + val data = repository.getBankSelectionBottomSheetData(selectedBankReferenceIds) + sendEvent( + SpendAnalysisScreenUiEvent.ShowBottomSheet( + SpendAnalysisScreenBottomSheets.BankSelection(data) + ) + ) + } + } + + fun handleOtherCategoriesBottomSheet(selectedMonth: SelectedMonth) { + viewModelScope.safeLaunch(Dispatchers.IO) { + val data = + repository.getOtherCategoriesBottomSheetData( + month = selectedMonth.month, + year = selectedMonth.year, + selectedBankReferenceIds = + state.value.screenData?.totalSpendSection?.selectedBankReferenceIds + ) + sendEvent( + SpendAnalysisScreenUiEvent.ShowBottomSheet( + type = SpendAnalysisScreenBottomSheets.OtherCategories(data) + ) + ) + } + } + + fun handleAverageInfoBottomSheet(averageValue: String?) { + viewModelScope.safeLaunch(Dispatchers.IO) { + val isTotalSyncCompleted = + dbDataStoreProvider.getBooleanData(key = IS_TOTAL_SYNC_COMPLETED).first() + if (isTotalSyncCompleted) { + sendEvent( + SpendAnalysisScreenUiEvent.ShowBottomSheet( + type = SpendAnalysisScreenBottomSheets.AverageInfo(averageValue) + ) + ) + } else { + val data = repository.getPastMonthDataLoadingBottomSheetData() + sendEvent( + SpendAnalysisScreenUiEvent.ShowBottomSheet( + type = SpendAnalysisScreenBottomSheets.PastMonthDataLoading(data) + ) + ) + } + } + } + + fun fetchConsentUrl() { + viewModelScope.safeLaunch(Dispatchers.IO) { + manageConsentUseCase.execute( + onLoading = { + sendEvent( + SpendAnalysisScreenUiEvent.UpdateHelpBottomSheetState( + HelpBottomSheetState.Loading + ) + ) + }, + onSuccess = { url -> + sendEvent( + SpendAnalysisScreenUiEvent.UpdateHelpBottomSheetState( + HelpBottomSheetState.Success(url) + ) + ) + }, + onFailure = { + sendEvent( + SpendAnalysisScreenUiEvent.UpdateHelpBottomSheetState( + HelpBottomSheetState.Error + ) + ) + } + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/model/TransactionDetailsScreenData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/model/TransactionDetailsScreenData.kt new file mode 100644 index 0000000000..64442a6f67 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/model/TransactionDetailsScreenData.kt @@ -0,0 +1,64 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactiondetails.model + +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetTransactionData + +data class TransactionDetailsScreenData( + val topNavBar: NavBarData, + val transactionInfo: TransactionInfo, + val categoryInfo: CategoryInfo, + val transactionSummary: TransactionSummary, +) + +data class NavBarData( + val actionLabel: String, +) + +data class TransactionInfo( + val amount: String, + val transactionOutcome: String, + val transactionDate: String, + val paymentDirectionIcon: IllustrationSource +) + +data class CategoryInfo( + val isCategorized: Boolean, + val categoryIcon: IllustrationSource, + val categoryTransactionData: CategoryBottomSheetTransactionData, + val categoryName: String, + val categoryActionIcon: IllustrationSource, + val categoryId: String +) + +data class TransactionSummary( + val counterpartyInfo: CounterpartyInfo, + val accountHolderInfo: AccountHolderInfo, + val transactionMetadata: List, +) + +data class AccountHolderInfo( + val paymentNarrative: String, + val bankDetails: BankAccountDisplayInfo, +) + +data class CounterpartyInfo( + val paymentNarrative: String, + val counterPartyName: String, +) + +data class BankAccountDisplayInfo( + val bankAccountDisplayText: String, + val bankIcon: IllustrationSource, +) + +data class TransactionMetadataItem( + val title: String, + val subtitle: String, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/model/TransactionDetailsScreenUiEffect.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/model/TransactionDetailsScreenUiEffect.kt new file mode 100644 index 0000000000..aff38ecd2c --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/model/TransactionDetailsScreenUiEffect.kt @@ -0,0 +1,26 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactiondetails.model + +import androidx.compose.runtime.Immutable +import com.navi.base.model.CtaData +import com.navi.common.basemvi.UiEffect + +@Immutable +sealed class TransactionDetailsScreenUiEffect : UiEffect { + @Immutable + sealed class Navigation : TransactionDetailsScreenUiEffect() { + data object Back : Navigation() + + data object Help : Navigation() + + @Immutable data class NavigateToCta(val ctaData: CtaData) : Navigation() + } + + data object FetchConsentUrl : TransactionDetailsScreenUiEffect() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/model/TransactionDetailsScreenUiEvent.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/model/TransactionDetailsScreenUiEvent.kt new file mode 100644 index 0000000000..e0ba67eb8c --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/model/TransactionDetailsScreenUiEvent.kt @@ -0,0 +1,35 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactiondetails.model + +import androidx.compose.runtime.Immutable +import com.navi.common.basemvi.UiEvent +import com.navi.moneymanager.common.model.bottomSheet.TransactionDetailsScreenBottomSheets +import com.navi.moneymanager.postonboard.help.model.HelpBottomSheetState + +@Immutable +sealed class TransactionDetailsScreenUiEvent : UiEvent { + + @Immutable + data class UpdateTransactionDetailsData(val data: TransactionDetailsScreenData) : + TransactionDetailsScreenUiEvent() + + @Immutable + data class UpdateCategoryInfo(val data: CategoryInfo) : TransactionDetailsScreenUiEvent() + + data class DismissBottomSheet( + val type: Class = + TransactionDetailsScreenBottomSheets.NoBottomSheet::class.java + ) : TransactionDetailsScreenUiEvent() + + data class ShowBottomSheet(val type: TransactionDetailsScreenBottomSheets) : + TransactionDetailsScreenUiEvent() + + data class UpdateHelpBottomSheetState(val state: HelpBottomSheetState) : + TransactionDetailsScreenUiEvent() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/model/TransactionDetailsScreenUiState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/model/TransactionDetailsScreenUiState.kt new file mode 100644 index 0000000000..5b5b9f47e6 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/model/TransactionDetailsScreenUiState.kt @@ -0,0 +1,29 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactiondetails.model + +import androidx.compose.runtime.Immutable +import com.navi.common.basemvi.UiState +import com.navi.moneymanager.common.model.bottomSheet.BottomSheetState +import com.navi.moneymanager.common.model.bottomSheet.TransactionDetailsScreenBottomSheets + +@Immutable +data class TransactionDetailsScreenUiState( + val screenData: TransactionDetailsScreenData? = null, + val bottomSheetState: + BottomSheetState, +) : UiState { + companion object { + val initialState = + TransactionDetailsScreenUiState( + screenData = null, + bottomSheetState = + BottomSheetState(type = TransactionDetailsScreenBottomSheets.NoBottomSheet) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/reducer/TransactionDetailsScreenReducer.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/reducer/TransactionDetailsScreenReducer.kt new file mode 100644 index 0000000000..3e8f7849dd --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/reducer/TransactionDetailsScreenReducer.kt @@ -0,0 +1,64 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactiondetails.reducer + +import com.navi.common.basemvi.BaseReducer +import com.navi.moneymanager.common.model.bottomSheet.BottomSheetState +import com.navi.moneymanager.common.model.bottomSheet.TransactionDetailsScreenBottomSheets +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionDetailsScreenUiEvent +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionDetailsScreenUiState + +class TransactionDetailsScreenReducer : + BaseReducer { + + override fun reduce( + previousState: TransactionDetailsScreenUiState, + event: TransactionDetailsScreenUiEvent + ): TransactionDetailsScreenUiState { + return when (event) { + is TransactionDetailsScreenUiEvent.UpdateTransactionDetailsData -> + (previousState.copy(screenData = event.data)) + is TransactionDetailsScreenUiEvent.UpdateCategoryInfo -> { + val updatedScreenData = previousState.screenData?.copy(categoryInfo = event.data) + previousState.copy(screenData = updatedScreenData) + } + is TransactionDetailsScreenUiEvent.DismissBottomSheet -> { + val currentBottomSheet = previousState.bottomSheetState.type + if ( + event.type == TransactionDetailsScreenBottomSheets.NoBottomSheet::class.java || + event.type.isInstance(currentBottomSheet) + ) { + previousState.copy( + bottomSheetState = previousState.bottomSheetState.copy(isVisible = false) + ) + } else previousState + } + is TransactionDetailsScreenUiEvent.ShowBottomSheet -> { + previousState.copy( + bottomSheetState = BottomSheetState(isVisible = true, type = event.type) + ) + } + is TransactionDetailsScreenUiEvent.UpdateHelpBottomSheetState -> { + val currentBottomSheetType = previousState.bottomSheetState.type + if ( + currentBottomSheetType is TransactionDetailsScreenBottomSheets.HelpBottomSheet + ) { + previousState.copy( + bottomSheetState = + previousState.bottomSheetState.copy( + type = + TransactionDetailsScreenBottomSheets.HelpBottomSheet( + event.state + ) + ) + ) + } else previousState + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/repository/TransactionDetailsScreenRepository.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/repository/TransactionDetailsScreenRepository.kt new file mode 100644 index 0000000000..9d12937271 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/repository/TransactionDetailsScreenRepository.kt @@ -0,0 +1,21 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactiondetails.repository + +import com.navi.moneymanager.common.dataprovider.domain.TransactionDataProvider +import javax.inject.Inject + +class TransactionDetailsScreenRepository +@Inject +constructor(private val transactionDataProvider: TransactionDataProvider) { + suspend fun getTransactionDetailScreenData(transactionId: String) = + transactionDataProvider.getTransactionDetailScreenData(transactionId) + + suspend fun getTransactionCategoryInfo(transactionId: String) = + transactionDataProvider.getTransactionCategoryInfo(transactionId) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/ui/TransactionCategorySection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/ui/TransactionCategorySection.kt new file mode 100644 index 0000000000..3cd86b8eb0 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/ui/TransactionCategorySection.kt @@ -0,0 +1,125 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactiondetails.ui + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +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.size +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.analytics.TransactionDetailsEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.base.MMCard +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.Constants.GREEN_TICK_MARK +import com.navi.moneymanager.common.utils.ShowCustomToast +import com.navi.moneymanager.common.utils.ToastDuration +import com.navi.moneymanager.common.utils.getTransactionCategorizedMessage +import com.navi.moneymanager.postonboard.monthlysummary.ui.bottomsheet.CategoryCommonBottomSheet +import com.navi.moneymanager.postonboard.monthlysummary.ui.bottomsheet.CustomToastView +import com.navi.moneymanager.postonboard.transactiondetails.model.CategoryInfo + +@Composable +fun TransactionCategorySection(categoryInfo: CategoryInfo) { + + val context = LocalContext.current + val showBottomSheet = remember { mutableStateOf(false) } + var showTransactionUpdatedToast by remember { mutableStateOf(false) } + var updatedTransactionCount by remember { mutableIntStateOf(0) } + + CategoryCommonBottomSheet( + screenName = MMScreen.TRANSACTION_DETAILS.screen, + showBottomSheet = showBottomSheet, + categoryTransactionData = categoryInfo.categoryTransactionData, + onDismiss = { showBottomSheet.value = false }, + onSuccessFullyUpdatedCategory = { + showTransactionUpdatedToast = true + updatedTransactionCount = it + } + ) + + if (showTransactionUpdatedToast) { + ShowCustomToast( + context = context, + toastDuration = ToastDuration.LENGTH_SHORT, + content = { + CustomToastView( + message = getTransactionCategorizedMessage(updatedTransactionCount, context), + illustrationType = + IllustrationType.Image(IllustrationSource.Resource(resId = GREEN_TICK_MARK)) + ) + } + ) + showTransactionUpdatedToast = false + } + + Spacer(modifier = Modifier.height(24.dp)) + + MMCard( + modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(), + onClick = { + TransactionDetailsEventTrackerImpl.onTransactionDetailsTransactionCategoryClicked( + category = categoryInfo.categoryId + ) + showBottomSheet.value = true + }, + elevation = 0.dp, + borderStroke = BorderStroke(width = 1.dp, color = MMColor.borderColor), + backgroundColor = if (categoryInfo.isCategorized) MMColor.bgAltColor else MMColor.white + ) { + Row( + modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Illustration( + modifier = Modifier.size(32.dp), + illustrationType = IllustrationType.Image(categoryInfo.categoryIcon) + ) + + Spacer(modifier = Modifier.width(14.dp)) + + MMText( + text = categoryInfo.categoryName, + color = MMColor.ctaPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + Spacer(modifier = Modifier.weight(1f)) + + Illustration( + modifier = Modifier.size(16.dp), + illustrationType = IllustrationType.Image(categoryInfo.categoryActionIcon) + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/ui/TransactionDetailsScaffoldRenderer.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/ui/TransactionDetailsScaffoldRenderer.kt new file mode 100644 index 0000000000..16501c8218 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/ui/TransactionDetailsScaffoldRenderer.kt @@ -0,0 +1,71 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactiondetails.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.navi.base.utils.EMPTY +import com.navi.common.R as commonR +import com.navi.moneymanager.common.analytics.TransactionDetailsEventTrackerImpl +import com.navi.moneymanager.common.ui.composable.MMTopBar +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionDetailsScreenData +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionDetailsScreenUiEffect +import com.navi.moneymanager.postonboard.transactiondetails.viewModel.TransactionDetailsViewModel + +@Composable +fun TransactionDetailsScaffoldRenderer( + data: TransactionDetailsScreenData, + getViewModel: () -> TransactionDetailsViewModel +) { + Scaffold( + modifier = Modifier.fillMaxSize().background(color = MMColor.white), + topBar = { + MMTopBar( + backgroundColor = MMColor.bgBannerColor, + title = EMPTY, + onNavigationIconClick = { + getViewModel().setEffect { TransactionDetailsScreenUiEffect.Navigation.Back } + }, + navigationIcon = commonR.drawable.navi_common_ic_arrow_left_white_v2, + actionIconText = data.topNavBar.actionLabel, + onActionClick = { + TransactionDetailsEventTrackerImpl.onTransactionDetailsHelpClicked() + getViewModel().setEffect { TransactionDetailsScreenUiEffect.Navigation.Help } + }, + titleColor = MMColor.white + ) + }, + content = { padding -> + Column( + modifier = Modifier.background(MMColor.white).padding(padding).fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TransactionInfoSection(data.transactionInfo) + Column( + modifier = + Modifier.fillMaxSize() + .verticalScroll(state = rememberScrollState()) + .background(color = MMColor.white), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TransactionCategorySection(data.categoryInfo) + TransactionSummarySection(data.transactionSummary) + } + } + } + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/ui/TransactionDetailsScreen.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/ui/TransactionDetailsScreen.kt new file mode 100644 index 0000000000..8f47a1eca2 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/ui/TransactionDetailsScreen.kt @@ -0,0 +1,140 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactiondetails.ui + +import android.os.Bundle +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.navi.base.model.CtaData +import com.navi.base.model.LineItem +import com.navi.base.utils.EMPTY +import com.navi.common.utils.Constants.WEB_URL +import com.navi.moneymanager.common.analytics.TransactionDetailsEventTrackerImpl +import com.navi.moneymanager.common.model.bottomSheet.TransactionDetailsScreenBottomSheets +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.navigation.utils.navigateToPreviousScreen +import com.navi.moneymanager.common.ui.composable.ScreenInit +import com.navi.moneymanager.common.ui.composable.bottomSheet.BottomSheet +import com.navi.moneymanager.common.utils.Constants.TRANSACTION_ID +import com.navi.moneymanager.common.utils.MMScreenEventLogger +import com.navi.moneymanager.common.utils.getFaqCta +import com.navi.moneymanager.entry.ui.activity.MMActivity +import com.navi.moneymanager.postonboard.help.ui.HelpBottomSheetContent +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionDetailsScreenUiEffect +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionDetailsScreenUiEvent +import com.navi.moneymanager.postonboard.transactiondetails.viewModel.TransactionDetailsViewModel +import com.navi.naviwidgets.utils.URL +import com.ramcosta.composedestinations.annotation.Destination +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach + +@Destination +@Composable +fun TransactionDetailsScreen( + bundle: Bundle? = null, + viewModel: TransactionDetailsViewModel = hiltViewModel() +) { + + MMScreenEventLogger( + onScreenLand = { TransactionDetailsEventTrackerImpl.onTransactionDetailsScreenLanded() }, + onScreenExit = { TransactionDetailsEventTrackerImpl.onTransactionDetailsScreenExit() } + ) + + val activity = LocalContext.current as MMActivity + val state by viewModel.state.collectAsStateWithLifecycle() + val transactionId = remember(bundle) { bundle?.getString(TRANSACTION_ID) ?: EMPTY } + + ScreenInit(screenName = MMScreen.TRANSACTION_DETAILS.screen, activity = activity) + + LaunchedEffect(Unit) { + viewModel.effect + .onEach { effect -> + when (effect) { + is TransactionDetailsScreenUiEffect.Navigation.Back -> { + navigateToPreviousScreen(activity) + } + is TransactionDetailsScreenUiEffect.Navigation.NavigateToCta -> { + activity.mmNavigator.navigateTo( + activity = activity, + ctaData = effect.ctaData + ) + } + TransactionDetailsScreenUiEffect.Navigation.Help -> { + viewModel.sendEvent( + TransactionDetailsScreenUiEvent.ShowBottomSheet( + type = TransactionDetailsScreenBottomSheets.HelpBottomSheet() + ) + ) + } + TransactionDetailsScreenUiEffect.FetchConsentUrl -> { + viewModel.fetchConsentUrl() + } + } + } + .collect() + } + + LaunchedEffect(transactionId) { viewModel.fetchTransactionDetailScreenData(transactionId) } + + BackHandler { viewModel.setEffect { TransactionDetailsScreenUiEffect.Navigation.Back } } + + state.screenData?.let { screenData -> + TransactionDetailsScaffoldRenderer(data = screenData, getViewModel = { viewModel }) + } + + BottomSheet( + state = state.bottomSheetState, + onDismiss = { type, uiEvent -> viewModel.sendEvent(uiEvent) } + ) { bottomSheet -> + TransactionDetailsBottomSheetContentHandler( + bottomSheet = bottomSheet, + onEvent = { viewModel.sendEvent(it) }, + onEffect = { viewModel.setEffect { it } } + ) + } +} + +@Composable +private fun TransactionDetailsBottomSheetContentHandler( + bottomSheet: TransactionDetailsScreenBottomSheets, + onEvent: (TransactionDetailsScreenUiEvent) -> Unit, + onEffect: (TransactionDetailsScreenUiEffect) -> Unit +) { + if (bottomSheet.sheetData == null) return + val onDismissAction = bottomSheet.createDismissAction(onEvent) + when (bottomSheet) { + is TransactionDetailsScreenBottomSheets.HelpBottomSheet -> { + HelpBottomSheetContent( + screenName = MMScreen.TRANSACTION_DETAILS.screen, + state = bottomSheet.state, + onFaqClicked = { + onEffect(TransactionDetailsScreenUiEffect.Navigation.NavigateToCta(getFaqCta())) + }, + manageConsent = { onEffect(TransactionDetailsScreenUiEffect.FetchConsentUrl) }, + navigateToUrl = { url -> + onEffect( + TransactionDetailsScreenUiEffect.Navigation.NavigateToCta( + CtaData( + url = WEB_URL, + parameters = listOf(LineItem(key = URL, value = url)) + ) + ) + ) + }, + onDismiss = onDismissAction + ) + } + TransactionDetailsScreenBottomSheets.NoBottomSheet -> {} + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/ui/TransactionInfoSection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/ui/TransactionInfoSection.kt new file mode 100644 index 0000000000..61456ce871 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/ui/TransactionInfoSection.kt @@ -0,0 +1,81 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactiondetails.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionInfo + +@Composable +fun TransactionInfoSection(transactionInfo: TransactionInfo) { + Column(modifier = Modifier.fillMaxWidth().background(color = MMColor.bgBannerColor)) { + Spacer(modifier = Modifier.height(2.dp)) + + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column { + MMText( + text = transactionInfo.amount, + color = MMColor.white, + fontSize = 24.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + + Spacer(modifier = Modifier.height(4.dp)) + + MMText( + text = transactionInfo.transactionOutcome, + color = Color.White, + fontSize = 20.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + + Spacer(modifier = Modifier.height(16.dp)) + + MMText( + text = transactionInfo.transactionDate, + color = MMColor.white, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + } + + Illustration( + modifier = Modifier.size(56.dp), + illustrationType = IllustrationType.Image(transactionInfo.paymentDirectionIcon) + ) + } + Spacer(modifier = Modifier.height(24.dp)) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/ui/TransactionSummarySection.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/ui/TransactionSummarySection.kt new file mode 100644 index 0000000000..4d21393acf --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/ui/TransactionSummarySection.kt @@ -0,0 +1,170 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactiondetails.ui + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Canvas +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.size +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.base.MMCard +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.transactiondetails.model.AccountHolderInfo +import com.navi.moneymanager.postonboard.transactiondetails.model.CounterpartyInfo +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionMetadataItem +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionSummary + +@Composable +fun TransactionSummarySection(transactionSummary: TransactionSummary) { + + Spacer(modifier = Modifier.height(24.dp)) + MMCard( + modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(), + elevation = 0.dp, + borderStroke = BorderStroke(width = 1.dp, color = MMColor.borderColor) + ) { + Column(modifier = Modifier.padding(16.dp)) { + SectionWithDivider { CounterPartyDetailSection(transactionSummary.counterpartyInfo) } + + SectionWithDivider { UserDetailSection(transactionSummary.accountHolderInfo) } + + TransactionMetaDataSection(transactionSummary.transactionMetadata) + } + } +} + +@Composable +fun CounterPartyDetailSection(counterpartyInfo: CounterpartyInfo) { + Column(modifier = Modifier.fillMaxWidth()) { + MMText( + text = counterpartyInfo.paymentNarrative, + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + Spacer(modifier = Modifier.height(6.dp)) + + MMText( + text = counterpartyInfo.counterPartyName, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR + ) + } + Spacer(modifier = Modifier.height(24.dp)) +} + +@Composable +fun UserDetailSection(accountHolderInfo: AccountHolderInfo) { + Column(modifier = Modifier.fillMaxWidth()) { + MMText( + text = accountHolderInfo.paymentNarrative, + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Illustration( + modifier = Modifier.size(16.dp), + illustrationType = IllustrationType.Image(accountHolderInfo.bankDetails.bankIcon) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + MMText( + text = accountHolderInfo.bankDetails.bankAccountDisplayText, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + } + } + Spacer(modifier = Modifier.height(24.dp)) +} + +@Composable +fun TransactionMetaDataSection(transactionMetadata: List) { + Column(modifier = Modifier.fillMaxWidth()) { + transactionMetadata.forEach { metaData -> + MMText( + text = metaData.title, + color = MMColor.textTertiary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + + Spacer(modifier = Modifier.height(6.dp)) + + MMText( + text = metaData.subtitle, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_HEADLINE_REGULAR + ) + + Spacer(modifier = Modifier.height(24.dp)) + } + } +} + +@Composable +fun SectionWithDivider(content: @Composable () -> Unit) { + content() + DashedLineDivider() + Spacer(modifier = Modifier.height(24.dp)) +} + +@Composable +fun DashedLineDivider() { + Canvas(modifier = Modifier.fillMaxWidth().height(3.dp)) { + val dashWidth = 15f + val dashGap = 10f + val strokeWidth = 3f + drawLine( + color = Color(0xFFE3E5E5), + start = Offset(0f, 0f), + end = Offset(size.width, 0f), + strokeWidth = strokeWidth, + pathEffect = PathEffect.dashPathEffect(floatArrayOf(dashWidth, dashGap), 0f), + cap = StrokeCap.Round + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/viewModel/TransactionDetailsViewModel.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/viewModel/TransactionDetailsViewModel.kt new file mode 100644 index 0000000000..1ee5e3b81f --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactiondetails/viewModel/TransactionDetailsViewModel.kt @@ -0,0 +1,84 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactiondetails.viewModel + +import androidx.lifecycle.viewModelScope +import com.navi.moneymanager.base.viewmodel.MMBaseViewModel +import com.navi.moneymanager.common.analytics.DataProviderEventTrackerImpl +import com.navi.moneymanager.postonboard.help.model.HelpBottomSheetState +import com.navi.moneymanager.postonboard.help.usecase.ManageConsentUseCase +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionDetailsScreenUiEffect +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionDetailsScreenUiEvent +import com.navi.moneymanager.postonboard.transactiondetails.model.TransactionDetailsScreenUiState +import com.navi.moneymanager.postonboard.transactiondetails.reducer.TransactionDetailsScreenReducer +import com.navi.moneymanager.postonboard.transactiondetails.repository.TransactionDetailsScreenRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers + +@HiltViewModel +class TransactionDetailsViewModel +@Inject +constructor( + val repository: TransactionDetailsScreenRepository, + private val manageConsentUseCase: ManageConsentUseCase +) : + MMBaseViewModel< + TransactionDetailsScreenUiState, + TransactionDetailsScreenUiEvent, + TransactionDetailsScreenUiEffect + >( + initialState = TransactionDetailsScreenUiState.initialState, + reducer = TransactionDetailsScreenReducer() + ) { + + fun fetchTransactionDetailScreenData(transactionId: String) { + viewModelScope.safeLaunch(Dispatchers.IO) { + repository.getTransactionDetailScreenData(transactionId).collect { + DataProviderEventTrackerImpl.transactionDetailsVmCollect() + sendEvent(TransactionDetailsScreenUiEvent.UpdateTransactionDetailsData(it)) + } + } + } + + fun updateTransactionCategoryData(transactionId: String) { + viewModelScope.safeLaunch(Dispatchers.IO) { + repository.getTransactionCategoryInfo(transactionId).collect { + sendEvent(TransactionDetailsScreenUiEvent.UpdateCategoryInfo(it)) + } + } + } + + fun fetchConsentUrl() { + viewModelScope.safeLaunch(Dispatchers.IO) { + manageConsentUseCase.execute( + onLoading = { + sendEvent( + TransactionDetailsScreenUiEvent.UpdateHelpBottomSheetState( + HelpBottomSheetState.Loading + ) + ) + }, + onSuccess = { url -> + sendEvent( + TransactionDetailsScreenUiEvent.UpdateHelpBottomSheetState( + HelpBottomSheetState.Success(url) + ) + ) + }, + onFailure = { + sendEvent( + TransactionDetailsScreenUiEvent.UpdateHelpBottomSheetState( + HelpBottomSheetState.Error + ) + ) + } + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/model/TransactionHistoryScreenNavigationData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/model/TransactionHistoryScreenNavigationData.kt new file mode 100644 index 0000000000..3a009d2143 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/model/TransactionHistoryScreenNavigationData.kt @@ -0,0 +1,33 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactionhistory.model + +import android.os.Parcelable +import androidx.compose.runtime.Immutable +import com.navi.common.navigation.NavigationScreenData +import kotlinx.parcelize.Parcelize + +@Parcelize +@Immutable +data class TransactionHistoryScreenNavigationData( + val filterList: List, +) : Parcelable, NavigationScreenData + +@Parcelize +@Immutable +data class TransactionFilterData( + val attributeKey: String, + val itemList: List, +) : Parcelable + +@Parcelize +@Immutable +data class TransactionFilterItem( + val valueName: String, + val isSelected: Boolean, +) : Parcelable diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/model/TransactionHistoryScreenUiEffect.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/model/TransactionHistoryScreenUiEffect.kt new file mode 100644 index 0000000000..396dbe72b3 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/model/TransactionHistoryScreenUiEffect.kt @@ -0,0 +1,25 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactionhistory.model + +import com.navi.common.basemvi.UiEffect +import com.navi.moneymanager.common.model.Transaction + +sealed class TransactionHistoryScreenUiEffect : UiEffect { + + sealed class Navigation : TransactionHistoryScreenUiEffect() { + data object Back : Navigation() + + data class TransactionDetails(val transactionId: String) : Navigation() + } + + data class OpenCategoryBottomSheet(val transaction: Transaction) : + TransactionHistoryScreenUiEffect() + + data class ShowToast(val count: Int) : TransactionHistoryScreenUiEffect() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/model/TransactionHistoryScreenUiEvent.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/model/TransactionHistoryScreenUiEvent.kt new file mode 100644 index 0000000000..73ee6d9908 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/model/TransactionHistoryScreenUiEvent.kt @@ -0,0 +1,52 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactionhistory.model + +import com.navi.common.basemvi.UiEvent +import com.navi.moneymanager.common.model.FilterItemData +import com.navi.moneymanager.common.model.MonthlyBalance +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.common.model.ZeroTransactionData + +sealed class TransactionHistoryScreenUiEvent : UiEvent { + + data class UpdateZeroTransactionData(val data: ZeroTransactionData? = null) : + TransactionHistoryScreenUiEvent() + + data class InitFilterOptionData( + val initialData: FilterItemData, + val initialFilter: List? = null + ) : TransactionHistoryScreenUiEvent() + + data object DismissFilterBottomSheet : TransactionHistoryScreenUiEvent() + + data object ShowFilterBottomSheet : TransactionHistoryScreenUiEvent() + + data class OnCategorySelection(val categoryName: String) : TransactionHistoryScreenUiEvent() + + data class OnFilterValueSelection( + val attributeName: String, + val filteredValue: String, + val selected: Boolean + ) : TransactionHistoryScreenUiEvent() + + data object ClearAllSelection : TransactionHistoryScreenUiEvent() + + data class OnFilterApplied( + val selectedFilterOptions: Map> + ) : TransactionHistoryScreenUiEvent() + + data class DeleteFilterItem(val item: FilterItemData.ValueData) : + TransactionHistoryScreenUiEvent() + + data class UpdateMonthlyBalanceData(val data: List) : + TransactionHistoryScreenUiEvent() + + data class UpdateTransactionData(val transaction: Transaction) : + TransactionHistoryScreenUiEvent() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/model/TransactionHistoryScreenUiState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/model/TransactionHistoryScreenUiState.kt new file mode 100644 index 0000000000..7464572f6a --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/model/TransactionHistoryScreenUiState.kt @@ -0,0 +1,52 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactionhistory.model + +import androidx.compose.runtime.Immutable +import com.navi.base.utils.orFalse +import com.navi.common.basemvi.UiState +import com.navi.moneymanager.common.model.FilterItemData +import com.navi.moneymanager.common.model.MonthlyBalance +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.common.model.ZeroTransactionData + +@Immutable +data class TransactionHistoryScreenUiState( + val zeroTransactionData: ZeroTransactionData? = null, + val monthlyBalanceData: List? = null, + val filterOptionData: FilterItemData, + val isFilterBottomSheetVisible: Boolean, + val selectedFilterOption: Map>, + val transaction: Transaction? = null, + val showToast: Boolean = false +) : UiState { + companion object { + val initialState = + TransactionHistoryScreenUiState( + filterOptionData = FilterItemData(emptyList()), + isFilterBottomSheetVisible = false, + selectedFilterOption = emptyMap() + ) + } + + fun shouldClearEnabled(): Boolean { + return this.filterOptionData.availableFilters + .any { filterItem -> filterItem.valueOptions.any { it.isSelected.orFalse() }.orFalse() } + .orFalse() + } + + fun getAllSelectedFilterValueOptions(): Map> { + return filterOptionData.availableFilters.associate { filterItem -> + filterItem.attributeKey to filterItem.valueOptions.filter { it.isSelected }.map { it } + } + } + + fun getTotalFilterCount(): Int { + return filterOptionData.availableFilters.sumOf { it.selectedValueCount } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/reducer/TransactionHistoryScreenReducer.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/reducer/TransactionHistoryScreenReducer.kt new file mode 100644 index 0000000000..e77cba5fc0 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/reducer/TransactionHistoryScreenReducer.kt @@ -0,0 +1,191 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactionhistory.reducer + +import com.navi.base.utils.orFalse +import com.navi.base.utils.orZero +import com.navi.common.basemvi.BaseReducer +import com.navi.common.extensions.or +import com.navi.moneymanager.common.model.FilterItemData +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenUiEvent +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenUiState + +class TransactionHistoryScreenReducer : + BaseReducer { + + override fun reduce( + previousState: TransactionHistoryScreenUiState, + event: TransactionHistoryScreenUiEvent + ): TransactionHistoryScreenUiState { + return when (event) { + is TransactionHistoryScreenUiEvent.UpdateZeroTransactionData -> { + previousState.copy(zeroTransactionData = event.data) + } + is TransactionHistoryScreenUiEvent.UpdateMonthlyBalanceData -> { + previousState.copy(monthlyBalanceData = event.data) + } + is TransactionHistoryScreenUiEvent.InitFilterOptionData -> { + val filterOptionData = + event.initialData.availableFilters + .map { filterItem -> + val availableFilter = + event.initialFilter + ?.firstOrNull { it.attributeKey == filterItem.attributeKey } + ?.itemList + if (availableFilter == null) { + filterItem + } else { + val updatedFilterValues = + filterItem.valueOptions.map { filterValue -> + filterValue.copy( + isSelected = + availableFilter + .firstOrNull { + it.valueName == filterValue.valueName + } + ?.isSelected + .or(filterValue.isSelected) + ) + } + filterItem.copy(valueOptions = updatedFilterValues) + } + } + .map { filterItem -> + val count = filterItem.valueOptions.count { it.isSelected }.orZero() + filterItem.copy(selectedValueCount = count) + } + previousState.copy( + filterOptionData = event.initialData.copy(availableFilters = filterOptionData), + selectedFilterOption = + filterOptionData.associate { filterItem -> + filterItem.attributeKey to + filterItem.valueOptions.filter { it.isSelected }.map { it } + }, + ) + } + is TransactionHistoryScreenUiEvent.DismissFilterBottomSheet -> { + val selectedFilterOptionData = + previousState.selectedFilterOption.values.flatten().mapTo(mutableSetOf()) { + it.valueName + } + + val updatedFilteredItems = + previousState.filterOptionData.availableFilters.map { filterItem -> + val updatedList = + filterItem.valueOptions.map { item -> + item.copy(isSelected = item.valueName in selectedFilterOptionData) + } + filterItem.copy( + valueOptions = updatedList, + selectedValueCount = updatedList.count { it.isSelected }.orZero() + ) + } + + previousState.copy( + isFilterBottomSheetVisible = false, + filterOptionData = + previousState.filterOptionData.copy(availableFilters = updatedFilteredItems) + ) + } + is TransactionHistoryScreenUiEvent.ShowFilterBottomSheet -> { + previousState.copy(isFilterBottomSheetVisible = true) + } + is TransactionHistoryScreenUiEvent.OnCategorySelection -> { + val updatedFilteredItems = + previousState.filterOptionData.availableFilters.map { filterItem -> + filterItem.copy(isSelected = filterItem.attributeKey == event.categoryName) + } + previousState.copy( + filterOptionData = + previousState.filterOptionData.copy(availableFilters = updatedFilteredItems) + ) + } + is TransactionHistoryScreenUiEvent.OnFilterValueSelection -> { + val updatedFilteredItems = + previousState.filterOptionData.availableFilters + .map { filterItem -> + if (filterItem.attributeKey == event.attributeName) { + // Map over valueOptions and update the isSelected valueName where + // needed + val updatedFilterValues = + filterItem.valueOptions.map { filterValue -> + if (filterValue.valueName == event.filteredValue) { + filterValue.copy(isSelected = event.selected) + } else { + filterValue + } + } + filterItem.copy(valueOptions = updatedFilterValues) + } else { + filterItem + } + } + .map { filterItem -> + val count = filterItem.valueOptions.count { it.isSelected }.orZero() + filterItem.copy(selectedValueCount = count) + } + previousState.copy( + filterOptionData = + previousState.filterOptionData.copy(availableFilters = updatedFilteredItems) + ) + } + is TransactionHistoryScreenUiEvent.ClearAllSelection -> { + val updatedFilteredItems = + previousState.filterOptionData.availableFilters + .map { filterItem -> + val filterValues = + filterItem.valueOptions.map { filterValue -> + filterValue.copy(isSelected = false) + } + filterItem.copy(valueOptions = filterValues) + } + .map { filterItem -> + val count = + filterItem.valueOptions.count { it.isSelected.orFalse() }.orZero() + filterItem.copy(selectedValueCount = count) + } + previousState.copy( + filterOptionData = + previousState.filterOptionData.copy(availableFilters = updatedFilteredItems) + ) + } + is TransactionHistoryScreenUiEvent.OnFilterApplied -> { + previousState.copy(selectedFilterOption = event.selectedFilterOptions) + } + is TransactionHistoryScreenUiEvent.DeleteFilterItem -> { + val updatedFilterItem = + previousState.filterOptionData.availableFilters.map { category -> + var updatedSelectedValueCount = category.selectedValueCount.orZero() + val updatedValueOptions = + category.valueOptions.map { filterItem -> + if (filterItem.valueName == event.item.valueName) { + updatedSelectedValueCount-- + filterItem.copy(isSelected = false) + } else { + filterItem + } + } + category.copy( + valueOptions = updatedValueOptions, + selectedValueCount = updatedSelectedValueCount + ) + } + previousState.copy( + selectedFilterOption = + previousState.selectedFilterOption.entries.associate { (key, value) -> + key to value.filter { it.valueName != event.item.valueName } + }, + filterOptionData = FilterItemData(availableFilters = updatedFilterItem) + ) + } + is TransactionHistoryScreenUiEvent.UpdateTransactionData -> { + previousState.copy(transaction = event.transaction) + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/repo/TransactionHistoryRepository.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/repo/TransactionHistoryRepository.kt new file mode 100644 index 0000000000..3d87c67627 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/repo/TransactionHistoryRepository.kt @@ -0,0 +1,34 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactionhistory.repo + +import com.navi.moneymanager.common.dataprovider.domain.TransactionHistoryDataProvider +import com.navi.moneymanager.common.model.MonthlyBalance +import javax.inject.Inject + +class TransactionHistoryRepository +@Inject +constructor(private val transactionHistoryDataProvider: TransactionHistoryDataProvider) { + + fun getZeroTransactionData() = transactionHistoryDataProvider.getZeroTransactionData() + + fun getTransactionPagingSource( + query: String? = null, + appliedFilters: List>>, + allFilters: List>>, + onMonthlyBalanceCalculated: (List) -> Unit + ) = + transactionHistoryDataProvider.getTransactionPagingSource( + query = query, + appliedFilters = appliedFilters, + allFilters = allFilters, + onMonthlyBalanceCalculated = onMonthlyBalanceCalculated + ) + + fun getInitFilterData() = transactionHistoryDataProvider.getInitFilterData() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/ui/TransactionHistoryBottomSheetContent.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/ui/TransactionHistoryBottomSheetContent.kt new file mode 100644 index 0000000000..493f0ea8fd --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/ui/TransactionHistoryBottomSheetContent.kt @@ -0,0 +1,325 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactionhistory.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.navi.base.utils.EMPTY +import com.navi.base.utils.orFalse +import com.navi.base.utils.orZero +import com.navi.common.utils.appendStrings +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.design.utils.NoRippleIndicationSource +import com.navi.elex.atoms.ElexCheckbox +import com.navi.elex.font.FontWeightEnum as ElexFontWeightEnum +import com.navi.elex.molecules.ElexButtonWithLoader +import com.navi.moneymanager.R +import com.navi.moneymanager.common.analytics.TransactionHistoryEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.COMMON_CROSS_BLACK +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.base.MMDivider +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenUiEffect +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenUiEvent +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenUiState + +@Composable +fun TransactionHistoryBottomSheetContent( + stateProvider: () -> TransactionHistoryScreenUiState, + eventHandler: (TransactionHistoryScreenUiEvent) -> Unit, + effectHandler: (TransactionHistoryScreenUiEffect) -> Unit +) { + val state by remember { derivedStateOf { stateProvider() } } + val filterData by remember(state) { mutableStateOf(state.filterOptionData) } + val isClearAllEnabled by remember(state) { mutableStateOf(state.shouldClearEnabled()) } + val selectedFilterOptions by + remember(state) { mutableStateOf(state.getAllSelectedFilterValueOptions()) } + val totalAppliedFilter by remember { derivedStateOf { state.getTotalFilterCount() } } + + val context = LocalContext.current + + Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(0.8F).background(MMColor.white)) { + Row( + Modifier.fillMaxWidth() + .wrapContentHeight() + .padding(start = 16.dp, top = 16.dp, end = 12.dp, bottom = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + MMText( + text = stringResource(R.string.filter), + modifier = Modifier.weight(2F), + color = MMColor.textPrimary, + fontSize = 20.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 28.sp + ) + Spacer(modifier = Modifier.weight(7F)) + Row( + modifier = + Modifier.weight(1F) + .onClickWithDebounce( + interactionSource = NoRippleIndicationSource(), + onClick = { + eventHandler( + TransactionHistoryScreenUiEvent.DismissFilterBottomSheet + ) + } + ) + ) { + Illustration( + modifier = Modifier.size(24.dp).weight(1F), + illustrationType = + IllustrationType.Image( + IllustrationSource.Remote( + url = COMMON_CROSS_BLACK, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ) + ) + } + } + MMDivider(color = MMColor.borderColor.copy(alpha = 0.8F)) + Row(modifier = Modifier.fillMaxWidth().weight(0.8F)) { + Column( + modifier = Modifier.weight(0.38F).fillMaxHeight().background(MMColor.ctaSecondary) + ) { + filterData.availableFilters.forEach { + FilterItemCategoryKey( + categoryKey = it.attributeKey, + selectedItemCountInCategory = it.selectedValueCount.orZero(), + selected = it.isSelected.orFalse() + ) { categoryName -> + eventHandler( + TransactionHistoryScreenUiEvent.OnCategorySelection(categoryName) + ) + } + } + } + Column( + modifier = + Modifier.weight(0.62F) + .fillMaxHeight() + .background(MMColor.white) + .verticalScroll(rememberScrollState()) + ) { + val selectedFilterKey = + filterData.availableFilters.firstOrNull { it.isSelected }?.attributeKey + filterData.availableFilters + .firstOrNull { it.isSelected.orFalse() } + ?.valueOptions + ?.forEach { + FilterItemCategoryValue( + filterKey = selectedFilterKey.orEmpty(), + valueLabel = it.valueName, + selected = it.isSelected + ) { + categoryFilterKey: String, + categoryFilterValue: String, + selected: Boolean -> + eventHandler( + TransactionHistoryScreenUiEvent.OnFilterValueSelection( + categoryFilterKey, + categoryFilterValue, + selected + ) + ) + } + } + } + } + MMDivider(color = MMColor.borderColor.copy(alpha = 0.8F)) + Row( + modifier = + Modifier.fillMaxWidth() + .weight(0.2F) + .background(MMColor.white) + .padding(top = 20.dp, start = 16.dp, end = 16.dp) + ) { + ElexButtonWithLoader( + loading = false, + text = context.getString(R.string.clear_all), + onClick = { + TransactionHistoryEventTrackerImpl + .onTransactionHistoryFilterBottomsheetClearAllClicked() + eventHandler(TransactionHistoryScreenUiEvent.ClearAllSelection) + }, + lottieSpec = LottieCompositionSpec.Url(EMPTY), + modifier = Modifier.wrapContentHeight().weight(1F).padding(end = 8.dp), + colors = ButtonDefaults.buttonColors(containerColor = MMColor.ctaSecondary), + textColor = + if (isClearAllEnabled.orFalse()) MMColor.ctaPrimary + else MMColor.ctaPrimary.copy(alpha = 0.6F), + fontSize = 14.sp, + fontWeight = ElexFontWeightEnum.NAVI_BODY_DEMI_BOLD, + contentPadding = PaddingValues(top = 16.dp, bottom = 16.dp) + ) + + val applyButtonLabel = + if (totalAppliedFilter > 0) { + context + .getString(R.string.apply_pascal_case) + .appendStrings(" (${totalAppliedFilter})") + } else { + context.getString(R.string.apply_pascal_case) + } + ElexButtonWithLoader( + loading = false, + text = applyButtonLabel, + onClick = { + TransactionHistoryEventTrackerImpl.onTransactionHistoryFilterApplied( + filterData.availableFilters.map { it.selectedValueCount }.toString() + ) + eventHandler( + TransactionHistoryScreenUiEvent.OnFilterApplied(selectedFilterOptions) + ) + eventHandler(TransactionHistoryScreenUiEvent.DismissFilterBottomSheet) + }, + lottieSpec = LottieCompositionSpec.Url(EMPTY), + modifier = Modifier.wrapContentHeight().weight(1F).padding(start = 8.dp), + colors = ButtonDefaults.buttonColors(containerColor = MMColor.ctaPrimary), + fontSize = 14.sp, + fontWeight = ElexFontWeightEnum.NAVI_BODY_DEMI_BOLD, + contentPadding = PaddingValues(top = 16.dp, bottom = 16.dp), + textColor = MMColor.white + ) + } + } +} + +@Composable +fun FilterItemCategoryKey( + categoryKey: String, + selectedItemCountInCategory: Int, + selected: Boolean, + onCategorySelected: (categoryKeyItemName: String) -> Unit, +) { + Row( + modifier = + Modifier.fillMaxWidth() + .wrapContentHeight() + .background( + color = if (selected) MMColor.bgAltColorSecond else MMColor.ctaSecondary + ) + .padding(start = 16.dp, top = 16.dp, bottom = 16.dp, end = 16.dp) + .onClickWithDebounce( + interactionSource = NoRippleIndicationSource(), + ) { + onCategorySelected.invoke(categoryKey) + }, + horizontalArrangement = Arrangement.SpaceBetween + ) { + MMText( + text = categoryKey, + color = if (selected) MMColor.textPrimary else MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = + if (selected) FontWeightEnum.NAVI_BODY_DEMI_BOLD + else FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 22.sp, + overflow = TextOverflow.Ellipsis + ) + if (selectedItemCountInCategory > 0) { + Box( + modifier = + Modifier.size(16.dp).drawBehind { + drawCircle(color = MMColor.white, radius = 16.div(2f).dp.toPx()) + } + ) { + MMText( + text = + if (selectedItemCountInCategory.orZero() > 9) { + "9+" + } else { + selectedItemCountInCategory.toString() + }, + modifier = Modifier.align(Alignment.Center), + color = MMColor.textPrimary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 16.sp + ) + } + } + } +} + +@Composable +fun FilterItemCategoryValue( + filterKey: String, + valueLabel: String, + selected: Boolean, + onValueSelected: + (categoryFilterKey: String, categoryFilterValue: String, selected: Boolean) -> Unit, +) { + + @Composable + fun Modifier.noRippleClickable(): Modifier = + this.onClickWithDebounce( + interactionSource = NoRippleIndicationSource(), + onClick = { onValueSelected(filterKey, valueLabel, !selected) } + ) + + Row( + modifier = Modifier.fillMaxWidth().wrapContentHeight().padding(12.dp).noRippleClickable(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + MMText( + text = valueLabel, + modifier = Modifier.weight(0.85F), + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + lineHeight = 22.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 1 + ) + ElexCheckbox( + modifier = Modifier.size(24.dp).noRippleClickable().weight(0.15F), + checked = selected, + enabled = true, + colors = + CheckboxDefaults.colors( + checkedColor = MMColor.ctaPrimary, + uncheckedColor = MMColor.inputFieldBorderColor + ) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/ui/TransactionHistoryContent.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/ui/TransactionHistoryContent.kt new file mode 100644 index 0000000000..22a0298ca1 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/ui/TransactionHistoryContent.kt @@ -0,0 +1,361 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactionhistory.ui + +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.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +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.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +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.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import com.navi.base.utils.EMPTY +import com.navi.common.commoncomposables.utils.loadingShimmerEffect +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.analytics.TransactionHistoryEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.model.MonthlyBalance +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.common.model.ZeroTransactionData +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.composable.transaction.Transaction +import com.navi.moneymanager.common.ui.composable.transaction.TransactionDivider +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.ItemsWithDivider +import com.navi.moneymanager.postonboard.transactionhistory.viewmodel.TransactionHistoryViewModel +import com.navi.naviwidgets.utils.NaviWidgetLottieUtils.TUK_TUK_LOTTIE + +@Composable +internal fun TransactionHistoryContent( + zeroTransactionData: ZeroTransactionData, + monthlyBalanceData: List, + onTransactionClick: (String) -> Unit, + onTransactionCategoryClick: (Transaction) -> Unit, + viewModelProvider: () -> TransactionHistoryViewModel +) { + val transactionPagingData = + viewModelProvider().transactionPagingSourceFlow.collectAsLazyPagingItems() + + if ( + transactionPagingData.loadState.refresh is LoadState.Loading && + transactionPagingData.itemCount == 0 + ) { + TransactionHistoryShimmer() + } + + if ( + transactionPagingData.loadState.refresh is LoadState.NotLoading && + transactionPagingData.itemCount == 0 + ) { + ZeroTransaction( + title = zeroTransactionData.title, + illustrationSource = zeroTransactionData.illustrationSource, + ) + } else { + TransactionList( + transactionPagingData = transactionPagingData, + monthlyBalanceData = monthlyBalanceData, + onTransactionClick = onTransactionClick, + onTransactionCategoryClick = onTransactionCategoryClick + ) + } +} + +@Composable +private fun TransactionList( + transactionPagingData: LazyPagingItems, + monthlyBalanceData: List, + onTransactionClick: (String) -> Unit, + onTransactionCategoryClick: (Transaction) -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + state = rememberLazyListState(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + ) { + items( + count = transactionPagingData.itemCount, + key = { index -> "${transactionPagingData[index]?.id}_$index" } + ) { index -> + Column(modifier = Modifier.fillMaxWidth().animateItem()) { + transactionPagingData[index]?.let { transaction -> + val currentMonthTag = transaction.monthTag + + val currentMonthData = + monthlyBalanceData.find { it.monthTag == currentMonthTag } + val currentMonthBalance: String = currentMonthData?.balance ?: EMPTY + val currentMonthTextColor: Color = + currentMonthData?.color ?: MMColor.textPrimary + + if (index == 0) { + MonthlyBalanceBar( + monthTag = currentMonthTag, + balance = currentMonthBalance, + textColor = currentMonthTextColor + ) + } else { + val previousMonthTag = + transactionPagingData[index - 1]?.monthTag ?: currentMonthTag + + if (previousMonthTag != currentMonthTag) { + Spacer(modifier = Modifier.height(height = 24.dp)) + + MonthlyBalanceBar( + monthTag = currentMonthTag, + balance = currentMonthBalance, + textColor = currentMonthTextColor + ) + } + } + Transaction( + transaction = transaction, + onClick = { transactionId -> + TransactionHistoryEventTrackerImpl + .onTransactionHistoryTransactionClicked(index + 1) + onTransactionClick(transactionId) + }, + onCategoryClick = { + TransactionHistoryEventTrackerImpl + .onTransactionHistoryTransactionCategoryClicked( + index + 1, + transaction.categoryId + ) + onTransactionCategoryClick(transaction) + } + ) + + if (index < transactionPagingData.itemCount - 1) { + val nextMonthTag = + transactionPagingData[index + 1]?.monthTag ?: currentMonthTag + + if (currentMonthTag == nextMonthTag) { + TransactionDivider() + } + } + } + } + } + + when (transactionPagingData.loadState.append) { + is LoadState.Loading -> { + item { + Illustration( + illustrationType = + IllustrationType.Lottie( + source = IllustrationSource.Resource(resId = TUK_TUK_LOTTIE) + ), + modifier = Modifier.fillMaxWidth().padding(all = 12.dp).size(size = 24.dp) + ) + } + } + is LoadState.Error -> {} + else -> Unit + } + } +} + +@Composable +private fun TransactionHistoryShimmer() { + Column( + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + ) { + Box(modifier = Modifier.height(36.dp).fillMaxWidth().loadingShimmerEffect()) + ItemsWithDivider(itemCount = 10, dividerItem = { TransactionDivider() }) { + TransactionHistoryShimmerItem() + } + } +} + +@Composable +private fun TransactionHistoryShimmerItem() { + Row( + modifier = + Modifier.padding(all = 16.dp).fillMaxWidth().height(intrinsicSize = IntrinsicSize.Min), + verticalAlignment = Alignment.Top, + ) { + Box( + modifier = Modifier.size(40.dp).clip(CircleShape).loadingShimmerEffect(), + ) + + Spacer(modifier = Modifier.width(width = 12.dp)) + + Column { + Row( + modifier = Modifier.wrapContentHeight(), + verticalAlignment = Alignment.Top, + ) { + Column( + modifier = Modifier.weight(weight = 1f), + ) { + Box( + modifier = + Modifier.height(20.dp) + .width(60.dp) + .clip(RoundedCornerShape(4.dp)) + .loadingShimmerEffect(), + ) + } + + Spacer(modifier = Modifier.width(width = 16.dp)) + + Column( + modifier = Modifier.wrapContentHeight(), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.SpaceBetween, + ) { + Box( + modifier = + Modifier.height(20.dp) + .width(70.dp) + .clip(RoundedCornerShape(4.dp)) + .loadingShimmerEffect(), + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.wrapContentHeight(), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(weight = 1f)) { + Box( + modifier = + Modifier.height(25.dp) + .width(70.dp) + .clip(RoundedCornerShape(32.dp)) + .loadingShimmerEffect(), + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.wrapContentHeight(), + verticalAlignment = Alignment.Bottom, + ) { + Column( + modifier = Modifier.weight(weight = 1f), + ) { + Box( + modifier = + Modifier.height(16.dp) + .width(70.dp) + .clip(RoundedCornerShape(4.dp)) + .loadingShimmerEffect(), + ) + } + + Spacer(modifier = Modifier.width(width = 24.dp)) + + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.SpaceBetween, + ) { + Box( + modifier = + Modifier.height(16.dp) + .width(80.dp) + .clip(RoundedCornerShape(4.dp)) + .loadingShimmerEffect(), + ) + } + } + } + } +} + +@Composable +private fun MonthlyBalanceBar(monthTag: String, balance: String, textColor: Color) { + Row( + modifier = + Modifier.fillMaxWidth() + .background(color = MMColor.bgAltColor) + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + MMText( + text = monthTag, + color = MMColor.textPrimary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 22.sp, + ) + + Spacer(modifier = Modifier.width(width = 8.dp)) + MMText( + text = balance, + color = textColor, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 22.sp, + ) + } +} + +@Composable +private fun ZeroTransaction( + title: String, + illustrationSource: IllustrationSource, +) { + Column( + modifier = + Modifier.fillMaxWidth() + .fillMaxHeight() + .padding( + start = 32.dp, + top = 32.dp, + end = 32.dp, + bottom = 64.dp, + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Illustration( + illustrationType = IllustrationType.Image(source = illustrationSource), + modifier = Modifier.size(size = 180.dp), + ) + + MMText( + text = title, + color = MMColor.gray, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + textAlign = TextAlign.Center, + lineHeight = 22.sp, + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/ui/TransactionHistoryScaffold.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/ui/TransactionHistoryScaffold.kt new file mode 100644 index 0000000000..ceaf5729ce --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/ui/TransactionHistoryScaffold.kt @@ -0,0 +1,274 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactionhistory.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideIn +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.horizontalScroll +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.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +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.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.navi.base.utils.isNotNullAndNotEmpty +import com.navi.base.utils.orZero +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.design.utils.NoRippleIndicationSource +import com.navi.elex.utils.setOnClick +import com.navi.moneymanager.R +import com.navi.moneymanager.common.analytics.TransactionHistoryEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.COMMON_CROSS_BLACK +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.IMAGE_TRANSPARENT_PLACEHOLDER_SMALL +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.model.FilterItemData +import com.navi.moneymanager.common.ui.composable.MMSearchAndFilter +import com.navi.moneymanager.common.ui.composable.MMTopBar +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenUiEffect +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenUiEvent +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenUiState +import com.navi.moneymanager.postonboard.transactionhistory.viewmodel.TransactionHistoryViewModel + +@Composable +internal fun TransactionHistoryScaffold( + modifier: Modifier, + transactionHistoryState: () -> TransactionHistoryScreenUiState, + getViewModel: () -> TransactionHistoryViewModel +) { + val searchQuery by getViewModel().searchQuery.collectAsStateWithLifecycle() + val isFilterApplied by + remember(transactionHistoryState()) { + derivedStateOf { + transactionHistoryState() + .selectedFilterOption + .flatMap { it.value } + .isNotNullAndNotEmpty() + } + } + + Scaffold( + modifier = modifier, + containerColor = MMColor.white, + topBar = { + Column(modifier = Modifier.fillMaxWidth().background(Color.White)) { + MMTopBar( + title = stringResource(R.string.mm_transaction_history_header), + onNavigationIconClick = { + getViewModel().setEffect { + TransactionHistoryScreenUiEffect.Navigation.Back + } + }, + ) + MMSearchAndFilter( + searchQuery = searchQuery, + onQueryChange = getViewModel()::onSearchQueryChanged, + placeholderText = stringResource(R.string.mm_transaction_search_placeholder), + isFilterApplied = isFilterApplied, + onFilterClick = { + TransactionHistoryEventTrackerImpl.onTransactionHistoryFilterIconClicked() + getViewModel() + .sendEvent(TransactionHistoryScreenUiEvent.ShowFilterBottomSheet) + } + ) + FilterSection( + filters = transactionHistoryState().selectedFilterOption.flatMap { it.value }, + getViewModel = getViewModel + ) + } + } + ) { + Column( + modifier = Modifier.background(Color.White).padding(it).fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + transactionHistoryState().zeroTransactionData?.let { zeroTransactionData -> + TransactionHistoryContent( + zeroTransactionData = zeroTransactionData, + monthlyBalanceData = + transactionHistoryState().monthlyBalanceData ?: emptyList(), + onTransactionClick = { transactionId -> + getViewModel().setEffect { + TransactionHistoryScreenUiEffect.Navigation.TransactionDetails( + transactionId = transactionId, + ) + } + }, + onTransactionCategoryClick = { transaction -> + getViewModel().setEffect { + TransactionHistoryScreenUiEffect.OpenCategoryBottomSheet( + transaction = transaction, + ) + } + }, + viewModelProvider = getViewModel + ) + } + } + } +} + +@Composable +private fun FilterSection( + filters: List? = null, + trimIndex: Int = 4, + getViewModel: () -> TransactionHistoryViewModel +) { + var shouldExpand by + remember(filters?.size.orZero() > trimIndex) { + mutableStateOf(filters?.size.orZero() > trimIndex) + } + + var selectedFilterItems: List? by remember { + mutableStateOf(emptyList()) + } + + LaunchedEffect(shouldExpand, filters) { + selectedFilterItems = + getViewModel() + .sanitizeFilterItems( + selectedFilterItems = filters, + shouldAddGroupedItem = shouldExpand, + trimIndex = trimIndex + ) + } + + AnimatedVisibility( + visible = filters?.size.orZero() > 0, + enter = slideIn(initialOffset = { IntOffset(0, -it.height) }) + ) { + Box( + modifier = Modifier.fillMaxWidth().padding(start = 0.dp, end = 0.dp, bottom = 24.dp), + contentAlignment = Alignment.CenterStart + ) { + Row( + modifier = + Modifier.wrapContentWidth() + .horizontalScroll(rememberScrollState()) + .wrapContentHeight(), + horizontalArrangement = Arrangement.Start + ) { + Spacer(modifier = Modifier.size(16.dp)) + selectedFilterItems?.mapIndexed { index, item -> + if (item.type is FilterItemData.ValueData.FilterChipType.ChipItem) { + SingleFilterItem(item) { + TransactionHistoryEventTrackerImpl.onTransactionHistoryFilterRemoved( + filterRank = index + 1 + ) + getViewModel() + .sendEvent(TransactionHistoryScreenUiEvent.DeleteFilterItem(it)) + } + } else { + Row( + modifier = + Modifier.wrapContentSize() + .background(MMColor.white) + .border( + border = + BorderStroke(width = 1.dp, color = MMColor.ctaPrimary), + shape = RoundedCornerShape(size = 4.dp) + ) + .padding(start = 10.dp, end = 10.dp, top = 8.dp, bottom = 8.dp) + .setOnClick { shouldExpand = !shouldExpand } + ) { + MMText( + text = "+${filters?.size.orZero() - trimIndex}", + color = MMColor.ctaPrimary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 18.sp, + ) + } + Spacer(modifier = Modifier.size(16.dp)) + return@Row + } + if (index < selectedFilterItems?.size.orZero() - 1) { + Spacer(Modifier.width(12.dp)) + } + } + Spacer(modifier = Modifier.size(16.dp)) + } + } + } +} + +@Composable +private fun SingleFilterItem( + item: FilterItemData.ValueData, + onclick: (item: FilterItemData.ValueData) -> Unit +) { + Row( + modifier = + Modifier.wrapContentSize() + .background(MMColor.white) + .border( + border = BorderStroke(width = 1.dp, color = MMColor.ctaPrimary), + shape = RoundedCornerShape(size = 4.dp) + ) + ) { + MMText( + text = item.valueName, + modifier = + Modifier.padding(start = 12.dp, top = 8.dp, bottom = 8.dp) + .align(Alignment.CenterVertically), + color = MMColor.ctaPrimary, + fontSize = 12.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + lineHeight = 18.sp + ) + Illustration( + modifier = + Modifier.padding(start = 8.dp, top = 8.dp, end = 12.dp, bottom = 8.dp) + .size(16.dp) + .align(Alignment.CenterVertically) + .onClickWithDebounce( + interactionSource = NoRippleIndicationSource(), + onClick = { onclick(item) } + ), + illustrationType = + IllustrationType.Image( + IllustrationSource.Remote( + url = COMMON_CROSS_BLACK, + placeholder = IMAGE_TRANSPARENT_PLACEHOLDER_SMALL + ) + ) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/ui/TransactionHistoryScreen.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/ui/TransactionHistoryScreen.kt new file mode 100644 index 0000000000..3e20486c64 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/ui/TransactionHistoryScreen.kt @@ -0,0 +1,183 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactionhistory.ui + +import android.os.Bundle +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.navi.base.model.CtaData +import com.navi.base.utils.orFalse +import com.navi.common.navigation.NavigationAction +import com.navi.elex.atoms.ElexBottomSheet +import com.navi.moneymanager.common.analytics.TransactionHistoryEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.navigation.utils.navigateToPreviousScreen +import com.navi.moneymanager.common.ui.composable.ScreenInit +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.Constants.GREEN_TICK_MARK +import com.navi.moneymanager.common.utils.Constants.TRANSACTION_ID +import com.navi.moneymanager.common.utils.MMScreenEventLogger +import com.navi.moneymanager.common.utils.ShowCustomToast +import com.navi.moneymanager.common.utils.ToastDuration +import com.navi.moneymanager.common.utils.getTransactionCategorizedMessage +import com.navi.moneymanager.entry.ui.activity.MMActivity +import com.navi.moneymanager.postonboard.monthlysummary.model.CategoryBottomSheetTransactionData +import com.navi.moneymanager.postonboard.monthlysummary.ui.bottomsheet.CategoryCommonBottomSheet +import com.navi.moneymanager.postonboard.monthlysummary.ui.bottomsheet.CustomToastView +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenNavigationData +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenUiEffect +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenUiEvent +import com.navi.moneymanager.postonboard.transactionhistory.viewmodel.TransactionHistoryViewModel +import com.ramcosta.composedestinations.annotation.Destination +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach + +@Destination +@Composable +fun TransactionHistoryScreen( + activity: MMActivity, + screenData: TransactionHistoryScreenNavigationData? = null, + viewModel: TransactionHistoryViewModel = hiltViewModel(), +) { + MMScreenEventLogger( + onScreenLand = { TransactionHistoryEventTrackerImpl.onTransactionHistoryScreenLanded() }, + onScreenExit = { TransactionHistoryEventTrackerImpl.onTransactionHistoryScreenExit() } + ) + + val state by viewModel.state.collectAsStateWithLifecycle() + val showBottomSheet = remember { mutableStateOf(false) } + var showTransactionUpdatedToast by remember { mutableStateOf(false) } + var updatedTransactionCount by remember { mutableIntStateOf(0) } + var firstTimeInit by rememberSaveable { mutableStateOf(true) } + + LaunchedEffect(Unit) { + if (firstTimeInit) { + viewModel.getInitFilterData( + initialFilter = screenData?.filterList, + ) + firstTimeInit = false + } + } + + ScreenInit(screenName = MMScreen.TRANSACTION_HISTORY.screen, activity = activity) + + LaunchedEffect(Unit) { + viewModel.effect + .onEach { effect -> + when (effect) { + TransactionHistoryScreenUiEffect.Navigation.Back -> { + if (state.isFilterBottomSheetVisible.orFalse()) { + viewModel.sendEvent( + TransactionHistoryScreenUiEvent.DismissFilterBottomSheet + ) + } else { + navigateToPreviousScreen(activity) + } + } + is TransactionHistoryScreenUiEffect.Navigation.TransactionDetails -> { + activity.mmNavigator.navigateTo( + activity = activity, + ctaData = CtaData(url = MMScreen.TRANSACTION_DETAILS.screen), + bundle = + Bundle().apply { putString(TRANSACTION_ID, effect.transactionId) }, + navigationAction = NavigationAction.Default, + ) + } + is TransactionHistoryScreenUiEffect.OpenCategoryBottomSheet -> { + viewModel.sendEvent( + TransactionHistoryScreenUiEvent.UpdateTransactionData( + effect.transaction + ) + ) + showBottomSheet.value = true + } + is TransactionHistoryScreenUiEffect.ShowToast -> { + updatedTransactionCount = effect.count + showTransactionUpdatedToast = true + } + } + } + .collect() + } + + LaunchedEffect(Unit) { viewModel.getZeroTransactionData() } + + BackHandler { viewModel.setEffect { TransactionHistoryScreenUiEffect.Navigation.Back } } + + TransactionHistoryScaffold( + modifier = Modifier.fillMaxSize(), + transactionHistoryState = { state }, + getViewModel = { viewModel } + ) + ElexBottomSheet( + visible = state.isFilterBottomSheetVisible.orFalse(), + skipPartiallyExpanded = true, + cancellable = true, + onDismissRequest = { + viewModel.sendEvent(TransactionHistoryScreenUiEvent.DismissFilterBottomSheet) + }, + dragHandle = null, + shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp), + containerColor = MMColor.white, + ) { + TransactionHistoryBottomSheetContent( + stateProvider = { state }, + eventHandler = viewModel::sendEvent, + effectHandler = { effect -> viewModel.setEffect { effect } } + ) + } + + CategoryCommonBottomSheet( + screenName = MMScreen.TRANSACTION_HISTORY.screen, + modifier = Modifier, + showBottomSheet = showBottomSheet, + categoryTransactionData = + CategoryBottomSheetTransactionData( + transactionId = state.transaction?.id.orEmpty(), + categoryId = state.transaction?.categoryId.orEmpty(), + counterPartyName = state.transaction?.counterPartyName.orEmpty(), + categoryName = state.transaction?.categoryName.orEmpty(), + transactionType = state.transaction?.type.orEmpty() + ), + onDismiss = { showBottomSheet.value = false }, + onSuccessFullyUpdatedCategory = { + updatedTransactionCount = it + showTransactionUpdatedToast = true + } + ) + + if (showTransactionUpdatedToast) { + ShowCustomToast( + context = activity, + toastDuration = ToastDuration.LENGTH_SHORT, + content = { + CustomToastView( + message = getTransactionCategorizedMessage(updatedTransactionCount, activity), + illustrationType = + IllustrationType.Image(IllustrationSource.Resource(resId = GREEN_TICK_MARK)) + ) + } + ) + showTransactionUpdatedToast = false + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/viewmodel/TransactionHistoryViewModel.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/viewmodel/TransactionHistoryViewModel.kt new file mode 100644 index 0000000000..550c4df05a --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/postonboard/transactionhistory/viewmodel/TransactionHistoryViewModel.kt @@ -0,0 +1,161 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.postonboard.transactionhistory.viewmodel + +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.navi.base.utils.EMPTY +import com.navi.moneymanager.base.viewmodel.MMBaseViewModel +import com.navi.moneymanager.common.analytics.DataProviderEventTrackerImpl +import com.navi.moneymanager.common.model.FilterItemData +import com.navi.moneymanager.common.model.MonthlyBalance +import com.navi.moneymanager.common.model.Transaction +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionFilterData +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenUiEffect +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenUiEvent +import com.navi.moneymanager.postonboard.transactionhistory.model.TransactionHistoryScreenUiState +import com.navi.moneymanager.postonboard.transactionhistory.reducer.TransactionHistoryScreenReducer +import com.navi.moneymanager.postonboard.transactionhistory.repo.TransactionHistoryRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.withContext + +typealias GroupedFilterOptions = List>> + +@HiltViewModel +class TransactionHistoryViewModel +@Inject +constructor(val repository: TransactionHistoryRepository) : + MMBaseViewModel< + TransactionHistoryScreenUiState, + TransactionHistoryScreenUiEvent, + TransactionHistoryScreenUiEffect + >( + initialState = TransactionHistoryScreenUiState.initialState, + reducer = TransactionHistoryScreenReducer() + ) { + private val _searchQuery = MutableStateFlow(EMPTY) + val searchQuery = _searchQuery.asStateFlow() + + @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) + val transactionPagingSourceFlow = + combine(_searchQuery.debounce(300), state.map { it.selectedFilterOption }) { + query, + filterData -> + query to filterData + } + .distinctUntilChanged() + .flatMapLatest { (query, filterData) -> + DataProviderEventTrackerImpl.transactionHistoryVmFetchData(query, filterData) + fetchTransactionPagingSource(query, state.value.filterOptionData to filterData) + } + .cachedIn(viewModelScope) + .flowOn(Dispatchers.IO) + + private suspend fun fetchTransactionPagingSource( + query: String, + filterData: Pair>> + ): Flow> = + withContext(Dispatchers.IO) { + val (appliedFilters, allFilters) = prepareFilters(filterData) + repository.getTransactionPagingSource( + query = query, + appliedFilters = appliedFilters, + allFilters = allFilters + ) { monthlyBalanceData -> + DataProviderEventTrackerImpl.transactionHistoryVmCollect(monthlyBalanceData.size) + updateMonthlyBalance(monthlyBalanceData) + } + } + + private fun prepareFilters( + filterData: Pair>> + ): Pair { + + val appliedFilters = + filterData.second.map { filter -> filter.key to filter.value.map { it.id } } + val allFilters = + filterData.first.availableFilters.map { filter -> + filter.attributeKey to filter.valueOptions.map { it.id } + } + return appliedFilters to allFilters + } + + private fun updateMonthlyBalance(data: List) { + viewModelScope.safeLaunch { + sendEvent(TransactionHistoryScreenUiEvent.UpdateMonthlyBalanceData(data)) + } + } + + fun onSearchQueryChanged(query: String) { + _searchQuery.update { query } + DataProviderEventTrackerImpl.transactionHistoryVmUpdate(query) + } + + fun getZeroTransactionData() { + viewModelScope.safeLaunch(Dispatchers.IO) { + repository.getZeroTransactionData().collect { transactionHistoryScreenData -> + sendEvent( + TransactionHistoryScreenUiEvent.UpdateZeroTransactionData( + data = transactionHistoryScreenData + ) + ) + } + } + } + + fun getInitFilterData(initialFilter: List? = null) { + viewModelScope.safeLaunch(Dispatchers.IO) { + repository.getInitFilterData().collect { data -> + sendEvent( + TransactionHistoryScreenUiEvent.InitFilterOptionData( + initialData = data, + initialFilter = initialFilter + ) + ) + } + } + } + + fun sanitizeFilterItems( + selectedFilterItems: List?, + shouldAddGroupedItem: Boolean, + trimIndex: Int + ): List { + return if (shouldAddGroupedItem) { + val copyList = selectedFilterItems?.toMutableList() + copyList?.add( + trimIndex, + FilterItemData.ValueData( + valueName = EMPTY, + type = FilterItemData.ValueData.FilterChipType.HiddenChipRepresentationItem, + id = EMPTY + ) + ) + copyList?.toList().orEmpty() + } else { + selectedFilterItems + ?.filter { it.type is FilterItemData.ValueData.FilterChipType.ChipItem } + .orEmpty() + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/AccountLinkStatusResponse.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/AccountLinkStatusResponse.kt new file mode 100644 index 0000000000..8240b62708 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/AccountLinkStatusResponse.kt @@ -0,0 +1,12 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.finarkein.model + +data class AccountLinkStatusResponse( + val status: String, +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/AccountLinkingStatusScreenUiEffect.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/AccountLinkingStatusScreenUiEffect.kt new file mode 100644 index 0000000000..eef32687f7 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/AccountLinkingStatusScreenUiEffect.kt @@ -0,0 +1,25 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.finarkein.model + +import androidx.compose.runtime.Immutable +import com.navi.base.model.CtaData +import com.navi.common.basemvi.UiEffect + +@Immutable +sealed class AccountLinkingStatusScreenUiEffect : UiEffect { + @Immutable + sealed class Navigation : AccountLinkingStatusScreenUiEffect() { + + data object Back : Navigation() + + data class NavigateToCta(val ctaData: CtaData) : Navigation() + + data object ErrorAndExit : Navigation() + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/AccountLinkingStatusScreenUiEvent.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/AccountLinkingStatusScreenUiEvent.kt new file mode 100644 index 0000000000..b13661bd4d --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/AccountLinkingStatusScreenUiEvent.kt @@ -0,0 +1,17 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.finarkein.model + +import androidx.compose.runtime.Immutable +import com.navi.common.basemvi.UiEvent + +@Immutable +sealed class AccountLinkingStatusScreenUiEvent : UiEvent { + data class ApiStatus(val data: AccountLinkingSuccessState) : + AccountLinkingStatusScreenUiEvent() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/AccountLinkingStatusScreenUiState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/AccountLinkingStatusScreenUiState.kt new file mode 100644 index 0000000000..ac2edfbef5 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/AccountLinkingStatusScreenUiState.kt @@ -0,0 +1,32 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.finarkein.model + +import androidx.compose.runtime.Immutable +import com.navi.common.basemvi.UiState +import com.navi.common.utils.EMPTY +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.repository.LottieRepository.Companion.CIRCULAR_LOADER_LOTTIE + +@Immutable +data class AccountLinkingStatusScreenUiState( + val accountLinkingScreenState: AccountLinkingSuccessState, +) : UiState { + companion object { + val initialState = + AccountLinkingStatusScreenUiState( + AccountLinkingSuccessState.Loading( + AccountLinkingData( + lottie = IllustrationSource.Resource(CIRCULAR_LOADER_LOTTIE), + lottieIteration = Integer.MAX_VALUE, + message = EMPTY + ) + ) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/AccountLinkingStatusState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/AccountLinkingStatusState.kt new file mode 100644 index 0000000000..0f93c1e27f --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/AccountLinkingStatusState.kt @@ -0,0 +1,22 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.finarkein.model + +import com.navi.moneymanager.common.illustration.model.IllustrationSource + +sealed class AccountLinkingSuccessState { + data class Loading(val data: AccountLinkingData) : AccountLinkingSuccessState() + + data class Success(val data: AccountLinkingData) : AccountLinkingSuccessState() +} + +data class AccountLinkingData( + val lottie: IllustrationSource, + val message: String, + val lottieIteration: Int +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/FinarkeinDataState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/FinarkeinDataState.kt new file mode 100644 index 0000000000..2be188fb8b --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/model/FinarkeinDataState.kt @@ -0,0 +1,16 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.finarkein.model + +import com.navi.moneymanager.common.network.model.FinarkeinDataResponse + +sealed interface FinarkeinDataState { + data class Success(val data: FinarkeinDataResponse) : FinarkeinDataState + + data object Failure : FinarkeinDataState +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/reducer/AccountLinkingStatusScreenReducer.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/reducer/AccountLinkingStatusScreenReducer.kt new file mode 100644 index 0000000000..129d15919c --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/reducer/AccountLinkingStatusScreenReducer.kt @@ -0,0 +1,26 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.finarkein.reducer + +import com.navi.common.basemvi.BaseReducer +import com.navi.moneymanager.preonboard.finarkein.model.AccountLinkingStatusScreenUiEvent +import com.navi.moneymanager.preonboard.finarkein.model.AccountLinkingStatusScreenUiState + +class AccountLinkingStatusScreenReducer : + BaseReducer { + override fun reduce( + previousState: AccountLinkingStatusScreenUiState, + event: AccountLinkingStatusScreenUiEvent + ): AccountLinkingStatusScreenUiState { + return when (event) { + is AccountLinkingStatusScreenUiEvent.ApiStatus -> { + previousState.copy(accountLinkingScreenState = event.data) + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/repo/AccountLinkingStatusRepository.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/repo/AccountLinkingStatusRepository.kt new file mode 100644 index 0000000000..667636f2d4 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/repo/AccountLinkingStatusRepository.kt @@ -0,0 +1,22 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.finarkein.repo + +import com.navi.common.network.models.RepoResult +import com.navi.moneymanager.common.dataprovider.domain.RemoteDataProvider +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.network.model.AccountDetailResponse +import javax.inject.Inject + +class AccountLinkingStatusRepository +@Inject +constructor(private val remoteDataProvider: RemoteDataProvider) { + + suspend fun fetchUserAccounts(): RepoResult = + remoteDataProvider.fetchUserAccounts(screenName = MMScreen.ACCOUNT_LINKING_SUCCESS.screen) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/ui/AccountLinkingStatusScreen.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/ui/AccountLinkingStatusScreen.kt new file mode 100644 index 0000000000..e73faa4682 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/ui/AccountLinkingStatusScreen.kt @@ -0,0 +1,137 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.finarkein.ui + +import android.os.Bundle +import androidx.activity.compose.BackHandler +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.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.navi.base.model.CtaData +import com.navi.common.navigation.NavigationAction +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.model.LottieProperties +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.navigation.utils.navigateToPreviousScreen +import com.navi.moneymanager.common.ui.composable.ScreenInit +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.entry.ui.activity.MMActivity +import com.navi.moneymanager.preonboard.finarkein.model.AccountLinkingData +import com.navi.moneymanager.preonboard.finarkein.model.AccountLinkingStatusScreenUiEffect +import com.navi.moneymanager.preonboard.finarkein.model.AccountLinkingSuccessState +import com.navi.moneymanager.preonboard.finarkein.viewmodel.AccountLinkingStatusViewModel +import com.ramcosta.composedestinations.annotation.Destination +import kotlinx.coroutines.delay + +@Destination +@Composable +fun AccountLinkingStatusScreen( + bundle: Bundle? = null, + viewModel: AccountLinkingStatusViewModel = hiltViewModel() +) { + val activity = LocalContext.current as MMActivity + val sharedVM = activity.getSharedVM() + val state by viewModel.state.collectAsStateWithLifecycle() + + ScreenInit(screenName = MMScreen.ACCOUNT_LINKING_SUCCESS.screen, activity = activity) + + LaunchedEffect(Unit) { + viewModel.effect.collect { effect -> + when (effect) { + is AccountLinkingStatusScreenUiEffect.Navigation.Back -> { + // Nothing to do here + } + is AccountLinkingStatusScreenUiEffect.Navigation.NavigateToCta -> { + activity.mmNavigator.navigateTo( + activity = activity, + ctaData = effect.ctaData, + navigationAction = NavigationAction.ClearStackAndNext + ) + } + is AccountLinkingStatusScreenUiEffect.Navigation.ErrorAndExit -> { + sharedVM.updateFinarkeinErrorBottomSheetState(show = true) + navigateToPreviousScreen(activity) + } + } + } + } + + BackHandler { viewModel.setEffect { AccountLinkingStatusScreenUiEffect.Navigation.Back } } + + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + when (val accountLinkingState = state.accountLinkingScreenState) { + is AccountLinkingSuccessState.Loading -> { + LoadingScreen(accountLinkingState.data) + } + is AccountLinkingSuccessState.Success -> { + AccountLinkingSuccessScreen(accountLinkingState.data, viewModel) + } + } + } +} + +@Composable +private fun LoadingScreen(data: AccountLinkingData) { + ScreenContent(data) +} + +@Composable +private fun AccountLinkingSuccessScreen( + data: AccountLinkingData, + viewModel: AccountLinkingStatusViewModel +) { + LaunchedEffect(Unit) { + delay(1000) // The delay is for tick animation completion + viewModel.setEffect { + AccountLinkingStatusScreenUiEffect.Navigation.NavigateToCta( + CtaData(url = MMScreen.DASHBOARD.screen) + ) + } + } + ScreenContent(data) +} + +@Composable +private fun ScreenContent(data: AccountLinkingData) { + Column(modifier = Modifier.fillMaxWidth()) { + Illustration( + illustrationType = + IllustrationType.Lottie( + source = data.lottie, + properties = LottieProperties(iterationCount = data.lottieIteration) + ), + modifier = Modifier.size(150.dp).align(Alignment.CenterHorizontally) + ) + MMText( + text = data.message, + modifier = Modifier.padding(top = 16.dp).align(Alignment.CenterHorizontally), + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + textAlign = TextAlign.Center, + lineHeight = 24.sp + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/viewmodel/AccountLinkingStatusViewModel.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/viewmodel/AccountLinkingStatusViewModel.kt new file mode 100644 index 0000000000..255e08ea5d --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/finarkein/viewmodel/AccountLinkingStatusViewModel.kt @@ -0,0 +1,139 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.finarkein.viewmodel + +import android.content.Context +import androidx.lifecycle.viewModelScope +import com.navi.common.extensions.or +import com.navi.common.network.models.isSuccessWithData +import com.navi.common.scheduler.TaskRepeater +import com.navi.moneymanager.R +import com.navi.moneymanager.base.viewmodel.MMBaseViewModel +import com.navi.moneymanager.common.analytics.AccountLinkingEventTrackerImpl +import com.navi.moneymanager.common.dataprovider.data.dashboard.helper.MMConfigResponseHelper +import com.navi.moneymanager.common.dataprovider.data.datastore.DataStoreInfoProvider +import com.navi.moneymanager.common.dataprovider.domain.BankAccountsDataProvider +import com.navi.moneymanager.common.dataprovider.domain.LocalDataSyncManager +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.repository.LottieRepository.Companion.SUCCESS_TICK_LOTTIE +import com.navi.moneymanager.common.model.database.AccountOverview +import com.navi.moneymanager.common.network.di.RoomDataStoreInfoProvider +import com.navi.moneymanager.common.network.model.AccountData +import com.navi.moneymanager.common.utils.Constants.LAST_REFRESH_SUCCESSFUL_TIMESTAMP +import com.navi.moneymanager.preonboard.finarkein.model.AccountLinkingData +import com.navi.moneymanager.preonboard.finarkein.model.AccountLinkingStatusScreenUiEffect +import com.navi.moneymanager.preonboard.finarkein.model.AccountLinkingStatusScreenUiEvent +import com.navi.moneymanager.preonboard.finarkein.model.AccountLinkingStatusScreenUiState +import com.navi.moneymanager.preonboard.finarkein.model.AccountLinkingSuccessState +import com.navi.moneymanager.preonboard.finarkein.reducer.AccountLinkingStatusScreenReducer +import com.navi.moneymanager.preonboard.finarkein.repo.AccountLinkingStatusRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first + +@HiltViewModel +class AccountLinkingStatusViewModel +@Inject +constructor( + @ApplicationContext val context: Context, + private val configResponseHelper: MMConfigResponseHelper, + private val repository: AccountLinkingStatusRepository, + private val bankAccountsDataProvider: BankAccountsDataProvider, + @RoomDataStoreInfoProvider private val dbDataStoreProvider: DataStoreInfoProvider, + private val localDataSyncManager: LocalDataSyncManager +) : + MMBaseViewModel< + AccountLinkingStatusScreenUiState, + AccountLinkingStatusScreenUiEvent, + AccountLinkingStatusScreenUiEffect + >( + initialState = AccountLinkingStatusScreenUiState.initialState, + reducer = AccountLinkingStatusScreenReducer() + ) { + private lateinit var taskRepeater: TaskRepeater + private lateinit var existingBankAccounts: List + + init { + viewModelScope.safeLaunch(Dispatchers.IO) { + existingBankAccounts = bankAccountsDataProvider.getBankAccounts().first() + val mmConfig = configResponseHelper.getMMConfig()?.accountLinkingPollingConfig + AccountLinkingEventTrackerImpl.onExistingAccountsAndConfigFetched() + taskRepeater = + getTaskRepeater( + maxAttempts = mmConfig?.maxAttempts.or(10), + taskIntervalSeconds = mmConfig?.taskIntervalSeconds.or(1), + initialDelay = mmConfig?.initialDelaySeconds.or(0) + ) + AccountLinkingEventTrackerImpl.onPollingStarted() + taskRepeater.startTask() + } + } + + private fun getTaskRepeater( + maxAttempts: Int, + taskIntervalSeconds: Long, + initialDelay: Long + ): TaskRepeater { + return TaskRepeater( + maxAttempts = maxAttempts, + taskIntervalSeconds = taskIntervalSeconds, + initialDelaySeconds = initialDelay, + onTimeout = { + AccountLinkingEventTrackerImpl.onAccountLinkingFailure() + setEffect { AccountLinkingStatusScreenUiEffect.Navigation.ErrorAndExit } + }, + task = { checkAccountLinkingStatus() } + ) + } + + private suspend fun checkAccountLinkingStatus() { + val response = repository.fetchUserAccounts() + if (response.isSuccessWithData()) { + val accounts = response.data?.accountDetails.orEmpty() + if (accounts.isNotEmpty() && isAnyNewAccountAdded(accounts)) { + resetLastRefreshTimestamp() + localDataSyncManager.insertAccounts(accounts) + sendSuccessEvent() + stopPolling() + } + } + } + + private fun stopPolling() { + if (::taskRepeater.isInitialized) { + taskRepeater.stopTask() + } + } + + private fun sendSuccessEvent() { + AccountLinkingEventTrackerImpl.onAccountLinkingSuccess() + sendEvent( + AccountLinkingStatusScreenUiEvent.ApiStatus( + AccountLinkingSuccessState.Success( + AccountLinkingData( + lottie = IllustrationSource.Resource(SUCCESS_TICK_LOTTIE), + lottieIteration = 1, + message = context.resources.getString(R.string.account_successfully_linked) + ) + ) + ) + ) + } + + private fun isAnyNewAccountAdded(newAccounts: List): Boolean { + val existingAccountIds = existingBankAccounts.map { it.linkedAccRef }.toSet() + // Check if there's any new linkedAccountReference not present in existingAccountIds + return newAccounts.any { it.linkedAccRef !in existingAccountIds } + } + + private suspend fun resetLastRefreshTimestamp() { + dbDataStoreProvider.saveLongData(key = LAST_REFRESH_SUCCESSFUL_TIMESTAMP, value = 0L) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/ConsentRevokedBottomSheetData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/ConsentRevokedBottomSheetData.kt new file mode 100644 index 0000000000..674c779fcf --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/ConsentRevokedBottomSheetData.kt @@ -0,0 +1,17 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.launcher.model + +import com.navi.moneymanager.common.illustration.model.IllustrationSource + +data class ConsentRevokedBottomSheetData( + val headerIcon: IllustrationSource, + val titleText: String, + val subTitleText: String, + val ctaText: String +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/GenericErrorBottomSheetData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/GenericErrorBottomSheetData.kt new file mode 100644 index 0000000000..dc6ec555b4 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/GenericErrorBottomSheetData.kt @@ -0,0 +1,14 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.launcher.model + +data class GenericErrorBottomSheetData( + val titleText: String? = null, + val subTitleText: String? = null, + val ctaText: String? = null +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/LauncherData.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/LauncherData.kt new file mode 100644 index 0000000000..4551b77a05 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/LauncherData.kt @@ -0,0 +1,32 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.launcher.model + +import com.navi.common.alchemist.model.AlchemistScreenDefinition +import com.navi.moneymanager.common.illustration.model.IllustrationSource + +data class OnboardingStatusData( + val isOnboarded: Boolean? = null, + val errorDetails: ErrorDetails? = null +) + +data class ValuePropScreenResponse( + val valuePropScreenData: AlchemistScreenDefinition? = null, + val errorDetails: ErrorDetails? = null +) + +enum class ApiType { + ONBOARDING_STATUS_API, + VALUE_PROP_SCREEN_DATA_API +} + +data class ErrorDetails( + val titleMessage: String, + val subtitleMessage: String, + val errorImage: IllustrationSource +) diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/LauncherScreenUiEffect.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/LauncherScreenUiEffect.kt new file mode 100644 index 0000000000..a237526034 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/LauncherScreenUiEffect.kt @@ -0,0 +1,22 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.launcher.model + +import android.os.Bundle +import androidx.compose.runtime.Immutable +import com.navi.common.basemvi.UiEffect + +@Immutable +sealed class LauncherScreenUiEffect : UiEffect { + @Immutable + sealed class Navigation : LauncherScreenUiEffect() { + data object Back : Navigation() + + data class NavigateToCta(val ctaUrl: String, val bundle: Bundle? = null) : Navigation() + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/LauncherScreenUiEvent.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/LauncherScreenUiEvent.kt new file mode 100644 index 0000000000..22d2846fc4 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/LauncherScreenUiEvent.kt @@ -0,0 +1,28 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.launcher.model + +import androidx.compose.runtime.Immutable +import com.navi.common.alchemist.model.AlchemistScreenDefinition +import com.navi.common.basemvi.UiEvent + +@Immutable +sealed class LauncherScreenUiEvent : UiEvent { + + @Immutable data class UpdateOnBoardingStatus(val status: Boolean) : LauncherScreenUiEvent() + + @Immutable + data class UpdateValuePropScreenData(val data: AlchemistScreenDefinition) : + LauncherScreenUiEvent() + + @Immutable + data class OnApiFailure(val apiType: ApiType, val errorDetails: ErrorDetails) : + LauncherScreenUiEvent() + + @Immutable data class OnApiRetry(val apiType: ApiType) : LauncherScreenUiEvent() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/LauncherScreenUiState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/LauncherScreenUiState.kt new file mode 100644 index 0000000000..b4e6753a2a --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/model/LauncherScreenUiState.kt @@ -0,0 +1,30 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.launcher.model + +import androidx.compose.runtime.Immutable +import com.navi.common.alchemist.model.AlchemistScreenDefinition +import com.navi.common.basemvi.UiState + +@Immutable +data class LauncherScreenUiState( + val isOnBoarded: Boolean?, + val valuePropScreenData: AlchemistScreenDefinition?, + val onBoardingStatusError: ErrorDetails?, + val valuePropScreenDataError: ErrorDetails? +) : UiState { + companion object { + val initialState = + LauncherScreenUiState( + isOnBoarded = null, + valuePropScreenData = null, + onBoardingStatusError = null, + valuePropScreenDataError = null + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/reducer/LauncherScreenReducer.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/reducer/LauncherScreenReducer.kt new file mode 100644 index 0000000000..38d13d7ac5 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/reducer/LauncherScreenReducer.kt @@ -0,0 +1,43 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.launcher.reducer + +import com.navi.common.basemvi.BaseReducer +import com.navi.moneymanager.preonboard.launcher.model.ApiType +import com.navi.moneymanager.preonboard.launcher.model.LauncherScreenUiEvent +import com.navi.moneymanager.preonboard.launcher.model.LauncherScreenUiState + +class LauncherScreenReducer : BaseReducer { + override fun reduce( + previousState: LauncherScreenUiState, + event: LauncherScreenUiEvent + ): LauncherScreenUiState { + return when (event) { + is LauncherScreenUiEvent.UpdateOnBoardingStatus -> { + previousState.copy(isOnBoarded = event.status) + } + is LauncherScreenUiEvent.UpdateValuePropScreenData -> { + previousState.copy(valuePropScreenData = event.data) + } + is LauncherScreenUiEvent.OnApiFailure -> { + if (event.apiType == ApiType.ONBOARDING_STATUS_API) { + previousState.copy(onBoardingStatusError = event.errorDetails) + } else { + previousState.copy(valuePropScreenDataError = event.errorDetails) + } + } + is LauncherScreenUiEvent.OnApiRetry -> { + if (event.apiType == ApiType.ONBOARDING_STATUS_API) { + previousState.copy(onBoardingStatusError = null) + } else { + previousState.copy(valuePropScreenDataError = null) + } + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/repository/LauncherRepository.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/repository/LauncherRepository.kt new file mode 100644 index 0000000000..d5188aead1 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/repository/LauncherRepository.kt @@ -0,0 +1,127 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.launcher.repository + +import com.navi.common.alchemist.model.AlchemistScreenDefinition +import com.navi.common.checkmate.core.CheckMateManager +import com.navi.common.checkmate.model.MetricInfo +import com.navi.common.network.ApiConstants.NO_INTERNET +import com.navi.common.network.models.RepoResult +import com.navi.common.network.models.isSuccessWithData +import com.navi.common.network.retrofit.ResponseCallback +import com.navi.moneymanager.common.dataprovider.domain.LauncherDataProvider +import com.navi.moneymanager.common.dataprovider.domain.RemoteDataProvider +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.repository.ImageRepository.Companion.ERROR_TRIANGLE +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.utils.Constants.NO_INTERNET_TEXT +import com.navi.moneymanager.common.utils.Constants.PLEASE_CHECK_YOUR_INTERNET_CONNECTION +import com.navi.moneymanager.common.utils.Constants.PLEASE_TRY_AFTER_SOME_TIME +import com.navi.moneymanager.common.utils.Constants.SOMETHING_WENT_WRONG +import com.navi.moneymanager.common.utils.DbCacheConstants.MONEY_MANAGER_VALUE_PROPOSITION_SCREEN_KEY +import com.navi.moneymanager.common.utils.ScreenNameConstants.MONEY_MANAGER_VALUE_PROPOSITION_SCREEN +import com.navi.moneymanager.preonboard.launcher.model.ErrorDetails +import com.navi.moneymanager.preonboard.launcher.model.OnboardingStatusData +import com.navi.moneymanager.preonboard.launcher.model.ValuePropScreenResponse +import javax.inject.Inject + +class LauncherRepository +@Inject +constructor( + private val remoteDataProvider: RemoteDataProvider, + private val launcherDataProvider: LauncherDataProvider +) : ResponseCallback() { + suspend fun fetchOnboardingStatus(): OnboardingStatusData { + val onboardingStatusResponse = + remoteDataProvider.fetchUserOnboardingStatus( + screenName = MMScreen.LAUNCHER.screen, + naeApplicable = true, + ) + if (onboardingStatusResponse.isSuccessWithData()) { + onboardingStatusResponse.data?.isOnboarded?.let { onBoardingStatus -> + return OnboardingStatusData(isOnboarded = onBoardingStatus) + } + } + return OnboardingStatusData( + isOnboarded = null, + errorDetails = createErrorDetails(onboardingStatusResponse.error?.statusCode) + ) + } + + suspend fun getValuePropScreenData(): ValuePropScreenResponse { + val valuePropositionResponse = + getAlchemistScreenFromNetwork(screenName = MONEY_MANAGER_VALUE_PROPOSITION_SCREEN) + + if (valuePropositionResponse.isSuccessWithData()) { + valuePropositionResponse.data?.let { screenDefinition -> + launcherDataProvider.saveAlchemistScreenToCache( + key = MONEY_MANAGER_VALUE_PROPOSITION_SCREEN_KEY, + screenDefinition = screenDefinition + ) + return ValuePropScreenResponse( + valuePropScreenData = screenDefinition, + errorDetails = null + ) + } + } + val cachedResponse = + launcherDataProvider.fetchAlchemistScreenFromCache( + key = MONEY_MANAGER_VALUE_PROPOSITION_SCREEN_KEY + ) + + return cachedResponse?.let { + ValuePropScreenResponse(valuePropScreenData = it, errorDetails = null) + } + ?: run { + CheckMateManager.logAppErrorEvent( + metricInfo = + MetricInfo.AppMetric>( + screen = MMScreen.LAUNCHER.screen, + isNae = { true } + ), + errorCode = "", + errorTitle = "Value Proposition Screen Not Loaded", + errorDes = "Value Proposition Screen failed to load", + ) + + ValuePropScreenResponse( + valuePropScreenData = null, + errorDetails = createErrorDetails(valuePropositionResponse.error?.statusCode) + ) + } + } + + suspend fun fetchAndSaveConfigResponse() { + val networkResponse = + remoteDataProvider.fetchMMConfigResponse(screenName = MMScreen.LAUNCHER.screen) + networkResponse.data?.let { launcherDataProvider.saveMMConfigToDB(it) } + } + + private suspend fun getAlchemistScreenFromNetwork( + screenName: String + ): RepoResult { + val networkResponse = remoteDataProvider.fetchAlchemistScreen(screenName) + return networkResponse + } + + private fun createErrorDetails(statusCode: Int?): ErrorDetails { + return if (statusCode == NO_INTERNET) { + ErrorDetails( + titleMessage = NO_INTERNET_TEXT, + subtitleMessage = PLEASE_CHECK_YOUR_INTERNET_CONNECTION, + errorImage = IllustrationSource.Resource(ERROR_TRIANGLE) + ) + } else { + ErrorDetails( + titleMessage = SOMETHING_WENT_WRONG, + subtitleMessage = PLEASE_TRY_AFTER_SOME_TIME, + errorImage = IllustrationSource.Resource(ERROR_TRIANGLE) + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/ui/ConsentRevokedBottomSheet.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/ui/ConsentRevokedBottomSheet.kt new file mode 100644 index 0000000000..4e25ff0a90 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/ui/ConsentRevokedBottomSheet.kt @@ -0,0 +1,104 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.launcher.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.analytics.ValuePropEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.preonboard.launcher.model.ConsentRevokedBottomSheetData + +@Composable +fun ConsentRevokedBottomSheetContent(onDismiss: () -> Unit, data: ConsentRevokedBottomSheetData) { + LaunchedEffect(Unit) { + ValuePropEventTrackerImpl.onValuePropConsentRevokedBottomSheetAppeared() + } + DisposableEffect(Unit) { + onDispose { ValuePropEventTrackerImpl.onValuePropConsentRevokedBottomSheetDisappeared() } + } + Column( + modifier = + Modifier.fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 32.dp), + horizontalAlignment = Alignment.Start + ) { + Illustration( + modifier = Modifier.size(24.dp), + illustrationType = IllustrationType.Image(data.headerIcon) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + MMText( + text = data.titleText, + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + letterSpacing = 0.sp, + lineHeight = 24.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + + Spacer(modifier = Modifier.height(8.dp)) + + MMText( + text = data.subTitleText, + color = MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + letterSpacing = 0.sp, + lineHeight = 22.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Row( + modifier = + Modifier.fillMaxWidth() + .onClickWithDebounce { onDismiss() } + .background(color = MMColor.ctaPrimary, shape = RoundedCornerShape(4.dp)), + horizontalArrangement = Arrangement.Center + ) { + MMText( + text = data.ctaText, + modifier = Modifier.padding(vertical = 16.dp), + color = MMColor.white, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + letterSpacing = 0.sp, + textAlign = TextAlign.Center, + lineHeight = 22.sp + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/ui/GenericErrorBottomSheet.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/ui/GenericErrorBottomSheet.kt new file mode 100644 index 0000000000..dba2279aa7 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/ui/GenericErrorBottomSheet.kt @@ -0,0 +1,134 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.launcher.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.analytics.DashboardEventTrackerImpl +import com.navi.moneymanager.common.analytics.ValuePropEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationSource +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.Constants.OKAY_GOT_IT +import com.navi.moneymanager.common.utils.Constants.PLEASE_TRY_AGAIN +import com.navi.moneymanager.common.utils.Constants.SOMETHING_WENT_WRONG +import com.navi.moneymanager.preonboard.launcher.model.GenericErrorBottomSheetData +import com.navi.naviwidgets.utils.NaviWidgetIconUtils.ICON_ERROR_OUTLINED_ROUND_EXCLAMATION + +@Composable +fun GenericErrorBottomSheet( + onDismiss: () -> Unit, + data: GenericErrorBottomSheetData, + screenName: String +) { + + LaunchedEffect(Unit) { + when (screenName) { + MMScreen.VALUE_PROP_SCREEN.name -> + ValuePropEventTrackerImpl.onValuePropGenericErrorBottomSheetAppeared() + MMScreen.DASHBOARD.name -> + DashboardEventTrackerImpl.onDashboardGenericErrorBottomSheetAppeared() + } + } + + DisposableEffect(Unit) { + onDispose { + when (screenName) { + MMScreen.VALUE_PROP_SCREEN.name -> + ValuePropEventTrackerImpl.onValuePropGenericErrorBottomSheetDisappeared() + MMScreen.DASHBOARD.name -> + DashboardEventTrackerImpl.onDashboardGenericErrorBottomSheetDisappeared() + } + } + } + + Column( + modifier = + Modifier.fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 32.dp), + horizontalAlignment = Alignment.Start + ) { + Illustration( + modifier = Modifier.size(24.dp), + illustrationType = + IllustrationType.Image( + source = + IllustrationSource.Resource(resId = ICON_ERROR_OUTLINED_ROUND_EXCLAMATION) + ) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + MMText( + text = data?.titleText ?: SOMETHING_WENT_WRONG, + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + letterSpacing = 0.sp, + lineHeight = 24.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + + Spacer(modifier = Modifier.height(8.dp)) + + MMText( + text = data?.subTitleText ?: PLEASE_TRY_AGAIN, + color = MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + letterSpacing = 0.sp, + lineHeight = 22.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Row( + modifier = + Modifier.fillMaxWidth() + .onClickWithDebounce { onDismiss() } + .background(color = MMColor.ctaPrimary, shape = RoundedCornerShape(4.dp)), + horizontalArrangement = Arrangement.Center + ) { + MMText( + text = data?.ctaText ?: OKAY_GOT_IT, + modifier = Modifier.padding(vertical = 16.dp), + color = MMColor.white, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + letterSpacing = 0.sp, + textAlign = TextAlign.Center, + lineHeight = 22.sp + ) + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/ui/LauncherErrorScreen.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/ui/LauncherErrorScreen.kt new file mode 100644 index 0000000000..3dbbb44d45 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/ui/LauncherErrorScreen.kt @@ -0,0 +1,138 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.launcher.ui + +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.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Scaffold +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.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.navi.base.utils.EMPTY +import com.navi.common.utils.onClickWithDebounce +import com.navi.design.font.FontWeightEnum +import com.navi.moneymanager.common.analytics.LauncherErrorEventTrackerImpl +import com.navi.moneymanager.common.illustration.model.IllustrationType +import com.navi.moneymanager.common.illustration.ui.Illustration +import com.navi.moneymanager.common.ui.composable.MMTopBar +import com.navi.moneymanager.common.ui.composable.base.MMText +import com.navi.moneymanager.common.ui.theme.color.MMColor +import com.navi.moneymanager.common.utils.Constants +import com.navi.moneymanager.common.utils.MMScreenEventLogger +import com.navi.moneymanager.preonboard.launcher.model.ErrorDetails +import com.navi.naviwidgets.R + +@Composable +fun LauncherErrorScreen( + errorDetails: ErrorDetails, + onTryAgainClick: () -> Unit, + onBackPressClick: () -> Unit +) { + + MMScreenEventLogger( + onScreenLand = { LauncherErrorEventTrackerImpl.onLauncherErrorScreenLanded() }, + onScreenExit = { LauncherErrorEventTrackerImpl.onLauncherErrorScreenExit() } + ) + + val errorTitleText = errorDetails.titleMessage + val errorSubTitleText = errorDetails.subtitleMessage + val errorImage = errorDetails.errorImage + val buttonText = Constants.ERROR_PAGE_CTA_TEXT + + Scaffold( + modifier = Modifier.fillMaxSize().background(color = MMColor.white), + topBar = { + MMTopBar( + backgroundColor = MMColor.white, + title = EMPTY, + onNavigationIconClick = { onBackPressClick() }, + navigationIcon = R.drawable.ic_cross_black, + actionIconText = EMPTY, + titleColor = MMColor.white, + ) + }, + content = { padding -> + Box( + modifier = + Modifier.fillMaxSize().background(color = MMColor.white).padding(padding), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.fillMaxSize().padding(horizontal = 16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Illustration( + modifier = Modifier.size(160.dp), + illustrationType = IllustrationType.Image(errorImage) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + MMText( + text = errorTitleText, + color = MMColor.textPrimary, + fontSize = 16.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + letterSpacing = 0.sp, + textAlign = TextAlign.Center, + lineHeight = 24.sp + ) + + Spacer(modifier = Modifier.height(8.dp)) + + MMText( + text = errorSubTitleText, + color = MMColor.textTertiary, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_REGULAR, + letterSpacing = 0.sp, + textAlign = TextAlign.Center, + lineHeight = 22.sp + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Box( + modifier = + Modifier.onClickWithDebounce { onTryAgainClick() } + .background( + color = MMColor.ctaPrimary, + shape = RoundedCornerShape(4.dp) + ), + ) { + MMText( + text = buttonText, + modifier = Modifier.padding(horizontal = 46.dp, vertical = 12.dp), + color = Color.White, + fontSize = 14.sp, + fontWeight = FontWeightEnum.NAVI_BODY_DEMI_BOLD, + letterSpacing = 0.sp, + textAlign = TextAlign.Center, + lineHeight = 22.sp + ) + } + + Spacer(modifier = Modifier.height(128.dp)) + } + } + } + ) +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/ui/LauncherLoadingShimmer.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/ui/LauncherLoadingShimmer.kt new file mode 100644 index 0000000000..47e358cfbd --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/ui/LauncherLoadingShimmer.kt @@ -0,0 +1,67 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.launcher.ui + +import androidx.compose.foundation.layout.Box +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.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.navi.common.commoncomposables.utils.loadingShimmerEffect + +@Composable +fun LauncherLoadingShimmer(modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxSize().padding(horizontal = 16.dp, vertical = 100.dp), + ) { + Box( + modifier = + Modifier.height(28.dp) + .fillMaxWidth(0.5f) + .clip(shape = RoundedCornerShape(4.dp)) + .loadingShimmerEffect() + ) + + Spacer(modifier = Modifier.height(2.dp)) + + Box( + modifier = + Modifier.height(18.dp) + .fillMaxWidth(0.75f) + .clip(shape = RoundedCornerShape(4.dp)) + .loadingShimmerEffect() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Box( + modifier = + Modifier.height(176.dp) + .fillMaxWidth() + .clip(shape = RoundedCornerShape(4.dp)) + .loadingShimmerEffect() + ) + + Spacer(modifier = Modifier.height(40.dp)) + + Box( + modifier = + Modifier.height(355.dp) + .fillMaxWidth() + .clip(shape = RoundedCornerShape(2.dp)) + .loadingShimmerEffect() + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/ui/LauncherScreen.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/ui/LauncherScreen.kt new file mode 100644 index 0000000000..7be6fff0d7 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/ui/LauncherScreen.kt @@ -0,0 +1,121 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.launcher.ui + +import android.os.Bundle +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.navi.base.model.CtaData +import com.navi.base.utils.orFalse +import com.navi.common.navigation.NavigationAction +import com.navi.moneymanager.common.analytics.LauncherEventTrackerImpl +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.ScreenInit +import com.navi.moneymanager.common.utils.Constants.IS_CONSENT_REVOKED +import com.navi.moneymanager.common.utils.MMScreenEventLogger +import com.navi.moneymanager.entry.ui.activity.MMActivity +import com.navi.moneymanager.preonboard.launcher.model.ApiType +import com.navi.moneymanager.preonboard.launcher.model.LauncherScreenUiEffect +import com.navi.moneymanager.preonboard.launcher.viewmodel.LauncherViewModel +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootNavGraph + +@RootNavGraph(start = true) +@Destination +@Composable +fun LauncherScreen( + activity: MMActivity, + bundle: Bundle? = null, + viewModel: LauncherViewModel = hiltViewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() + val isConsentRevoked = remember { bundle?.getBoolean(IS_CONSENT_REVOKED).orFalse() } + + ScreenInit(screenName = MMScreen.LAUNCHER.screen, activity = activity) + MMScreenEventLogger( + onScreenLand = { LauncherEventTrackerImpl.onLauncherScreenLanded() }, + onScreenExit = { LauncherEventTrackerImpl.onLauncherScreenExit() } + ) + + LaunchedEffect(Unit) { + viewModel.effect.collect { effect -> + when (effect) { + is LauncherScreenUiEffect.Navigation.Back -> { + activity.finish() + } + is LauncherScreenUiEffect.Navigation.NavigateToCta -> { + activity.mmNavigator.navigateTo( + activity = activity, + ctaData = CtaData(url = effect.ctaUrl), + bundle = effect.bundle, + navigationAction = NavigationAction.ClearStackAndNext + ) + } + } + } + } + LaunchedEffect(isConsentRevoked) { viewModel.fetchData(isConsentRevoked) } + + BackHandler { viewModel.setEffect { LauncherScreenUiEffect.Navigation.Back } } + + Box(modifier = Modifier.navigationBarsPadding()) { + when { + state.isOnBoarded == true -> { + viewModel.setEffect { + LauncherScreenUiEffect.Navigation.NavigateToCta(MMScreen.DASHBOARD.screen) + } + } + state.valuePropScreenData != null -> { + state.valuePropScreenData?.let { screenDefinition -> + activity.getSharedVM().setValuePropScreenDefinition(screenDefinition) + viewModel.setEffect { + LauncherScreenUiEffect.Navigation.NavigateToCta( + MMScreen.VALUE_PROP_SCREEN.screen, + Bundle().apply { putBoolean(IS_CONSENT_REVOKED, isConsentRevoked) } + ) + } + } + } + state.onBoardingStatusError != null -> { + state.onBoardingStatusError?.let { error -> + LauncherErrorScreen( + errorDetails = error, + onTryAgainClick = { viewModel.retryApiCall(ApiType.ONBOARDING_STATUS_API) }, + onBackPressClick = { + viewModel.setEffect { LauncherScreenUiEffect.Navigation.Back } + } + ) + } + } + state.valuePropScreenDataError != null -> { + state.valuePropScreenDataError?.let { error -> + LauncherErrorScreen( + errorDetails = error, + onTryAgainClick = { + viewModel.retryApiCall(ApiType.VALUE_PROP_SCREEN_DATA_API) + }, + onBackPressClick = { + viewModel.setEffect { LauncherScreenUiEffect.Navigation.Back } + } + ) + } + } + else -> { + LauncherLoadingShimmer() + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/viewmodel/LauncherViewModel.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/viewmodel/LauncherViewModel.kt new file mode 100644 index 0000000000..607ed079bf --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/launcher/viewmodel/LauncherViewModel.kt @@ -0,0 +1,102 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.launcher.viewmodel + +import androidx.lifecycle.viewModelScope +import com.navi.moneymanager.base.viewmodel.MMBaseViewModel +import com.navi.moneymanager.common.manager.MMLibManager +import com.navi.moneymanager.preonboard.launcher.model.ApiType +import com.navi.moneymanager.preonboard.launcher.model.LauncherScreenUiEffect +import com.navi.moneymanager.preonboard.launcher.model.LauncherScreenUiEvent +import com.navi.moneymanager.preonboard.launcher.model.LauncherScreenUiState +import com.navi.moneymanager.preonboard.launcher.reducer.LauncherScreenReducer +import com.navi.moneymanager.preonboard.launcher.repository.LauncherRepository +import dagger.Lazy +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers + +@HiltViewModel +class LauncherViewModel +@Inject +constructor( + private val launcherRepository: Lazy, + private val mmLibManager: MMLibManager +) : + MMBaseViewModel( + initialState = LauncherScreenUiState.initialState, + reducer = LauncherScreenReducer() + ) { + + fun fetchData(isConsentRevoked: Boolean) { + viewModelScope.safeLaunch(Dispatchers.IO) { + if (isConsentRevoked) { + mmLibManager.clearMoneyManagerData() + fetchConfigApi() + fetchValuePropScreen() + } else { + fetchOnboardingStatus() + } + } + } + + private fun fetchOnboardingStatus() { + viewModelScope.safeLaunch(Dispatchers.IO) { + val response = launcherRepository.get().fetchOnboardingStatus() + response.isOnboarded?.let { isOnBoarded -> + sendEvent(LauncherScreenUiEvent.UpdateOnBoardingStatus(isOnBoarded)) + if (!isOnBoarded) { + fetchConfigApi() + fetchValuePropScreen() + } + } + ?: response.errorDetails?.let { + sendEvent( + LauncherScreenUiEvent.OnApiFailure( + ApiType.ONBOARDING_STATUS_API, + response.errorDetails + ) + ) + } + } + } + + private suspend fun fetchValuePropScreen() { + val response = launcherRepository.get().getValuePropScreenData() + response.valuePropScreenData?.let { data -> + sendEvent(LauncherScreenUiEvent.UpdateValuePropScreenData(data)) + } + ?: response.errorDetails?.let { + sendEvent( + LauncherScreenUiEvent.OnApiFailure( + ApiType.VALUE_PROP_SCREEN_DATA_API, + response.errorDetails + ) + ) + } + } + + private suspend fun fetchConfigApi() { + launcherRepository.get().fetchAndSaveConfigResponse() + } + + fun retryApiCall(apiType: ApiType) { + viewModelScope.safeLaunch(Dispatchers.IO) { + when (apiType) { + ApiType.ONBOARDING_STATUS_API -> { + sendEvent(LauncherScreenUiEvent.OnApiRetry(ApiType.ONBOARDING_STATUS_API)) + fetchOnboardingStatus() + } + ApiType.VALUE_PROP_SCREEN_DATA_API -> { + sendEvent(LauncherScreenUiEvent.OnApiRetry(ApiType.VALUE_PROP_SCREEN_DATA_API)) + fetchValuePropScreen() + } + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/handler/ActionsHandler.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/handler/ActionsHandler.kt new file mode 100644 index 0000000000..136a437c0c --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/handler/ActionsHandler.kt @@ -0,0 +1,51 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.valueprop.handler + +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.navi.analytics.utils.NaviTrackEvent +import com.navi.base.model.CtaData +import com.navi.common.uitron.model.action.ApiType +import com.navi.common.uitron.model.action.CtaAction +import com.navi.uitron.model.action.AnalyticsAction +import com.navi.uitron.model.action.TriggerApiAction +import com.navi.uitron.model.data.UiTronAction +import javax.inject.Inject +import timber.log.Timber + +class ActionsHandler @Inject constructor() { + fun onActionTriggered( + uiTronAction: UiTronAction?, + onCtaActionEvent: (CtaData) -> Unit, + onFetchFinarkeinDataEvent: ((TriggerApiAction) -> Unit)? = null + ) { + when (uiTronAction) { + is CtaAction -> { + uiTronAction.ctaData?.let { onCtaActionEvent(it) } + } + is AnalyticsAction -> { + NaviTrackEvent.trackEvent( + uiTronAction.eventName ?: "", + uiTronAction.eventProperties + ) + } + is TriggerApiAction -> { + when (uiTronAction.apiType) { + ApiType.MMFetchFinarkeinData.name -> { + onFetchFinarkeinDataEvent?.invoke(uiTronAction) + } + } + } + else -> { + FirebaseCrashlytics.getInstance() + .log("${uiTronAction?.type} Action not handled now") + Timber.d("Action not handled now") + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/model/ValuePropScreenUiEffect.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/model/ValuePropScreenUiEffect.kt new file mode 100644 index 0000000000..a7a7576c5f --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/model/ValuePropScreenUiEffect.kt @@ -0,0 +1,32 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.valueprop.model + +import androidx.compose.runtime.Immutable +import com.navi.base.model.CtaData +import com.navi.common.basemvi.UiEffect +import com.navi.uitron.model.action.TriggerApiAction + +@Immutable +sealed class ValuePropScreenUiEffect : UiEffect { + + @Immutable + sealed class Navigation : ValuePropScreenUiEffect() { + data object Back : Navigation() + + @Immutable data class NavigateToCta(val ctaData: CtaData) : Navigation() + + data object DismissFinarkeinBottomSheet : Navigation() + } + + @Immutable + data class FetchFinarkeinData(val triggerApiAction: TriggerApiAction) : + ValuePropScreenUiEffect() + + @Immutable data object RetryFetchFinarkeinData : ValuePropScreenUiEffect() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/model/ValuePropScreenUiEvent.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/model/ValuePropScreenUiEvent.kt new file mode 100644 index 0000000000..870c98bc7d --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/model/ValuePropScreenUiEvent.kt @@ -0,0 +1,33 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.valueprop.model + +import androidx.compose.runtime.Immutable +import com.navi.common.alchemist.model.AlchemistScreenDefinition +import com.navi.common.basemvi.UiEvent +import com.navi.moneymanager.common.model.bottomSheet.ValuePropScreenBottomSheets +import com.navi.moneymanager.postonboard.dashboard.model.FinarkeinSheetStatus + +@Immutable +sealed class ValuePropScreenUiEvent : UiEvent { + @Immutable data class RenderUI(val data: AlchemistScreenDefinition) : ValuePropScreenUiEvent() + + @Immutable + data class ShowBottomSheet(val type: ValuePropScreenBottomSheets) : ValuePropScreenUiEvent() + + @Immutable + data class DismissBottomSheet( + val type: Class = + ValuePropScreenBottomSheets.NoBottomSheet::class.java + ) : ValuePropScreenUiEvent() + + @Immutable + data class UpdateFinarkeinBottomSheetType( + val type: FinarkeinSheetStatus, + ) : ValuePropScreenUiEvent() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/model/ValuePropScreenUiState.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/model/ValuePropScreenUiState.kt new file mode 100644 index 0000000000..9bc06e25b6 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/model/ValuePropScreenUiState.kt @@ -0,0 +1,29 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.valueprop.model + +import androidx.compose.runtime.Immutable +import com.navi.common.alchemist.model.AlchemistScreenDefinition +import com.navi.common.basemvi.UiState +import com.navi.moneymanager.common.model.bottomSheet.BottomSheetState +import com.navi.moneymanager.common.model.bottomSheet.ValuePropScreenBottomSheets + +@Immutable +data class ValuePropScreenUiState( + val screenData: AlchemistScreenDefinition? = null, + val bottomSheetState: BottomSheetState +) : UiState { + companion object { + val initialState = + ValuePropScreenUiState( + screenData = null, + bottomSheetState = + BottomSheetState(type = ValuePropScreenBottomSheets.NoBottomSheet) + ) + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/reducer/ValuePropScreenReducer.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/reducer/ValuePropScreenReducer.kt new file mode 100644 index 0000000000..24320e039e --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/reducer/ValuePropScreenReducer.kt @@ -0,0 +1,58 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.valueprop.reducer + +import com.navi.common.basemvi.BaseReducer +import com.navi.moneymanager.common.model.bottomSheet.BottomSheetState +import com.navi.moneymanager.common.model.bottomSheet.ValuePropScreenBottomSheets +import com.navi.moneymanager.preonboard.valueprop.model.ValuePropScreenUiEvent +import com.navi.moneymanager.preonboard.valueprop.model.ValuePropScreenUiState + +class ValuePropScreenReducer : BaseReducer { + + override fun reduce( + previousState: ValuePropScreenUiState, + event: ValuePropScreenUiEvent + ): ValuePropScreenUiState { + return when (event) { + is ValuePropScreenUiEvent.RenderUI -> { + previousState.copy(screenData = event.data) + } + is ValuePropScreenUiEvent.ShowBottomSheet -> { + previousState.copy( + bottomSheetState = BottomSheetState(isVisible = true, type = event.type) + ) + } + is ValuePropScreenUiEvent.DismissBottomSheet -> { + val currentBottomSheet = previousState.bottomSheetState.type + if ( + event.type == ValuePropScreenBottomSheets.NoBottomSheet::class.java || + event.type.isInstance(currentBottomSheet) + ) { + previousState.copy( + bottomSheetState = previousState.bottomSheetState.copy(isVisible = false) + ) + } else previousState + } + is ValuePropScreenUiEvent.UpdateFinarkeinBottomSheetType -> { + if ( + previousState.bottomSheetState.type + is ValuePropScreenBottomSheets.FinarkeinError + ) { + val updatedBottomSheetData = + previousState.bottomSheetState.type.data?.copy(sheetStatus = event.type) + val updatedBottomSheetType = + previousState.bottomSheetState.type.copy(data = updatedBottomSheetData) + val updatedBottomSheetState = + previousState.bottomSheetState.copy(type = updatedBottomSheetType) + previousState.copy(bottomSheetState = updatedBottomSheetState) + } else previousState + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/repo/ValuePropScreenRepository.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/repo/ValuePropScreenRepository.kt new file mode 100644 index 0000000000..2f47aa7eb3 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/repo/ValuePropScreenRepository.kt @@ -0,0 +1,29 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.valueprop.repo + +import com.navi.moneymanager.common.dataprovider.domain.DashboardDataProvider +import com.navi.moneymanager.common.dataprovider.domain.RemoteDataProvider +import com.navi.moneymanager.common.navigation.utils.MMScreen +import javax.inject.Inject + +class ValuePropScreenRepository +@Inject +constructor( + private val remoteDataProvider: RemoteDataProvider, + private val dashboardDataProvider: DashboardDataProvider +) { + suspend fun fetchFinarkeinData() = + remoteDataProvider.fetchFinarkeinData(screenName = MMScreen.VALUE_PROP_SCREEN.screen) + + suspend fun getFinarkeinErrorBottomSheetData() = + dashboardDataProvider.getFinarkeinErrorBottomSheetData() + + suspend fun getConsentRevokedBottomSheetData() = + dashboardDataProvider.getConsentRevokedBottomSheetData() +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/ui/ValuePropScaffoldRenderer.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/ui/ValuePropScaffoldRenderer.kt new file mode 100644 index 0000000000..d603984f33 --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/ui/ValuePropScaffoldRenderer.kt @@ -0,0 +1,73 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.valueprop.ui + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.LocalOverscrollConfiguration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.navi.common.alchemist.model.AlchemistScreenDefinition +import com.navi.common.commoncomposables.ui.AlchemistWidgetRenderer +import com.navi.moneymanager.preonboard.valueprop.viewmodel.ValuePropViewModel +import com.navi.uitron.utils.setBackground + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ValuePropScaffoldRenderer( + valuePropScreenState: AlchemistScreenDefinition, + viewModel: () -> ValuePropViewModel, + modifier: Modifier = Modifier +) { + Scaffold( + modifier = + modifier.setBackground( + backgroundColor = null, + uiTronShape = null, + brushData = valuePropScreenState.screenStructure?.content?.backgroundBrushData + ), + topBar = { + Column(modifier = Modifier.fillMaxWidth().statusBarsPadding()) { + valuePropScreenState.screenStructure?.header?.widgets?.forEach { + AlchemistWidgetRenderer(widget = it, viewModel = viewModel()) + } + } + }, + bottomBar = { + Column(modifier = Modifier.fillMaxWidth().navigationBarsPadding()) { + valuePropScreenState.screenStructure?.footer?.widgets?.forEach { + AlchemistWidgetRenderer(widget = it, viewModel = viewModel()) + } + } + }, + containerColor = Color.Transparent + ) { innerPadding -> + CompositionLocalProvider(LocalOverscrollConfiguration provides null) { + Column( + modifier = + Modifier.fillMaxSize() + .padding(innerPadding) + .verticalScroll(rememberScrollState()) + ) { + valuePropScreenState.screenStructure?.content?.widgets?.forEach { + AlchemistWidgetRenderer(widget = it, viewModel = viewModel()) + } + } + } + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/ui/ValuePropScreen.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/ui/ValuePropScreen.kt new file mode 100644 index 0000000000..606e781a6c --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/ui/ValuePropScreen.kt @@ -0,0 +1,225 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.valueprop.ui + +import android.os.Bundle +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.navi.base.utils.orFalse +import com.navi.common.navigation.NavigationAction +import com.navi.moneymanager.common.analytics.ValuePropEventTrackerImpl +import com.navi.moneymanager.common.model.bottomSheet.ValuePropScreenBottomSheets +import com.navi.moneymanager.common.navigation.utils.MMScreen +import com.navi.moneymanager.common.ui.composable.ScreenInit +import com.navi.moneymanager.common.ui.composable.bottomSheet.BottomSheet +import com.navi.moneymanager.common.utils.Constants.IS_CONSENT_REVOKED +import com.navi.moneymanager.common.utils.Constants.OKAY_GOT_IT +import com.navi.moneymanager.common.utils.Constants.PLEASE_TRY_AGAIN +import com.navi.moneymanager.common.utils.Constants.SOMETHING_WENT_WRONG +import com.navi.moneymanager.common.utils.MMScreenEventLogger +import com.navi.moneymanager.entry.ui.activity.MMActivity +import com.navi.moneymanager.postonboard.dashboard.model.FinarkeinSheetStatus +import com.navi.moneymanager.postonboard.dashboard.ui.FinarkeinErrorBottomSheetContent +import com.navi.moneymanager.preonboard.finarkein.model.FinarkeinDataState +import com.navi.moneymanager.preonboard.launcher.model.GenericErrorBottomSheetData +import com.navi.moneymanager.preonboard.launcher.ui.ConsentRevokedBottomSheetContent +import com.navi.moneymanager.preonboard.launcher.ui.GenericErrorBottomSheet +import com.navi.moneymanager.preonboard.valueprop.model.ValuePropScreenUiEffect +import com.navi.moneymanager.preonboard.valueprop.model.ValuePropScreenUiEvent +import com.navi.moneymanager.preonboard.valueprop.viewmodel.ValuePropViewModel +import com.ramcosta.composedestinations.annotation.Destination + +@Destination +@Composable +fun ValuePropScreen( + bundle: Bundle? = null, + viewModel: ValuePropViewModel = hiltViewModel(), + activity: MMActivity +) { + val sharedVM = activity.getSharedVM() + + val state by viewModel.state.collectAsStateWithLifecycle() + val isConsentRevoked = remember { bundle?.getBoolean(IS_CONSENT_REVOKED).orFalse() } + + ScreenInit(screenName = MMScreen.VALUE_PROP_SCREEN.screen, activity = activity) + + MMScreenEventLogger( + onScreenLand = { ValuePropEventTrackerImpl.onValuePropScreenLanded() }, + onScreenExit = { ValuePropEventTrackerImpl.onValuePropScreenExit() } + ) + + LaunchedEffect(Unit) { + sharedVM.showFinarkeinErrorBottomSheet.collect { show -> + if (show) { + viewModel.showFinarkeinErrorBottomSheet() + } + } + } + + LaunchedEffect(isConsentRevoked) { + if (isConsentRevoked) { + viewModel.showConsentRevokedBottomSheet() + } + } + + LaunchedEffect(Unit) { + sharedVM.valuePropScreenDefinition?.let { + viewModel.sendEvent(ValuePropScreenUiEvent.RenderUI(it)) + } + } + + LaunchedEffect(Unit) { + viewModel.effect.collect { effect -> + when (effect) { + is ValuePropScreenUiEffect.Navigation.NavigateToCta -> { + activity.mmNavigator.navigateTo( + activity = activity, + ctaData = effect.ctaData, + bundle = effect.ctaData.bundle, + navigationAction = NavigationAction.Default + ) + } + is ValuePropScreenUiEffect.Navigation.Back -> { + activity.finish() + } + is ValuePropScreenUiEffect.Navigation.DismissFinarkeinBottomSheet -> { + sharedVM.updateFinarkeinErrorBottomSheetState(show = false) + } + is ValuePropScreenUiEffect.FetchFinarkeinData -> { + viewModel.fetchFinarkeinData(effect.triggerApiAction) { + when (it) { + FinarkeinDataState.Failure -> { + viewModel.sendEvent( + ValuePropScreenUiEvent.ShowBottomSheet( + type = + ValuePropScreenBottomSheets.GenericErrorBottomSheet( + data = + GenericErrorBottomSheetData( + titleText = SOMETHING_WENT_WRONG, + subTitleText = PLEASE_TRY_AGAIN, + ctaText = OKAY_GOT_IT + ) + ) + ) + ) + } + is FinarkeinDataState.Success -> { + activity.launchFinarkeinSdk( + accessToken = it.data.vendorAuthToken.orEmpty(), + requestId = it.data.vendorRequestId.orEmpty(), + redirectUrl = it.data.vendorRedirectUrl.orEmpty(), + ) + } + } + } + } + is ValuePropScreenUiEffect.RetryFetchFinarkeinData -> { + viewModel.retryFetchFinarkeinData { + when (it) { + FinarkeinDataState.Failure -> { + viewModel.sendEvent( + ValuePropScreenUiEvent.ShowBottomSheet( + type = + ValuePropScreenBottomSheets.GenericErrorBottomSheet( + data = + GenericErrorBottomSheetData( + titleText = SOMETHING_WENT_WRONG, + subTitleText = PLEASE_TRY_AGAIN, + ctaText = OKAY_GOT_IT + ) + ) + ) + ) + } + is FinarkeinDataState.Success -> { + activity.launchFinarkeinSdk( + accessToken = it.data.vendorAuthToken.orEmpty(), + requestId = it.data.vendorRequestId.orEmpty(), + redirectUrl = it.data.vendorRedirectUrl.orEmpty(), + ) + } + } + } + } + } + } + } + + BackHandler { viewModel.setEffect { ValuePropScreenUiEffect.Navigation.Back } } + + if (state.screenData != null) { + ValuePropScaffoldRenderer( + valuePropScreenState = state.screenData!!, + viewModel = { viewModel }, + modifier = Modifier.fillMaxSize() + ) + } + + BottomSheet( + state = state.bottomSheetState, + onDismiss = { type, uiEvent -> viewModel.sendEvent(uiEvent) } + ) { bottomSheet -> + ValuePropBottomSheetContentHandler( + bottomSheet = bottomSheet, + onEvent = { viewModel.sendEvent(it) }, + onEffect = { viewModel.setEffect { it } } + ) + } +} + +@Composable +fun ValuePropBottomSheetContentHandler( + bottomSheet: ValuePropScreenBottomSheets, + onEvent: (ValuePropScreenUiEvent) -> Unit, + onEffect: (ValuePropScreenUiEffect) -> Unit +) { + if (bottomSheet.sheetData == null) return + val onDismissAction = bottomSheet.createDismissAction(onEvent) + when (bottomSheet) { + is ValuePropScreenBottomSheets.FinarkeinError -> { + FinarkeinErrorBottomSheetContent( + screenName = MMScreen.VALUE_PROP_SCREEN.name, + data = bottomSheet.data!!, + onRetry = { + onEvent( + ValuePropScreenUiEvent.UpdateFinarkeinBottomSheetType( + FinarkeinSheetStatus.Retry + ) + ) + onEffect(ValuePropScreenUiEffect.Navigation.DismissFinarkeinBottomSheet) + onEffect(ValuePropScreenUiEffect.RetryFetchFinarkeinData) + }, + onDismiss = { + onEffect(ValuePropScreenUiEffect.Navigation.DismissFinarkeinBottomSheet) + onDismissAction() + } + ) + } + is ValuePropScreenBottomSheets.ConsentRevokedBottomSheet -> { + ConsentRevokedBottomSheetContent( + data = bottomSheet.data!!, + onDismiss = { onDismissAction() } + ) + } + is ValuePropScreenBottomSheets.GenericErrorBottomSheet -> { + GenericErrorBottomSheet( + data = bottomSheet.data!!, + onDismiss = { onDismissAction() }, + screenName = MMScreen.VALUE_PROP_SCREEN.name + ) + } + ValuePropScreenBottomSheets.NoBottomSheet -> {} + } +} diff --git a/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/viewmodel/ValuePropViewModel.kt b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/viewmodel/ValuePropViewModel.kt new file mode 100644 index 0000000000..7aa4212a3f --- /dev/null +++ b/android/navi-money-manager/src/main/kotlin/com/navi/moneymanager/preonboard/valueprop/viewmodel/ValuePropViewModel.kt @@ -0,0 +1,110 @@ +/* + * + * * Copyright © 2024 by Navi Technologies Limited + * * All rights reserved. Strictly confidential + * + */ + +package com.navi.moneymanager.preonboard.valueprop.viewmodel + +import androidx.lifecycle.viewModelScope +import com.navi.common.network.models.isSuccessWithData +import com.navi.moneymanager.base.viewmodel.MMBaseViewModel +import com.navi.moneymanager.common.model.bottomSheet.ValuePropScreenBottomSheets +import com.navi.moneymanager.common.utils.checkFinarkeinDataValidity +import com.navi.moneymanager.preonboard.finarkein.model.FinarkeinDataState +import com.navi.moneymanager.preonboard.valueprop.handler.ActionsHandler +import com.navi.moneymanager.preonboard.valueprop.model.ValuePropScreenUiEffect +import com.navi.moneymanager.preonboard.valueprop.model.ValuePropScreenUiEvent +import com.navi.moneymanager.preonboard.valueprop.model.ValuePropScreenUiState +import com.navi.moneymanager.preonboard.valueprop.reducer.ValuePropScreenReducer +import com.navi.moneymanager.preonboard.valueprop.repo.ValuePropScreenRepository +import com.navi.uitron.model.action.TriggerApiAction +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers + +@HiltViewModel +class ValuePropViewModel +@Inject +constructor( + private val actionsHandler: ActionsHandler, + private val valuePropScreenRepository: ValuePropScreenRepository +) : + MMBaseViewModel( + initialState = ValuePropScreenUiState.initialState, + reducer = ValuePropScreenReducer() + ) { + + init { + viewModelScope.safeLaunch(Dispatchers.IO) { observeActionCallback() } + } + + private suspend fun observeActionCallback() { + getActionCallback().collect { uiTronAction -> + actionsHandler.onActionTriggered( + uiTronAction = uiTronAction, + onCtaActionEvent = { + setEffect { ValuePropScreenUiEffect.Navigation.NavigateToCta(it) } + }, + onFetchFinarkeinDataEvent = { + setEffect { ValuePropScreenUiEffect.FetchFinarkeinData(it) } + } + ) + } + } + + fun fetchFinarkeinData( + triggerApiAction: TriggerApiAction, + callback: (FinarkeinDataState) -> Unit + ) { + viewModelScope.safeLaunch(Dispatchers.IO) { + val response = valuePropScreenRepository.fetchFinarkeinData() + if (response.isSuccessWithData() && checkFinarkeinDataValidity(response.data)) { + callback(FinarkeinDataState.Success(response.data!!)) + handleActions(triggerApiAction.onSuccess) + } else { + callback(FinarkeinDataState.Failure) + handleActions(triggerApiAction.onFailure) + } + } + } + + fun retryFetchFinarkeinData(callback: (FinarkeinDataState) -> Unit) { + viewModelScope.safeLaunch(Dispatchers.IO) { + val response = valuePropScreenRepository.fetchFinarkeinData() + sendEvent( + ValuePropScreenUiEvent.DismissBottomSheet( + ValuePropScreenBottomSheets.FinarkeinError::class.java + ) + ) + if (response.isSuccessWithData() && checkFinarkeinDataValidity(response.data)) { + callback(FinarkeinDataState.Success(response.data!!)) + } else { + callback(FinarkeinDataState.Failure) + } + } + } + + fun showFinarkeinErrorBottomSheet() { + viewModelScope.safeLaunch(Dispatchers.IO) { + val data = valuePropScreenRepository.getFinarkeinErrorBottomSheetData() + sendEvent( + ValuePropScreenUiEvent.ShowBottomSheet( + type = ValuePropScreenBottomSheets.FinarkeinError(data) + ) + ) + } + } + + fun showConsentRevokedBottomSheet() { + viewModelScope.safeLaunch(Dispatchers.IO) { + val data = valuePropScreenRepository.getConsentRevokedBottomSheetData() + sendEvent( + ValuePropScreenUiEvent.ShowBottomSheet( + type = ValuePropScreenBottomSheets.ConsentRevokedBottomSheet(data) + ) + ) + } + } +} diff --git a/android/navi-money-manager/src/main/res/drawable/all_bank_image.xml b/android/navi-money-manager/src/main/res/drawable/all_bank_image.xml new file mode 100644 index 0000000000..7a5478bd2a --- /dev/null +++ b/android/navi-money-manager/src/main/res/drawable/all_bank_image.xml @@ -0,0 +1,44 @@ + + + + + + + + + + diff --git a/android/navi-money-manager/src/main/res/drawable/all_banks_icon_small.xml b/android/navi-money-manager/src/main/res/drawable/all_banks_icon_small.xml new file mode 100644 index 0000000000..db7f8e5ec4 --- /dev/null +++ b/android/navi-money-manager/src/main/res/drawable/all_banks_icon_small.xml @@ -0,0 +1,38 @@ + + + + + + + + diff --git a/android/navi-money-manager/src/main/res/drawable/plus_icon.xml b/android/navi-money-manager/src/main/res/drawable/plus_icon.xml new file mode 100644 index 0000000000..a3d8fc14fd --- /dev/null +++ b/android/navi-money-manager/src/main/res/drawable/plus_icon.xml @@ -0,0 +1,14 @@ + + + + diff --git a/android/navi-money-manager/src/main/res/raw/chip_loader_lottie.json b/android/navi-money-manager/src/main/res/raw/chip_loader_lottie.json new file mode 100644 index 0000000000..4807e9ea60 --- /dev/null +++ b/android/navi-money-manager/src/main/res/raw/chip_loader_lottie.json @@ -0,0 +1,1116 @@ +{ + "v": "5.9.0", + "fr": 60, + "ip": 0, + "op": 60, + "w": 24, + "h": 24, + "nm": "Loader_24x24px", + "ddd": 0, + "assets": [ + { + "id": "comp_0", + "nm": "Loader_32X32px", + "fr": 60, + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 0, + "nm": "Loader_64x64px", + "refId": "comp_1", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 16, + 16, + 0 + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 32, + 32, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 0, + "k": [ + 50, + 50, + 100 + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "w": 64, + "h": 64, + "ip": 0, + "op": 60.0600600600601, + "st": 0, + "bm": 0 + } + ] + }, + { + "id": "comp_1", + "nm": "Loader_64x64px", + "fr": 29.9700012207031, + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 0, + "nm": "Loader 500x500", + "refId": "comp_2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 32, + 32, + 0 + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 250, + 250, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 0, + "k": [ + 13, + 13, + 100 + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "w": 500, + "h": 500, + "ip": 0, + "op": 60, + "st": 0, + "bm": 0 + } + ] + }, + { + "id": "comp_2", + "nm": "Loader 500x500", + "fr": 30, + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 4, + "nm": "Layer 3 Outlines", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 0, + "s": [ + 0 + ] + }, + { + "t": 58, + "s": [ + 360 + ] + } + ], + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 250.125, + 249.875, + 0 + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 241.545, + 241.544, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 14.359, + 0 + ], + [ + 0, + 14.359 + ], + [ + 104.446, + 0 + ], + [ + 0, + 14.359 + ], + [ + -14.359, + 0 + ], + [ + -45.599, + -45.599 + ], + [ + 0, + -64.485 + ] + ], + "o": [ + [ + -14.359, + 0 + ], + [ + 0, + -104.446 + ], + [ + -14.359, + 0 + ], + [ + 0, + -14.359 + ], + [ + 64.485, + 0 + ], + [ + 45.598, + 45.598 + ], + [ + 0, + 14.359 + ] + ], + "v": [ + [ + 107.71, + 133.71 + ], + [ + 81.71, + 107.71 + ], + [ + -107.71, + -81.71 + ], + [ + -133.71, + -107.71 + ], + [ + -107.71, + -133.71 + ], + [ + 63, + -62.999 + ], + [ + 133.71, + 107.71 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "gf", + "o": { + "a": 0, + "k": 100, + "ix": 10 + }, + "r": 1, + "bm": 0, + "g": { + "p": 5, + "k": { + "a": 0, + "k": [ + 0, + 0.122, + 0, + 0.165, + 0.164, + 0.122, + 0, + 0.165, + 0.327, + 0.122, + 0, + 0.165, + 0.664, + 0.122, + 0, + 0.165, + 1, + 0.122, + 0, + 0.165, + 0, + 0, + 0.5, + 0.5, + 1, + 1 + ], + "ix": 9 + } + }, + "s": { + "a": 0, + "k": [ + -120.719, + -108.977 + ], + "ix": 5 + }, + "e": { + "a": 0, + "k": [ + 111.406, + 131.836 + ], + "ix": 6 + }, + "t": 1, + "nm": "Gradient Fill 1", + "mn": "ADBE Vector Graphic - G-Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 349.13, + 133.96 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 1", + "np": 2, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + }, + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 104.446, + 0 + ], + [ + 0, + -104.446 + ], + [ + -104.446, + 0 + ], + [ + 0, + 104.446 + ] + ], + "o": [ + [ + -104.446, + 0 + ], + [ + 0, + 104.446 + ], + [ + 104.446, + 0 + ], + [ + 0, + -104.446 + ] + ], + "v": [ + [ + 0, + -189.419 + ], + [ + -189.42, + 0.001 + ], + [ + 0, + 189.42 + ], + [ + 189.42, + 0.001 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ind": 1, + "ty": "sh", + "ix": 2, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 64.485, + 0 + ], + [ + 45.598, + 45.598 + ], + [ + 0, + 64.485 + ], + [ + -45.599, + 45.599 + ], + [ + -64.485, + 0 + ], + [ + -45.598, + -45.598 + ], + [ + 0, + -64.485 + ], + [ + 45.599, + -45.598 + ] + ], + "o": [ + [ + -64.485, + 0 + ], + [ + -45.599, + -45.598 + ], + [ + 0, + -64.485 + ], + [ + 45.598, + -45.598 + ], + [ + 64.485, + 0 + ], + [ + 45.599, + 45.599 + ], + [ + 0, + 64.485 + ], + [ + -45.598, + 45.598 + ] + ], + "v": [ + [ + 0, + 241.42 + ], + [ + -170.709, + 170.71 + ], + [ + -241.42, + 0.001 + ], + [ + -170.709, + -170.709 + ], + [ + 0, + -241.419 + ], + [ + 170.709, + -170.709 + ], + [ + 241.42, + 0.001 + ], + [ + 170.709, + 170.71 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 2", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.121568627656, + 0, + 0.164705887437, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 25, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 241.42, + 241.67 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 100, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 3", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 2, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60, + "st": 0, + "bm": 0 + }, + { + "ddd": 0, + "ind": 2, + "ty": 4, + "nm": "Layer 3 Outlines 2", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 250.125, + 249.875, + 0 + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 241.545, + 241.544, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ind": 0, + "ty": "sh", + "ix": 1, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 104.446, + 0 + ], + [ + 0, + -104.446 + ], + [ + -104.446, + 0 + ], + [ + 0, + 104.446 + ] + ], + "o": [ + [ + -104.446, + 0 + ], + [ + 0, + 104.446 + ], + [ + 104.446, + 0 + ], + [ + 0, + -104.446 + ] + ], + "v": [ + [ + 0, + -189.419 + ], + [ + -189.42, + 0.001 + ], + [ + 0, + 189.42 + ], + [ + 189.42, + 0.001 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 1", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ind": 1, + "ty": "sh", + "ix": 2, + "ks": { + "a": 0, + "k": { + "i": [ + [ + 64.485, + 0 + ], + [ + 45.598, + 45.598 + ], + [ + 0, + 64.485 + ], + [ + -45.599, + 45.599 + ], + [ + -64.485, + 0 + ], + [ + -45.598, + -45.598 + ], + [ + 0, + -64.485 + ], + [ + 45.599, + -45.598 + ] + ], + "o": [ + [ + -64.485, + 0 + ], + [ + -45.599, + -45.598 + ], + [ + 0, + -64.485 + ], + [ + 45.598, + -45.598 + ], + [ + 64.485, + 0 + ], + [ + 45.599, + 45.599 + ], + [ + 0, + 64.485 + ], + [ + -45.598, + 45.598 + ] + ], + "v": [ + [ + 0, + 241.42 + ], + [ + -170.709, + 170.71 + ], + [ + -241.42, + 0.001 + ], + [ + -170.709, + -170.709 + ], + [ + 0, + -241.419 + ], + [ + 170.709, + -170.709 + ], + [ + 241.42, + 0.001 + ], + [ + 170.709, + 170.71 + ] + ], + "c": true + }, + "ix": 2 + }, + "nm": "Path 2", + "mn": "ADBE Vector Shape - Group", + "hd": false + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 1, + 1, + 1, + 1 + ], + "ix": 4 + }, + "o": { + "a": 0, + "k": 100, + "ix": 5 + }, + "r": 1, + "bm": 0, + "nm": "Fill 1", + "mn": "ADBE Vector Graphic - Fill", + "hd": false + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 241.42, + 241.67 + ], + "ix": 2 + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ], + "ix": 1 + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ], + "ix": 3 + }, + "r": { + "a": 0, + "k": 0, + "ix": 6 + }, + "o": { + "a": 0, + "k": 25, + "ix": 7 + }, + "sk": { + "a": 0, + "k": 0, + "ix": 4 + }, + "sa": { + "a": 0, + "k": 0, + "ix": 5 + }, + "nm": "Transform" + } + ], + "nm": "Group 2", + "np": 3, + "cix": 2, + "bm": 0, + "ix": 1, + "mn": "ADBE Vector Group", + "hd": false + } + ], + "ip": 0, + "op": 60, + "st": 0, + "bm": 0 + } + ] + } + ], + "layers": [ + { + "ddd": 0, + "ind": 1, + "ty": 0, + "nm": "Loader_32X32px", + "refId": "comp_0", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100, + "ix": 11 + }, + "r": { + "a": 0, + "k": 0, + "ix": 10 + }, + "p": { + "a": 0, + "k": [ + 12, + 12, + 0 + ], + "ix": 2, + "l": 2 + }, + "a": { + "a": 0, + "k": [ + 16, + 16, + 0 + ], + "ix": 1, + "l": 2 + }, + "s": { + "a": 0, + "k": [ + 75, + 75, + 100 + ], + "ix": 6, + "l": 2 + } + }, + "ao": 0, + "w": 32, + "h": 32, + "ip": 0, + "op": 300, + "st": 0, + "bm": 0 + } + ], + "markers": [] +} \ No newline at end of file diff --git a/android/navi-money-manager/src/main/res/raw/circular_loader_lottie.json b/android/navi-money-manager/src/main/res/raw/circular_loader_lottie.json new file mode 100644 index 0000000000..ad1527650c --- /dev/null +++ b/android/navi-money-manager/src/main/res/raw/circular_loader_lottie.json @@ -0,0 +1 @@ +{"v":"5.9.0","fr":29.9700012207031,"ip":0,"op":54.0000021994651,"w":160,"h":160,"nm":"Loader_160px","ddd":0,"assets":[{"id":"comp_0","nm":"Loader Green purple_550px","fr":29.9700012207031,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"White tick Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[263.758,266.696,0],"ix":2,"l":2},"a":{"a":0,"k":[255.42,215.618,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-101.126,0.719],[-30.999,71.303],[102.964,-63.445]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":54,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[255.42,215.618],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.276],"y":[0]},"t":66,"s":[0]},{"t":74.0000030140818,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":55.0000022401959,"op":205.000008349821,"st":55.0000022401959,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Green circle Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[275.076,275.076,0],"ix":2,"l":2},"a":{"a":0,"k":[217.968,217.968,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.273,0.273,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":57.334,"s":[0,0,100]},{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":65.5,"s":[110,110,100]},{"t":69.0000028104276,"s":[95,95,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-119.748,0],[0,-119.747],[119.748,0],[0,119.748]],"o":[[119.748,0],[0,119.748],[-119.748,0],[0,-119.747]],"v":[[0,-216.822],[216.822,0],[0,216.822],[-216.822,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.133333333333,0.81568627451,0.505882352941,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[217.968,217.968],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":55.0000022401959,"op":205.000008349821,"st":55.0000022401959,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"White Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[275,275,0],"ix":2,"l":2},"a":{"a":0,"k":[251.11,251.11,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.273,0.273,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":55,"s":[0,0,100]},{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":63.166,"s":[110,110,100]},{"t":66.666252715372,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-138.071,0],[0,-138.071],[138.071,0],[0,138.071]],"o":[[138.071,0],[0,138.071],[-138.071,0],[0,-138.071]],"v":[[0,-250],[250,0],[0,250],[-250,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.328941285376,0.943673406863,0.647552490234,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[251.11,251.11],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":55.0000022401959,"op":205.000008349821,"st":55.0000022401959,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Green Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":53.9625021979377,"s":[360]}],"ix":10},"p":{"a":0,"k":[275,275,0],"ix":2,"l":2},"a":{"a":0,"k":[267.962,267.962,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-126.89,0],[0,-126.89],[126.89,0],[0,126.89]],"o":[[126.89,0],[0,126.89],[-126.89,0],[0,-126.89]],"v":[[0,-229.755],[229.755,0],[0,229.755],[-229.755,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.254901960784,0.898039215686,0.588235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":44,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[267.962,267.962],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":17.925,"s":[0]},{"t":55.0000022401959,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":43.5850017752534,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Light purple Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[275,275,0],"ix":2,"l":2},"a":{"a":0,"k":[267.962,267.962,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-126.89,0],[0,-126.89],[126.89,0],[0,126.89]],"o":[[126.89,0],[0,126.89],[-126.89,0],[0,-126.89]],"v":[[0,-229.755],[229.755,0],[0,229.755],[-229.755,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.949019607843,0.949019607843,0.949019607843,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":44,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[267.962,267.962],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Loader Green purple_550px","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[80,80,0],"ix":2,"l":2},"a":{"a":0,"k":[275,275,0],"ix":1,"l":2},"s":{"a":0,"k":[29,29,100],"ix":6,"l":2}},"ao":0,"w":550,"h":550,"ip":-1.00000004073083,"op":149.000006068894,"st":-1.00000004073083,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/android/navi-money-manager/src/main/res/raw/mm_mock.json b/android/navi-money-manager/src/main/res/raw/mm_mock.json new file mode 100644 index 0000000000..959821c597 --- /dev/null +++ b/android/navi-money-manager/src/main/res/raw/mm_mock.json @@ -0,0 +1,250 @@ +{ + "config_response": { + "timestampConfig": { + "currentTimestamp": 1729083768, + "threshold": 1000, + "currentMonthStartTimestamp": 1727817600, + "twelveMonthsOldTimestamp": 1701398400 + }, + "dataSyncPollingConfig": { + "maxAttempts": 5, + "initialDelaySeconds": 1, + "taskIntervalSeconds": 3 + }, + "accountLinkingPollingConfig": { + "maxAttempts": 10, + "initialDelaySeconds": 0, + "taskIntervalSeconds": 1 + }, + "paginationConfig": { + "pageSize": 100 + }, + "categories": [ + { + "categoryId": "ATM", + "categoryName": "ATM", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/atm.svg" + }, + { + "categoryId": "BILLS", + "categoryName": "Bills", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/utilities.svg", + "isRecommended": true + }, + { + "categoryId": "COMMUTE", + "categoryName": "Commute", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/commute.svg" + }, + { + "categoryId": "CREDIT_CARD_PAYMENTS", + "categoryName": "Credit card payments", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/credit-card.svg", + "isRecommended": true + }, + { + "categoryId": "EDUCATION", + "categoryName": "Education", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/education.svg" + }, + { + "categoryId": "EMIS", + "categoryName": "EMIs", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/emi.svg", + "isRecommended": true + }, + { + "categoryId": "ENTERTAINMENT", + "categoryName": "Entertainment", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/entertainment.svg" + }, + { + "categoryId": "FOOD_AND_DRINKS", + "categoryName": "Food & drinks", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/restaurants-and-deliveries.svg" + }, + { + "categoryId": "FUEL", + "categoryName": "Fuel", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/fuel.svg" + }, + { + "categoryId": "GROCERIES", + "categoryName": "Groceries", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/groceries.svg" + }, + { + "categoryId": "INSURANCE", + "categoryName": "Insurance", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/insurance.svg" + }, + { + "categoryId": "INVESTMENTS", + "categoryName": "Investments", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/investments.svg", + "isRecommended": true + }, + { + "categoryId": "MONEY_TRANSFERS", + "categoryName": "Money transfers", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/money-transfers.svg" + }, + { + "categoryId": "SELF_TRANSFER", + "categoryName": "Self transfer", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/self-transfer.svg" + }, + { + "categoryId": "SHOPPING", + "categoryName": "Shopping", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/shopping.svg" + }, + { + "categoryId": "TRAVEL", + "categoryName": "Travel", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/travel.svg" + }, + { + "categoryId": "UNCATEGORIZED", + "categoryName": "Add category", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/uncategorized.svg" + }, + { + "categoryId": "WALLETS", + "categoryName": "Wallets", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/wallet.svg" + } + ] + + }, + "config_response_srs": { + "timestampConfig": { + "currentTimestamp": 1729083768, + "currentMonthStartTimestamp": 1727817600, + "threshold": 0, + "twelveMonthsOldTimestamp": 1701398400 + }, + "dataSyncPollingConfig": { + "maxAttempts": 5, + "initialDelaySeconds": 1, + "taskIntervalSeconds": 3 + }, + "accountLinkingPollingConfig": { + "maxAttempts": 10, + "initialDelaySeconds": 0, + "taskIntervalSeconds": 1 + }, + "paginationConfig": { + "pageSize": 100 + }, + "onboardingBottomSheetTimeout": 40000, + "categories": [ + { + "categoryId": "ATM", + "categoryName": "ATM", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/atm.svg" + }, + { + "categoryId": "BILLS", + "categoryName": "Bills", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/utilities.svg", + "isRecommended": true + }, + { + "categoryId": "COMMUTE", + "categoryName": "Commute", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/commute.svg" + }, + { + "categoryId": "CREDIT_CARD_PAYMENTS", + "categoryName": "Credit card payments", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/credit-card.svg", + "isRecommended": true + }, + { + "categoryId": "EDUCATION", + "categoryName": "Education", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/education.svg" + }, + { + "categoryId": "EMIS", + "categoryName": "EMIs", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/emi.svg", + "isRecommended": true + }, + { + "categoryId": "ENTERTAINMENT", + "categoryName": "Entertainment", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/entertainment.svg" + }, + { + "categoryId": "FOOD_AND_DRINKS", + "categoryName": "Food & drinks", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/restaurants-and-deliveries.svg" + }, + { + "categoryId": "FUEL", + "categoryName": "Fuel", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/fuel.svg" + }, + { + "categoryId": "GROCERIES", + "categoryName": "Groceries", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/groceries.svg" + }, + { + "categoryId": "INSURANCE", + "categoryName": "Insurance", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/insurance.svg" + }, + { + "categoryId": "INVESTMENTS", + "categoryName": "Investments", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/investments.svg", + "isRecommended": true + }, + { + "categoryId": "MONEY_TRANSFERS", + "categoryName": "Money transfers", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/money-transfers.svg" + }, + { + "categoryId": "SELF_TRANSFER", + "categoryName": "Self transfer", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/self-transfer.svg" + }, + { + "categoryId": "SHOPPING", + "categoryName": "Shopping", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/shopping.svg" + }, + { + "categoryId": "TRAVEL", + "categoryName": "Travel", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/travel.svg" + }, + { + "categoryId": "UNCATEGORIZED", + "categoryName": "Add category", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/uncategorized.svg" + }, + { + "categoryId": "WALLETS", + "categoryName": "Wallets", + "categoryIcon": "https://public-assets.prod.navi-sa.in/money-manager/svg/category/wallet.svg" + } + ] + }, + "refreshData": { + "requestId": "4d23cf69-b24d-430a-b931-adbe1616889f", + "lastRefreshSuccessTimestamp": 1729082948 + }, + "pollingData": { + "accountDetailsStatus": "COMPLETED", + "currMonthTxnsStatus": "COMPLETED", + "oldTxnsStatus": "COMPLETED" + }, + "consentData": { + "url": "https://www.google.com/" + } +} \ No newline at end of file diff --git a/android/navi-money-manager/src/main/res/raw/success_tick_lottie.json b/android/navi-money-manager/src/main/res/raw/success_tick_lottie.json new file mode 100644 index 0000000000..731546c231 --- /dev/null +++ b/android/navi-money-manager/src/main/res/raw/success_tick_lottie.json @@ -0,0 +1 @@ +{"v":"5.9.0","fr":29.9700012207031,"ip":54.0000021994651,"op":84.0000034213901,"w":160,"h":160,"nm":"Successful_160px","ddd":0,"assets":[{"id":"comp_0","nm":"Loader Green purple_550px","fr":29.9700012207031,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"White tick Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[263.758,266.696,0],"ix":2,"l":2},"a":{"a":0,"k":[255.42,215.618,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-101.126,0.719],[-30.999,71.303],[102.964,-63.445]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":54,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[255.42,215.618],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.276],"y":[0]},"t":66,"s":[0]},{"t":74.0000030140818,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":55.0000022401959,"op":205.000008349821,"st":55.0000022401959,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Green circle Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[275.076,275.076,0],"ix":2,"l":2},"a":{"a":0,"k":[217.968,217.968,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.273,0.273,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":57.334,"s":[0,0,100]},{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":65.5,"s":[110,110,100]},{"t":69.0000028104276,"s":[95,95,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-119.748,0],[0,-119.747],[119.748,0],[0,119.748]],"o":[[119.748,0],[0,119.748],[-119.748,0],[0,-119.747]],"v":[[0,-216.822],[216.822,0],[0,216.822],[-216.822,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.133333333333,0.81568627451,0.505882352941,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[217.968,217.968],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":55.0000022401959,"op":205.000008349821,"st":55.0000022401959,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"White Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[275,275,0],"ix":2,"l":2},"a":{"a":0,"k":[251.11,251.11,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.273,0.273,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":55,"s":[0,0,100]},{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":63.166,"s":[110,110,100]},{"t":66.666252715372,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-138.071,0],[0,-138.071],[138.071,0],[0,138.071]],"o":[[138.071,0],[0,138.071],[-138.071,0],[0,-138.071]],"v":[[0,-250],[250,0],[0,250],[-250,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.328941285376,0.943673406863,0.647552490234,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[251.11,251.11],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":55.0000022401959,"op":205.000008349821,"st":55.0000022401959,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Green Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":53.9625021979377,"s":[360]}],"ix":10},"p":{"a":0,"k":[275,275,0],"ix":2,"l":2},"a":{"a":0,"k":[267.962,267.962,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-126.89,0],[0,-126.89],[126.89,0],[0,126.89]],"o":[[126.89,0],[0,126.89],[-126.89,0],[0,-126.89]],"v":[[0,-229.755],[229.755,0],[0,229.755],[-229.755,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.254901960784,0.898039215686,0.588235294118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":44,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[267.962,267.962],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":17.925,"s":[0]},{"t":55.0000022401959,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":43.5850017752534,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Light purple Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[275,275,0],"ix":2,"l":2},"a":{"a":0,"k":[267.962,267.962,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-126.89,0],[0,-126.89],[126.89,0],[0,126.89]],"o":[[126.89,0],[0,126.89],[-126.89,0],[0,-126.89]],"v":[[0,-229.755],[229.755,0],[0,229.755],[-229.755,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.949019607843,0.949019607843,0.949019607843,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":44,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[267.962,267.962],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Loader Green purple_550px","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[80,80,0],"ix":2,"l":2},"a":{"a":0,"k":[275,275,0],"ix":1,"l":2},"s":{"a":0,"k":[29,29,100],"ix":6,"l":2}},"ao":0,"w":550,"h":550,"ip":-1.00000004073083,"op":149.000006068894,"st":-1.00000004073083,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/android/navi-money-manager/src/main/res/values/strings.xml b/android/navi-money-manager/src/main/res/values/strings.xml new file mode 100644 index 0000000000..1a89e9b512 --- /dev/null +++ b/android/navi-money-manager/src/main/res/values/strings.xml @@ -0,0 +1,126 @@ + + + + All banks + All accounts + Add account + Last updated + Fetching bank + Fetching your past transactions… + Total balance + Take a closer look at where your money goes + Hi + Hello + just now + 1 min ago + %1$d mins ago + 1 hr ago + %1$d hrs ago + 1 day ago + %1$d days ago + Spend Analysis Section + Self transfer + Not included in spends + TOP SPEND CATEGORIES + View more + Monthly summary + Total spends + No transactions to \ncategorise + Add another account + Category %1$d + Transaction name + Fetching transactions… + ₹0.00 + Account(s) successfully\nlinked + Paid + Received + Add category + Paid to + Paid by + Paid from + Received in + Mode + Narration + Categorising spends + It’s taking longer than usual + The bank is taking longer than expected to share the data.\n\nYou can either wait or come later. We’ll notify you whenever your data gets fetched. + Notify me + Done + Please do not press back or close the app. This usually takes a few minutes. + Connecting with your banks + Tracking recent transactions + Unknown + Fetching your past months transactions + Select month + Select categories + Apply + Please wait while we process your past transactions + Okay, got it + Okay got it + APPLY + Total spend + Avg : + SPEND CATEGORIES + Recent transactions + View all transactions + View transaction history + Transaction history + No transactions found + Name, category & amount + Others + On %1$s + Recommended categories + MORE CATEGORIES + Confirm + Select the transactions you wish to categorise with + %1$s similar transaction found + Auto-categorise all future transactions like this under + Categorise & proceed + Skip past transactions for now + Refreshing… + Fetching your transactions + This usually takes a few minutes. You will be notified once it is done + Past spends + Fetching past spends + Select bank account(s) + Apply + Average :\ + We\'ve analyzed your spending over the last 5 months to give you an average monthly spend, helping you track expenses and budget smarter. + Retry + Do later + Please try again + Something went wrong + Filter + Clear all + Credit + Debit + Date & time + Category + Something went wrong try again + transactions categorized + transaction categorized + What are uncategorised expenses? + Uncategorised expenses are transactions that don\'t yet have a specific category. \n\nYou can assign them to a category by clicking the \'+ Add category\' button. + Highest first + Lowest first + Recent first + Sort transactions + new transaction + new transactions + Your account(s) have been removed successfully + To continue tracking your money, you\'ll need to complete the onboarding process again + Select all + Get help + Frequently asked questions + Manage consent + Please wait for some time to add more banks + Please wait a moment while we finish syncing your current bank accounts. This usually takes a few minutes. Once done, you’ll be able to add more banks. + < 1% + Choose a category + + diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/error/ErrorHandler.kt b/android/navi-rr/src/main/java/com/navi/rr/common/error/ErrorHandler.kt index 7c63a5cd31..18972c9fb4 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/common/error/ErrorHandler.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/common/error/ErrorHandler.kt @@ -7,6 +7,7 @@ package com.navi.rr.common.error +import com.navi.common.R as commonR import com.navi.common.network.ApiConstants import com.navi.rr.R import com.navi.rr.common.models.RRErrorData @@ -63,7 +64,7 @@ class ErrorHandlerImpl @Inject constructor() : ErrorHandler { override fun getErrorImageAsPerStatusCode(statusCode: Int): Int { return when (statusCode) { ApiConstants.NO_INTERNET -> R.drawable.no_internet_connection - else -> R.drawable.something_went_wrong_triangle + else -> commonR.drawable.something_went_wrong_triangle } } diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/navigation/registry/RewardComposableRegistry.kt b/android/navi-rr/src/main/java/com/navi/rr/common/navigation/registry/RewardComposableRegistry.kt index 77b5a8bdbd..6a4fff7642 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/common/navigation/registry/RewardComposableRegistry.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/common/navigation/registry/RewardComposableRegistry.kt @@ -9,6 +9,7 @@ package com.navi.rr.common.navigation.registry import android.os.Bundle import androidx.compose.runtime.Composable +import com.navi.common.navigation.NavigationScreenData import com.navi.common.navigation.registry.ComposableRegistry import com.navi.rr.destinations.Destination import com.navi.rr.destinations.LeaderboardScreenDestination @@ -26,7 +27,11 @@ import javax.inject.Inject @ActivityRetainedScoped class RewardComposableRegistry @Inject constructor() : ComposableRegistry { - override fun findDirectionByName(destinationName: String, bundle: Bundle?): Direction? { + override fun findDirectionByName( + destinationName: String, + bundle: Bundle?, + data: NavigationScreenData? + ): Direction? { return Composables.entries .find { it.destinationName == destinationName } ?.directionProvider diff --git a/android/navi-rr/src/main/java/com/navi/rr/common/vm/RRBaseVM.kt b/android/navi-rr/src/main/java/com/navi/rr/common/vm/RRBaseVM.kt index 9ddfdd2be7..3990cf6370 100644 --- a/android/navi-rr/src/main/java/com/navi/rr/common/vm/RRBaseVM.kt +++ b/android/navi-rr/src/main/java/com/navi/rr/common/vm/RRBaseVM.kt @@ -10,7 +10,7 @@ package com.navi.rr.common.vm import androidx.lifecycle.viewModelScope import com.navi.common.constants.SCREEN_NAME import com.navi.common.forge.model.WidgetModelDefinition -import com.navi.common.utils.FireabaseEventFacade +import com.navi.common.utils.FirebaseEventFacade import com.navi.common.viewmodel.BaseVM import com.navi.rr.common.bottomsheet.RRBottomSheetStateHolder import com.navi.rr.common.error.ErrorHandler @@ -43,7 +43,7 @@ abstract class RRBaseVM : BaseVM(), ErrorHandler by ErrorHandlerImpl() { val analyticsHandler by lazy { NaviRRAnalytics.naviRRAnalytics } - @Inject lateinit var eventUtils: FireabaseEventFacade + @Inject lateinit var eventUtils: FirebaseEventFacade private val defaultContext get() = SupervisorJob() + Dispatchers.IO + exceptionHandler diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/NaviWidgetIconUtils.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/NaviWidgetIconUtils.kt index e1deda60b5..d14c8c2479 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/NaviWidgetIconUtils.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/NaviWidgetIconUtils.kt @@ -502,7 +502,7 @@ object NaviWidgetIconUtils { private const val ANTI_CLOCKWISE_ARROW_WITH_RUPEE = "ANTI_CLOCKWISE_ARROW_WITH_RUPEE" private const val ICON_ADD_NOMINEE = "ICON_ADD_NOMINEE" private const val ICON_ROUND_ORANGE_ARROW = "ICON_ROUND_ORANGE_ARROW" - private const val ICON_ROUND_PURPLE_RIGHT_ARROW = "ICON_ROUND_PURPLE_RIGHT_ARROW" + const val ICON_ROUND_PURPLE_RIGHT_ARROW = "ICON_ROUND_PURPLE_RIGHT_ARROW" private const val HEART_WITH_HEARTBEAT = "HEART_WITH_HEARTBEAT" private const val CROSS_WHITE = "CROSS_WHITE" private const val ICON_CROSS_WHITE = "ICON_CROSS_WHITE" @@ -657,6 +657,13 @@ object NaviWidgetIconUtils { const val IMAGE_PLACEHOLDER_LARGE = "IMAGE_PLACEHOLDER_LARGE" private const val IMAGE_PLACEHOLDER_X_LARGE = "IMAGE_PLACEHOLDER_X_LARGE" private const val IMAGE_PLACEHOLDER_XX_LARGE = "IMAGE_PLACEHOLDER_XX_LARGE" + private const val IMAGE_TRANSPARENT_PLACEHOLDER_SMALL = "IMAGE_TRANSPARENT_PLACEHOLDER_SMALL" + private const val IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM = "IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM" + private const val IMAGE_TRANSPARENT_PLACEHOLDER_LARGE = "IMAGE_TRANSPARENT_PLACEHOLDER_LARGE" + private const val IMAGE_TRANSPARENT_PLACEHOLDER_X_LARGE = + "IMAGE_TRANSPARENT_PLACEHOLDER_X_LARGE" + private const val IMAGE_TRANSPARENT_PLACEHOLDER_XX_LARGE = + "IMAGE_TRANSPARENT_PLACEHOLDER_XX_LARGE" private const val PURPLE_CHEVRON = "PURPLE_CHEVRON" private const val GREEN_TICK_MARK = "GREEN_TICK_MARK" private const val RED_TICK_MARK = "RED_TICK_MARK" @@ -738,7 +745,7 @@ object NaviWidgetIconUtils { private const val REFUND_IMAGE = "REFUND_IMAGE" private const val CHEVRON_RIGHT_PURPLE_WHITE_BG = "CHEVRON_RIGHT_PURPLE_WHITE_BG" private const val PURPLE_FILTER_ICON = "PURPLE_FILTER_ICON" - private const val NEW_INFO_ICON = "NEW_INFO_ICON" + const val NEW_INFO_ICON = "NEW_INFO_ICON" private const val RED_EXCLAMATION_TOAST = "RED_EXCLAMATION_TOAST" private const val NEW_GREEN_TICK_ICON = "NEW_GREEN_TICK_ICON" const val INFO_ICON_PURPLE = "INFO_ICON_PURPLE" @@ -798,7 +805,7 @@ object NaviWidgetIconUtils { private const val PAN_CARD = "PAN_CARD" private const val BBPS_LOGO = "BBPS_LOGO" private const val NAVI_COIN_16 = "NAVI_COIN_16" - private const val DOWN_ARROW_BLACK_16 = "DOWN_ARROW_BLACK_16" + const val DOWN_ARROW_BLACK_16 = "DOWN_ARROW_BLACK_16" private const val ICON_LIGHT_ORANGE_DISK = "ICON_LIGHT_ORANGE_DISK" private const val ICON_ORANGE_DISK = "ICON_ORANGE_DISK" private const val ICON_YELLOW_CLOCK = "ICON_YELLOW_CLOCK" @@ -847,8 +854,7 @@ object NaviWidgetIconUtils { private const val HOME_PAGE_UPI_LITE_LOGO = "HOME_PAGE_UPI_LITE_LOGO" private const val SCAN_AND_PAY_ICON = "SCAN_AND_PAY_ICON" private const val SCAN_AND_PAY_STRIP_ICON = "SCAN_AND_PAY_STRIP_ICON" - private const val HOME_PAGE_UPI_SECTION_RIGHT_ARROW_ICON = - "HOME_PAGE_UPI_SECTION_RIGHT_ARROW_ICON" + const val HOME_PAGE_UPI_SECTION_RIGHT_ARROW_ICON = "HOME_PAGE_UPI_SECTION_RIGHT_ARROW_ICON" private const val PAY_TO_MOBILE_NUMBER_IMAGE = "PAY_TO_MOBILE_NUMBER_IMAGE" private const val PAY_TO_UPI_ID_OR_BANK_IMAGE = "PAY_TO_UPI_ID_OR_BANK_IMAGE" private const val TRANSACTION_HISTORY_CARD_IMAGE = "TRANSACTION_HISTORY_CARD_IMAGE" @@ -1556,6 +1562,13 @@ object NaviWidgetIconUtils { IMAGE_PLACEHOLDER_LARGE -> R.drawable.image_placeholder_large IMAGE_PLACEHOLDER_X_LARGE -> R.drawable.image_placeholder_x_large IMAGE_PLACEHOLDER_XX_LARGE -> R.drawable.image_placeholder_xx_large + IMAGE_TRANSPARENT_PLACEHOLDER_SMALL -> R.drawable.image_transparent_placeholder_small + IMAGE_TRANSPARENT_PLACEHOLDER_MEDIUM -> R.drawable.image_transparent_placeholder_medium + IMAGE_TRANSPARENT_PLACEHOLDER_LARGE -> R.drawable.image_transparent_placeholder_large + IMAGE_TRANSPARENT_PLACEHOLDER_X_LARGE -> + R.drawable.image_transparent_placeholder_x_large + IMAGE_TRANSPARENT_PLACEHOLDER_XX_LARGE -> + R.drawable.image_transparent_placeholder_xx_large PURPLE_CHEVRON -> R.drawable.ic_new_purple_chevron GREEN_TICK_MARK -> R.drawable.ic_green_success_new RED_TICK_MARK -> R.drawable.ic_red_alert_error_new diff --git a/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/NaviWidgetLottieUtils.kt b/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/NaviWidgetLottieUtils.kt index c64e4ec142..675cb2c4fc 100644 --- a/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/NaviWidgetLottieUtils.kt +++ b/android/navi-widgets/src/main/java/com/navi/naviwidgets/utils/NaviWidgetLottieUtils.kt @@ -11,10 +11,14 @@ import com.navi.naviwidgets.R object NaviWidgetLottieUtils { private const val NAVI_COIN_ROTATION_20X20 = "NAVI_COIN_ROTATION_20X20" + const val TUK_TUK_LOTTIE = "TUK_TUK_LOTTIE" + const val TUK_TUK_WHITE_LOTTIE = "TUK_TUK_WHITE_LOTTIE" fun getLottieFromLottieCode(lottieCode: String?): Int { return when (lottieCode) { NAVI_COIN_ROTATION_20X20 -> R.raw.coin_rotation_20_cross_20 + TUK_TUK_LOTTIE -> R.raw.cta_loader_purple + TUK_TUK_WHITE_LOTTIE -> R.raw.cta_loader else -> -1 } } diff --git a/android/navi-widgets/src/main/res/drawable/image_transparent_placeholder_large.xml b/android/navi-widgets/src/main/res/drawable/image_transparent_placeholder_large.xml new file mode 100644 index 0000000000..9f3f7fa04b --- /dev/null +++ b/android/navi-widgets/src/main/res/drawable/image_transparent_placeholder_large.xml @@ -0,0 +1,16 @@ + + + + diff --git a/android/navi-widgets/src/main/res/drawable/image_transparent_placeholder_medium.xml b/android/navi-widgets/src/main/res/drawable/image_transparent_placeholder_medium.xml new file mode 100644 index 0000000000..322e46e0f2 --- /dev/null +++ b/android/navi-widgets/src/main/res/drawable/image_transparent_placeholder_medium.xml @@ -0,0 +1,16 @@ + + + + diff --git a/android/navi-widgets/src/main/res/drawable/image_transparent_placeholder_small.xml b/android/navi-widgets/src/main/res/drawable/image_transparent_placeholder_small.xml new file mode 100644 index 0000000000..2ebf1f4d2f --- /dev/null +++ b/android/navi-widgets/src/main/res/drawable/image_transparent_placeholder_small.xml @@ -0,0 +1,16 @@ + + + + diff --git a/android/navi-widgets/src/main/res/drawable/image_transparent_placeholder_x_large.xml b/android/navi-widgets/src/main/res/drawable/image_transparent_placeholder_x_large.xml new file mode 100644 index 0000000000..66069e03c7 --- /dev/null +++ b/android/navi-widgets/src/main/res/drawable/image_transparent_placeholder_x_large.xml @@ -0,0 +1,16 @@ + + + + diff --git a/android/navi-widgets/src/main/res/drawable/image_transparent_placeholder_xx_large.xml b/android/navi-widgets/src/main/res/drawable/image_transparent_placeholder_xx_large.xml new file mode 100644 index 0000000000..abf2d6ddca --- /dev/null +++ b/android/navi-widgets/src/main/res/drawable/image_transparent_placeholder_xx_large.xml @@ -0,0 +1,16 @@ + + + + diff --git a/android/settings.gradle b/android/settings.gradle index 830ea0475e..8d87bdc08d 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -35,6 +35,8 @@ include ':npci-upi-cl' include ':navi-ap' include ':navi-mqtt' include ':navi-cycs' +include ':navi-money-manager' +include ':navi-code' include ':react-native-code-push' include ':benchmark'