Refactor - [:feature:make-transfer] Applied Spotless & Detekt Compose Rules (#1724)

* Refactor: Applied Spotless & Detekt Compose Rules

- :feature:accounts
- :feature:auth
- :feature:editpassword
- :feature:finance

* Refactor - [:feature:history] Applied Spotless & Detekt Compose Rules

* Refactor - [:feature:home] Applied Spotless & Detekt Compose Rules

* Refactor - [:feature:invoices] Applied Spotless & Detekt Compose Rules

* Refactor - [:feature:kyc] Applied Spotless & Detekt Compose Rules

* Refactor - [:feature:make-transfer] Applied Spotless & Detekt Compose Rules
This commit is contained in:
Sk Niyaj Ali 2024-08-11 23:06:53 +05:30 committed by GitHub
parent 6f87da5869
commit e45c9b6f59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
151 changed files with 2981 additions and 1922 deletions

7
.editorconfig Normal file
View File

@ -0,0 +1,7 @@
# https://editorconfig.org/
# This configuration is used by ktlint when spotless invokes it
[*.{kt,kts}]
ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true
ktlint_function_naming_ignore_when_annotated_with=Composable, Test, Preview

View File

@ -52,20 +52,21 @@ jobs:
name: mobile-wallet name: mobile-wallet
path: mifospay/build/outputs/apk/debug/ path: mifospay/build/outputs/apk/debug/
lintCheck: # Turing off detekt check until migration finished
name: Static Analysis # lintCheck:
runs-on: ubuntu-latest # name: Static Analysis
steps: # runs-on: ubuntu-latest
- uses: actions/checkout@v2 # steps:
# - uses: actions/checkout@v2
# Setup JDK 17 #
- name: Setup JDK 17 # # Setup JDK 17
uses: actions/setup-java@v1 # - name: Setup JDK 17
with: # uses: actions/setup-java@v1
java-version: 17 # with:
# java-version: 17
- name: Detekt For All Modules #
run: ./gradlew detekt # - name: Detekt For All Modules
# run: ./gradlew detekt
pmd: pmd:
name: PMD name: PMD

View File

@ -1,11 +1,10 @@
import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
dependencies { dependencies {
classpath(libs.google.oss.licenses.plugin) { classpath(libs.google.oss.licenses.plugin) {
exclude(group = "com.google.protobuf") exclude(group = "com.google.protobuf")
} }
classpath(libs.spotless.gradle)
} }
} }
@ -28,11 +27,14 @@ plugins {
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.detekt) alias(libs.plugins.detekt)
alias(libs.plugins.detekt.compiler) alias(libs.plugins.detekt.compiler)
alias(libs.plugins.module.graph) apply true // Plugin applied to allow module graph generation // Plugin applied to allow module graph generation
alias(libs.plugins.module.graph) apply true
alias(libs.plugins.spotless)
} }
val detektFormatting = libs.detekt.formatting val detektFormatting = libs.detekt.formatting
val twitterComposeRules = libs.twitter.detekt.compose val twitterComposeRules = libs.twitter.detekt.compose
val ktlintVersion = "1.0.1"
val reportMerge by tasks.registering(io.gitlab.arturbosch.detekt.report.ReportMergeTask::class) { val reportMerge by tasks.registering(io.gitlab.arturbosch.detekt.report.ReportMergeTask::class) {
output.set(rootProject.layout.buildDirectory.file("reports/detekt/merge.html")) // or "reports/detekt/merge.sarif" output.set(rootProject.layout.buildDirectory.file("reports/detekt/merge.html")) // or "reports/detekt/merge.sarif"
@ -41,17 +43,40 @@ val reportMerge by tasks.registering(io.gitlab.arturbosch.detekt.report.ReportMe
subprojects { subprojects {
apply { apply {
plugin("io.gitlab.arturbosch.detekt") plugin("io.gitlab.arturbosch.detekt")
} plugin("com.diffplug.spotless")
detekt {
config.from(rootProject.files("config/detekt/detekt.yml"))
reports.xml.required.set(true)
} }
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach { tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
config.from(rootProject.files("config/detekt/detekt.yml"))
reports.xml.required.set(true)
finalizedBy(reportMerge) finalizedBy(reportMerge)
} }
extensions.configure<com.diffplug.gradle.spotless.SpotlessExtension> {
kotlin {
target("**/*.kt")
targetExclude("**/build/**/*.kt")
ktlint(ktlintVersion).editorConfigOverride(
mapOf(
"android" to "true",
),
)
licenseHeaderFile(rootProject.file("spotless/copyright.kt"))
}
format("kts") {
target("**/*.kts")
targetExclude("**/build/**/*.kts")
// Look for the first line that doesn't have a block comment (assumed to be the license)
licenseHeaderFile(rootProject.file("spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)")
}
format("xml") {
target("**/*.xml")
targetExclude("**/build/**/*.xml")
// Look for the first XML tag that isn't a comment (<!--) or the xml declaration (<?xml)
licenseHeaderFile(rootProject.file("spotless/copyright.xml"), "(<[^!?])")
}
}
reportMerge { reportMerge {
input.from(tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().map { input.from(tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().map {
it.htmlReportFile } it.htmlReportFile }

View File

@ -805,7 +805,7 @@ style:
NestedClassesVisibility: NestedClassesVisibility:
active: true active: true
NewLineAtEndOfFile: NewLineAtEndOfFile:
active: false # Turning off for current implementation active: true
NoTabs: NoTabs:
active: false active: false
NullableBooleanCheck: NullableBooleanCheck:
@ -936,4 +936,50 @@ style:
WildcardImport: WildcardImport:
active: true active: true
excludeImports: excludeImports:
- "java.util.*" - "java.util.*"
TwitterCompose:
CompositionLocalAllowlist:
active: true
# You can optionally define a list of CompositionLocals that are allowed here
# allowedCompositionLocals: LocalSomething,LocalSomethingElse
CompositionLocalNaming:
active: true
ContentEmitterReturningValues:
active: true
# You can optionally add your own composables here
# contentEmitters: MyComposable,MyOtherComposable
ModifierComposable:
active: true
ModifierMissing:
active: true
ModifierReused:
active: true
ModifierWithoutDefault:
active: true
MultipleEmitters:
active: true
# You can optionally add your own composables here
# contentEmitters: MyComposable,MyOtherComposable
MutableParams:
active: true
ComposableNaming:
active: true
# You can optionally disable the checks in this rule for regex matches against the composable name (e.g. molecule presenters)
# allowedComposableFunctionNames: .*Presenter,.*MoleculePresenter
ComposableParamOrder:
active: true
PreviewNaming:
active: true
PreviewPublic:
active: true
# You can optionally disable that only previews with @PreviewParameter are flagged
# previewPublicOnlyIfParams: false
RememberMissing:
active: true
UnstableCollections:
active: false
ViewModelForwarding:
active: true
ViewModelInjection:
active: true

View File

@ -5,7 +5,7 @@ import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import java.text.NumberFormat import java.text.NumberFormat
import java.util.* import java.util.Currency
object Utils { object Utils {
@ -31,4 +31,10 @@ object Utils {
return accountBalanceFormatter.format(balance) return accountBalanceFormatter.format(balance)
} }
fun <T> List<T>.toArrayList(): ArrayList<T> {
val array: ArrayList<T> = ArrayList()
for (index in this) array.add(index)
return array
}
} }

View File

@ -0,0 +1,18 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.domain.usecase.history
import com.mifospay.core.model.domain.Transaction
interface HistoryContract {
interface TransactionsHistoryAsync {
fun onTransactionsFetchCompleted(transactions: List<Transaction>?)
}
}

View File

@ -1,19 +1,25 @@
package org.mifospay.feature /*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.core.data.domain.usecase.history
import com.mifospay.core.model.domain.Transaction import com.mifospay.core.model.domain.Transaction
import org.mifospay.core.data.base.TaskLooper import org.mifospay.core.data.base.TaskLooper
import org.mifospay.core.data.base.UseCase.UseCaseCallback import org.mifospay.core.data.base.UseCase.UseCaseCallback
import org.mifospay.core.data.base.UseCaseFactory import org.mifospay.core.data.base.UseCaseFactory
import org.mifospay.core.data.base.UseCaseHandler import org.mifospay.core.data.base.UseCaseHandler
import org.mifospay.core.data.domain.usecase.account.FetchAccount
import org.mifospay.core.data.domain.usecase.account.FetchAccountTransactions import org.mifospay.core.data.domain.usecase.account.FetchAccountTransactions
import javax.inject.Inject import javax.inject.Inject
@Suppress("UnusedPrivateProperty")
class TransactionsHistory @Inject constructor( class TransactionsHistory @Inject constructor(
private val mUsecaseHandler: UseCaseHandler, private val mUseCaseHandler: UseCaseHandler,
private val fetchAccountTransactionsUseCase: FetchAccountTransactions, private val fetchAccountTransactionsUseCase: FetchAccountTransactions,
private val mFetchAccountUseCase: FetchAccount
) { ) {
var delegate: HistoryContract.TransactionsHistoryAsync? = null var delegate: HistoryContract.TransactionsHistoryAsync? = null
@ -31,7 +37,8 @@ class TransactionsHistory @Inject constructor(
} }
fun fetchTransactionsHistory(accountId: Long) { fun fetchTransactionsHistory(accountId: Long) {
mUsecaseHandler.execute(fetchAccountTransactionsUseCase, mUseCaseHandler.execute(
fetchAccountTransactionsUseCase,
FetchAccountTransactions.RequestValues(accountId), FetchAccountTransactions.RequestValues(accountId),
object : UseCaseCallback<FetchAccountTransactions.ResponseValue?> { object : UseCaseCallback<FetchAccountTransactions.ResponseValue?> {
override fun onSuccess(response: FetchAccountTransactions.ResponseValue?) { override fun onSuccess(response: FetchAccountTransactions.ResponseValue?) {
@ -42,6 +49,7 @@ class TransactionsHistory @Inject constructor(
override fun onError(message: String) { override fun onError(message: String) {
transactions = null transactions = null
} }
}) },
)
} }
} }

View File

@ -31,7 +31,6 @@ class FetchInvoice @Inject constructor(private val mFineractRepository: Fineract
val clientId = params?.get(0) // "clientId" val clientId = params?.get(0) // "clientId"
val invoiceId = params?.get(1) // "invoiceId" val invoiceId = params?.get(1) // "invoiceId"
if (clientId != null && invoiceId != null) { if (clientId != null && invoiceId != null) {
mFineractRepository.fetchInvoice(clientId, invoiceId) mFineractRepository.fetchInvoice(clientId, invoiceId)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())

View File

@ -1,5 +1,6 @@
package org.mifospay.core.data.domain.usecase.invoice package org.mifospay.core.data.domain.usecase.invoice
import android.util.Log
import org.mifospay.core.data.base.UseCase import org.mifospay.core.data.base.UseCase
import com.mifospay.core.model.entity.Invoice import com.mifospay.core.model.entity.Invoice
import org.mifospay.core.data.fineract.repository.FineractRepository import org.mifospay.core.data.fineract.repository.FineractRepository
@ -26,6 +27,7 @@ class FetchInvoices @Inject constructor(private val mFineractRepository: Finerac
.subscribe(object : Subscriber<List<Invoice>>() { .subscribe(object : Subscriber<List<Invoice>>() {
override fun onCompleted() {} override fun onCompleted() {}
override fun onError(e: Throwable) { override fun onError(e: Throwable) {
Log.e("Invoices", e.message.toString())
useCaseCallback.onError(e.toString()) useCaseCallback.onError(e.toString())
} }

View File

@ -5,16 +5,18 @@ import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@Composable @Composable
fun MifosScaffold( fun MifosScaffold(
topBarTitle: Int? = null,
backPress: () -> Unit, backPress: () -> Unit,
modifier: Modifier = Modifier,
topBarTitle: Int? = null,
floatingActionButtonContent: FloatingActionButtonContent? = null, floatingActionButtonContent: FloatingActionButtonContent? = null,
snackbarHost: @Composable () -> Unit = {}, snackbarHost: @Composable () -> Unit = {},
scaffoldContent: @Composable (PaddingValues) -> Unit, scaffoldContent: @Composable (PaddingValues) -> Unit,
actions: @Composable RowScope.() -> Unit = {} actions: @Composable RowScope.() -> Unit = {},
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
@ -22,7 +24,7 @@ fun MifosScaffold(
MifosTopBar( MifosTopBar(
topBarTitle = topBarTitle, topBarTitle = topBarTitle,
backPress = backPress, backPress = backPress,
actions = actions actions = actions,
) )
} }
}, },
@ -31,17 +33,18 @@ fun MifosScaffold(
FloatingActionButton( FloatingActionButton(
onClick = content.onClick, onClick = content.onClick,
contentColor = content.contentColor, contentColor = content.contentColor,
content = content.content content = content.content,
) )
} }
}, },
snackbarHost = snackbarHost, snackbarHost = snackbarHost,
content = scaffoldContent, content = scaffoldContent,
modifier = modifier,
) )
} }
data class FloatingActionButtonContent( data class FloatingActionButtonContent(
val onClick: (() -> Unit), val onClick: (() -> Unit),
val contentColor: Color, val contentColor: Color,
val content: (@Composable () -> Unit) val content: (@Composable () -> Unit),
) )

View File

@ -16,11 +16,8 @@ dependencies {
api(projects.core.designsystem) api(projects.core.designsystem)
api(projects.core.model) api(projects.core.model)
api(projects.core.common) api(projects.core.common)
api(libs.androidx.metrics) api(libs.androidx.metrics)
api(projects.core.analytics) api(projects.core.analytics)
api(projects.core.designsystem)
api(projects.core.model)
implementation(libs.accompanist.pager) implementation(libs.accompanist.pager)
implementation(libs.androidx.browser) implementation(libs.androidx.browser)

View File

@ -16,11 +16,6 @@ import com.google.accompanist.pager.PagerState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.mifospay.core.ui.utility.TabContent import org.mifospay.core.ui.utility.TabContent
/**
* @author pratyush
* @since 23/3/24
*/
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun MifosScrollableTabRow( fun MifosScrollableTabRow(
@ -35,11 +30,10 @@ fun MifosScrollableTabRow(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
ScrollableTabRow( ScrollableTabRow(
modifier = modifier,
containerColor = containerColor, containerColor = containerColor,
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
edgePadding = edgePadding, edgePadding = edgePadding,
indicator = {},
divider = {},
) { ) {
tabContents.forEachIndexed { index, currentTab -> tabContents.forEachIndexed { index, currentTab ->
Tab( Tab(
@ -59,9 +53,8 @@ fun MifosScrollableTabRow(
HorizontalPager( HorizontalPager(
count = tabContents.size, count = tabContents.size,
state = pagerState, state = pagerState
modifier = modifier ) {
) { page -> tabContents[it].content.invoke()
tabContents.getOrNull(page)?.content?.invoke() ?: Text("Page $page")
} }
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
plugins { plugins {
alias(libs.plugins.mifospay.android.feature) alias(libs.plugins.mifospay.android.feature)
alias(libs.plugins.mifospay.android.library.compose) alias(libs.plugins.mifospay.android.library.compose)

View File

@ -1,24 +0,0 @@
package org.mifospay.feature.bank.accounts
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("org.mifospay.feature.bank.accounts.test", appContext.packageName)
}
}

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest> </manifest>

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.bank.accounts package org.mifospay.feature.bank.accounts
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -55,33 +64,54 @@ class AccountViewModel @Inject constructor() : ViewModel() {
private fun fetchSampleLinkedAccounts(): List<BankAccountDetails> { private fun fetchSampleLinkedAccounts(): List<BankAccountDetails> {
return listOf( return listOf(
BankAccountDetails( BankAccountDetails(
"SBI", "Ankur Sharma", "New Delhi", "SBI",
mRandom.nextInt().toString() + " ", "Savings" "Ankur Sharma",
"New Delhi",
mRandom.nextInt().toString() + " ",
"Savings",
), ),
BankAccountDetails( BankAccountDetails(
"HDFC", "Mandeep Singh", "Uttar Pradesh", "HDFC",
mRandom.nextInt().toString() + " ", "Savings" "Mandeep Singh",
"Uttar Pradesh",
mRandom.nextInt().toString() + " ",
"Savings",
), ),
BankAccountDetails( BankAccountDetails(
"ANDHRA", "Rakesh anna", "Telegana", "ANDHRA",
mRandom.nextInt().toString() + " ", "Savings" "Rakesh anna",
"Telegana",
mRandom.nextInt().toString() + " ",
"Savings",
), ),
BankAccountDetails( BankAccountDetails(
"PNB", "luv Pro", "Gujrat", "PNB",
mRandom.nextInt().toString() + " ", "Savings" "luv Pro",
"Gujrat",
mRandom.nextInt().toString() + " ",
"Savings",
), ),
BankAccountDetails( BankAccountDetails(
"HDF", "Harry potter", "Hogwarts", "HDF",
mRandom.nextInt().toString() + " ", "Savings" "Harry potter",
"Hogwarts",
mRandom.nextInt().toString() + " ",
"Savings",
), ),
BankAccountDetails( BankAccountDetails(
"GCI", "JIGME", "JAMMU", "GCI",
mRandom.nextInt().toString() + " ", "Savings" "JIGME",
"JAMMU",
mRandom.nextInt().toString() + " ",
"Savings",
), ),
BankAccountDetails( BankAccountDetails(
"FCI", "NISHU BOII", "ASSAM", "FCI",
mRandom.nextInt().toString() + " ", "Savings" "NISHU BOII",
) "ASSAM",
mRandom.nextInt().toString() + " ",
"Savings",
),
) )
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.bank.accounts package org.mifospay.feature.bank.accounts
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -20,19 +29,21 @@ import com.mifospay.core.model.domain.BankAccountDetails
import org.mifospay.core.designsystem.component.MifosCard import org.mifospay.core.designsystem.component.MifosCard
@Composable @Composable
fun AccountsItem( internal fun AccountsItem(
bankAccountDetails: BankAccountDetails, bankAccountDetails: BankAccountDetails,
onAccountClicked: () -> Unit onAccountClicked: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
MifosCard( MifosCard(
modifier = modifier,
onClick = { onAccountClicked.invoke() }, onClick = { onAccountClicked.invoke() },
colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surface) colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surface),
) { ) {
Column { Column {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 16.dp) .padding(top = 16.dp),
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.feature_accounts_ic_bank), painter = painterResource(id = R.drawable.feature_accounts_ic_bank),
@ -40,7 +51,7 @@ fun AccountsItem(
modifier = Modifier modifier = Modifier
.align(Alignment.CenterVertically) .align(Alignment.CenterVertically)
.padding(start = 16.dp, end = 16.dp) .padding(start = 16.dp, end = 16.dp)
.size(39.dp) .size(39.dp),
) )
Column { Column {
@ -52,18 +63,18 @@ fun AccountsItem(
text = bankAccountDetails.bankName.toString(), text = bankAccountDetails.bankName.toString(),
modifier = Modifier.padding(top = 4.dp), modifier = Modifier.padding(top = 4.dp),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface,
) )
} }
Column( Column(
horizontalAlignment = Alignment.End, horizontalAlignment = Alignment.End,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) { ) {
Text( Text(
text = bankAccountDetails.branch.toString(), text = bankAccountDetails.branch.toString(),
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
fontSize = 12.sp, fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface,
) )
} }
} }
@ -76,6 +87,6 @@ fun AccountsItem(
private fun AccountsItemPreview() { private fun AccountsItemPreview() {
AccountsItem( AccountsItem(
bankAccountDetails = BankAccountDetails("A", "B", "C"), bankAccountDetails = BankAccountDetails("A", "B", "C"),
onAccountClicked = {} onAccountClicked = {},
) )
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.bank.accounts package org.mifospay.feature.bank.accounts
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -34,15 +43,17 @@ import org.mifospay.core.ui.utility.AddCardChip
@Composable @Composable
fun AccountsScreen( fun AccountsScreen(
navigateToBankAccountDetailScreen: (BankAccountDetails, Int) -> Unit,
navigateToLinkBankAccountScreen: () -> Unit,
modifier: Modifier = Modifier,
viewModel: AccountViewModel = hiltViewModel(), viewModel: AccountViewModel = hiltViewModel(),
navigateToBankAccountDetailScreen: (BankAccountDetails,Int) -> Unit,
navigateToLinkBankAccountScreen: () -> Unit
) { ) {
val accountsUiState by viewModel.accountsUiState.collectAsStateWithLifecycle() val accountsUiState by viewModel.accountsUiState.collectAsStateWithLifecycle()
val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle()
val bankAccountDetailsList by viewModel.bankAccountDetailsList.collectAsStateWithLifecycle() val bankAccountDetailsList by viewModel.bankAccountDetailsList.collectAsStateWithLifecycle()
AccountScreen( AccountScreen(
modifier = modifier,
accountsUiState = accountsUiState, accountsUiState = accountsUiState,
onAddAccount = { onAddAccount = {
navigateToLinkBankAccountScreen.invoke() navigateToLinkBankAccountScreen.invoke()
@ -54,27 +65,30 @@ fun AccountsScreen(
}, },
onUpdateAccount = { bankAccountDetails, index -> onUpdateAccount = { bankAccountDetails, index ->
viewModel.updateBankAccount(index, bankAccountDetails) viewModel.updateBankAccount(index, bankAccountDetails)
navigateToBankAccountDetailScreen.invoke(bankAccountDetails,index) navigateToBankAccountDetailScreen.invoke(bankAccountDetails, index)
} },
) )
} }
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun AccountScreen( private fun AccountScreen(
accountsUiState: AccountsUiState, accountsUiState: AccountsUiState,
onAddAccount: () -> Unit, onAddAccount: () -> Unit,
bankAccountDetailsList: List<BankAccountDetails>, bankAccountDetailsList: List<BankAccountDetails>,
isRefreshing: Boolean, isRefreshing: Boolean,
onRefresh: () -> Unit, onRefresh: () -> Unit,
onUpdateAccount: (BankAccountDetails, Int) -> Unit onUpdateAccount: (BankAccountDetails, Int) -> Unit,
modifier: Modifier = Modifier,
) { ) {
val pullRefreshState = rememberPullRefreshState(isRefreshing, onRefresh) val pullRefreshState = rememberPullRefreshState(isRefreshing, onRefresh)
Box(Modifier.pullRefresh(pullRefreshState)) { Box(modifier.pullRefresh(pullRefreshState)) {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
when (accountsUiState) { when (accountsUiState) {
AccountsUiState.Empty -> { AccountsUiState.Empty -> {
NoLinkedAccountsScreen { onAddAccount.invoke() } NoLinkedAccountsScreen(
onAddBtn = onAddAccount,
)
} }
AccountsUiState.Error -> { AccountsUiState.Error -> {
@ -83,7 +97,7 @@ fun AccountScreen(
title = stringResource(id = R.string.feature_accounts_error_oops), title = stringResource(id = R.string.feature_accounts_error_oops),
subTitle = stringResource(id = R.string.feature_accounts_unexpected_error_subtitle), subTitle = stringResource(id = R.string.feature_accounts_unexpected_error_subtitle),
iconTint = MaterialTheme.colorScheme.onSurface, iconTint = MaterialTheme.colorScheme.onSurface,
iconImageVector = Icons.Rounded.Info iconImageVector = Icons.Rounded.Info,
) )
} }
@ -91,14 +105,14 @@ fun AccountScreen(
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.fillMaxSize() .fillMaxSize(),
) { ) {
item { item {
Text( Text(
text = stringResource(id = R.string.feature_accounts_linked_bank_account), text = stringResource(id = R.string.feature_accounts_linked_bank_account),
fontSize = 16.sp, fontSize = 16.sp,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 48.dp, start = 24.dp) modifier = Modifier.padding(top = 48.dp, start = 24.dp),
) )
} }
items(bankAccountDetailsList) { bankAccountDetails -> items(bankAccountDetailsList) { bankAccountDetails ->
@ -107,10 +121,10 @@ fun AccountScreen(
bankAccountDetails = bankAccountDetails, bankAccountDetails = bankAccountDetails,
onAccountClicked = { onAccountClicked = {
onUpdateAccount(bankAccountDetails, index) onUpdateAccount(bankAccountDetails, index)
} },
) )
HorizontalDivider( HorizontalDivider(
modifier = Modifier.padding(8.dp) modifier = Modifier.padding(8.dp),
) )
} }
item { item {
@ -118,13 +132,13 @@ fun AccountScreen(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .padding(16.dp)
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface),
) { ) {
AddCardChip( AddCardChip(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
onAddBtn = onAddAccount, onAddBtn = onAddAccount,
text = R.string.feature_accounts_add_account, text = R.string.feature_accounts_add_account,
btnText = R.string.feature_accounts_add_cards btnText = R.string.feature_accounts_add_cards,
) )
} }
} }
@ -134,7 +148,7 @@ fun AccountScreen(
AccountsUiState.Loading -> { AccountsUiState.Loading -> {
MfLoadingWheel( MfLoadingWheel(
contentDesc = stringResource(R.string.feature_accounts_loading), contentDesc = stringResource(R.string.feature_accounts_loading),
backgroundColor = MaterialTheme.colorScheme.surface backgroundColor = MaterialTheme.colorScheme.surface,
) )
} }
} }
@ -142,26 +156,29 @@ fun AccountScreen(
PullRefreshIndicator( PullRefreshIndicator(
refreshing = isRefreshing, refreshing = isRefreshing,
state = pullRefreshState, state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter) modifier = Modifier.align(Alignment.TopCenter),
) )
} }
} }
@Composable @Composable
fun NoLinkedAccountsScreen(onAddBtn: () -> Unit) { private fun NoLinkedAccountsScreen(
onAddBtn: () -> Unit,
modifier: Modifier = Modifier,
) {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center,
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Text(text = stringResource(R.string.feature_accounts_no_linked_bank_accounts)) Text(text = stringResource(R.string.feature_accounts_no_linked_bank_accounts))
AddCardChip( AddCardChip(
modifier = Modifier, modifier = Modifier,
onAddBtn = onAddBtn, onAddBtn = onAddBtn,
text = R.string.feature_accounts_add_account, text = R.string.feature_accounts_add_account,
btnText = R.string.feature_accounts_add_cards btnText = R.string.feature_accounts_add_cards,
) )
} }
} }
@ -170,7 +187,14 @@ fun NoLinkedAccountsScreen(onAddBtn: () -> Unit) {
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun AccountScreenLoadingPreview() { private fun AccountScreenLoadingPreview() {
AccountScreen(accountsUiState = AccountsUiState.Loading, {}, emptyList(), false, {}, { _, _ -> }) AccountScreen(
accountsUiState = AccountsUiState.Loading,
{},
emptyList(),
false,
{},
{ _, _ -> },
)
} }
@Preview(showBackground = true) @Preview(showBackground = true)
@ -188,7 +212,7 @@ private fun AccountListScreenPreview() {
sampleLinkedAccount, sampleLinkedAccount,
false, false,
{}, {},
{ _, _ -> } { _, _ -> },
) )
} }
@ -200,7 +224,10 @@ private fun AccountErrorScreenPreview() {
val sampleLinkedAccount = List(10) { val sampleLinkedAccount = List(10) {
BankAccountDetails( BankAccountDetails(
"SBI", "Ankur Sharma", "New Delhi", "SBI",
"XXXXXXXX9990XXX " + " ", "Savings" "Ankur Sharma",
"New Delhi",
"XXXXXXXX9990XXX " + " ",
"Savings",
) )
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.bank.accounts.choose.sim package org.mifospay.feature.bank.accounts.choose.sim
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
@ -40,14 +49,14 @@ import org.mifospay.core.designsystem.theme.MifosTheme
import org.mifospay.feature.bank.accounts.R import org.mifospay.feature.bank.accounts.R
@Composable @Composable
fun ChooseSimDialogSheet( internal fun ChooseSimDialogSheet(
onSimSelected: (Int) -> Unit, onSimSelected: (Int) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
MifosBottomSheet( MifosBottomSheet(
content = { content = {
ChooseSimDialogSheetContent( ChooseSimDialogSheetContent(
onSimSelected = onSimSelected onSimSelected = onSimSelected,
) )
}, },
onDismiss = { onDismiss = {
@ -63,7 +72,7 @@ fun ChooseSimDialogSheet(
*/ */
@Composable @Composable
@Suppress("LongMethod") @Suppress("LongMethod")
fun ChooseSimDialogSheetContent( private fun ChooseSimDialogSheetContent(
onSimSelected: (Int) -> Unit, onSimSelected: (Int) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -82,67 +91,67 @@ fun ChooseSimDialogSheetContent(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.padding(8.dp) .padding(8.dp),
) { ) {
Text( Text(
text = stringResource(id = R.string.feature_accounts_verify_mobile_number), text = stringResource(id = R.string.feature_accounts_verify_mobile_number),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface,
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = stringResource(id = R.string.feature_accounts_confirm_mobile_number_message), text = stringResource(id = R.string.feature_accounts_confirm_mobile_number_message),
style = MaterialTheme.typography.bodySmall.copy( style = MaterialTheme.typography.bodySmall.copy(
textAlign = TextAlign.Center textAlign = TextAlign.Center,
), ),
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface,
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
modifier = Modifier.padding(horizontal = 24.dp), modifier = Modifier.padding(horizontal = 24.dp),
text = stringResource(id = R.string.feature_accounts_bank_account_mobile_verification_conditions), text = stringResource(id = R.string.feature_accounts_bank_account_mobile_verification_conditions),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface,
) )
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
SimCard( SimCard(
simNumber = 1, simNumber = 1,
isSelected = selectedSim == 1, isSelected = selectedSim == 1,
onSimSelected = { selectedSim = 1 } onSimSelected = { selectedSim = 1 },
) )
Spacer(modifier = Modifier.width(24.dp)) Spacer(modifier = Modifier.width(24.dp))
Text( Text(
text = stringResource(id = R.string.feature_accounts_or), text = stringResource(id = R.string.feature_accounts_or),
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface,
) )
Spacer(modifier = Modifier.width(24.dp)) Spacer(modifier = Modifier.width(24.dp))
SimCard( SimCard(
simNumber = 2, simNumber = 2,
isSelected = selectedSim == 2, isSelected = selectedSim == 2,
onSimSelected = { selectedSim = 2 } onSimSelected = { selectedSim = 2 },
) )
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = stringResource(id = R.string.feature_accounts_regular_charges_will_apply), text = stringResource(id = R.string.feature_accounts_regular_charges_will_apply),
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall,
) )
AnimatedVisibility( AnimatedVisibility(
visible = showMessage visible = showMessage,
) { ) {
Text( Text(
text = message, text = message,
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(vertical = 4.dp) modifier = Modifier.padding(vertical = 4.dp),
) )
} }
@ -156,7 +165,7 @@ fun ChooseSimDialogSheetContent(
} else { } else {
onSimSelected(selectedSim) onSimSelected(selectedSim)
} }
} },
) { ) {
Text(text = stringResource(id = R.string.feature_accounts_confirm)) Text(text = stringResource(id = R.string.feature_accounts_confirm))
} }
@ -165,7 +174,7 @@ fun ChooseSimDialogSheetContent(
} }
@Composable @Composable
fun SimCard( private fun SimCard(
simNumber: Int, simNumber: Int,
isSelected: Boolean, isSelected: Boolean,
onSimSelected: () -> Unit, onSimSelected: () -> Unit,
@ -174,7 +183,9 @@ fun SimCard(
val drawable: Painter = painterResource( val drawable: Painter = painterResource(
id = if (isSelected) { id = if (isSelected) {
R.drawable.feature_accounts_sim_card_selected R.drawable.feature_accounts_sim_card_selected
} else R.drawable.feature_accounts_sim_card_unselected } else {
R.drawable.feature_accounts_sim_card_unselected
},
) )
Image( Image(
painter = drawable, painter = drawable,
@ -182,17 +193,17 @@ fun SimCard(
contentScale = ContentScale.Fit, contentScale = ContentScale.Fit,
modifier = modifier modifier = modifier
.size(50.dp) .size(50.dp)
.clickable { onSimSelected() } .clickable { onSimSelected() },
) )
} }
@Preview @Preview
@Composable @Composable
fun SimSelectionPreview() { private fun SimSelectionPreview() {
MifosTheme { MifosTheme {
Surface { Surface {
ChooseSimDialogSheetContent( ChooseSimDialogSheetContent(
onSimSelected = {} onSimSelected = {},
) )
} }
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.bank.accounts.details package org.mifospay.feature.bank.accounts.details
import androidx.compose.foundation.border import androidx.compose.foundation.border
@ -26,12 +35,12 @@ import org.mifospay.core.designsystem.component.MifosTopBar
import org.mifospay.feature.bank.accounts.R import org.mifospay.feature.bank.accounts.R
@Composable @Composable
fun BankAccountDetailScreen( internal fun BankAccountDetailScreen(
bankAccountDetails: BankAccountDetails, bankAccountDetails: BankAccountDetails,
onSetupUpiPin: () -> Unit, onSetupUpiPin: () -> Unit,
onChangeUpiPin: () -> Unit, onChangeUpiPin: () -> Unit,
onForgotUpiPin: () -> Unit, onForgotUpiPin: () -> Unit,
navigateBack: () -> Unit navigateBack: () -> Unit,
) { ) {
BankAccountDetailScreen( BankAccountDetailScreen(
bankName = bankAccountDetails.bankName.toString(), bankName = bankAccountDetails.bankName.toString(),
@ -43,12 +52,12 @@ fun BankAccountDetailScreen(
onSetupUpiPin = onSetupUpiPin, onSetupUpiPin = onSetupUpiPin,
onChangeUpiPin = onChangeUpiPin, onChangeUpiPin = onChangeUpiPin,
onForgotUpiPin = onForgotUpiPin, onForgotUpiPin = onForgotUpiPin,
navigateBack = navigateBack navigateBack = navigateBack,
) )
} }
@Composable @Composable
fun BankAccountDetailScreen( private fun BankAccountDetailScreen(
bankName: String, bankName: String,
accountHolderName: String, accountHolderName: String,
branchName: String, branchName: String,
@ -58,109 +67,131 @@ fun BankAccountDetailScreen(
onSetupUpiPin: () -> Unit, onSetupUpiPin: () -> Unit,
onChangeUpiPin: () -> Unit, onChangeUpiPin: () -> Unit,
onForgotUpiPin: () -> Unit, onForgotUpiPin: () -> Unit,
navigateBack: () -> Unit navigateBack: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Column(modifier = Modifier.fillMaxSize()) { Column(
MifosTopBar(topBarTitle = R.string.feature_accounts_bank_account_details) { navigateBack.invoke() } modifier = modifier
.fillMaxSize(),
) {
MifosTopBar(
topBarTitle = R.string.feature_accounts_bank_account_details,
backPress = navigateBack,
)
Column( Column(
modifier = Modifier modifier = Modifier
.padding(20.dp) .padding(20.dp)
.border(2.dp, MaterialTheme.colorScheme.onSurface) .border(2.dp, MaterialTheme.colorScheme.onSurface)
.padding(20.dp) .padding(20.dp),
) { ) {
BankAccountDetailRows( BankAccountDetailRows(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
detail = R.string.feature_accounts_bank_name, detail = R.string.feature_accounts_bank_name,
detailValue = bankName detailValue = bankName,
) )
BankAccountDetailRows( BankAccountDetailRows(
modifier = Modifier.fillMaxWidth().padding(top = 10.dp), modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
detail = R.string.feature_accounts_ac_holder_name, detail = R.string.feature_accounts_ac_holder_name,
detailValue = accountHolderName detailValue = accountHolderName,
) )
BankAccountDetailRows( BankAccountDetailRows(
modifier = Modifier.fillMaxWidth().padding(top = 10.dp), modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
detail = R.string.feature_accounts_branch_name, detail = R.string.feature_accounts_branch_name,
detailValue = branchName detailValue = branchName,
) )
BankAccountDetailRows( BankAccountDetailRows(
modifier = Modifier.fillMaxWidth().padding(top = 10.dp), modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
detail = R.string.feature_accounts_ifsc, detail = R.string.feature_accounts_ifsc,
detailValue = ifsc detailValue = ifsc,
) )
BankAccountDetailRows( BankAccountDetailRows(
modifier = Modifier.fillMaxWidth().padding(top = 10.dp), modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
detail = R.string.feature_accounts_type, detail = R.string.feature_accounts_type,
detailValue = type detailValue = type,
) )
} }
Row( Row(
modifier = Modifier.fillMaxWidth().padding(20.dp), modifier = Modifier
horizontalArrangement = Arrangement.SpaceBetween .fillMaxWidth()
.padding(20.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
BankAccountDetailButton( BankAccountDetailButton(
btnText = R.string.feature_accounts_setup_upi, btnText = R.string.feature_accounts_setup_upi,
onClick = { onSetupUpiPin.invoke() }, onClick = { onSetupUpiPin.invoke() },
isUpiEnabled = !isUpiEnabled, isUpiEnabled = !isUpiEnabled,
hasTrailingIcon = false hasTrailingIcon = false,
) )
BankAccountDetailButton( BankAccountDetailButton(
btnText = R.string.feature_accounts_delete_bank, btnText = R.string.feature_accounts_delete_bank,
onClick = {}, onClick = {},
isUpiEnabled = !isUpiEnabled isUpiEnabled = !isUpiEnabled,
) )
} }
Column( Column(
modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(20.dp) modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(20.dp),
) { ) {
BankAccountDetailButton( BankAccountDetailButton(
btnText = R.string.feature_accounts_change_upi_pin, btnText = R.string.feature_accounts_change_upi_pin,
onClick = { onChangeUpiPin.invoke() }, onClick = { onChangeUpiPin.invoke() },
isUpiEnabled = isUpiEnabled, isUpiEnabled = isUpiEnabled,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) )
BankAccountDetailButton( BankAccountDetailButton(
btnText = R.string.feature_accounts_forgot_upi_pin, btnText = R.string.feature_accounts_forgot_upi_pin,
onClick = { onForgotUpiPin.invoke() }, onClick = { onForgotUpiPin.invoke() },
isUpiEnabled = isUpiEnabled, isUpiEnabled = isUpiEnabled,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) )
} }
} }
} }
@Composable @Composable
fun BankAccountDetailRows( private fun BankAccountDetailRows(
modifier: Modifier, detail: Int, detailValue: String detail: Int,
detailValue: String,
modifier: Modifier = Modifier,
) { ) {
Row( Row(
modifier = modifier, modifier = modifier,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = stringResource(id = detail), text = stringResource(id = detail),
modifier = Modifier.padding(end = 10.dp), modifier = Modifier.padding(end = 10.dp),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface,
) )
Text(text = detailValue, Text(
text = detailValue,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface) color = MaterialTheme.colorScheme.onSurface,
)
} }
} }
@Composable @Composable
fun BankAccountDetailButton( private fun BankAccountDetailButton(
modifier: Modifier = Modifier,
btnText: Int, btnText: Int,
onClick: () -> Unit, onClick: () -> Unit,
isUpiEnabled: Boolean, isUpiEnabled: Boolean,
hasTrailingIcon: Boolean = false modifier: Modifier = Modifier,
hasTrailingIcon: Boolean = false,
) { ) {
if (isUpiEnabled) { if (isUpiEnabled) {
Button( Button(
@ -168,23 +199,23 @@ fun BankAccountDetailButton(
colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.primary), colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.primary),
modifier = modifier modifier = modifier
.padding(start = 20.dp, end = 20.dp), .padding(start = 20.dp, end = 20.dp),
contentPadding = PaddingValues(20.dp) contentPadding = PaddingValues(20.dp),
) { ) {
Row( Row(
modifier = modifier, modifier = Modifier,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = stringResource(id = btnText), text = stringResource(id = btnText),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onPrimary color = MaterialTheme.colorScheme.onPrimary,
) )
if (hasTrailingIcon) { if (hasTrailingIcon) {
Icon( Icon(
imageVector = Icons.Filled.ChevronRight, imageVector = Icons.Filled.ChevronRight,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimary tint = MaterialTheme.colorScheme.onPrimary,
) )
} }
} }
@ -192,29 +223,30 @@ fun BankAccountDetailButton(
} }
} }
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun BankAccountDetailUpiDisabledPreview() { private fun BankAccountDetailUpiDisabledPreview() {
BankAccountDetailScreen("Mifos Bank", BankAccountDetailScreen(
"Mifos Bank",
"Mifos Account Holder", "Mifos Account Holder",
"Mifos Branch", "Mifos Branch",
"IFSC", "IFSC",
"type", "type",
false, false,
{}, {}, {}, {} {}, {}, {}, {},
) )
} }
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun BankAccountDetailUpiEnabledPreview() { private fun BankAccountDetailUpiEnabledPreview() {
BankAccountDetailScreen("Mifos Bank", BankAccountDetailScreen(
"Mifos Bank",
"Mifos Account Holder", "Mifos Account Holder",
"Mifos Branch", "Mifos Branch",
"IFSC", "IFSC",
"type", "type",
true, true,
{}, {}, {}, {} {}, {}, {}, {},
) )
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.bank.accounts.link package org.mifospay.feature.bank.accounts.link
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@ -58,11 +67,10 @@ import org.mifospay.core.ui.DevicePreviews
import org.mifospay.feature.bank.accounts.R import org.mifospay.feature.bank.accounts.R
import org.mifospay.feature.bank.accounts.choose.sim.ChooseSimDialogSheet import org.mifospay.feature.bank.accounts.choose.sim.ChooseSimDialogSheet
@Composable @Composable
fun LinkBankAccountRoute( internal fun LinkBankAccountRoute(
viewModel: LinkBankAccountViewModel = hiltViewModel(), viewModel: LinkBankAccountViewModel = hiltViewModel(),
onBackClick: () -> Unit onBackClick: () -> Unit,
) { ) {
val bankUiState by viewModel.bankListUiState.collectAsStateWithLifecycle() val bankUiState by viewModel.bankListUiState.collectAsStateWithLifecycle()
var showSimBottomSheet by rememberSaveable { mutableStateOf(false) } var showSimBottomSheet by rememberSaveable { mutableStateOf(false) }
@ -79,7 +87,7 @@ fun LinkBankAccountRoute(
onBackClick() onBackClick()
} }
} }
} },
) )
} }
@ -93,22 +101,23 @@ fun LinkBankAccountRoute(
viewModel.updateSelectedBank(it) viewModel.updateSelectedBank(it)
showSimBottomSheet = true showSimBottomSheet = true
}, },
onBackClick = onBackClick onBackClick = onBackClick,
) )
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LinkBankAccountScreen( private fun LinkBankAccountScreen(
bankUiState: BankUiState, bankUiState: BankUiState,
showOverlyProgressBar: Boolean, showOverlyProgressBar: Boolean,
onBankSearch: (String) -> Unit, onBankSearch: (String) -> Unit,
onBankSelected: (Bank) -> Unit, onBankSelected: (Bank) -> Unit,
onBackClick: () -> Unit onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Scaffold( Scaffold(
modifier = Modifier.background(color = MaterialTheme.colorScheme.surface), modifier = modifier
.background(color = MaterialTheme.colorScheme.surface),
topBar = { topBar = {
MifosTopAppBar( MifosTopAppBar(
titleRes = R.string.feature_accounts_link_bank_account, titleRes = R.string.feature_accounts_link_bank_account,
@ -119,15 +128,17 @@ fun LinkBankAccountScreen(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
), ),
) )
}) { paddingValues -> },
) { paddingValues ->
Box( Box(
modifier = Modifier.padding(paddingValues) modifier = Modifier
.padding(paddingValues),
) { ) {
when (bankUiState) { when (bankUiState) {
is BankUiState.Loading -> { is BankUiState.Loading -> {
MfLoadingWheel( MfLoadingWheel(
contentDesc = stringResource(R.string.feature_accounts_loading), contentDesc = stringResource(R.string.feature_accounts_loading),
backgroundColor = MaterialTheme.colorScheme.surface backgroundColor = MaterialTheme.colorScheme.surface,
) )
} }
@ -135,7 +146,7 @@ fun LinkBankAccountScreen(
BankListScreenContent( BankListScreenContent(
banks = bankUiState.banks, banks = bankUiState.banks,
onBankSearch = onBankSearch, onBankSearch = onBankSearch,
onBankSelected = onBankSelected onBankSelected = onBankSelected,
) )
} }
} }
@ -148,21 +159,23 @@ fun LinkBankAccountScreen(
} }
@Composable @Composable
fun BankListScreenContent( private fun BankListScreenContent(
banks: List<Bank>, banks: List<Bank>,
onBankSearch: (String) -> Unit, onBankSearch: (String) -> Unit,
onBankSelected: (Bank) -> Unit onBankSelected: (Bank) -> Unit,
modifier: Modifier = Modifier,
) { ) {
var searchQuery by rememberSaveable { mutableStateOf("") } var searchQuery by rememberSaveable { mutableStateOf("") }
Column( Column(
modifier = Modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.background(color = MaterialTheme.colorScheme.surface) .background(color = MaterialTheme.colorScheme.surface)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState()),
) { ) {
MifosOutlinedTextField(modifier = Modifier MifosOutlinedTextField(
.fillMaxWidth() modifier = Modifier
.padding(horizontal = 16.dp), .fillMaxWidth()
.padding(horizontal = 16.dp),
value = searchQuery, value = searchQuery,
onValueChange = { onValueChange = {
searchQuery = it searchQuery = it
@ -171,7 +184,8 @@ fun BankListScreenContent(
label = R.string.feature_accounts_search, label = R.string.feature_accounts_search,
trailingIcon = { trailingIcon = {
Icon(imageVector = Icons.Filled.Search, contentDescription = null) Icon(imageVector = Icons.Filled.Search, contentDescription = null)
}) },
)
if (searchQuery.isBlank()) { if (searchQuery.isBlank()) {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
@ -179,23 +193,23 @@ fun BankListScreenContent(
text = stringResource(id = R.string.feature_accounts_popular_banks), text = stringResource(id = R.string.feature_accounts_popular_banks),
style = TextStyle( style = TextStyle(
MaterialTheme.colorScheme.onSurface, MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium,
), ),
modifier = Modifier.padding(start = 16.dp) modifier = Modifier.padding(start = 16.dp),
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
PopularBankGridBody( PopularBankGridBody(
banks = banks.filter { it.bankType == BankType.POPULAR }, banks = banks.filter { it.bankType == BankType.POPULAR },
onBankSelected = onBankSelected onBankSelected = onBankSelected,
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Text( Text(
text = stringResource(id = R.string.feature_accounts_other_banks), text = stringResource(id = R.string.feature_accounts_other_banks),
style = TextStyle( style = TextStyle(
MaterialTheme.colorScheme.onSurface, MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium,
), ),
modifier = Modifier.padding(start = 16.dp) modifier = Modifier.padding(start = 16.dp),
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
} }
@ -203,34 +217,37 @@ fun BankListScreenContent(
BankListBody( BankListBody(
banks = if (searchQuery.isBlank()) { banks = if (searchQuery.isBlank()) {
banks.filter { it.bankType == BankType.OTHER } banks.filter { it.bankType == BankType.OTHER }
} else banks, } else {
onBankSelected = onBankSelected banks
},
onBankSelected = onBankSelected,
) )
} }
} }
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun PopularBankGridBody( private fun PopularBankGridBody(
banks: List<Bank>, banks: List<Bank>,
onBankSelected: (Bank) -> Unit onBankSelected: (Bank) -> Unit,
modifier: Modifier = Modifier,
) { ) {
MifosCard( MifosCard(
modifier = Modifier, modifier = modifier,
shape = RoundedCornerShape(0.dp), shape = RoundedCornerShape(0.dp),
elevation = 2.dp, elevation = 2.dp,
colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surface) colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surface),
) { ) {
FlowRow( FlowRow(
modifier = Modifier, modifier = Modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
maxItemsInEachRow = 3 maxItemsInEachRow = 3,
) { ) {
banks.forEach { banks.forEach {
PopularBankItemBody( PopularBankItemBody(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
bank = it, bank = it,
onBankSelected = onBankSelected onBankSelected = onBankSelected,
) )
} }
} }
@ -238,10 +255,10 @@ fun PopularBankGridBody(
} }
@Composable @Composable
fun PopularBankItemBody( private fun PopularBankItemBody(
modifier: Modifier,
bank: Bank, bank: Bank,
onBankSelected: (Bank) -> Unit onBankSelected: (Bank) -> Unit,
modifier: Modifier = Modifier,
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@ -262,18 +279,19 @@ fun PopularBankItemBody(
Text( Text(
text = bank.name, text = bank.name,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 4.dp, bottom = 16.dp) modifier = Modifier.padding(top = 4.dp, bottom = 16.dp),
) )
} }
} }
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun BankListBody( private fun BankListBody(
banks: List<Bank>, banks: List<Bank>,
onBankSelected: (Bank) -> Unit onBankSelected: (Bank) -> Unit,
modifier: Modifier = Modifier,
) { ) {
FlowColumn { FlowColumn(modifier) {
banks.forEach { bank -> banks.forEach { bank ->
BankListItemBody(bank = bank, onBankSelected = onBankSelected) BankListItemBody(bank = bank, onBankSelected = onBankSelected)
} }
@ -281,14 +299,15 @@ fun BankListBody(
} }
@Composable @Composable
fun BankListItemBody( private fun BankListItemBody(
bank: Bank, bank: Bank,
onBankSelected: (Bank) -> Unit onBankSelected: (Bank) -> Unit,
modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = Modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.clickable { onBankSelected(bank) } .clickable { onBankSelected(bank) },
) { ) {
HorizontalDivider() HorizontalDivider()
Row( Row(
@ -296,7 +315,7 @@ fun BankListItemBody(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 24.dp, top = 8.dp, bottom = 8.dp) .padding(start = 24.dp, top = 8.dp, bottom = 8.dp),
) { ) {
Image( Image(
modifier = Modifier.size(32.dp), modifier = Modifier.size(32.dp),
@ -305,7 +324,8 @@ fun BankListItemBody(
) )
Text( Text(
modifier = Modifier.padding(start = 16.dp, end = 16.dp), modifier = Modifier.padding(start = 16.dp, end = 16.dp),
text = bank.name, style = TextStyle(fontSize = 14.sp) text = bank.name,
style = TextStyle(fontSize = 14.sp),
) )
} }
} }
@ -323,7 +343,7 @@ private fun LinkBankAccountScreenPreview(
showOverlyProgressBar = false, showOverlyProgressBar = false,
onBankSelected = { }, onBankSelected = { },
onBankSearch = { }, onBankSearch = { },
onBackClick = { } onBackClick = { },
) )
} }
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.bank.accounts.link package org.mifospay.feature.bank.accounts.link
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -24,17 +33,17 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class LinkBankAccountViewModel @Inject constructor( class LinkBankAccountViewModel @Inject constructor(
localAssetRepository: MifosLocalAssetRepository localAssetRepository: MifosLocalAssetRepository,
) : ViewModel() { ) : ViewModel() {
private val _searchQuery = MutableStateFlow("") private val searchQuery = MutableStateFlow("")
private var selectedBank by mutableStateOf<Bank?>(null) private var selectedBank by mutableStateOf<Bank?>(null)
private val _bankAccountDetails: MutableStateFlow<BankAccountDetails?> = MutableStateFlow(null) private val accountDetails: MutableStateFlow<BankAccountDetails?> = MutableStateFlow(null)
val bankAccountDetails: StateFlow<BankAccountDetails?> = _bankAccountDetails.asStateFlow() val bankAccountDetails: StateFlow<BankAccountDetails?> = accountDetails.asStateFlow()
fun updateSearchQuery(query: String) { fun updateSearchQuery(query: String) {
_searchQuery.update { query } searchQuery.update { query }
} }
fun updateSelectedBank(bank: Bank) { fun updateSelectedBank(bank: Bank) {
@ -42,9 +51,9 @@ class LinkBankAccountViewModel @Inject constructor(
} }
val bankListUiState: StateFlow<BankUiState> = combine( val bankListUiState: StateFlow<BankUiState> = combine(
_searchQuery, searchQuery,
localAssetRepository.getBanks(), localAssetRepository.getBanks(),
::Pair ::Pair,
).map { searchQueryAndBanks -> ).map { searchQueryAndBanks ->
val searchQuery = searchQueryAndBanks.first val searchQuery = searchQueryAndBanks.first
val localBanks = searchQueryAndBanks.second.map { val localBanks = searchQueryAndBanks.second.map {
@ -55,7 +64,7 @@ class LinkBankAccountViewModel @Inject constructor(
addAll(localBanks) addAll(localBanks)
}.distinctBy { it.name } }.distinctBy { it.name }
BankUiState.Success( BankUiState.Success(
banks.filter { it.name.contains(searchQuery.lowercase(), ignoreCase = true) } banks.filter { it.name.contains(searchQuery.lowercase(), ignoreCase = true) },
) )
}.stateIn( }.stateIn(
scope = viewModelScope, scope = viewModelScope,
@ -70,17 +79,20 @@ class LinkBankAccountViewModel @Inject constructor(
Bank("PNB Bank", R.drawable.feature_accounts_logo_pnb, BankType.POPULAR), Bank("PNB Bank", R.drawable.feature_accounts_logo_pnb, BankType.POPULAR),
Bank("HDFC Bank", R.drawable.feature_accounts_logo_hdfc, BankType.POPULAR), Bank("HDFC Bank", R.drawable.feature_accounts_logo_hdfc, BankType.POPULAR),
Bank("ICICI Bank", R.drawable.feature_accounts_logo_icici, BankType.POPULAR), Bank("ICICI Bank", R.drawable.feature_accounts_logo_icici, BankType.POPULAR),
Bank("AXIS Bank", R.drawable.feature_accounts_logo_axis, BankType.POPULAR) Bank("AXIS Bank", R.drawable.feature_accounts_logo_axis, BankType.POPULAR),
) )
} }
fun fetchBankAccountDetails(onBankDetailsSuccess: () -> Unit) { fun fetchBankAccountDetails(onBankDetailsSuccess: () -> Unit) {
// TODO:: UPI API implement, Implement with real API, // TODO:: UPI API implement, Implement with real API,
// It revert back to Account Screen after successful BankAccount Add // It revert back to Account Screen after successful BankAccount Add
_bankAccountDetails.update { accountDetails.update {
BankAccountDetails( BankAccountDetails(
selectedBank?.name, "Ankur Sharma", "New Delhi", selectedBank?.name,
mRandom.nextInt().toString() + " ", "Savings" "Ankur Sharma",
"New Delhi",
mRandom.nextInt().toString() + " ",
"Savings",
) )
} }
onBankDetailsSuccess.invoke() onBankDetailsSuccess.invoke()

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.bank.accounts.link package org.mifospay.feature.bank.accounts.link
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@ -20,6 +29,6 @@ class LinkBankUiStatePreviewParameterProvider : PreviewParameterProvider<BankUiS
} }
override val values: Sequence<BankUiState> = sequenceOf( override val values: Sequence<BankUiState> = sequenceOf(
BankUiState.Success(banks = banks) BankUiState.Success(banks = banks),
) )
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.bank.accounts.navigation package org.mifospay.feature.bank.accounts.navigation
import androidx.navigation.NavController import androidx.navigation.NavController
@ -16,14 +25,14 @@ fun NavGraphBuilder.bankAccountDetailScreen(
onSetupUpiPin: (BankAccountDetails, Int) -> Unit, onSetupUpiPin: (BankAccountDetails, Int) -> Unit,
onChangeUpiPin: (BankAccountDetails, Int) -> Unit, onChangeUpiPin: (BankAccountDetails, Int) -> Unit,
onForgotUpiPin: (BankAccountDetails, Int) -> Unit, onForgotUpiPin: (BankAccountDetails, Int) -> Unit,
onBackClick: (BankAccountDetails, Int) -> Unit onBackClick: (BankAccountDetails, Int) -> Unit,
) { ) {
composable( composable(
route = "$BANK_ACCOUNT_DETAIL_ROUTE/{${Constants.BANK_ACCOUNT_DETAILS}}/{${Constants.INDEX}}", route = "$BANK_ACCOUNT_DETAIL_ROUTE/{${Constants.BANK_ACCOUNT_DETAILS}}/{${Constants.INDEX}}",
arguments = listOf( arguments = listOf(
navArgument(Constants.BANK_ACCOUNT_DETAILS) { type = NavType.StringType }, navArgument(Constants.BANK_ACCOUNT_DETAILS) { type = NavType.StringType },
navArgument(Constants.INDEX) { type = NavType.IntType } navArgument(Constants.INDEX) { type = NavType.IntType },
) ),
) { backStackEntry -> ) { backStackEntry ->
val bankAccountDetails = val bankAccountDetails =
backStackEntry.arguments?.getParcelable(Constants.BANK_ACCOUNT_DETAILS) backStackEntry.arguments?.getParcelable(Constants.BANK_ACCOUNT_DETAILS)
@ -47,7 +56,7 @@ fun NavGraphBuilder.bankAccountDetailScreen(
// TODO: Use global snackbar // TODO: Use global snackbar
} }
}, },
navigateBack = { onBackClick(bankAccountDetails, index) } navigateBack = { onBackClick(bankAccountDetails, index) },
) )
} }
} }
@ -55,7 +64,7 @@ fun NavGraphBuilder.bankAccountDetailScreen(
fun NavController.navigateToBankAccountDetail( fun NavController.navigateToBankAccountDetail(
bankAccountDetails: BankAccountDetails, bankAccountDetails: BankAccountDetails,
index: Int, index: Int,
navOptions: NavOptions? = null navOptions: NavOptions? = null,
) { ) {
this.navigate("$BANK_ACCOUNT_DETAIL_ROUTE/$bankAccountDetails/$index", navOptions) this.navigate("$BANK_ACCOUNT_DETAIL_ROUTE/$bankAccountDetails/$index", navOptions)
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.bank.accounts.navigation package org.mifospay.feature.bank.accounts.navigation
import androidx.navigation.NavController import androidx.navigation.NavController
@ -12,11 +21,11 @@ fun NavController.navigateToLinkBankAccount(navOptions: NavOptions? = null) =
navigate(LINK_BANK_ACCOUNT_ROUTE, navOptions) navigate(LINK_BANK_ACCOUNT_ROUTE, navOptions)
fun NavGraphBuilder.linkBankAccountScreen( fun NavGraphBuilder.linkBankAccountScreen(
onBackClick: () -> Unit onBackClick: () -> Unit,
) { ) {
composable(route = LINK_BANK_ACCOUNT_ROUTE) { composable(route = LINK_BANK_ACCOUNT_ROUTE) {
LinkBankAccountRoute( LinkBankAccountRoute(
onBackClick = onBackClick onBackClick = onBackClick,
) )
} }
} }

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="60dp" android:width="60dp"
android:height="60dp" android:height="60dp"

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="60dp" android:width="60dp"
android:height="60dp" android:height="60dp"

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<resources> <resources>
<color name="feature_accounts_colorBlack87">#DE000000</color> <color name="feature_accounts_colorBlack87">#DE000000</color>
<color name="feature_accounts_colorTextPrimary">@color/feature_accounts_colorBlack87</color> <color name="feature_accounts_colorTextPrimary">@color/feature_accounts_colorBlack87</color>

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<resources> <resources>
<string name="feature_accounts_bank_account_details">Bank Account Details</string> <string name="feature_accounts_bank_account_details">Bank Account Details</string>
<string name="feature_accounts_bank_name">Bank Name</string> <string name="feature_accounts_bank_name">Bank Name</string>

View File

@ -1,16 +0,0 @@
package org.mifospay.feature.bank.accounts
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
plugins { plugins {
alias(libs.plugins.mifospay.android.feature) alias(libs.plugins.mifospay.android.feature)
alias(libs.plugins.mifospay.android.library.compose) alias(libs.plugins.mifospay.android.library.compose)

View File

@ -1,24 +0,0 @@
package org.mifospay.mobilewallet.mifospay.feature.auth
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("org.mifos.mobilewallet.mifospay.auth.test", appContext.packageName)
}
}

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest> </manifest>

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.auth.login package org.mifospay.feature.auth.login
import android.os.Bundle import android.os.Bundle

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.auth.login package org.mifospay.feature.auth.login
import android.content.Context import android.content.Context
@ -53,8 +62,9 @@ import org.mifospay.feature.auth.socialSignup.SocialSignupMethodContentScreen
import org.mifospay.feature.passcode.PassCodeActivity import org.mifospay.feature.passcode.PassCodeActivity
@Composable @Composable
fun LoginScreen( internal fun LoginScreen(
viewModel: LoginViewModel = hiltViewModel() modifier: Modifier = Modifier,
viewModel: LoginViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current val context = LocalContext.current
val showProgress by viewModel.showProgress.collectAsStateWithLifecycle() val showProgress by viewModel.showProgress.collectAsStateWithLifecycle()
@ -65,6 +75,7 @@ fun LoginScreen(
} }
LoginScreenContent( LoginScreenContent(
modifier = modifier,
showProgress = showProgress, showProgress = showProgress,
login = { username, password -> login = { username, password ->
viewModel.loginUser( viewModel.loginUser(
@ -72,9 +83,9 @@ fun LoginScreen(
password = password, password = password,
onLoginFailed = { message -> onLoginFailed = { message ->
Toast.makeText(context, message, Toast.LENGTH_SHORT).show() Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
} },
) )
} },
) )
if (isLoginSuccess) { if (isLoginSuccess) {
@ -84,20 +95,21 @@ fun LoginScreen(
@Composable @Composable
@Suppress("LongMethod") @Suppress("LongMethod")
fun LoginScreenContent( private fun LoginScreenContent(
showProgress: Boolean, showProgress: Boolean,
login: (username: String, password: String) -> Unit, login: (username: String, password: String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
var showSignUpScreen by rememberSaveable { mutableStateOf(false) } var showSignUpScreen by rememberSaveable { mutableStateOf(false) }
var userName by rememberSaveable(stateSaver = TextFieldValue.Saver) { var userName by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf( mutableStateOf(
TextFieldValue("") TextFieldValue(""),
) )
} }
var password by rememberSaveable(stateSaver = TextFieldValue.Saver) { var password by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf( mutableStateOf(
TextFieldValue("") TextFieldValue(""),
) )
} }
var passwordVisibility: Boolean by remember { mutableStateOf(false) } var passwordVisibility: Boolean by remember { mutableStateOf(false) }
@ -108,25 +120,25 @@ fun LoginScreenContent(
} }
} }
Box { Box(modifier) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(top = 100.dp, start = 48.dp, end = 48.dp), .padding(top = 100.dp, start = 48.dp, end = 48.dp),
horizontalAlignment = Alignment.Start horizontalAlignment = Alignment.Start,
) { ) {
Text( Text(
text = stringResource(id = R.string.feature_auth_login), text = stringResource(id = R.string.feature_auth_login),
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary,
) )
Text( Text(
modifier = Modifier modifier = Modifier
.padding(top = 32.dp), .padding(top = 32.dp),
text = stringResource(id = R.string.feature_auth_welcome_back), text = stringResource(id = R.string.feature_auth_welcome_back),
style = styleNormal18sp.copy(color = grey) style = styleNormal18sp.copy(color = grey),
) )
Spacer(modifier = Modifier.padding(top = 32.dp)) Spacer(modifier = Modifier.padding(top = 32.dp))
MifosOutlinedTextField( MifosOutlinedTextField(
@ -135,7 +147,7 @@ fun LoginScreenContent(
onValueChange = { onValueChange = {
userName = it userName = it
}, },
label = R.string.feature_auth_username label = R.string.feature_auth_username,
) )
Spacer(modifier = Modifier.padding(top = 16.dp)) Spacer(modifier = Modifier.padding(top = 16.dp))
MifosOutlinedTextField( MifosOutlinedTextField(
@ -147,15 +159,19 @@ fun LoginScreenContent(
label = R.string.feature_auth_password, label = R.string.feature_auth_password,
visualTransformation = if (passwordVisibility) { visualTransformation = if (passwordVisibility) {
VisualTransformation.None VisualTransformation.None
} else PasswordVisualTransformation(), } else {
PasswordVisualTransformation()
},
trailingIcon = { trailingIcon = {
val image = if (passwordVisibility) val image = if (passwordVisibility) {
Icons.Filled.Visibility Icons.Filled.Visibility
else Icons.Filled.VisibilityOff } else {
Icons.Filled.VisibilityOff
}
IconButton(onClick = { passwordVisibility = !passwordVisibility }) { IconButton(onClick = { passwordVisibility = !passwordVisibility }) {
Icon(imageVector = image, null) Icon(imageVector = image, null)
} }
} },
) )
Button( Button(
modifier = Modifier modifier = Modifier
@ -166,12 +182,12 @@ fun LoginScreenContent(
onClick = { onClick = {
login.invoke(userName.text, password.text) login.invoke(userName.text, password.text)
}, },
contentPadding = PaddingValues(12.dp) contentPadding = PaddingValues(12.dp),
) { ) {
Text( Text(
text = stringResource(id = R.string.feature_auth_login).uppercase(), text = stringResource(id = R.string.feature_auth_login).uppercase(),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onPrimary color = MaterialTheme.colorScheme.onPrimary,
) )
} }
// Hide reset password for now // Hide reset password for now
@ -197,12 +213,12 @@ fun LoginScreenContent(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 24.dp), .padding(top = 24.dp),
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center,
) { ) {
Text( Text(
text = "Dont have an account yet? ", text = "Dont have an account yet? ",
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface,
) )
Text( Text(
modifier = Modifier.clickable { modifier = Modifier.clickable {
@ -218,11 +234,10 @@ fun LoginScreenContent(
if (showProgress) { if (showProgress) {
MfOverlayLoadingWheel( MfOverlayLoadingWheel(
contentDesc = stringResource(id = R.string.feature_auth_logging_in) contentDesc = stringResource(id = R.string.feature_auth_logging_in),
) )
} }
} }
} }
/** /**
@ -237,11 +252,11 @@ private fun startPassCodeActivity(context: Context) {
@Preview(showSystemUi = true, device = "id:pixel_5") @Preview(showSystemUi = true, device = "id:pixel_5")
@Composable @Composable
fun LoanScreenPreview() { private fun LoanScreenPreview() {
MifosTheme { MifosTheme {
LoginScreenContent( LoginScreenContent(
showProgress = false, showProgress = false,
login = { _, _ -> } login = { _, _ -> },
) )
} }
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.auth.login package org.mifospay.feature.auth.login
import android.util.Log import android.util.Log
@ -43,7 +52,6 @@ class LoginViewModel @Inject constructor(
_isLoginSuccess.update { isLoginSuccess } _isLoginSuccess.update { isLoginSuccess }
} }
/** /**
* Authenticate User with username and password * Authenticate User with username and password
* @param username * @param username
@ -53,14 +61,16 @@ class LoginViewModel @Inject constructor(
fun loginUser( fun loginUser(
username: String, username: String,
password: String, password: String,
onLoginFailed: (String) -> Unit onLoginFailed: (String) -> Unit,
) { ) {
updateProgressState(true) updateProgressState(true)
authenticateUserUseCase.walletRequestValues = authenticateUserUseCase.walletRequestValues =
AuthenticateUser.RequestValues(username, password) AuthenticateUser.RequestValues(username, password)
val requestValue = authenticateUserUseCase.walletRequestValues val requestValue = authenticateUserUseCase.walletRequestValues
mUsecaseHandler.execute(authenticateUserUseCase, requestValue, mUsecaseHandler.execute(
authenticateUserUseCase,
requestValue,
object : UseCaseCallback<AuthenticateUser.ResponseValue> { object : UseCaseCallback<AuthenticateUser.ResponseValue> {
override fun onSuccess(response: AuthenticateUser.ResponseValue) { override fun onSuccess(response: AuthenticateUser.ResponseValue) {
saveAuthTokenInPref(response.user) saveAuthTokenInPref(response.user)
@ -72,16 +82,17 @@ class LoginViewModel @Inject constructor(
updateProgressState(false) updateProgressState(false)
onLoginFailed(message) onLoginFailed(message)
} }
}) },
)
} }
/** /**
* Fetch user details return by authenticated user * Fetch user details return by authenticated user
* @param user * @param user
*/ */
private fun fetchUserDetails(user: User) { private fun fetchUserDetails(user: User) {
mUsecaseHandler.execute(fetchUserDetailsUseCase, mUsecaseHandler.execute(
fetchUserDetailsUseCase,
FetchUserDetails.RequestValues(user.userId), FetchUserDetails.RequestValues(user.userId),
object : UseCaseCallback<FetchUserDetails.ResponseValue> { object : UseCaseCallback<FetchUserDetails.ResponseValue> {
override fun onSuccess(response: FetchUserDetails.ResponseValue) { override fun onSuccess(response: FetchUserDetails.ResponseValue) {
@ -92,7 +103,8 @@ class LoginViewModel @Inject constructor(
updateProgressState(false) updateProgressState(false)
Log.d("Login User Detailed: ", message) Log.d("Login User Detailed: ", message)
} }
}) },
)
} }
/** /**
@ -116,7 +128,8 @@ class LoginViewModel @Inject constructor(
override fun onError(message: String) { override fun onError(message: String) {
updateProgressState(false) updateProgressState(false)
} }
}) },
)
} }
private fun saveAuthTokenInPref(user: User) { private fun saveAuthTokenInPref(user: User) {
@ -128,7 +141,7 @@ class LoginViewModel @Inject constructor(
*/ */
private fun saveUserDetails( private fun saveUserDetails(
user: User, user: User,
userWithRole: UserWithRole userWithRole: UserWithRole,
) { ) {
val userName = user.username val userName = user.username
val userID = user.userId val userID = user.userId
@ -147,4 +160,4 @@ class LoginViewModel @Inject constructor(
preferencesHelper.saveMobile(client.mobileNo) preferencesHelper.saveMobile(client.mobileNo)
preferencesHelper.client = client preferencesHelper.client = client
} }
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.auth.mobileVerify package org.mifospay.feature.auth.mobileVerify
import android.widget.Toast import android.widget.Toast
@ -41,16 +50,17 @@ import org.mifospay.core.designsystem.component.MifosOutlinedTextField
import org.mifospay.core.designsystem.theme.MifosTheme import org.mifospay.core.designsystem.theme.MifosTheme
import org.mifospay.feature.auth.R import org.mifospay.feature.auth.R
@Composable @Composable
fun MobileVerificationScreen( internal fun MobileVerificationScreen(
onOtpVerificationSuccess: (String) -> Unit,
modifier: Modifier = Modifier,
viewModel: MobileVerificationViewModel = hiltViewModel(), viewModel: MobileVerificationViewModel = hiltViewModel(),
onOtpVerificationSuccess: (String) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val uiState by viewModel.uiState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle()
MobileVerificationScreen(uiState = uiState, MobileVerificationScreen(
uiState = uiState,
showProgressState = viewModel.showProgress, showProgressState = viewModel.showProgress,
verifyMobileAndRequestOtp = { phone, fullPhone -> verifyMobileAndRequestOtp = { phone, fullPhone ->
viewModel.verifyMobileAndRequestOtp(fullPhone, phone) { viewModel.verifyMobileAndRequestOtp(fullPhone, phone) {
@ -63,18 +73,19 @@ fun MobileVerificationScreen(
viewModel.verifyOTP(validatedOtp) { viewModel.verifyOTP(validatedOtp) {
onOtpVerificationSuccess(fullNumber) onOtpVerificationSuccess(fullNumber)
} }
} },
modifier = modifier,
) )
} }
@Composable @Composable
fun MobileVerificationScreen( private fun MobileVerificationScreen(
uiState: MobileVerificationUiState, uiState: MobileVerificationUiState,
showProgressState: Boolean = false,
verifyMobileAndRequestOtp: (String, String) -> Unit, verifyMobileAndRequestOtp: (String, String) -> Unit,
verifyOtp: (String, String) -> Unit verifyOtp: (String, String) -> Unit,
modifier: Modifier = Modifier,
showProgressState: Boolean = false,
) { ) {
var phoneNumber by rememberSaveable { mutableStateOf("") } var phoneNumber by rememberSaveable { mutableStateOf("") }
var fullPhoneNumber by rememberSaveable { mutableStateOf("") } var fullPhoneNumber by rememberSaveable { mutableStateOf("") }
var isNumberValid: Boolean by rememberSaveable { mutableStateOf(false) } var isNumberValid: Boolean by rememberSaveable { mutableStateOf(false) }
@ -90,19 +101,18 @@ fun MobileVerificationScreen(
} }
} }
Box { Box(modifier) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(color = Color.White) .background(color = Color.White)
.focusable(!showProgressState), .focusable(!showProgressState),
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(color = MaterialTheme.colorScheme.primary), .background(color = MaterialTheme.colorScheme.primary),
verticalArrangement = Arrangement.Top verticalArrangement = Arrangement.Top,
) { ) {
Text( Text(
modifier = Modifier.padding(top = 48.dp, start = 24.dp, end = 24.dp), modifier = Modifier.padding(top = 48.dp, start = 24.dp, end = 24.dp),
@ -111,11 +121,14 @@ fun MobileVerificationScreen(
} else { } else {
stringResource(id = R.string.feature_auth_enter_otp) stringResource(id = R.string.feature_auth_enter_otp)
}, },
style = MaterialTheme.typography.titleLarge.copy(color = MaterialTheme.colorScheme.onPrimary) style = MaterialTheme.typography.titleLarge.copy(color = MaterialTheme.colorScheme.onPrimary),
) )
Text( Text(
modifier = Modifier.padding( modifier = Modifier.padding(
top = 4.dp, bottom = 32.dp, start = 24.dp, end = 24.dp top = 4.dp,
bottom = 32.dp,
start = 24.dp,
end = 24.dp,
), ),
text = if (uiState == MobileVerificationUiState.VerifyPhone) { text = if (uiState == MobileVerificationUiState.VerifyPhone) {
stringResource(id = R.string.feature_auth_enter_mobile_number_description) stringResource(id = R.string.feature_auth_enter_mobile_number_description)
@ -123,7 +136,7 @@ fun MobileVerificationScreen(
stringResource(id = R.string.feature_auth_enter_otp_received_on_your_registered_device) stringResource(id = R.string.feature_auth_enter_otp_received_on_your_registered_device)
}, },
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onPrimary color = MaterialTheme.colorScheme.onPrimary,
) )
} }
@ -137,7 +150,7 @@ fun MobileVerificationScreen(
phoneNumber = phone phoneNumber = phone
fullPhoneNumber = fullPhone fullPhoneNumber = fullPhone
isNumberValid = valid isNumberValid = valid
} },
) )
} }
@ -145,11 +158,12 @@ fun MobileVerificationScreen(
EnterOtpScreen( EnterOtpScreen(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 48.dp, vertical = 24.dp) .padding(horizontal = 48.dp, vertical = 24.dp),
) { isValidated, otp -> onOtpValidated = { isValidated, otp ->
isOtpValidated = isValidated isOtpValidated = isValidated
validatedOtp = otp validatedOtp = otp
} },
)
} }
} }
@ -171,8 +185,9 @@ fun MobileVerificationScreen(
stringResource(id = R.string.feature_auth_verify_phone).uppercase() stringResource(id = R.string.feature_auth_verify_phone).uppercase()
} else { } else {
stringResource(id = R.string.feature_auth_verify_otp).uppercase() stringResource(id = R.string.feature_auth_verify_otp).uppercase()
}, style = MaterialTheme.typography.labelLarge, },
color = MaterialTheme.colorScheme.onPrimary style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onPrimary,
) )
} }
} }
@ -184,29 +199,29 @@ fun MobileVerificationScreen(
} }
@Composable @Composable
fun EnterPhoneScreen( private fun EnterPhoneScreen(
modifier: Modifier, onNumberUpdated: (String, String, Boolean) -> Unit,
onNumberUpdated: (String, String, Boolean) -> Unit modifier: Modifier = Modifier,
) { ) {
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
TogiCountryCodePicker( TogiCountryCodePicker(
modifier = modifier, modifier = modifier,
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
colors = TextFieldDefaults.outlinedTextFieldColors( colors = TextFieldDefaults.outlinedTextFieldColors(
focusedBorderColor = MaterialTheme.colorScheme.primary focusedBorderColor = MaterialTheme.colorScheme.primary,
), ),
onValueChange = { (code, phone), isValid -> onValueChange = { (code, phone), isValid ->
onNumberUpdated(phone, code + phone, isValid) onNumberUpdated(phone, code + phone, isValid)
}, },
label = { Text(stringResource(id = R.string.feature_auth_phone_number)) }, label = { Text(stringResource(id = R.string.feature_auth_phone_number)) },
keyboardActions = KeyboardActions { keyboardController?.hide() } keyboardActions = KeyboardActions { keyboardController?.hide() },
) )
} }
@Composable @Composable
fun EnterOtpScreen( private fun EnterOtpScreen(
modifier: Modifier, onOtpValidated: (Boolean, String) -> Unit,
onOtpValidated: (Boolean, String) -> Unit modifier: Modifier = Modifier,
) { ) {
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
var otp by rememberSaveable(stateSaver = TextFieldValue.Saver) { var otp by rememberSaveable(stateSaver = TextFieldValue.Saver) {
@ -221,20 +236,21 @@ fun EnterOtpScreen(
onOtpValidated(otp.text.length == 6, otp.text) onOtpValidated(otp.text.length == 6, otp.text)
}, },
label = R.string.feature_auth_enter_otp, label = R.string.feature_auth_enter_otp,
keyboardActions = KeyboardActions { keyboardController?.hide() } keyboardActions = KeyboardActions { keyboardController?.hide() },
) )
} }
@Composable @Composable
fun ShowProgressScreen( private fun ShowProgressScreen(
uiState: MobileVerificationUiState, uiState: MobileVerificationUiState,
modifier: Modifier = Modifier,
) { ) {
Box( Box(
modifier = Modifier modifier = modifier
.fillMaxSize() .fillMaxSize()
.background(color = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)) .background(color = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f))
.focusable(), .focusable(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center,
) { ) {
MifosLoadingWheel( MifosLoadingWheel(
modifier = Modifier.wrapContentSize(), modifier = Modifier.wrapContentSize(),
@ -242,31 +258,33 @@ fun ShowProgressScreen(
Constants.SENDING_OTP_TO_YOUR_MOBILE_NUMBER Constants.SENDING_OTP_TO_YOUR_MOBILE_NUMBER
} else { } else {
Constants.VERIFYING_OTP Constants.VERIFYING_OTP
} },
) )
} }
} }
@Preview @Preview
@Composable @Composable
fun MobileVerificationScreenVerifyPhonePreview() { private fun MobileVerificationScreenVerifyPhonePreview() {
MifosTheme { MifosTheme {
MobileVerificationScreen(uiState = MobileVerificationUiState.VerifyPhone, MobileVerificationScreen(
uiState = MobileVerificationUiState.VerifyPhone,
showProgressState = false, showProgressState = false,
verifyMobileAndRequestOtp = { _, _ -> }, verifyMobileAndRequestOtp = { _, _ -> },
verifyOtp = { _, _ -> } verifyOtp = { _, _ -> },
) )
} }
} }
@Preview @Preview
@Composable @Composable
fun MobileVerificationScreenVerifyOtpPreview() { private fun MobileVerificationScreenVerifyOtpPreview() {
MifosTheme { MifosTheme {
MobileVerificationScreen(uiState = MobileVerificationUiState.VerifyOtp, MobileVerificationScreen(
uiState = MobileVerificationUiState.VerifyOtp,
showProgressState = false, showProgressState = false,
verifyMobileAndRequestOtp = { _, _ -> }, verifyMobileAndRequestOtp = { _, _ -> },
verifyOtp = { _, _ -> } verifyOtp = { _, _ -> },
) )
} }
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.auth.mobileVerify package org.mifospay.feature.auth.mobileVerify
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -20,7 +29,7 @@ import javax.inject.Inject
@Suppress("UnusedParameter") @Suppress("UnusedParameter")
class MobileVerificationViewModel @Inject constructor( class MobileVerificationViewModel @Inject constructor(
private val mUseCaseHandler: UseCaseHandler, private val mUseCaseHandler: UseCaseHandler,
private val searchClientUseCase: SearchClient private val searchClientUseCase: SearchClient,
) : ViewModel() { ) : ViewModel() {
private val _uiState = private val _uiState =
@ -35,10 +44,11 @@ class MobileVerificationViewModel @Inject constructor(
fun verifyMobileAndRequestOtp( fun verifyMobileAndRequestOtp(
fullNumber: String, fullNumber: String,
mobileNo: String, mobileNo: String,
onError: (String?) -> Unit onError: (String?) -> Unit,
) { ) {
showProgress = true showProgress = true
mUseCaseHandler.execute(searchClientUseCase, mUseCaseHandler.execute(
searchClientUseCase,
fullNumber.let { SearchClient.RequestValues(it) }, fullNumber.let { SearchClient.RequestValues(it) },
object : UseCase.UseCaseCallback<SearchClient.ResponseValue> { object : UseCase.UseCaseCallback<SearchClient.ResponseValue> {
override fun onSuccess(response: SearchClient.ResponseValue) { override fun onSuccess(response: SearchClient.ResponseValue) {
@ -49,7 +59,8 @@ class MobileVerificationViewModel @Inject constructor(
override fun onError(message: String) { override fun onError(message: String) {
requestOtp(fullNumber) requestOtp(fullNumber)
} }
}) },
)
} }
/** /**

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.auth.navigation package org.mifospay.feature.auth.navigation
import androidx.navigation.NavController import androidx.navigation.NavController
@ -10,7 +19,7 @@ const val LOGIN_ROUTE = "login_route"
@Suppress("UnusedParameter") @Suppress("UnusedParameter")
fun NavGraphBuilder.loginScreen( fun NavGraphBuilder.loginScreen(
onDismissSignUp: () -> Unit, onDismissSignUp: () -> Unit,
onNavigateToMobileVerificationScreen:(Int,String,String,String,String,) -> Unit onNavigateToMobileVerificationScreen: (Int, String, String, String, String) -> Unit,
) { ) {
composable(route = LOGIN_ROUTE) { composable(route = LOGIN_ROUTE) {
LoginScreen( LoginScreen(
@ -22,4 +31,4 @@ fun NavGraphBuilder.loginScreen(
fun NavController.navigateToLogin() { fun NavController.navigateToLogin() {
this.navigate(LOGIN_ROUTE) this.navigate(LOGIN_ROUTE)
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
@file:Suppress("MaxLineLength") @file:Suppress("MaxLineLength")
package org.mifospay.feature.auth.navigation package org.mifospay.feature.auth.navigation
@ -13,17 +22,32 @@ import org.mifospay.feature.auth.mobileVerify.MobileVerificationScreen
const val MOBILE_VERIFICATION_ROUTE = "mobile_verification_route" const val MOBILE_VERIFICATION_ROUTE = "mobile_verification_route"
fun NavGraphBuilder.mobileVerificationScreen( fun NavGraphBuilder.mobileVerificationScreen(
onOtpVerificationSuccess: (String, Map<String, Any?>) -> Unit onOtpVerificationSuccess: (String, Map<String, Any?>) -> Unit,
) { ) {
composable( composable(
route = "$MOBILE_VERIFICATION_ROUTE?mifosSignedUp={mifosSignedUp}&googleDisplayName={googleDisplayName}&googleEmail={googleEmail}&googleFamilyName={googleFamilyName}&googleGivenName={googleGivenName}", route = "$MOBILE_VERIFICATION_ROUTE?mifosSignedUp={mifosSignedUp}&googleDisplayName={googleDisplayName}&googleEmail={googleEmail}&googleFamilyName={googleFamilyName}&googleGivenName={googleGivenName}",
arguments = listOf( arguments = listOf(
navArgument("mifosSignedUp") { type = NavType.IntType; defaultValue = 0 }, navArgument("mifosSignedUp") {
navArgument("googleDisplayName") { type = NavType.StringType; nullable = true }, type = NavType.IntType
navArgument("googleEmail") { type = NavType.StringType; nullable = true }, defaultValue = 0
navArgument("googleFamilyName") { type = NavType.StringType; nullable = true }, },
navArgument("googleGivenName") { type = NavType.StringType; nullable = true } navArgument("googleDisplayName") {
) type = NavType.StringType
nullable = true
},
navArgument("googleEmail") {
type = NavType.StringType
nullable = true
},
navArgument("googleFamilyName") {
type = NavType.StringType
nullable = true
},
navArgument("googleGivenName") {
type = NavType.StringType
nullable = true
},
),
) { backStackEntry -> ) { backStackEntry ->
val mifosSignedUp = backStackEntry.arguments?.getInt("mifosSignedUp") ?: 0 val mifosSignedUp = backStackEntry.arguments?.getInt("mifosSignedUp") ?: 0
val googleDisplayName = backStackEntry.arguments?.getString("googleDisplayName") val googleDisplayName = backStackEntry.arguments?.getString("googleDisplayName")
@ -40,10 +64,10 @@ fun NavGraphBuilder.mobileVerificationScreen(
Constants.GOOGLE_FAMILY_NAME to googleFamilyName, Constants.GOOGLE_FAMILY_NAME to googleFamilyName,
Constants.GOOGLE_GIVEN_NAME to googleGivenName, Constants.GOOGLE_GIVEN_NAME to googleGivenName,
Constants.COUNTRY to "Canada", Constants.COUNTRY to "Canada",
Constants.MOBILE_NUMBER to fullNumber Constants.MOBILE_NUMBER to fullNumber,
) )
onOtpVerificationSuccess(fullNumber, extraData) onOtpVerificationSuccess(fullNumber, extraData)
} },
) )
} }
} }
@ -53,8 +77,7 @@ fun NavController.navigateToMobileVerification(
googleDisplayName: String?, googleDisplayName: String?,
googleEmail: String?, googleEmail: String?,
googleFamilyName: String?, googleFamilyName: String?,
googleGivenName: String? googleGivenName: String?,
) { ) {
this.navigate("$MOBILE_VERIFICATION_ROUTE?mifosSignedUp=$mifosSignedUp&googleDisplayName=$googleDisplayName&googleEmail=$googleEmail&googleFamilyName=$googleFamilyName&googleGivenName=$googleGivenName") this.navigate("$MOBILE_VERIFICATION_ROUTE?mifosSignedUp=$mifosSignedUp&googleDisplayName=$googleDisplayName&googleEmail=$googleEmail&googleFamilyName=$googleFamilyName&googleGivenName=$googleGivenName")
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
@file:Suppress("MaxLineLength") @file:Suppress("MaxLineLength")
package org.mifospay.feature.auth.navigation package org.mifospay.feature.auth.navigation
@ -14,19 +23,40 @@ const val SIGNUP_ROUTE = "signup_route"
@Suppress("UnusedParameter") @Suppress("UnusedParameter")
fun NavGraphBuilder.signupScreen( fun NavGraphBuilder.signupScreen(
onLoginSuccess: () -> Unit, onLoginSuccess: () -> Unit,
onRegisterSuccess: () -> Unit onRegisterSuccess: () -> Unit,
) { ) {
composable( composable(
route = "$SIGNUP_ROUTE?savingProductId={savingProductId}&mobileNumber={mobileNumber}&country={country}&email={email}&firstName={firstName}&lastName={lastName}&businessName={businessName}", route = "$SIGNUP_ROUTE?savingProductId={savingProductId}&mobileNumber={mobileNumber}&country={country}&email={email}&firstName={firstName}&lastName={lastName}&businessName={businessName}",
arguments = listOf( arguments = listOf(
navArgument("savingProductId") { type = NavType.IntType; defaultValue = 0 }, navArgument("savingProductId") {
navArgument("mobileNumber") { type = NavType.StringType; defaultValue = "" }, type = NavType.IntType
navArgument("country") { type = NavType.StringType; defaultValue = "" }, defaultValue = 0
navArgument("email") { type = NavType.StringType; defaultValue = "" }, },
navArgument("firstName") { type = NavType.StringType; defaultValue = "" }, navArgument("mobileNumber") {
navArgument("lastName") { type = NavType.StringType; defaultValue = "" }, type = NavType.StringType
navArgument("businessName") { type = NavType.StringType; defaultValue = "" } defaultValue = ""
) },
navArgument("country") {
type = NavType.StringType
defaultValue = ""
},
navArgument("email") {
type = NavType.StringType
defaultValue = ""
},
navArgument("firstName") {
type = NavType.StringType
defaultValue = ""
},
navArgument("lastName") {
type = NavType.StringType
defaultValue = ""
},
navArgument("businessName") {
type = NavType.StringType
defaultValue = ""
},
),
) { backStackEntry -> ) { backStackEntry ->
val savingProductId = backStackEntry.arguments?.getInt("savingProductId") ?: 0 val savingProductId = backStackEntry.arguments?.getInt("savingProductId") ?: 0
val mobileNumber = backStackEntry.arguments?.getString("mobileNumber") ?: "" val mobileNumber = backStackEntry.arguments?.getString("mobileNumber") ?: ""
@ -44,7 +74,7 @@ fun NavGraphBuilder.signupScreen(
email = email, email = email,
firstName = firstName, firstName = firstName,
lastName = lastName, lastName = lastName,
businessName = businessName businessName = businessName,
) )
} }
} }
@ -56,12 +86,12 @@ fun NavController.navigateToSignup(
email: String = "", email: String = "",
firstName: String = "", firstName: String = "",
lastName: String = "", lastName: String = "",
businessName: String = "" businessName: String = "",
) { ) {
this.navigate( this.navigate(
"$SIGNUP_ROUTE?savingProductId=$savingProductId" + "$SIGNUP_ROUTE?savingProductId=$savingProductId" +
"&mobileNumber=$mobileNumber&country=$country&email=$email" + "&mobileNumber=$mobileNumber&country=$country&email=$email" +
"&firstName=$firstName&lastName=$lastName&businessName=$businessName" "&firstName=$firstName&lastName=$lastName&businessName=$businessName",
) )
} }
@ -74,4 +104,4 @@ fun onRegisterSuccess(s: String?) {
// Toast.makeText(this, "Registered successfully.", Toast.LENGTH_SHORT).show() // Toast.makeText(this, "Registered successfully.", Toast.LENGTH_SHORT).show()
// startActivity(Intent(this@SignupActivity, LoginActivity::class.java)) // startActivity(Intent(this@SignupActivity, LoginActivity::class.java))
// finish() // finish()
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.auth.signup package org.mifospay.feature.auth.signup
import android.widget.Toast import android.widget.Toast
@ -49,18 +58,18 @@ import org.mifospay.feature.auth.R
import org.mifospay.feature.auth.utils.ValidateUtil.isValidEmail import org.mifospay.feature.auth.utils.ValidateUtil.isValidEmail
import java.util.Locale import java.util.Locale
@Composable @Composable
fun SignupScreen( internal fun SignupScreen(
viewModel: SignupViewModel = hiltViewModel(), savingProductId: Int,
mobileNumber: String,
country: String,
email: String,
firstName: String,
lastName: String,
businessName: String,
onLoginSuccess: () -> Unit, onLoginSuccess: () -> Unit,
savingProductId:Int, modifier: Modifier = Modifier,
mobileNumber:String, viewModel: SignupViewModel = hiltViewModel(),
country:String,
email:String,
firstName:String,
lastName:String,
businessName:String
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -72,9 +81,9 @@ fun SignupScreen(
mobileNumber = mobileNumber, mobileNumber = mobileNumber,
countryName = country, countryName = country,
email = email, email = email,
firstName = firstName, firstName = firstName,
lastName = lastName, lastName = lastName,
businessName = businessName businessName = businessName,
) )
} }
LaunchedEffect(viewModel.isLoginSuccess) { LaunchedEffect(viewModel.isLoginSuccess) {
@ -84,6 +93,7 @@ fun SignupScreen(
} }
SignupScreenContent( SignupScreenContent(
modifier = modifier,
showProgressState = viewModel.showProgress, showProgressState = viewModel.showProgress,
data = viewModel.signupData, data = viewModel.signupData,
stateList = stateList, stateList = stateList,
@ -91,27 +101,28 @@ fun SignupScreen(
viewModel.registerUser(it) { message -> viewModel.registerUser(it) { message ->
Toast.makeText(context, message, Toast.LENGTH_SHORT).show() Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
} }
} },
) )
} }
@Composable @Composable
@Suppress("LongMethod", "CyclomaticComplexMethod") @Suppress("LongMethod", "CyclomaticComplexMethod")
fun SignupScreenContent( private fun SignupScreenContent(
showProgressState: Boolean = false,
data: SignupData, data: SignupData,
stateList: List<State>, stateList: List<State>,
onCompleteRegistration: (SignupData) -> Unit onCompleteRegistration: (SignupData) -> Unit,
modifier: Modifier = Modifier,
showProgressState: Boolean = false,
) { ) {
val context = LocalContext.current val context = LocalContext.current
var firstName by rememberSaveable { mutableStateOf(data.firstName ?: "") } var firstName by rememberSaveable { mutableStateOf(data.firstName ?: "") }
var lastName by rememberSaveable { mutableStateOf(data.lastName ?: "") } var lastName by rememberSaveable { mutableStateOf(data.lastName ?: "") }
var email by rememberSaveable { mutableStateOf(data.email ?: "") } var email by rememberSaveable { mutableStateOf(data.email ?: "") }
var userName by rememberSaveable { var userName by rememberSaveable {
mutableStateOf(data.email?.ifEmpty { "" } mutableStateOf(
?: data.email?.let { it.substring(0, it.indexOf('@')) } ?: "" data.email?.ifEmpty { "" }
?: data.email?.let { it.substring(0, it.indexOf('@')) } ?: "",
) )
} }
var addressLine1 by rememberSaveable { mutableStateOf("") } var addressLine1 by rememberSaveable { mutableStateOf("") }
@ -127,16 +138,19 @@ fun SignupScreenContent(
var selectedState by rememberSaveable { mutableStateOf<State?>(null) } var selectedState by rememberSaveable { mutableStateOf<State?>(null) }
fun validateAllFields() { fun validateAllFields() {
val isAnyFieldEmpty = firstName.isEmpty() || lastName.isEmpty() || email.isEmpty() val isAnyFieldEmpty = firstName.isEmpty() || lastName.isEmpty() || email.isEmpty() ||
|| userName.isEmpty() || addressLine1.isEmpty() || addressLine2.isEmpty() userName.isEmpty() || addressLine1.isEmpty() || addressLine2.isEmpty() ||
|| pinCode.isEmpty() || password.isEmpty() || confirmPassword.isEmpty() pinCode.isEmpty() || password.isEmpty() || confirmPassword.isEmpty() ||
|| selectedState == null selectedState == null
val isNameOfBusinessEmpty = data.mifosSavingsProductId == MIFOS_MERCHANT_SAVINGS_PRODUCT_ID
&& nameOfBusiness.isEmpty() val isNameOfBusinessEmpty = data.mifosSavingsProductId == MIFOS_MERCHANT_SAVINGS_PRODUCT_ID &&
nameOfBusiness.isEmpty()
if (!email.isValidEmail()) { if (!email.isValidEmail()) {
Toast.makeText( Toast.makeText(
context, context.getString(R.string.feature_auth_validate_email), Toast.LENGTH_SHORT context,
context.getString(R.string.feature_auth_validate_email),
Toast.LENGTH_SHORT,
).show() ).show()
return return
} }
@ -145,7 +159,7 @@ fun SignupScreenContent(
Toast.makeText( Toast.makeText(
context, context,
context.getString(R.string.feature_auth_all_fields_are_mandatory), context.getString(R.string.feature_auth_all_fields_are_mandatory),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT,
).show() ).show()
return return
} }
@ -162,12 +176,12 @@ fun SignupScreenContent(
pinCode = pinCode, pinCode = pinCode,
businessName = nameOfBusiness, businessName = nameOfBusiness,
password = password, password = password,
stateId = selectedState?.id stateId = selectedState?.id,
) )
onCompleteRegistration.invoke(signUpData) onCompleteRegistration.invoke(signUpData)
} }
Box { Box(modifier) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -179,33 +193,37 @@ fun SignupScreenContent(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(color = MaterialTheme.colorScheme.primary), .background(color = MaterialTheme.colorScheme.primary),
verticalArrangement = Arrangement.Top verticalArrangement = Arrangement.Top,
) { ) {
Text( Text(
modifier = Modifier.padding(top = 48.dp, start = 24.dp, end = 24.dp), modifier = Modifier.padding(top = 48.dp, start = 24.dp, end = 24.dp),
text = stringResource(id = R.string.feature_auth_complete_your_registration), text = stringResource(id = R.string.feature_auth_complete_your_registration),
style = MaterialTheme.typography.titleLarge.copy(color = MaterialTheme.colorScheme.onPrimary) style = MaterialTheme.typography.titleLarge.copy(color = MaterialTheme.colorScheme.onPrimary),
) )
Text( Text(
modifier = Modifier.padding( modifier = Modifier.padding(
top = 4.dp, bottom = 32.dp, start = 24.dp, end = 24.dp top = 4.dp,
bottom = 32.dp,
start = 24.dp,
end = 24.dp,
), ),
text = stringResource(id = R.string.feature_auth_all_fields_are_mandatory), text = stringResource(id = R.string.feature_auth_all_fields_are_mandatory),
style = MaterialTheme.typography.bodySmall.copy(color = Color.White) style = MaterialTheme.typography.bodySmall.copy(color = Color.White),
) )
} }
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 32.dp) .padding(horizontal = 32.dp)
.focusable(!showProgressState) .focusable(!showProgressState),
) { ) {
UserInfoTextField( UserInfoTextField(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 16.dp), .padding(top = 16.dp),
label = stringResource(id = R.string.feature_auth_first_name), label = stringResource(id = R.string.feature_auth_first_name),
value = firstName value = firstName,
) { ) {
firstName = it firstName = it
} }
@ -214,7 +232,7 @@ fun SignupScreenContent(
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp), .padding(top = 8.dp),
label = stringResource(id = R.string.feature_auth_last_name), label = stringResource(id = R.string.feature_auth_last_name),
value = lastName value = lastName,
) { ) {
lastName = it lastName = it
} }
@ -223,7 +241,7 @@ fun SignupScreenContent(
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp), .padding(top = 8.dp),
label = stringResource(id = R.string.feature_auth_username), label = stringResource(id = R.string.feature_auth_username),
value = userName value = userName,
) { ) {
userName = it userName = it
} }
@ -244,7 +262,7 @@ fun SignupScreenContent(
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp), .padding(top = 8.dp),
label = stringResource(id = R.string.feature_auth_email), label = stringResource(id = R.string.feature_auth_email),
value = email value = email,
) { ) {
email = it email = it
} }
@ -254,7 +272,7 @@ fun SignupScreenContent(
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp), .padding(top = 8.dp),
label = stringResource(id = R.string.feature_auth_name_of_business), label = stringResource(id = R.string.feature_auth_name_of_business),
value = nameOfBusiness value = nameOfBusiness,
) { ) {
nameOfBusiness = it nameOfBusiness = it
} }
@ -264,7 +282,7 @@ fun SignupScreenContent(
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp), .padding(top = 8.dp),
label = stringResource(id = R.string.feature_auth_address_line_1), label = stringResource(id = R.string.feature_auth_address_line_1),
value = addressLine1 value = addressLine1,
) { ) {
addressLine1 = it addressLine1 = it
} }
@ -273,14 +291,14 @@ fun SignupScreenContent(
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp), .padding(top = 8.dp),
label = stringResource(id = R.string.feature_auth_address_line_2), label = stringResource(id = R.string.feature_auth_address_line_2),
value = addressLine2 value = addressLine2,
) { ) {
addressLine2 = it addressLine2 = it
} }
UserInfoTextField( UserInfoTextField(
modifier = Modifier.padding(top = 8.dp), modifier = Modifier.padding(top = 8.dp),
label = stringResource(id = R.string.feature_auth_pin_code), label = stringResource(id = R.string.feature_auth_pin_code),
value = pinCode value = pinCode,
) { ) {
pinCode = it pinCode = it
} }
@ -288,7 +306,7 @@ fun SignupScreenContent(
MifosStateDropDownOutlinedTextField( MifosStateDropDownOutlinedTextField(
value = selectedState?.name ?: "", value = selectedState?.name ?: "",
label = stringResource(id = R.string.feature_auth_state), label = stringResource(id = R.string.feature_auth_state),
stateList = stateList stateList = stateList,
) { ) {
selectedState = it selectedState = it
} }
@ -307,7 +325,7 @@ fun SignupScreenContent(
) { ) {
Text( Text(
text = stringResource(id = R.string.feature_auth_complete), text = stringResource(id = R.string.feature_auth_complete),
style = styleMedium16sp.copy(color = MaterialTheme.colorScheme.onPrimary) style = styleMedium16sp.copy(color = MaterialTheme.colorScheme.onPrimary),
) )
} }
} }
@ -321,19 +339,19 @@ fun SignupScreenContent(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MifosStateDropDownOutlinedTextField( private fun MifosStateDropDownOutlinedTextField(
modifier: Modifier = Modifier,
value: String, value: String,
label: String, label: String,
stateList: List<State>, stateList: List<State>,
onSelectedState: (State) -> Unit modifier: Modifier = Modifier,
onSelectedState: (State) -> Unit = {},
) { ) {
var expanded by rememberSaveable { mutableStateOf(false) } var expanded by rememberSaveable { mutableStateOf(false) }
ExposedDropdownMenuBox( ExposedDropdownMenuBox(
expanded = expanded, expanded = expanded,
onExpandedChange = { onExpandedChange = {
expanded = !expanded expanded = !expanded
} },
) { ) {
OutlinedTextField( OutlinedTextField(
modifier = modifier.menuAnchor(), modifier = modifier.menuAnchor(),
@ -343,21 +361,21 @@ fun MifosStateDropDownOutlinedTextField(
label = { Text(label) }, label = { Text(label) },
trailingIcon = { trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
} },
) )
DropdownMenu( DropdownMenu(
expanded = expanded, expanded = expanded,
onDismissRequest = { onDismissRequest = {
expanded = false expanded = false
}) },
{ ) {
stateList.forEach { stateList.forEach {
DropdownMenuItem( DropdownMenuItem(
text = { Text(text = it.name) }, text = { Text(text = it.name) },
onClick = { onClick = {
expanded = false expanded = false
onSelectedState(it) onSelectedState(it)
} },
) )
} }
} }
@ -365,11 +383,11 @@ fun MifosStateDropDownOutlinedTextField(
} }
@Composable @Composable
fun UserInfoTextField( private fun UserInfoTextField(
modifier: Modifier = Modifier,
label: String, label: String,
value: String, value: String,
onValueChange: (String) -> Unit modifier: Modifier = Modifier,
onValueChange: (String) -> Unit = {},
) { ) {
MfOutlinedTextField( MfOutlinedTextField(
modifier = modifier, modifier = modifier,
@ -377,12 +395,12 @@ fun UserInfoTextField(
label = label, label = label,
isError = value.isEmpty(), isError = value.isEmpty(),
errorMessage = stringResource(id = R.string.feature_auth_mandatory), errorMessage = stringResource(id = R.string.feature_auth_mandatory),
onValueChange = onValueChange onValueChange = onValueChange,
) )
} }
@Composable @Composable
fun PasswordAndConfirmPassword( private fun PasswordAndConfirmPassword(
password: String, password: String,
onPasswordChange: (String) -> Unit, onPasswordChange: (String) -> Unit,
confirmPassword: String, confirmPassword: String,
@ -391,8 +409,9 @@ fun PasswordAndConfirmPassword(
onTogglePasswordVisibility: () -> Unit, onTogglePasswordVisibility: () -> Unit,
isConfirmPasswordVisible: Boolean, isConfirmPasswordVisible: Boolean,
onConfirmTogglePasswordVisibility: () -> Unit, onConfirmTogglePasswordVisibility: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Column { Column(modifier) {
MfPasswordTextField( MfPasswordTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
password = password, password = password,
@ -402,10 +421,12 @@ fun PasswordAndConfirmPassword(
stringResource(id = R.string.feature_auth_password_cannot_be_empty) stringResource(id = R.string.feature_auth_password_cannot_be_empty)
} else if (password.length < 6) { } else if (password.length < 6) {
stringResource(id = R.string.feature_auth_password_must_be_least_6_characters) stringResource(id = R.string.feature_auth_password_must_be_least_6_characters)
} else null, } else {
null
},
onPasswordChange = onPasswordChange, onPasswordChange = onPasswordChange,
isPasswordVisible = isPasswordVisible, isPasswordVisible = isPasswordVisible,
onTogglePasswordVisibility = onTogglePasswordVisibility onTogglePasswordVisibility = onTogglePasswordVisibility,
) )
MfPasswordTextField( MfPasswordTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -416,19 +437,25 @@ fun PasswordAndConfirmPassword(
stringResource(id = R.string.feature_auth_confirm_password_cannot_empty) stringResource(id = R.string.feature_auth_confirm_password_cannot_empty)
} else if (password != confirmPassword) { } else if (password != confirmPassword) {
stringResource(id = R.string.feature_auth_passwords_do_not_match) stringResource(id = R.string.feature_auth_passwords_do_not_match)
} else null, } else {
null
},
onPasswordChange = onConfirmPasswordChange, onPasswordChange = onConfirmPasswordChange,
isPasswordVisible = isConfirmPasswordVisible, isPasswordVisible = isConfirmPasswordVisible,
onTogglePasswordVisibility = onConfirmTogglePasswordVisibility onTogglePasswordVisibility = onConfirmTogglePasswordVisibility,
) )
if (password.length >= 6) { if (password.length >= 6) {
Text( Text(
modifier = Modifier.padding(top = 8.dp), modifier = Modifier.padding(top = 8.dp),
text = "${stringResource(id = R.string.feature_auth_password_strength)}${ text = "${stringResource(id = R.string.feature_auth_password_strength)}${
getPasswordStrength(password).replaceFirstChar { getPasswordStrength(password).replaceFirstChar {
if (it.isLowerCase()) it.titlecase( if (it.isLowerCase()) {
Locale.ENGLISH it.titlecase(
) else it.toString() Locale.ENGLISH,
)
} else {
it.toString()
}
} }
}", }",
color = getPasswordStrengthColor(password), color = getPasswordStrengthColor(password),
@ -437,7 +464,6 @@ fun PasswordAndConfirmPassword(
} }
} }
private fun getPasswordStrength(password: String): String { private fun getPasswordStrength(password: String): String {
val hasUpperCase = password.any { it.isUpperCase() } val hasUpperCase = password.any { it.isUpperCase() }
val hasLowerCase = password.any { it.isLowerCase() } val hasLowerCase = password.any { it.isLowerCase() }
@ -448,12 +474,12 @@ private fun getPasswordStrength(password: String): String {
hasUpperCase.toInt(), hasUpperCase.toInt(),
hasLowerCase.toInt(), hasLowerCase.toInt(),
hasNumbers.toInt(), hasNumbers.toInt(),
hasSymbols.toInt() hasSymbols.toInt(),
).sum() ).sum()
return PasswordStrength.entries[numTypesPresent].name return PasswordStrength.entries[numTypesPresent].name
} }
fun Boolean.toInt() = if (this) 1 else 0 private fun Boolean.toInt() = if (this) 1 else 0
private fun getPasswordStrengthColor(password: String): Color { private fun getPasswordStrengthColor(password: String): Color {
val strength = getPasswordStrength(password) val strength = getPasswordStrength(password)
@ -469,11 +495,11 @@ private fun getPasswordStrengthColor(password: String): Color {
@Preview @Preview
@Composable @Composable
fun SignupScreenPreview() { private fun SignupScreenPreview() {
SignupScreenContent( SignupScreenContent(
showProgressState = false, showProgressState = false,
data = SignupData(), data = SignupData(),
stateList = listOf(), stateList = listOf(),
onCompleteRegistration = { } onCompleteRegistration = { },
) )
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.auth.signup package org.mifospay.feature.auth.signup
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -33,7 +42,6 @@ import org.mifospay.core.data.repository.local.LocalAssetRepository
import org.mifospay.core.datastore.PreferencesHelper import org.mifospay.core.datastore.PreferencesHelper
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SignupViewModel @Inject constructor( class SignupViewModel @Inject constructor(
localAssetRepository: LocalAssetRepository, localAssetRepository: LocalAssetRepository,
@ -46,7 +54,7 @@ class SignupViewModel @Inject constructor(
private val authenticateUserUseCase: AuthenticateUser, private val authenticateUserUseCase: AuthenticateUser,
private val fetchClientDataUseCase: FetchClientData, private val fetchClientDataUseCase: FetchClientData,
private val deleteUserUseCase: DeleteUser, private val deleteUserUseCase: DeleteUser,
private val fetchUserDetailsUseCase: FetchUserDetails private val fetchUserDetailsUseCase: FetchUserDetails,
) : ViewModel() { ) : ViewModel() {
var showProgress by mutableStateOf(false) var showProgress by mutableStateOf(false)
@ -62,7 +70,7 @@ class SignupViewModel @Inject constructor(
email: String?, email: String?,
firstName: String?, firstName: String?,
lastName: String?, lastName: String?,
businessName: String? businessName: String?,
) { ) {
signupData = signupData.copy( signupData = signupData.copy(
mifosSavingsProductId = savingProductId, mifosSavingsProductId = savingProductId,
@ -71,19 +79,19 @@ class SignupViewModel @Inject constructor(
email = email, email = email,
firstName = firstName!!, firstName = firstName!!,
lastName = lastName!!, lastName = lastName!!,
businessName = businessName businessName = businessName,
) )
} }
val states: StateFlow<List<State>> = combine( val states: StateFlow<List<State>> = combine(
localAssetRepository.getCountries(), localAssetRepository.getCountries(),
localAssetRepository.getStateList(), localAssetRepository.getStateList(),
::Pair ::Pair,
) )
.map { .map {
val countries = it.first val countries = it.first
signupData = signupData.copy( signupData = signupData.copy(
countryId = countries.find { it.name == signupData.countryName }?.id ?: "" countryId = countries.find { it.name == signupData.countryName }?.id ?: "",
) )
it.second.filter { it.countryId == signupData.countryId } it.second.filter { it.countryId == signupData.countryId }
} }
@ -101,7 +109,8 @@ class SignupViewModel @Inject constructor(
// 2. Create user // 2. Create user
// 3. Create Client // 3. Create Client
// 4. Update User and connect client with user // 4. Update User and connect client with user
useCaseHandler.execute(searchClientUseCase, useCaseHandler.execute(
searchClientUseCase,
SearchClient.RequestValues("${signupData.userName}@mifos"), SearchClient.RequestValues("${signupData.userName}@mifos"),
object : UseCase.UseCaseCallback<SearchClient.ResponseValue> { object : UseCase.UseCaseCallback<SearchClient.ResponseValue> {
override fun onSuccess(response: SearchClient.ResponseValue) { override fun onSuccess(response: SearchClient.ResponseValue) {
@ -111,15 +120,21 @@ class SignupViewModel @Inject constructor(
override fun onError(message: String) { override fun onError(message: String) {
createUser(showToastMessage) createUser(showToastMessage)
} }
}) },
)
} }
private fun createUser(showToastMessage: (String) -> Unit) { private fun createUser(showToastMessage: (String) -> Unit) {
val newUser = NewUser( val newUser = NewUser(
signupData.userName, signupData.firstName, signupData.lastName, signupData.userName,
signupData.email, signupData.password signupData.firstName,
signupData.lastName,
signupData.email,
signupData.password,
) )
useCaseHandler.execute(createUserUseCase, CreateUser.RequestValues(newUser), useCaseHandler.execute(
createUserUseCase,
CreateUser.RequestValues(newUser),
object : UseCase.UseCaseCallback<CreateUser.ResponseValue> { object : UseCase.UseCaseCallback<CreateUser.ResponseValue> {
override fun onSuccess(response: CreateUser.ResponseValue) { override fun onSuccess(response: CreateUser.ResponseValue) {
createClient(response.userId, showToastMessage) createClient(response.userId, showToastMessage)
@ -129,16 +144,18 @@ class SignupViewModel @Inject constructor(
DebugUtil.log(message) DebugUtil.log(message)
showToastMessage(message) showToastMessage(message)
} }
}) },
)
} }
private fun createClient(userId: Int, showToastMessage: (String) -> Unit) { private fun createClient(userId: Int, showToastMessage: (String) -> Unit) {
val newClient = com.mifospay.core.model.domain.client.NewClient( val newClient = com.mifospay.core.model.domain.client.NewClient(
signupData.businessName, signupData.userName, signupData.addressLine1, signupData.businessName, signupData.userName, signupData.addressLine1,
signupData.addressLine2, signupData.city, signupData.pinCode, signupData.stateId, signupData.addressLine2, signupData.city, signupData.pinCode, signupData.stateId,
signupData.countryId, signupData.mobileNumber, signupData.mifosSavingsProductId signupData.countryId, signupData.mobileNumber, signupData.mifosSavingsProductId,
) )
useCaseHandler.execute(createClientUseCase, useCaseHandler.execute(
createClientUseCase,
CreateClient.RequestValues(newClient), CreateClient.RequestValues(newClient),
object : UseCase.UseCaseCallback<CreateClient.ResponseValue> { object : UseCase.UseCaseCallback<CreateClient.ResponseValue> {
override fun onSuccess(response: CreateClient.ResponseValue) { override fun onSuccess(response: CreateClient.ResponseValue) {
@ -154,15 +171,17 @@ class SignupViewModel @Inject constructor(
showToastMessage(message) showToastMessage(message)
deleteUser(userId) deleteUser(userId)
} }
}) },
)
} }
private fun updateClient( private fun updateClient(
clients: ArrayList<Int>, clients: ArrayList<Int>,
userId: Int, userId: Int,
showToastMessage: (String) -> Unit showToastMessage: (String) -> Unit,
) { ) {
useCaseHandler.execute(updateUserUseCase, useCaseHandler.execute(
updateUserUseCase,
UpdateUser.RequestValues(UpdateUserEntityClients(clients), userId), UpdateUser.RequestValues(UpdateUserEntityClients(clients), userId),
object : UseCase.UseCaseCallback<UpdateUser.ResponseValue?> { object : UseCase.UseCaseCallback<UpdateUser.ResponseValue?> {
override fun onSuccess(response: UpdateUser.ResponseValue?) { override fun onSuccess(response: UpdateUser.ResponseValue?) {
@ -174,17 +193,20 @@ class SignupViewModel @Inject constructor(
DebugUtil.log(message) DebugUtil.log(message)
showToastMessage("update client error") showToastMessage("update client error")
} }
}) },
)
} }
private fun loginUser( private fun loginUser(
username: String?, username: String?,
password: String?, password: String?,
showToastMessage: (String) -> Unit showToastMessage: (String) -> Unit,
) { ) {
authenticateUserUseCase.walletRequestValues = AuthenticateUser.RequestValues(username!!, password!!) authenticateUserUseCase.walletRequestValues = AuthenticateUser.RequestValues(username!!, password!!)
val requestValue = authenticateUserUseCase.walletRequestValues val requestValue = authenticateUserUseCase.walletRequestValues
useCaseHandler.execute(authenticateUserUseCase, requestValue, useCaseHandler.execute(
authenticateUserUseCase,
requestValue,
object : UseCase.UseCaseCallback<AuthenticateUser.ResponseValue> { object : UseCase.UseCaseCallback<AuthenticateUser.ResponseValue> {
override fun onSuccess(response: AuthenticateUser.ResponseValue) { override fun onSuccess(response: AuthenticateUser.ResponseValue) {
createAuthenticatedService(response.user) createAuthenticatedService(response.user)
@ -195,11 +217,13 @@ class SignupViewModel @Inject constructor(
override fun onError(message: String) { override fun onError(message: String) {
showToastMessage("Login Failed") showToastMessage("Login Failed")
} }
}) },
)
} }
private fun fetchUserDetails(user: User) { private fun fetchUserDetails(user: User) {
useCaseHandler.execute(fetchUserDetailsUseCase, useCaseHandler.execute(
fetchUserDetailsUseCase,
FetchUserDetails.RequestValues(user.userId), FetchUserDetails.RequestValues(user.userId),
object : UseCase.UseCaseCallback<FetchUserDetails.ResponseValue> { object : UseCase.UseCaseCallback<FetchUserDetails.ResponseValue> {
override fun onSuccess(response: FetchUserDetails.ResponseValue) { override fun onSuccess(response: FetchUserDetails.ResponseValue) {
@ -209,11 +233,14 @@ class SignupViewModel @Inject constructor(
override fun onError(message: String) { override fun onError(message: String) {
DebugUtil.log(message) DebugUtil.log(message)
} }
}) },
)
} }
private fun fetchClientData(showToastMessage: (String) -> Unit) { private fun fetchClientData(showToastMessage: (String) -> Unit) {
useCaseHandler.execute(fetchClientDataUseCase, null, useCaseHandler.execute(
fetchClientDataUseCase,
null,
object : UseCase.UseCaseCallback<FetchClientData.ResponseValue> { object : UseCase.UseCaseCallback<FetchClientData.ResponseValue> {
override fun onSuccess(response: FetchClientData.ResponseValue) { override fun onSuccess(response: FetchClientData.ResponseValue) {
saveClientDetails(response.clientDetails) saveClientDetails(response.clientDetails)
@ -225,7 +252,8 @@ class SignupViewModel @Inject constructor(
override fun onError(message: String) { override fun onError(message: String) {
showToastMessage("Fetch Client Error") showToastMessage("Fetch Client Error")
} }
}) },
)
} }
private fun createAuthenticatedService(user: User) { private fun createAuthenticatedService(user: User) {
@ -235,7 +263,7 @@ class SignupViewModel @Inject constructor(
private fun saveUserDetails( private fun saveUserDetails(
user: User, user: User,
userWithRole: UserWithRole userWithRole: UserWithRole,
) { ) {
val userName = user.username val userName = user.username
val userID = user.userId val userID = user.userId
@ -251,11 +279,14 @@ class SignupViewModel @Inject constructor(
} }
private fun deleteUser(userId: Int) { private fun deleteUser(userId: Int) {
useCaseHandler.execute(deleteUserUseCase, DeleteUser.RequestValues(userId), useCaseHandler.execute(
deleteUserUseCase,
DeleteUser.RequestValues(userId),
object : UseCase.UseCaseCallback<DeleteUser.ResponseValue> { object : UseCase.UseCaseCallback<DeleteUser.ResponseValue> {
override fun onSuccess(response: DeleteUser.ResponseValue) {} override fun onSuccess(response: DeleteUser.ResponseValue) {}
override fun onError(message: String) {} override fun onError(message: String) {}
}) },
)
} }
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
@file:Suppress("MaxLineLength") @file:Suppress("MaxLineLength")
package org.mifospay.feature.auth.socialSignup package org.mifospay.feature.auth.socialSignup
@ -61,18 +70,22 @@ const val TAG = "Social Login"
// Followed this https://medium.com/@nirmale.ashwin9696/a-comprehensive-guide-to-google-sign-in-integration-with-credential-manager-in-android-apps-05286f8f5848 // Followed this https://medium.com/@nirmale.ashwin9696/a-comprehensive-guide-to-google-sign-in-integration-with-credential-manager-in-android-apps-05286f8f5848
// Keeping until we fix sign up // Keeping until we fix sign up
@Composable @Composable
fun SocialSignupMethodContentScreen( internal fun SocialSignupMethodContentScreen(
onDismissSignUp: () -> Unit modifier: Modifier = Modifier,
onDismissSignUp: () -> Unit = {},
) { ) {
SocialSignupMethodScreen(onDismissSignUp = onDismissSignUp) SocialSignupMethodScreen(
modifier = modifier,
onDismissSignUp = onDismissSignUp,
)
} }
@Composable @Composable
@Suppress("NestedBlockDepth") @Suppress("NestedBlockDepth")
fun SocialSignupMethodScreen( private fun SocialSignupMethodScreen(
onDismissSignUp: () -> Unit modifier: Modifier = Modifier,
onDismissSignUp: () -> Unit = {},
) { ) {
val context = LocalContext.current val context = LocalContext.current
var mifosSavingProductId by remember { mutableIntStateOf(0) } var mifosSavingProductId by remember { mutableIntStateOf(0) }
var showProgress by remember { mutableStateOf(false) } var showProgress by remember { mutableStateOf(false) }
@ -91,7 +104,6 @@ fun SocialSignupMethodScreen(
.addCredentialOption(googleIdOption) .addCredentialOption(googleIdOption)
.build() .build()
fun signUpWithMifos() { fun signUpWithMifos() {
googleIdTokenCredential.signUpWithMifos(context, mifosSavingProductId) { googleIdTokenCredential.signUpWithMifos(context, mifosSavingProductId) {
coroutineScope.launch { coroutineScope.launch {
@ -115,7 +127,7 @@ fun SocialSignupMethodScreen(
Toast.makeText( Toast.makeText(
context, context,
Constants.GOOGLE_SIGN_IN_FAILED, Constants.GOOGLE_SIGN_IN_FAILED,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT,
).show() ).show()
} }
} catch (e: GoogleIdTokenParsingException) { } catch (e: GoogleIdTokenParsingException) {
@ -134,7 +146,6 @@ fun SocialSignupMethodScreen(
} }
} }
fun signUpCredentialManager() { fun signUpCredentialManager() {
coroutineScope.launch { coroutineScope.launch {
try { try {
@ -161,6 +172,7 @@ fun SocialSignupMethodScreen(
} }
MifosBottomSheet( MifosBottomSheet(
modifier = modifier,
content = { content = {
SignupMethodContentScreen( SignupMethodContentScreen(
showProgress = showProgress, showProgress = showProgress,
@ -171,37 +183,37 @@ fun SocialSignupMethodScreen(
onSignupAsCustomer = { checkedGoogleAccount -> onSignupAsCustomer = { checkedGoogleAccount ->
mifosSavingProductId = MIFOS_CONSUMER_SAVINGS_PRODUCT_ID mifosSavingProductId = MIFOS_CONSUMER_SAVINGS_PRODUCT_ID
signUp(checkedGoogleAccount) signUp(checkedGoogleAccount)
} },
) )
}, },
onDismiss = { onDismiss = {
onDismissSignUp.invoke() onDismissSignUp.invoke()
} },
) )
} }
@Composable @Composable
@Suppress("LongMethod") @Suppress("LongMethod")
fun SignupMethodContentScreen( private fun SignupMethodContentScreen(
showProgress: Boolean, showProgress: Boolean,
onSignUpAsMerchant: (Boolean) -> Unit, onSignUpAsMerchant: (Boolean) -> Unit,
onSignupAsCustomer: (Boolean) -> Unit, onSignupAsCustomer: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) { ) {
var checkedGoogleAccountState by remember { mutableStateOf(true) } var checkedGoogleAccountState by remember { mutableStateOf(true) }
Box( Box(
modifier = Modifier, modifier = modifier,
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(color = MaterialTheme.colorScheme.surface), .background(color = MaterialTheme.colorScheme.surface),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Text( Text(
modifier = Modifier.padding(top = 16.dp), modifier = Modifier.padding(top = 16.dp),
text = stringResource(id = R.string.feature_auth_create_an_account) text = stringResource(id = R.string.feature_auth_create_an_account),
) )
OutlinedButton( OutlinedButton(
modifier = Modifier.padding(top = 48.dp), modifier = Modifier.padding(top = 48.dp),
@ -210,36 +222,36 @@ fun SignupMethodContentScreen(
}, },
border = BorderStroke(1.dp, Color.LightGray), border = BorderStroke(1.dp, Color.LightGray),
shape = RoundedCornerShape(4.dp), shape = RoundedCornerShape(4.dp),
colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.primary) colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.primary),
) { ) {
Text( Text(
text = stringResource(id = R.string.feature_auth_sign_up_as_merchant).uppercase(), text = stringResource(id = R.string.feature_auth_sign_up_as_merchant).uppercase(),
style = MaterialTheme.typography.labelMedium style = MaterialTheme.typography.labelMedium,
) )
} }
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 24.dp), .padding(top = 24.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
HorizontalDivider( HorizontalDivider(
modifier = Modifier modifier = Modifier
.padding(start = 24.dp, end = 8.dp) .padding(start = 24.dp, end = 8.dp)
.weight(.4f), .weight(.4f),
thickness = 1.dp thickness = 1.dp,
) )
Text( Text(
modifier = Modifier modifier = Modifier
.wrapContentWidth() .wrapContentWidth()
.weight(.1f), .weight(.1f),
text = stringResource(id = R.string.feature_auth_or) text = stringResource(id = R.string.feature_auth_or),
) )
HorizontalDivider( HorizontalDivider(
modifier = Modifier modifier = Modifier
.padding(start = 8.dp, end = 24.dp) .padding(start = 8.dp, end = 24.dp)
.weight(.4f), .weight(.4f),
thickness = 1.dp thickness = 1.dp,
) )
} }
OutlinedButton( OutlinedButton(
@ -249,11 +261,11 @@ fun SignupMethodContentScreen(
}, },
border = BorderStroke(1.dp, Color.LightGray), border = BorderStroke(1.dp, Color.LightGray),
shape = RoundedCornerShape(4.dp), shape = RoundedCornerShape(4.dp),
colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.primary) colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.primary),
) { ) {
Text( Text(
text = stringResource(id = R.string.feature_auth_sign_up_as_customer).uppercase(), text = stringResource(id = R.string.feature_auth_sign_up_as_customer).uppercase(),
style = MaterialTheme.typography.labelMedium style = MaterialTheme.typography.labelMedium,
) )
} }
Row( Row(
@ -262,18 +274,18 @@ fun SignupMethodContentScreen(
.clickable { .clickable {
checkedGoogleAccountState = !checkedGoogleAccountState checkedGoogleAccountState = !checkedGoogleAccountState
}, },
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Checkbox( Checkbox(
checked = checkedGoogleAccountState, checked = checkedGoogleAccountState,
onCheckedChange = { onCheckedChange = {
checkedGoogleAccountState = !checkedGoogleAccountState checkedGoogleAccountState = !checkedGoogleAccountState
}, },
colors = CheckboxDefaults.colors(MaterialTheme.colorScheme.primary) colors = CheckboxDefaults.colors(MaterialTheme.colorScheme.primary),
) )
Text( Text(
text = stringResource(id = R.string.feature_auth_ease_my_sign_up_using_google_account), text = stringResource(id = R.string.feature_auth_ease_my_sign_up_using_google_account),
style = MaterialTheme.typography.labelSmall style = MaterialTheme.typography.labelSmall,
) )
} }
HorizontalDivider(thickness = 48.dp, color = Color.Transparent) HorizontalDivider(thickness = 48.dp, color = Color.Transparent)
@ -283,7 +295,7 @@ fun SignupMethodContentScreen(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 140.dp), .padding(top = 140.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center,
) { ) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.size(64.dp), modifier = Modifier.size(64.dp),
@ -296,20 +308,20 @@ fun SignupMethodContentScreen(
} }
@Suppress("UnusedParameter") @Suppress("UnusedParameter")
fun GoogleIdTokenCredential?.signUpWithMifos( private fun GoogleIdTokenCredential?.signUpWithMifos(
context: Context, context: Context,
mifosSavingsProductId: Int, mifosSavingsProductId: Int,
signOutGoogleClient: () -> Unit signOutGoogleClient: () -> Unit,
) { ) {
val googleIdTokenCredential = this val googleIdTokenCredential = this
//Todo:navigate to MobileVerificationScreen with googleIdTokenCredential.givenName,profilePictureUri, // Todo:navigate to MobileVerificationScreen with googleIdTokenCredential.givenName,profilePictureUri,
// familyName,mifosSavingsProductId,displayName,data.getString("com.google.android.libraries.identity.googleid.BUNDLE_KEY_ID") // familyName,mifosSavingsProductId,displayName,data.getString("com.google.android.libraries.identity.googleid.BUNDLE_KEY_ID")
signOutGoogleClient.invoke() signOutGoogleClient.invoke()
} }
@Preview @Preview
@Composable @Composable
fun SignupMethodContentScreenPreview() { private fun SignupMethodContentScreenPreview() {
MaterialTheme { MaterialTheme {
SignupMethodContentScreen(true, {}, {}) SignupMethodContentScreen(true, {}, {})
} }

View File

@ -1,7 +1,16 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.auth.utils package org.mifospay.feature.auth.utils
import android.util.Patterns import android.util.Patterns
object ValidateUtil { object ValidateUtil {
fun String.isValidEmail() = this.isNotEmpty() && Patterns.EMAIL_ADDRESS.matcher(this).matches() fun String.isValidEmail() = this.isNotEmpty() && Patterns.EMAIL_ADDRESS.matcher(this).matches()
} }

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<resources> <resources>
<string name="feature_auth_login">Login</string> <string name="feature_auth_login">Login</string>
<string name="feature_auth_welcome_back">Welcome back!</string> <string name="feature_auth_welcome_back">Welcome back!</string>

View File

@ -1,16 +0,0 @@
package org.mifospay.mobilewallet.mifospay.auth
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
plugins { plugins {
alias(libs.plugins.mifospay.android.feature) alias(libs.plugins.mifospay.android.feature)
alias(libs.plugins.mifospay.android.library.compose) alias(libs.plugins.mifospay.android.library.compose)

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<manifest> <manifest>
</manifest> </manifest>

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.editpassword package org.mifospay.feature.editpassword
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -35,13 +44,15 @@ import org.mifospay.core.designsystem.component.MifosScaffold
import org.mifospay.core.designsystem.theme.MifosTheme import org.mifospay.core.designsystem.theme.MifosTheme
@Composable @Composable
fun EditPasswordScreen( internal fun EditPasswordScreen(
viewModel: EditPasswordViewModel = hiltViewModel(),
onBackPress: () -> Unit, onBackPress: () -> Unit,
onCancelChanges: () -> Unit onCancelChanges: () -> Unit,
modifier: Modifier = Modifier,
viewModel: EditPasswordViewModel = hiltViewModel(),
) { ) {
val editPasswordUiState by viewModel.editPasswordUiState.collectAsStateWithLifecycle() val editPasswordUiState by viewModel.editPasswordUiState.collectAsStateWithLifecycle()
EditPasswordScreen( EditPasswordScreen(
modifier = modifier,
editPasswordUiState = editPasswordUiState, editPasswordUiState = editPasswordUiState,
onCancelChanges = onCancelChanges, onCancelChanges = onCancelChanges,
onBackPress = onBackPress, onBackPress = onBackPress,
@ -49,18 +60,19 @@ fun EditPasswordScreen(
viewModel.updatePassword( viewModel.updatePassword(
currentPassword = currentPass, currentPassword = currentPass,
newPassword = newPass, newPassword = newPass,
newPasswordRepeat = confirmPass newPasswordRepeat = confirmPass,
) )
} },
) )
} }
@Composable @Composable
fun EditPasswordScreen( private fun EditPasswordScreen(
editPasswordUiState: EditPasswordUiState, editPasswordUiState: EditPasswordUiState,
onCancelChanges: () -> Unit, onCancelChanges: () -> Unit,
onBackPress: () -> Unit, onBackPress: () -> Unit,
onSave: (currentPass: String, newPass: String, confirmPass: String) -> Unit onSave: (currentPass: String, newPass: String, confirmPass: String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
var currentPassword by rememberSaveable { mutableStateOf("") } var currentPassword by rememberSaveable { mutableStateOf("") }
@ -88,26 +100,25 @@ fun EditPasswordScreen(
EditPasswordUiState.Success -> { EditPasswordUiState.Success -> {
coroutineScope.launch { coroutineScope.launch {
currentSnackbarHostState.showSnackbar( currentSnackbarHostState.showSnackbar(
context.getString(R.string.feature_editpassword_password_changed_successfully) context.getString(R.string.feature_editpassword_password_changed_successfully),
) )
} }
} }
} }
} }
MifosScaffold( MifosScaffold(
modifier = modifier,
topBarTitle = R.string.feature_editpassword_change_password, topBarTitle = R.string.feature_editpassword_change_password,
snackbarHost = { snackbarHost = {
SnackbarHost(hostState = snackbarHostState) SnackbarHost(hostState = snackbarHostState)
}, },
backPress = onBackPress, backPress = onBackPress,
scaffoldContent = { paddingValues -> scaffoldContent = { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues),
) { ) {
MfPasswordTextField( MfPasswordTextField(
modifier = Modifier modifier = Modifier
@ -119,7 +130,6 @@ fun EditPasswordScreen(
isPasswordVisible = isConfirmPasswordVisible, isPasswordVisible = isConfirmPasswordVisible,
onTogglePasswordVisibility = { onTogglePasswordVisibility = {
isConfirmPasswordVisible = !isConfirmPasswordVisible isConfirmPasswordVisible = !isConfirmPasswordVisible
}, },
onPasswordChange = { currentPassword = it }, onPasswordChange = { currentPassword = it },
) )
@ -131,12 +141,16 @@ fun EditPasswordScreen(
password = newPassword, password = newPassword,
label = stringResource(id = R.string.feature_editpassword_new_password), label = stringResource(id = R.string.feature_editpassword_new_password),
isError = newPassword.isNotEmpty() && newPassword.length < 6, isError = newPassword.isNotEmpty() && newPassword.length < 6,
errorMessage = if (newPassword.isNotEmpty() && newPassword.length < 6) stringResource( errorMessage = if (newPassword.isNotEmpty() && newPassword.length < 6) {
id = R.string.feature_editpassword_password_length_error stringResource(
) else null, id = R.string.feature_editpassword_password_length_error,
)
} else {
null
},
isPasswordVisible = isNewPasswordVisible, isPasswordVisible = isNewPasswordVisible,
onTogglePasswordVisibility = { isNewPasswordVisible = !isNewPasswordVisible }, onTogglePasswordVisibility = { isNewPasswordVisible = !isNewPasswordVisible },
onPasswordChange = { newPassword = it } onPasswordChange = { newPassword = it },
) )
MfPasswordTextField( MfPasswordTextField(
modifier = Modifier modifier = Modifier
@ -147,21 +161,25 @@ fun EditPasswordScreen(
isError = newPassword != confirmNewPassword && confirmNewPassword.isNotEmpty(), isError = newPassword != confirmNewPassword && confirmNewPassword.isNotEmpty(),
errorMessage = if (newPassword != errorMessage = if (newPassword !=
confirmNewPassword && confirmNewPassword.isNotEmpty() confirmNewPassword && confirmNewPassword.isNotEmpty()
) stringResource( ) {
id = R.string.feature_editpassword_password_mismatch_error stringResource(
) else null, id = R.string.feature_editpassword_password_mismatch_error,
)
} else {
null
},
isPasswordVisible = isConfirmNewPasswordVisible, isPasswordVisible = isConfirmNewPasswordVisible,
onTogglePasswordVisibility = { onTogglePasswordVisibility = {
isConfirmNewPasswordVisible = !isConfirmNewPasswordVisible isConfirmNewPasswordVisible = !isConfirmNewPasswordVisible
}, },
onPasswordChange = { confirmNewPassword = it } onPasswordChange = { confirmNewPassword = it },
) )
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 20.dp, start = 16.dp, end = 16.dp), .padding(top = 20.dp, start = 16.dp, end = 16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
MifosButton( MifosButton(
onClick = { onCancelChanges.invoke() }, onClick = { onCancelChanges.invoke() },
@ -169,7 +187,7 @@ fun EditPasswordScreen(
.weight(1f) .weight(1f)
.padding(8.dp), .padding(8.dp),
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
content = { Text(text = stringResource(id = R.string.feature_editpassword_cancel)) } content = { Text(text = stringResource(id = R.string.feature_editpassword_cancel)) },
) )
MifosButton( MifosButton(
modifier = Modifier modifier = Modifier
@ -179,11 +197,11 @@ fun EditPasswordScreen(
onSave.invoke(currentPassword, newPassword, confirmNewPassword) onSave.invoke(currentPassword, newPassword, confirmNewPassword)
}, },
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
content = { Text(text = stringResource(id = R.string.feature_editpassword_save)) } content = { Text(text = stringResource(id = R.string.feature_editpassword_save)) },
) )
} }
} }
} },
) )
} }
@ -192,14 +210,14 @@ class EditPasswordUiStateProvider : PreviewParameterProvider<EditPasswordUiState
get() = sequenceOf( get() = sequenceOf(
EditPasswordUiState.Loading, EditPasswordUiState.Loading,
EditPasswordUiState.Success, EditPasswordUiState.Success,
EditPasswordUiState.Error("Some Error Occurred") EditPasswordUiState.Error("Some Error Occurred"),
) )
} }
@Preview @Preview
@Composable @Composable
private fun EditPasswordScreenPreview( private fun EditPasswordScreenPreview(
@PreviewParameter(EditPasswordUiStateProvider::class) editPasswordUiState: EditPasswordUiState @PreviewParameter(EditPasswordUiStateProvider::class) editPasswordUiState: EditPasswordUiState,
) { ) {
MifosTheme { MifosTheme {
EditPasswordScreen(editPasswordUiState = editPasswordUiState, {}, {}, { _, _, _ -> }) EditPasswordScreen(editPasswordUiState = editPasswordUiState, {}, {}, { _, _, _ -> })

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.editpassword package org.mifospay.feature.editpassword
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -19,7 +28,7 @@ class EditPasswordViewModel @Inject constructor(
private val mUseCaseHandler: UseCaseHandler, private val mUseCaseHandler: UseCaseHandler,
private val mPreferencesHelper: PreferencesHelper, private val mPreferencesHelper: PreferencesHelper,
private val authenticateUserUseCase: AuthenticateUser, private val authenticateUserUseCase: AuthenticateUser,
private val updateUserUseCase: UpdateUser private val updateUserUseCase: UpdateUser,
) : ViewModel() { ) : ViewModel() {
private val _editPasswordUiState = private val _editPasswordUiState =
@ -29,11 +38,11 @@ class EditPasswordViewModel @Inject constructor(
fun updatePassword( fun updatePassword(
currentPassword: String?, currentPassword: String?,
newPassword: String?, newPassword: String?,
newPasswordRepeat: String? newPasswordRepeat: String?,
) { ) {
_editPasswordUiState.value = EditPasswordUiState.Loading _editPasswordUiState.value = EditPasswordUiState.Loading
if (isNotEmpty(currentPassword) && isNotEmpty(newPassword) if (isNotEmpty(currentPassword) && isNotEmpty(newPassword) &&
&& isNotEmpty(newPasswordRepeat) isNotEmpty(newPasswordRepeat)
) { ) {
when { when {
currentPassword == newPassword -> { currentPassword == newPassword -> {
@ -45,7 +54,7 @@ class EditPasswordViewModel @Inject constructor(
newPasswordRepeat?.let { it1 -> newPasswordRepeat?.let { it1 ->
isNewPasswordValid( isNewPasswordValid(
it, it,
it1 it1,
) )
} }
} == true -> { } == true -> {
@ -75,19 +84,21 @@ class EditPasswordViewModel @Inject constructor(
private fun updatePassword(currentPassword: String, newPassword: String) { private fun updatePassword(currentPassword: String, newPassword: String) {
// authenticate and then update // authenticate and then update
mUseCaseHandler.execute(authenticateUserUseCase, mUseCaseHandler.execute(
authenticateUserUseCase,
AuthenticateUser.RequestValues( AuthenticateUser.RequestValues(
mPreferencesHelper.username, mPreferencesHelper.username,
currentPassword currentPassword,
), ),
object : UseCase.UseCaseCallback<AuthenticateUser.ResponseValue> { object : UseCase.UseCaseCallback<AuthenticateUser.ResponseValue> {
override fun onSuccess(response: AuthenticateUser.ResponseValue) { override fun onSuccess(response: AuthenticateUser.ResponseValue) {
mUseCaseHandler.execute(updateUserUseCase, mUseCaseHandler.execute(
updateUserUseCase,
UpdateUser.RequestValues( UpdateUser.RequestValues(
UpdateUserEntityPassword( UpdateUserEntityPassword(
newPassword newPassword,
), ),
mPreferencesHelper.userId.toInt() mPreferencesHelper.userId.toInt(),
), ),
object : UseCase.UseCaseCallback<UpdateUser.ResponseValue?> { object : UseCase.UseCaseCallback<UpdateUser.ResponseValue?> {
override fun onSuccess(response: UpdateUser.ResponseValue?) { override fun onSuccess(response: UpdateUser.ResponseValue?) {
@ -97,13 +108,15 @@ class EditPasswordViewModel @Inject constructor(
override fun onError(message: String) { override fun onError(message: String) {
_editPasswordUiState.value = EditPasswordUiState.Error(message) _editPasswordUiState.value = EditPasswordUiState.Error(message)
} }
}) },
)
} }
override fun onError(message: String) { override fun onError(message: String) {
_editPasswordUiState.value = EditPasswordUiState.Error("Wrong Password") _editPasswordUiState.value = EditPasswordUiState.Error("Wrong Password")
} }
}) },
)
} }
} }
@ -111,4 +124,4 @@ sealed interface EditPasswordUiState {
data object Loading : EditPasswordUiState data object Loading : EditPasswordUiState
data object Success : EditPasswordUiState data object Success : EditPasswordUiState
data class Error(val message: String) : EditPasswordUiState data class Error(val message: String) : EditPasswordUiState
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.editpassword.navigation package org.mifospay.feature.editpassword.navigation
import androidx.navigation.NavController import androidx.navigation.NavController
@ -9,16 +18,16 @@ const val EDIT_PASSWORD_ROUTE = "edit_password_route"
fun NavGraphBuilder.editPasswordScreen( fun NavGraphBuilder.editPasswordScreen(
onBackPress: () -> Unit, onBackPress: () -> Unit,
onCancelChanges: () -> Unit onCancelChanges: () -> Unit,
) { ) {
composable(route = EDIT_PASSWORD_ROUTE) { composable(route = EDIT_PASSWORD_ROUTE) {
EditPasswordScreen( EditPasswordScreen(
onBackPress = onBackPress, onBackPress = onBackPress,
onCancelChanges = onCancelChanges onCancelChanges = onCancelChanges,
) )
} }
} }
fun NavController.navigateToEditPassword() { fun NavController.navigateToEditPassword() {
this.navigate(EDIT_PASSWORD_ROUTE) this.navigate(EDIT_PASSWORD_ROUTE)
} }

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<resources> <resources>
<string name="feature_editpassword_password_changed_successfully">Password changed successfull</string> <string name="feature_editpassword_password_changed_successfully">Password changed successfull</string>
<string name="feature_editpassword_change_password">Change Password</string> <string name="feature_editpassword_change_password">Change Password</string>

View File

@ -1,16 +0,0 @@
package org.mifospay.feature.editpassword
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
plugins { plugins {
alias(libs.plugins.mifospay.android.feature) alias(libs.plugins.mifospay.android.feature)
alias(libs.plugins.mifospay.android.library.compose) alias(libs.plugins.mifospay.android.library.compose)

View File

@ -1,2 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<manifest></manifest> <manifest></manifest>

View File

@ -1,6 +1,15 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.faq package org.mifospay.feature.faq
data class FAQ( internal data class FAQ(
var question: Int, var question: Int,
var answer: Int? = null, var answer: Int? = null,
) )

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.faq package org.mifospay.feature.faq
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -5,7 +14,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class FAQViewModel @Inject constructor() : ViewModel() { internal class FAQViewModel @Inject constructor() : ViewModel() {
/** /**
* Retrieves a list of Frequently Asked Questions (FAQs). * Retrieves a list of Frequently Asked Questions (FAQs).
@ -22,7 +31,7 @@ class FAQViewModel @Inject constructor() : ViewModel() {
FAQ(R.string.feature_faq_question1, R.string.feature_faq_answer1), FAQ(R.string.feature_faq_question1, R.string.feature_faq_answer1),
FAQ(R.string.feature_faq_question2, R.string.feature_faq_answer2), FAQ(R.string.feature_faq_question2, R.string.feature_faq_answer2),
FAQ(R.string.feature_faq_question3, R.string.feature_faq_answer3), FAQ(R.string.feature_faq_question3, R.string.feature_faq_answer3),
FAQ(R.string.feature_faq_question4, R.string.feature_faq_answer4) FAQ(R.string.feature_faq_question4, R.string.feature_faq_answer4),
) )
} }
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.faq package org.mifospay.feature.faq
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -14,31 +23,41 @@ import org.mifospay.core.designsystem.component.MifosTopBar
import org.mifospay.core.ui.FaqItemScreen import org.mifospay.core.ui.FaqItemScreen
@Composable @Composable
fun FaqScreenRoute( internal fun FaqScreenRoute(
navigateBack: () -> Unit, navigateBack: () -> Unit,
faqViewModel: FAQViewModel = hiltViewModel() modifier: Modifier = Modifier,
faqViewModel: FAQViewModel = hiltViewModel(),
) { ) {
FaqScreen(navigateBack = { navigateBack.invoke() }, faqViewModel.getFAQ()) FaqScreen(
modifier = modifier,
navigateBack = navigateBack,
faqList = faqViewModel.getFAQ(),
)
} }
@Composable @Composable
fun FaqScreen( private fun FaqScreen(
navigateBack: () -> Unit, navigateBack: () -> Unit,
faqList: List<FAQ> faqList: List<FAQ>,
modifier: Modifier = Modifier,
) { ) {
Column(modifier = Modifier.fillMaxSize()) { Column(
modifier = modifier
.fillMaxSize(),
) {
MifosTopBar( MifosTopBar(
topBarTitle = R.string.feature_faq_frequently_asked_questions, topBarTitle = R.string.feature_faq_frequently_asked_questions,
backPress = { navigateBack.invoke() }) backPress = { navigateBack.invoke() },
)
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.fillMaxWidth() .fillMaxWidth(),
) { ) {
itemsIndexed(items = faqList) { _, faqItem -> itemsIndexed(items = faqList) { _, faqItem ->
FaqItemScreen( FaqItemScreen(
question = stringResource(id = faqItem.question), question = stringResource(id = faqItem.question),
answer = faqItem.answer?.let { stringResource(id = it) } answer = faqItem.answer?.let { stringResource(id = it) },
) )
} }
} }
@ -47,13 +66,14 @@ fun FaqScreen(
@Preview(showSystemUi = true) @Preview(showSystemUi = true)
@Composable @Composable
fun FaqScreenPreview() { private fun FaqScreenPreview() {
FaqScreen( FaqScreen(
{}, listOf( {},
listOf(
FAQ(R.string.feature_faq_question1, R.string.feature_faq_answer1), FAQ(R.string.feature_faq_question1, R.string.feature_faq_answer1),
FAQ(R.string.feature_faq_question2, R.string.feature_faq_answer2), FAQ(R.string.feature_faq_question2, R.string.feature_faq_answer2),
FAQ(R.string.feature_faq_question3, R.string.feature_faq_answer3), FAQ(R.string.feature_faq_question3, R.string.feature_faq_answer3),
FAQ(R.string.feature_faq_question4, R.string.feature_faq_answer4) FAQ(R.string.feature_faq_question4, R.string.feature_faq_answer4),
) ),
) )
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.faq.navigation package org.mifospay.feature.faq.navigation
import androidx.navigation.NavController import androidx.navigation.NavController
@ -13,11 +22,11 @@ fun NavController.navigateToFAQ(navOptions: NavOptions? = null) {
} }
fun NavGraphBuilder.faqScreen( fun NavGraphBuilder.faqScreen(
navigateBack: () -> Unit navigateBack: () -> Unit,
) { ) {
composable(route = FAQ_ROUTE) { composable(route = FAQ_ROUTE) {
FaqScreenRoute( FaqScreenRoute(
navigateBack = navigateBack navigateBack = navigateBack,
) )
} }
} }

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<resources> <resources>
<string name="feature_faq_question1">How can I add an Account?</string> <string name="feature_faq_question1">How can I add an Account?</string>
<string name="feature_faq_question2">How can I access my Payment History?</string> <string name="feature_faq_question2">How can I access my Payment History?</string>

View File

@ -1,16 +0,0 @@
package com.example.faq
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
plugins { plugins {
alias(libs.plugins.mifospay.android.feature) alias(libs.plugins.mifospay.android.feature)
alias(libs.plugins.mifospay.android.library.compose) alias(libs.plugins.mifospay.android.library.compose)
@ -8,10 +17,5 @@ android {
} }
dependencies { dependencies {
implementation(projects.core.data)
implementation(projects.feature.kyc)
implementation(projects.feature.savedcards)
implementation(projects.feature.accounts)
implementation(projects.feature.merchants)
implementation(libs.accompanist.pager) implementation(libs.accompanist.pager)
} }

View File

@ -1,24 +0,0 @@
package org.mifospay.feature.finance
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("org.mifospay.feature.finance.test", appContext.packageName)
}
}

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest> </manifest>

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.finance package org.mifospay.feature.finance
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -6,50 +15,21 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.google.accompanist.pager.rememberPagerState import com.google.accompanist.pager.rememberPagerState
import com.mifospay.core.model.domain.BankAccountDetails
import org.mifospay.core.ui.MifosScrollableTabRow import org.mifospay.core.ui.MifosScrollableTabRow
import org.mifospay.core.ui.utility.TabContent import org.mifospay.core.ui.utility.TabContent
import org.mifospay.feature.bank.accounts.AccountsScreen
import org.mifospay.feature.kyc.KYCScreen
import org.mifospay.feature.merchants.ui.MerchantScreen
import org.mifospay.feature.savedcards.CardsScreen
@Suppress("UnusedParameter")
@Composable @Composable
fun FinanceRoute( internal fun FinanceRoute(
onAddBtn: () -> Unit, tabContents: List<TabContent>,
onLevel1Clicked: () -> Unit, modifier: Modifier = Modifier,
onLevel2Clicked: () -> Unit,
onLevel3Clicked: () -> Unit,
navigateToBankAccountDetailScreen: (BankAccountDetails, Int) -> Unit,
navigateToLinkBankAccountScreen: () -> Unit
) { ) {
val pagerState = rememberPagerState(initialPage = 0) val pagerState = rememberPagerState(initialPage = 0)
val tabContents = listOf( Column(modifier = modifier.fillMaxSize()) {
TabContent(FinanceScreenContents.ACCOUNTS.name) { MifosScrollableTabRow(
AccountsScreen( tabContents = tabContents,
navigateToBankAccountDetailScreen = navigateToBankAccountDetailScreen, pagerState = pagerState,
navigateToLinkBankAccountScreen = navigateToLinkBankAccountScreen )
)
},
TabContent(FinanceScreenContents.CARDS.name) {
CardsScreen(onEditCard = {})
},
TabContent(FinanceScreenContents.MERCHANTS.name) {
MerchantScreen()
},
TabContent(FinanceScreenContents.KYC.name) {
KYCScreen(
onLevel1Clicked = onLevel1Clicked,
onLevel2Clicked = onLevel2Clicked,
onLevel3Clicked = onLevel3Clicked
)
}
)
Column(modifier = Modifier.fillMaxSize()) {
MifosScrollableTabRow(tabContents = tabContents, pagerState = pagerState)
} }
} }
@ -57,18 +37,13 @@ enum class FinanceScreenContents {
ACCOUNTS, ACCOUNTS,
CARDS, CARDS,
MERCHANTS, MERCHANTS,
KYC KYC,
} }
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun FinanceScreenPreview() { private fun FinanceScreenPreview() {
FinanceRoute( FinanceRoute(
onAddBtn = {}, tabContents = emptyList(),
onLevel1Clicked = {},
onLevel2Clicked = {},
onLevel3Clicked = {},
navigateToBankAccountDetailScreen = { _, _ -> },
navigateToLinkBankAccountScreen = {}
) )
} }

View File

@ -1,10 +1,19 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.finance.navigation package org.mifospay.feature.finance.navigation
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import com.mifospay.core.model.domain.BankAccountDetails import org.mifospay.core.ui.utility.TabContent
import org.mifospay.feature.finance.FinanceRoute import org.mifospay.feature.finance.FinanceRoute
const val FINANCE_ROUTE = "finance_route" const val FINANCE_ROUTE = "finance_route"
@ -12,21 +21,9 @@ const val FINANCE_ROUTE = "finance_route"
fun NavController.navigateToFinance(navOptions: NavOptions) = navigate(FINANCE_ROUTE, navOptions) fun NavController.navigateToFinance(navOptions: NavOptions) = navigate(FINANCE_ROUTE, navOptions)
fun NavGraphBuilder.financeScreen( fun NavGraphBuilder.financeScreen(
onAddBtn: () -> Unit, tabContents: List<TabContent>,
onLevel1Clicked: () -> Unit,
onLevel2Clicked: () -> Unit,
onLevel3Clicked: () -> Unit,
navigateToBankAccountDetailScreen: (BankAccountDetails,Int) -> Unit,
navigateToLinkBankAccountScreen: () -> Unit
) { ) {
composable(route = FINANCE_ROUTE) { composable(route = FINANCE_ROUTE) {
FinanceRoute( FinanceRoute(tabContents = tabContents)
onAddBtn = onAddBtn,
onLevel1Clicked = onLevel1Clicked,
onLevel2Clicked = onLevel2Clicked,
onLevel3Clicked = onLevel3Clicked,
navigateToBankAccountDetailScreen = navigateToBankAccountDetailScreen,
navigateToLinkBankAccountScreen = navigateToLinkBankAccountScreen
)
} }
} }

View File

@ -1,16 +0,0 @@
package org.mifospay.feature.finance
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
plugins { plugins {
alias(libs.plugins.mifospay.android.feature) alias(libs.plugins.mifospay.android.feature)
alias(libs.plugins.mifospay.android.library.compose) alias(libs.plugins.mifospay.android.library.compose)
@ -8,6 +17,5 @@ android {
} }
dependencies { dependencies {
implementation(projects.feature.receipt)
implementation(projects.core.data) implementation(projects.core.data)
} }

View File

@ -1,24 +0,0 @@
package org.mifospay.history
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("org.mifospay.history.test", appContext.packageName)
}
}

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest> </manifest>

View File

@ -1,12 +0,0 @@
package org.mifospay.feature
import com.mifospay.core.model.domain.Transaction
/**
* Created by naman on 17/8/17.
*/
interface HistoryContract {
interface TransactionsHistoryAsync {
fun onTransactionsFetchCompleted(transactions: List<Transaction>?)
}
}

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.history package org.mifospay.feature.history
import android.widget.Toast import android.widget.Toast
@ -46,9 +55,10 @@ import org.mifospay.feature.transaction.detail.TransactionDetailScreen
@Composable @Composable
fun HistoryScreen( fun HistoryScreen(
viewModel: HistoryViewModel = hiltViewModel(),
viewReceipt: (String) -> Unit, viewReceipt: (String) -> Unit,
accountClicked: (String, ArrayList<Transaction>) -> Unit, accountClicked: (String, ArrayList<Transaction>) -> Unit,
modifier: Modifier = Modifier,
viewModel: HistoryViewModel = hiltViewModel(),
) { ) {
val historyUiState by viewModel.historyUiState.collectAsStateWithLifecycle() val historyUiState by viewModel.historyUiState.collectAsStateWithLifecycle()
@ -56,14 +66,16 @@ fun HistoryScreen(
historyUiState = historyUiState, historyUiState = historyUiState,
viewReceipt = viewReceipt, viewReceipt = viewReceipt,
accountClicked = accountClicked, accountClicked = accountClicked,
modifier = modifier,
) )
} }
@Composable @Composable
fun HistoryScreen( private fun HistoryScreen(
historyUiState: HistoryUiState, historyUiState: HistoryUiState,
viewReceipt: (String) -> Unit, viewReceipt: (String) -> Unit,
accountClicked: (String, ArrayList<Transaction>) -> Unit, accountClicked: (String, ArrayList<Transaction>) -> Unit,
modifier: Modifier = Modifier,
) { ) {
var selectedChip by remember { mutableStateOf(TransactionType.OTHER) } var selectedChip by remember { mutableStateOf(TransactionType.OTHER) }
var filteredTransactions by remember { mutableStateOf(emptyList<Transaction>()) } var filteredTransactions by remember { mutableStateOf(emptyList<Transaction>()) }
@ -77,7 +89,7 @@ fun HistoryScreen(
title = stringResource(id = R.string.feature_history_error_oops), title = stringResource(id = R.string.feature_history_error_oops),
subTitle = stringResource(id = R.string.feature_history_empty_no_transaction_history_title), subTitle = stringResource(id = R.string.feature_history_empty_no_transaction_history_title),
iconTint = MaterialTheme.colorScheme.primary, iconTint = MaterialTheme.colorScheme.primary,
iconImageVector = Icons.Rounded.Info iconImageVector = Icons.Rounded.Info,
) )
} }
@ -87,7 +99,7 @@ fun HistoryScreen(
title = stringResource(id = R.string.feature_history_error_oops), title = stringResource(id = R.string.feature_history_error_oops),
subTitle = stringResource(id = R.string.feature_history_unexpected_error_subtitle), subTitle = stringResource(id = R.string.feature_history_unexpected_error_subtitle),
iconTint = MaterialTheme.colorScheme.primary, iconTint = MaterialTheme.colorScheme.primary,
iconImageVector = Icons.Rounded.Info iconImageVector = Icons.Rounded.Info,
) )
} }
@ -104,22 +116,22 @@ fun HistoryScreen(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(10.dp), .padding(10.dp),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Chip( Chip(
selected = selectedChip == TransactionType.OTHER, selected = selectedChip == TransactionType.OTHER,
onClick = { selectedChip = TransactionType.OTHER }, onClick = { selectedChip = TransactionType.OTHER },
label = stringResource(R.string.feature_history_all) label = stringResource(R.string.feature_history_all),
) )
Chip( Chip(
selected = selectedChip == TransactionType.CREDIT, selected = selectedChip == TransactionType.CREDIT,
onClick = { selectedChip = TransactionType.CREDIT }, onClick = { selectedChip = TransactionType.CREDIT },
label = stringResource(R.string.feature_history_credits) label = stringResource(R.string.feature_history_credits),
) )
Chip( Chip(
selected = selectedChip == TransactionType.DEBIT, selected = selectedChip == TransactionType.DEBIT,
onClick = { selectedChip = TransactionType.DEBIT }, onClick = { selectedChip = TransactionType.DEBIT },
label = stringResource(R.string.feature_history_debits) label = stringResource(R.string.feature_history_debits),
) )
} }
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
@ -130,72 +142,74 @@ fun HistoryScreen(
modifier = Modifier modifier = Modifier
.padding(start = 24.dp, end = 24.dp) .padding(start = 24.dp, end = 24.dp)
.clickable { transactionDetailState = it }, .clickable { transactionDetailState = it },
transaction = it transaction = it,
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
} }
} }
} }
} }
} }
HistoryUiState.Loading -> { HistoryUiState.Loading -> {
MifosLoadingWheel( MifosLoadingWheel(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
contentDesc = stringResource(R.string.feature_history_loading) contentDesc = stringResource(R.string.feature_history_loading),
) )
} }
} }
if (transactionDetailState != null) { if (transactionDetailState != null) {
MifosBottomSheet( MifosBottomSheet(
modifier = modifier,
content = { content = {
TransactionDetailScreen( TransactionDetailScreen(
transaction = transactionDetailState!!, transaction = transactionDetailState!!,
viewReceipt = { transactionDetailState?.transactionId?.let { viewReceipt(it) } }, viewReceipt = { transactionDetailState?.transactionId?.let { viewReceipt(it) } },
accountClicked = { accountClicked(it, ArrayList(transactionsList)) } accountClicked = { accountClicked(it, ArrayList(transactionsList)) },
) )
}, },
onDismiss = { transactionDetailState = null } onDismiss = { transactionDetailState = null },
) )
} }
} }
@Composable @Composable
fun Chip( private fun Chip(
selected: Boolean, selected: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
label: String label: String,
modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val backgroundColor = if (selected) MaterialTheme.colorScheme.primary else lightGrey val backgroundColor = if (selected) MaterialTheme.colorScheme.primary else lightGrey
Button( Button(
modifier = modifier,
onClick = { onClick = {
onClick() onClick()
Toast.makeText(context, label, Toast.LENGTH_SHORT).show() Toast.makeText(context, label, Toast.LENGTH_SHORT).show()
}, },
colors = ButtonDefaults.buttonColors(backgroundColor) colors = ButtonDefaults.buttonColors(backgroundColor),
) { ) {
Text( Text(
modifier = Modifier.padding(top = 4.dp, bottom = 4.dp, start = 16.dp, end = 16.dp), modifier = Modifier.padding(top = 4.dp, bottom = 4.dp, start = 16.dp, end = 16.dp),
text = label, text = label,
color = if (selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface color = if (selected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface,
) )
} }
} }
class HistoryPreviewProvider : PreviewParameterProvider<HistoryUiState> { internal class HistoryPreviewProvider : PreviewParameterProvider<HistoryUiState> {
override val values: Sequence<HistoryUiState> override val values: Sequence<HistoryUiState>
get() = sequenceOf( get() = sequenceOf(
HistoryUiState.Empty, HistoryUiState.Empty,
HistoryUiState.Loading, HistoryUiState.Loading,
HistoryUiState.Error("Error Screen"), HistoryUiState.Error("Error Screen"),
HistoryUiState.HistoryList(sampleHistoryList) HistoryUiState.HistoryList(sampleHistoryList),
) )
} }
val sampleHistoryList = List(10) { index -> internal val sampleHistoryList = List(10) { index ->
Transaction( Transaction(
transactionId = "txn_123456789", transactionId = "txn_123456789",
clientId = 1001L, clientId = 1001L,
@ -206,14 +220,14 @@ val sampleHistoryList = List(10) { index ->
transactionType = TransactionType.CREDIT, transactionType = TransactionType.CREDIT,
transferId = 3003L, transferId = 3003L,
transferDetail = TransferDetail(), transferDetail = TransferDetail(),
receiptId = "receipt_123456789" receiptId = "receipt_123456789",
) )
} }
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun HistoryScreenPreview( private fun HistoryScreenPreview(
@PreviewParameter(HistoryPreviewProvider::class) historyUiState: HistoryUiState @PreviewParameter(HistoryPreviewProvider::class) historyUiState: HistoryUiState,
) { ) {
HistoryScreen(historyUiState = historyUiState, viewReceipt = {}, accountClicked = { _, _ -> }) HistoryScreen(historyUiState = historyUiState, viewReceipt = {}, accountClicked = { _, _ -> })
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.history package org.mifospay.feature.history
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -17,7 +26,7 @@ class HistoryViewModel @Inject constructor(
private val mUseCaseHandler: UseCaseHandler, private val mUseCaseHandler: UseCaseHandler,
private val mLocalRepository: LocalRepository, private val mLocalRepository: LocalRepository,
private val mFetchAccountUseCase: FetchAccount, private val mFetchAccountUseCase: FetchAccount,
private val fetchAccountTransactionsUseCase: FetchAccountTransactions private val fetchAccountTransactionsUseCase: FetchAccountTransactions,
) : ViewModel() { ) : ViewModel() {
private val _historyUiState = MutableStateFlow<HistoryUiState>(HistoryUiState.Loading) private val _historyUiState = MutableStateFlow<HistoryUiState>(HistoryUiState.Loading)
@ -25,7 +34,8 @@ class HistoryViewModel @Inject constructor(
private fun fetchTransactions() { private fun fetchTransactions() {
_historyUiState.value = HistoryUiState.Loading _historyUiState.value = HistoryUiState.Loading
mUseCaseHandler.execute(mFetchAccountUseCase, mUseCaseHandler.execute(
mFetchAccountUseCase,
FetchAccount.RequestValues(mLocalRepository.clientDetails.clientId), FetchAccount.RequestValues(mLocalRepository.clientDetails.clientId),
object : UseCase.UseCaseCallback<FetchAccount.ResponseValue> { object : UseCase.UseCaseCallback<FetchAccount.ResponseValue> {
override fun onSuccess(response: FetchAccount.ResponseValue) { override fun onSuccess(response: FetchAccount.ResponseValue) {
@ -37,23 +47,28 @@ class HistoryViewModel @Inject constructor(
override fun onError(message: String) { override fun onError(message: String) {
_historyUiState.value = HistoryUiState.Error(message) _historyUiState.value = HistoryUiState.Error(message)
} }
}) },
)
} }
fun fetchTransactionsHistory(accountId: Long) { fun fetchTransactionsHistory(accountId: Long) {
mUseCaseHandler.execute(fetchAccountTransactionsUseCase, mUseCaseHandler.execute(
fetchAccountTransactionsUseCase,
FetchAccountTransactions.RequestValues(accountId), FetchAccountTransactions.RequestValues(accountId),
object : UseCase.UseCaseCallback<FetchAccountTransactions.ResponseValue?> { object : UseCase.UseCaseCallback<FetchAccountTransactions.ResponseValue?> {
override fun onSuccess(response: FetchAccountTransactions.ResponseValue?) { override fun onSuccess(response: FetchAccountTransactions.ResponseValue?) {
if (response?.transactions?.isNotEmpty() == true) if (response?.transactions?.isNotEmpty() == true) {
_historyUiState.value = HistoryUiState.HistoryList(response.transactions) _historyUiState.value = HistoryUiState.HistoryList(response.transactions)
else _historyUiState.value = HistoryUiState.Empty } else {
_historyUiState.value = HistoryUiState.Empty
}
} }
override fun onError(message: String) { override fun onError(message: String) {
_historyUiState.value = HistoryUiState.Error(message) _historyUiState.value = HistoryUiState.Error(message)
} }
}) },
)
} }
init { init {
@ -66,4 +81,4 @@ sealed class HistoryUiState {
data object Empty : HistoryUiState() data object Empty : HistoryUiState()
data class Error(val message: String) : HistoryUiState() data class Error(val message: String) : HistoryUiState()
data class HistoryList(val list: List<Transaction>) : HistoryUiState() data class HistoryList(val list: List<Transaction>) : HistoryUiState()
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.specific.transactions package org.mifospay.feature.specific.transactions
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -12,7 +21,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.Info
import androidx.compose.material3.Divider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@ -32,6 +41,7 @@ import com.mifospay.core.model.domain.Transaction
import com.mifospay.core.model.domain.TransactionType import com.mifospay.core.model.domain.TransactionType
import com.mifospay.core.model.domain.client.Client import com.mifospay.core.model.domain.client.Client
import com.mifospay.core.model.entity.accounts.savings.SavingAccount import com.mifospay.core.model.entity.accounts.savings.SavingAccount
import org.mifospay.common.Utils.toArrayList
import org.mifospay.core.designsystem.component.MfLoadingWheel import org.mifospay.core.designsystem.component.MfLoadingWheel
import org.mifospay.core.designsystem.component.MifosScaffold import org.mifospay.core.designsystem.component.MifosScaffold
import org.mifospay.core.designsystem.icon.MifosIcons import org.mifospay.core.designsystem.icon.MifosIcons
@ -43,36 +53,39 @@ import org.mifospay.core.ui.EmptyContentScreen
import org.mifospay.core.ui.ErrorScreenContent import org.mifospay.core.ui.ErrorScreenContent
import org.mifospay.feature.history.R import org.mifospay.feature.history.R
@Composable @Composable
fun SpecificTransactionsScreen( internal fun SpecificTransactionsScreen(
accountNumber: String, accountNumber: String,
transactions: ArrayList<Transaction>, transactions: List<Transaction>,
viewModel: SpecificTransactionsViewModel = hiltViewModel(),
backPress: () -> Unit, backPress: () -> Unit,
transactionItemClicked: (String) -> Unit transactionItemClicked: (String) -> Unit,
modifier: Modifier = Modifier,
viewModel: SpecificTransactionsViewModel = hiltViewModel(),
) { ) {
val uiState = viewModel.uiState.collectAsStateWithLifecycle() val uiState = viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(key1 = Unit) { LaunchedEffect(key1 = Unit) {
viewModel.setArguments(transactions, accountNumber) viewModel.setArguments(transactions.toArrayList(), accountNumber)
viewModel.getSpecificTransactions() viewModel.getSpecificTransactions()
} }
SpecificTransactionsScreen( SpecificTransactionsScreen(
uiState = uiState.value, uiState = uiState.value,
backPress = backPress, backPress = backPress,
transactionItemClicked = transactionItemClicked transactionItemClicked = transactionItemClicked,
modifier = modifier,
) )
} }
@Composable @Composable
fun SpecificTransactionsScreen( private fun SpecificTransactionsScreen(
uiState: SpecificTransactionsUiState, uiState: SpecificTransactionsUiState,
backPress: () -> Unit, backPress: () -> Unit,
transactionItemClicked: (String) -> Unit, transactionItemClicked: (String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
MifosScaffold( MifosScaffold(
modifier = modifier,
topBarTitle = R.string.feature_history_specific_transactions_history, topBarTitle = R.string.feature_history_specific_transactions_history,
backPress = backPress, backPress = backPress,
scaffoldContent = { paddingValues -> scaffoldContent = { paddingValues ->
@ -89,7 +102,7 @@ fun SpecificTransactionsScreen(
SpecificTransactionsUiState.Loading -> { SpecificTransactionsUiState.Loading -> {
MfLoadingWheel( MfLoadingWheel(
contentDesc = stringResource(R.string.feature_history_loading), contentDesc = stringResource(R.string.feature_history_loading),
backgroundColor = MaterialTheme.colorScheme.surface backgroundColor = MaterialTheme.colorScheme.surface,
) )
} }
@ -100,39 +113,42 @@ fun SpecificTransactionsScreen(
title = stringResource(id = R.string.feature_history_error_oops), title = stringResource(id = R.string.feature_history_error_oops),
subTitle = stringResource(id = R.string.feature_history_no_transactions_found), subTitle = stringResource(id = R.string.feature_history_no_transactions_found),
iconTint = MaterialTheme.colorScheme.onSurface, iconTint = MaterialTheme.colorScheme.onSurface,
iconImageVector = Icons.Rounded.Info iconImageVector = Icons.Rounded.Info,
) )
} else { } else {
SpecificTransactionsContent( SpecificTransactionsContent(
transactionList = uiState.transactionsList, transactionList = uiState.transactionsList,
transactionItemClicked = transactionItemClicked transactionItemClicked = transactionItemClicked,
) )
} }
} }
} }
} }
} },
) )
} }
@Composable @Composable
fun SpecificTransactionsContent( private fun SpecificTransactionsContent(
transactionList: ArrayList<Transaction>, transactionList: List<Transaction>,
transactionItemClicked: (String) -> Unit transactionItemClicked: (String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
LazyColumn { LazyColumn(modifier = modifier) {
itemsIndexed(items = transactionList) { index, transaction -> itemsIndexed(
items = transactionList,
) { index, transaction ->
SpecificTransactionItem( SpecificTransactionItem(
transaction = transaction, transaction = transaction,
modifier = Modifier modifier = Modifier
.padding(12.dp) .padding(12.dp)
.clickable { .clickable {
transaction.transactionId?.let { transactionItemClicked(it) } transaction.transactionId?.let { transactionItemClicked(it) }
} },
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
if (index != transactionList.lastIndex) { if (index != transactionList.lastIndex) {
Divider() HorizontalDivider()
} }
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
} }
@ -142,43 +158,43 @@ fun SpecificTransactionsContent(
@Composable @Composable
fun SpecificTransactionItem( fun SpecificTransactionItem(
transaction: Transaction, transaction: Transaction,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
) { ) {
Column(modifier = modifier.padding(horizontal = 12.dp)) { Column(modifier = modifier.padding(horizontal = 12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
SpecificTransactionAccountInfo( SpecificTransactionAccountInfo(
modifier = Modifier.weight(1f),
account = transaction.transferDetail.fromAccount, account = transaction.transferDetail.fromAccount,
client = transaction.transferDetail.fromClient client = transaction.transferDetail.fromClient,
modifier = Modifier.weight(1f),
) )
Icon(imageVector = MifosIcons.SendRightTilted, contentDescription = null) Icon(imageVector = MifosIcons.SendRightTilted, contentDescription = null)
SpecificTransactionAccountInfo( SpecificTransactionAccountInfo(
modifier = Modifier.weight(1f),
account = transaction.transferDetail.toAccount, account = transaction.transferDetail.toAccount,
client = transaction.transferDetail.toClient client = transaction.transferDetail.toClient,
modifier = Modifier.weight(1f),
) )
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Row( Row(
modifier = Modifier.padding(horizontal = 8.dp), modifier = Modifier.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Column { Column {
Text( Text(
text = stringResource(id = R.string.feature_receipt_transaction_id) + transaction.transactionId, text = stringResource(id = R.string.feature_history_transaction_id) + transaction.transactionId,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary,
) )
Text( Text(
text = stringResource(id = R.string.feature_receipt_transaction_date) + transaction.date, text = stringResource(id = R.string.feature_history_transaction_date) + transaction.date,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
) )
Text( Text(
text = when (transaction.transactionType) { text = when (transaction.transactionType) {
TransactionType.DEBIT -> stringResource(id = R.string.feature_history_debits) TransactionType.DEBIT -> stringResource(id = R.string.feature_history_debits)
TransactionType.CREDIT -> stringResource(id = R.string.feature_history_credits) TransactionType.CREDIT -> stringResource(id = R.string.feature_history_credits)
TransactionType.OTHER -> stringResource(id = R.string.feature_receipt_other) TransactionType.OTHER -> stringResource(id = R.string.feature_history_other)
}, },
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
) )
@ -198,11 +214,11 @@ fun SpecificTransactionItem(
} }
@Composable @Composable
fun SpecificTransactionAccountInfo( internal fun SpecificTransactionAccountInfo(
modifier: Modifier = Modifier,
account: SavingAccount, account: SavingAccount,
client: Client, client: Client,
accountClicked: (String) -> Unit = {} modifier: Modifier = Modifier,
accountClicked: (String) -> Unit = {},
) { ) {
Column( Column(
modifier = modifier.clickable { modifier = modifier.clickable {
@ -222,7 +238,7 @@ fun SpecificTransactionAccountInfo(
} }
} }
class SpecificTransactionsUiStateProvider : internal class SpecificTransactionsUiStateProvider :
PreviewParameterProvider<SpecificTransactionsUiState> { PreviewParameterProvider<SpecificTransactionsUiState> {
override val values: Sequence<SpecificTransactionsUiState> override val values: Sequence<SpecificTransactionsUiState>
get() = sequenceOf( get() = sequenceOf(
@ -235,9 +251,9 @@ class SpecificTransactionsUiStateProvider :
@Preview(showSystemUi = true) @Preview(showSystemUi = true)
@Composable @Composable
fun ShowQrScreenPreview( private fun ShowQrScreenPreview(
@PreviewParameter(SpecificTransactionsUiStateProvider::class) @PreviewParameter(SpecificTransactionsUiStateProvider::class)
uiState: SpecificTransactionsUiState uiState: SpecificTransactionsUiState,
) { ) {
MifosTheme { MifosTheme {
SpecificTransactionsScreen( SpecificTransactionsScreen(
@ -250,8 +266,8 @@ fun ShowQrScreenPreview(
@Preview @Preview
@Composable @Composable
fun SpecificTransactionItemPreview() { private fun SpecificTransactionItemPreview() {
Surface { Surface {
SpecificTransactionItem(transaction = Transaction()) SpecificTransactionItem(transaction = Transaction())
} }
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.specific.transactions package org.mifospay.feature.specific.transactions
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -14,7 +23,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SpecificTransactionsViewModel @Inject constructor( class SpecificTransactionsViewModel @Inject constructor(
private val mUseCaseFactory: UseCaseFactory, private val mUseCaseFactory: UseCaseFactory,
private var mTaskLooper: TaskLooper? = null private var mTaskLooper: TaskLooper? = null,
) : ViewModel() { ) : ViewModel() {
private val _uiState: MutableStateFlow<SpecificTransactionsUiState> = private val _uiState: MutableStateFlow<SpecificTransactionsUiState> =
@ -39,16 +48,18 @@ class SpecificTransactionsViewModel @Inject constructor(
val transferId = transaction.transferId val transferId = transaction.transferId
mTaskLooper?.addTask( mTaskLooper?.addTask(
useCase = mUseCaseFactory.getUseCase(Constants.FETCH_ACCOUNT_TRANSFER_USECASE) useCase = mUseCaseFactory.getUseCase(Constants.FETCH_ACCOUNT_TRANSFER_USECASE)
as UseCase<FetchAccountTransfer.RequestValues, FetchAccountTransfer.ResponseValue>, as UseCase<FetchAccountTransfer.RequestValues, FetchAccountTransfer.ResponseValue>,
values = FetchAccountTransfer.RequestValues(transferId), values = FetchAccountTransfer.RequestValues(transferId),
taskData = TaskLooper.TaskData( taskData = TaskLooper.TaskData(
org.mifospay.common.Constants.TRANSFER_DETAILS, i org.mifospay.common.Constants.TRANSFER_DETAILS,
) i,
),
) )
} }
mTaskLooper!!.listen(object : TaskLooper.Listener { mTaskLooper!!.listen(object : TaskLooper.Listener {
override fun <R : UseCase.ResponseValue?> onTaskSuccess( override fun <R : UseCase.ResponseValue?> onTaskSuccess(
taskData: TaskLooper.TaskData, response: R taskData: TaskLooper.TaskData,
response: R,
) { ) {
when (taskData.taskName) { when (taskData.taskName) {
org.mifospay.common.Constants.TRANSFER_DETAILS -> { org.mifospay.common.Constants.TRANSFER_DETAILS -> {
@ -89,4 +100,4 @@ sealed class SpecificTransactionsUiState {
data object Loading : SpecificTransactionsUiState() data object Loading : SpecificTransactionsUiState()
data object Error : SpecificTransactionsUiState() data object Error : SpecificTransactionsUiState()
data class Success(val transactionsList: ArrayList<Transaction>) : SpecificTransactionsUiState() data class Success(val transactionsList: ArrayList<Transaction>) : SpecificTransactionsUiState()
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
@file:Suppress("MaxLineLength") @file:Suppress("MaxLineLength")
package org.mifospay.feature.specific.transactions.navigation package org.mifospay.feature.specific.transactions.navigation
@ -15,27 +24,32 @@ const val SPECIFIC_TRANSACTIONS_ROUTE = "specific_transactions_route"
fun NavGraphBuilder.specificTransactionsScreen( fun NavGraphBuilder.specificTransactionsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
onTransactionItemClicked: (String) -> Unit onTransactionItemClicked: (String) -> Unit,
) { ) {
composable( composable(
route = "$SPECIFIC_TRANSACTIONS_ROUTE?${Constants.ACCOUNT_NUMBER}={accountNumber}&${Constants.TRANSACTIONS}={transactions}", route = "$SPECIFIC_TRANSACTIONS_ROUTE?${Constants.ACCOUNT_NUMBER}={accountNumber}&${Constants.TRANSACTIONS}={transactions}",
arguments = listOf( arguments = listOf(
navArgument(Constants.ACCOUNT_NUMBER) { type = NavType.StringType }, navArgument(Constants.ACCOUNT_NUMBER) { type = NavType.StringType },
navArgument(Constants.TRANSACTIONS) { type = NavType.StringType } navArgument(Constants.TRANSACTIONS) { type = NavType.StringType },
) ),
) { backStackEntry -> ) { backStackEntry ->
val accountNumber = backStackEntry.arguments?.getString(Constants.ACCOUNT_NUMBER) ?: "" val accountNumber = backStackEntry.arguments?.getString(Constants.ACCOUNT_NUMBER) ?: ""
val transactions = backStackEntry.arguments?.getParcelableArrayList<Transaction>(Constants.TRANSACTIONS) ?: arrayListOf() val transactions =
backStackEntry.arguments?.getParcelableArrayList<Transaction>(Constants.TRANSACTIONS)
?: arrayListOf()
SpecificTransactionsScreen( SpecificTransactionsScreen(
accountNumber = accountNumber, accountNumber = accountNumber,
transactions = transactions, transactions = transactions.toList(),
backPress = onBackClick, backPress = onBackClick,
transactionItemClicked = onTransactionItemClicked transactionItemClicked = onTransactionItemClicked,
) )
} }
} }
fun NavController.navigateToSpecificTransactions(accountNumber: String, transactions: ArrayList<Transaction>) { fun NavController.navigateToSpecificTransactions(
accountNumber: String,
transactions: ArrayList<Transaction>,
) {
this.navigate("$SPECIFIC_TRANSACTIONS_ROUTE?${Constants.ACCOUNT_NUMBER}=$accountNumber&${Constants.TRANSACTIONS}=$transactions") this.navigate("$SPECIFIC_TRANSACTIONS_ROUTE?${Constants.ACCOUNT_NUMBER}=$accountNumber&${Constants.TRANSACTIONS}=$transactions")
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.transaction.detail package org.mifospay.feature.transaction.detail
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@ -36,15 +45,14 @@ import org.mifospay.core.ui.ErrorScreenContent
import org.mifospay.feature.history.R import org.mifospay.feature.history.R
import org.mifospay.feature.specific.transactions.SpecificTransactionAccountInfo import org.mifospay.feature.specific.transactions.SpecificTransactionAccountInfo
@Composable @Composable
fun TransactionDetailScreen( internal fun TransactionDetailScreen(
viewModel: TransactionDetailViewModel = hiltViewModel(),
transaction: Transaction, transaction: Transaction,
viewReceipt: () -> Unit, viewReceipt: () -> Unit,
accountClicked: (String) -> Unit, accountClicked: (String) -> Unit,
modifier: Modifier = Modifier,
viewModel: TransactionDetailViewModel = hiltViewModel(),
) { ) {
val uiState = viewModel.transactionDetailUiState.collectAsStateWithLifecycle() val uiState = viewModel.transactionDetailUiState.collectAsStateWithLifecycle()
LaunchedEffect(key1 = transaction) { LaunchedEffect(key1 = transaction) {
@ -55,21 +63,23 @@ fun TransactionDetailScreen(
uiState = uiState.value, uiState = uiState.value,
transaction = transaction, transaction = transaction,
viewReceipt = viewReceipt, viewReceipt = viewReceipt,
accountClicked = accountClicked accountClicked = accountClicked,
modifier = modifier,
) )
} }
@Composable @Composable
fun TransactionDetailScreen( private fun TransactionDetailScreen(
uiState: TransactionDetailUiState, uiState: TransactionDetailUiState,
transaction: Transaction, transaction: Transaction,
viewReceipt: () -> Unit, viewReceipt: () -> Unit,
accountClicked: (String) -> Unit, accountClicked: (String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
Box( Box(
modifier = Modifier modifier = modifier
.padding(20.dp) .padding(20.dp)
.height(300.dp) .height(300.dp),
) { ) {
when (uiState) { when (uiState) {
is TransactionDetailUiState.Error -> { is TransactionDetailUiState.Error -> {
@ -78,7 +88,7 @@ fun TransactionDetailScreen(
is TransactionDetailUiState.Loading -> { is TransactionDetailUiState.Loading -> {
MfLoadingWheel( MfLoadingWheel(
backgroundColor = Color.Black.copy(alpha = 0.6f) backgroundColor = Color.Black.copy(alpha = 0.6f),
) )
} }
@ -86,7 +96,7 @@ fun TransactionDetailScreen(
TransactionsDetailContent( TransactionsDetailContent(
transaction = transaction, transaction = transaction,
viewReceipt = viewReceipt, viewReceipt = viewReceipt,
accountClicked = accountClicked accountClicked = accountClicked,
) )
} }
} }
@ -94,36 +104,36 @@ fun TransactionDetailScreen(
} }
@Composable @Composable
fun TransactionsDetailContent( private fun TransactionsDetailContent(
modifier: Modifier = Modifier,
transaction: Transaction, transaction: Transaction,
viewReceipt: () -> Unit, viewReceipt: () -> Unit,
accountClicked: (String) -> Unit, accountClicked: (String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
Column( Column(
modifier = modifier.padding(12.dp), modifier = modifier.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.padding(horizontal = 12.dp) .padding(horizontal = 12.dp)
.fillMaxWidth(), .fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Column { Column {
Text( Text(
text = stringResource(id = R.string.feature_receipt_transaction_id) + transaction.transactionId, text = stringResource(id = R.string.feature_history_transaction_id) + transaction.transactionId,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary,
) )
Text( Text(
text = stringResource(id = R.string.feature_receipt_transaction_date) + transaction.date, text = stringResource(id = R.string.feature_history_transaction_date) + transaction.date,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
) )
if (transaction.receiptId != null) { if (transaction.receiptId != null) {
Text( Text(
text = stringResource(id = R.string.feature_receipt_pan_id) + transaction.receiptId, text = stringResource(id = R.string.feature_history_pan_id) + transaction.receiptId,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
) )
} }
@ -145,31 +155,32 @@ fun TransactionsDetailContent(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
account = transaction.transferDetail.fromAccount, account = transaction.transferDetail.fromAccount,
client = transaction.transferDetail.fromClient, client = transaction.transferDetail.fromClient,
accountClicked = accountClicked accountClicked = accountClicked,
)
Image(
painter = painterResource(id = R.drawable.feature_history_ic_send),
contentDescription = null,
) )
Image(painter = painterResource(id = R.drawable.feature_history_ic_send), contentDescription = null)
SpecificTransactionAccountInfo( SpecificTransactionAccountInfo(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
account = transaction.transferDetail.toAccount, account = transaction.transferDetail.toAccount,
client = transaction.transferDetail.toClient, client = transaction.transferDetail.toClient,
accountClicked = accountClicked accountClicked = accountClicked,
) )
} }
HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp)) HorizontalDivider(modifier = Modifier.padding(vertical = 12.dp))
Text( Text(
text = stringResource(id = R.string.feature_receipt_view_Receipt), text = stringResource(id = R.string.feature_history_view_Receipt),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier.clickable { viewReceipt() } modifier = Modifier.clickable { viewReceipt() },
) )
} }
} }
class TransactionDetailUiStateProvider : class TransactionDetailUiStateProvider :
PreviewParameterProvider<TransactionDetailUiState> { PreviewParameterProvider<TransactionDetailUiState> {
override val values: Sequence<TransactionDetailUiState> override val values: Sequence<TransactionDetailUiState>
@ -182,7 +193,10 @@ class TransactionDetailUiStateProvider :
@Preview(showSystemUi = true) @Preview(showSystemUi = true)
@Composable @Composable
fun ShowQrScreenPreview(@PreviewParameter(TransactionDetailUiStateProvider::class) uiState: TransactionDetailUiState) { private fun ShowQrScreenPreview(
@PreviewParameter(TransactionDetailUiStateProvider::class)
uiState: TransactionDetailUiState,
) {
MifosTheme { MifosTheme {
TransactionDetailScreen( TransactionDetailScreen(
uiState = uiState, uiState = uiState,
@ -191,4 +205,4 @@ fun ShowQrScreenPreview(@PreviewParameter(TransactionDetailUiStateProvider::clas
accountClicked = {}, accountClicked = {},
) )
} }
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.transaction.detail package org.mifospay.feature.transaction.detail
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -12,7 +21,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class TransactionDetailViewModel @Inject constructor( class TransactionDetailViewModel @Inject constructor(
private val mUseCaseHandler: UseCaseHandler, private val mUseCaseHandler: UseCaseHandler,
private val mFetchAccountTransferUseCase: FetchAccountTransfer private val mFetchAccountTransferUseCase: FetchAccountTransfer,
) : ViewModel() { ) : ViewModel() {
private val _transactionDetailUiState: MutableStateFlow<TransactionDetailUiState> = private val _transactionDetailUiState: MutableStateFlow<TransactionDetailUiState> =
@ -20,7 +29,8 @@ class TransactionDetailViewModel @Inject constructor(
val transactionDetailUiState get() = _transactionDetailUiState val transactionDetailUiState get() = _transactionDetailUiState
fun getTransferDetail(transferId: Long) { fun getTransferDetail(transferId: Long) {
mUseCaseHandler.execute(mFetchAccountTransferUseCase, mUseCaseHandler.execute(
mFetchAccountTransferUseCase,
FetchAccountTransfer.RequestValues(transferId), FetchAccountTransfer.RequestValues(transferId),
object : UseCase.UseCaseCallback<FetchAccountTransfer.ResponseValue?> { object : UseCase.UseCaseCallback<FetchAccountTransfer.ResponseValue?> {
override fun onSuccess(response: FetchAccountTransfer.ResponseValue?) { override fun onSuccess(response: FetchAccountTransfer.ResponseValue?) {
@ -31,7 +41,7 @@ class TransactionDetailViewModel @Inject constructor(
override fun onError(message: String) { override fun onError(message: String) {
_transactionDetailUiState.value = TransactionDetailUiState.Error _transactionDetailUiState.value = TransactionDetailUiState.Error
} }
} },
) )
} }
} }
@ -40,4 +50,4 @@ sealed class TransactionDetailUiState {
data object Loading : TransactionDetailUiState() data object Loading : TransactionDetailUiState()
data object Error : TransactionDetailUiState() data object Error : TransactionDetailUiState()
data class Success(val transferDetail: TransferDetail?) : TransactionDetailUiState() data class Success(val transferDetail: TransferDetail?) : TransactionDetailUiState()
} }

View File

@ -1,3 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/> <path android:fillColor="@android:color/white" android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>

View File

@ -1,5 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<resources> <resources>
<string name="feature_history_transaction_id">Transaction ID : </string>
<string name="feature_history_transaction_date">Transaction Date : </string>
<string name="feature_history_other">Other</string>
<string name="feature_history_pan_id">PAN ID</string>
<string name="feature_history_view_Receipt">View Receipt</string>
<string name="feature_history_error_oops">Oops!</string> <string name="feature_history_error_oops">Oops!</string>
<string name="feature_history_empty_no_transaction_history_title">Your history is empty</string> <string name="feature_history_empty_no_transaction_history_title">Your history is empty</string>
<string name="feature_history_unexpected_error_subtitle">An unexpected error occurred. Please try again.</string> <string name="feature_history_unexpected_error_subtitle">An unexpected error occurred. Please try again.</string>

View File

@ -1,16 +0,0 @@
package org.mifospay.history
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
plugins { plugins {
alias(libs.plugins.mifospay.android.feature) alias(libs.plugins.mifospay.android.feature)
alias(libs.plugins.mifospay.android.library.compose) alias(libs.plugins.mifospay.android.library.compose)
@ -8,6 +17,5 @@ android {
} }
dependencies { dependencies {
implementation(projects.feature.history)
implementation(projects.core.data) implementation(projects.core.data)
} }

View File

@ -1,24 +0,0 @@
package org.mifospay.feature.home
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("org.mifospay.feature.home.test", appContext.packageName)
}
}

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest> </manifest>

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.home package org.mifospay.feature.home
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
@ -48,10 +57,11 @@ import org.mifospay.core.ui.ErrorScreenContent
import org.mifospay.core.ui.TransactionItemScreen import org.mifospay.core.ui.TransactionItemScreen
@Composable @Composable
fun HomeRoute( internal fun HomeRoute(
homeViewModel: HomeViewModel = hiltViewModel(),
onRequest: (String) -> Unit, onRequest: (String) -> Unit,
onPay: () -> Unit onPay: () -> Unit,
modifier: Modifier = Modifier,
homeViewModel: HomeViewModel = hiltViewModel(),
) { ) {
val homeUIState by homeViewModel val homeUIState by homeViewModel
.homeUIState .homeUIState
@ -61,7 +71,7 @@ fun HomeRoute(
is HomeUiState.Loading -> { is HomeUiState.Loading -> {
MfLoadingWheel( MfLoadingWheel(
contentDesc = stringResource(R.string.feature_home_loading), contentDesc = stringResource(R.string.feature_home_loading),
backgroundColor = MaterialTheme.colorScheme.surface backgroundColor = MaterialTheme.colorScheme.surface,
) )
} }
@ -73,7 +83,8 @@ fun HomeRoute(
onRequest = { onRequest = {
onRequest.invoke(successState.vpa ?: "") onRequest.invoke(successState.vpa ?: "")
}, },
onPay = onPay onPay = onPay,
modifier = modifier,
) )
} }
@ -81,23 +92,23 @@ fun HomeRoute(
ErrorScreenContent( ErrorScreenContent(
onClickRetry = { onClickRetry = {
homeViewModel.fetchAccountDetails() homeViewModel.fetchAccountDetails()
} },
) )
} }
} }
} }
@Composable @Composable
fun HomeScreen( private fun HomeScreen(
account: Account?, account: Account?,
transactions: List<Transaction>, transactions: List<Transaction>,
onRequest: () -> Unit, onRequest: () -> Unit,
onPay: () -> Unit onPay: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
LazyColumn( LazyColumn(
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.background(color = MaterialTheme.colorScheme.surface) .background(color = MaterialTheme.colorScheme.surface)
.padding(start = 32.dp, end = 32.dp), .padding(start = 32.dp, end = 32.dp),
) { ) {
@ -107,7 +118,7 @@ fun HomeScreen(
item { item {
PayRequestScreen( PayRequestScreen(
onRequest = onRequest, onRequest = onRequest,
onPay = onPay onPay = onPay,
) )
} }
if (transactions.isNotEmpty()) { if (transactions.isNotEmpty()) {
@ -116,7 +127,7 @@ fun HomeScreen(
modifier = Modifier.padding(top = 32.dp), modifier = Modifier.padding(top = 32.dp),
text = "Recent Transactions", text = "Recent Transactions",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface,
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
} }
@ -131,19 +142,22 @@ fun HomeScreen(
} }
@Composable @Composable
fun MifosWalletCardScreen(account: Account?) { private fun MifosWalletCardScreen(
modifier: Modifier = Modifier,
account: Account? = null,
) {
Card( Card(
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.height(225.dp) .height(225.dp)
.padding(top = 20.dp, bottom = 32.dp), .padding(top = 20.dp, bottom = 32.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.onSurface) colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.onSurface),
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.padding(start = 36.dp), .padding(start = 36.dp),
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center,
) { ) {
val walletBalanceLabel = val walletBalanceLabel =
if (account != null) "(${account.currency.displayLabel})" else "" if (account != null) "(${account.currency.displayLabel})" else ""
@ -152,96 +166,109 @@ fun MifosWalletCardScreen(account: Account?) {
style = TextStyle( style = TextStyle(
fontSize = 12.sp, fontSize = 12.sp,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
color = lightGrey color = lightGrey,
) ),
) )
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
val accountBalance = val accountBalance =
if (account != null) Utils.getFormattedAccountBalance( if (account != null) {
account.balance, account.currency.code Utils.getFormattedAccountBalance(
) else "0" account.balance,
account.currency.code,
)
} else {
"0"
}
Text( Text(
text = accountBalance, text = accountBalance,
style = TextStyle( style = TextStyle(
fontSize = 42.sp, fontSize = 42.sp,
fontWeight = FontWeight(600), fontWeight = FontWeight(600),
color = MaterialTheme.colorScheme.surface color = MaterialTheme.colorScheme.surface,
) ),
) )
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
val currencyEqual = if (account != null) { val currencyEqual = if (account != null) {
"${account.currency.code}1 ${account.currency.displayLabel}" "${account.currency.code}1 ${account.currency.displayLabel}"
} else "" } else {
""
}
Text( Text(
text = currencyEqual, text = currencyEqual,
style = TextStyle( style = TextStyle(
fontSize = 12.sp, fontSize = 12.sp,
fontWeight = FontWeight(500), fontWeight = FontWeight(500),
color = lightGrey color = lightGrey,
) ),
) )
} }
} }
} }
@Composable @Composable
fun PayRequestScreen( private fun PayRequestScreen(
onRequest: () -> Unit, onRequest: () -> Unit,
onPay: () -> Unit onPay: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
PayCard( PayCard(
modifier = Modifier.weight(1f),
title = "Request", title = "Request",
icon = R.drawable.core_ui_money_in icon = R.drawable.core_ui_money_in,
) { {
onRequest.invoke() onRequest.invoke()
} },
modifier = Modifier.weight(1f),
)
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
PayCard( PayCard(
modifier = Modifier.weight(1f),
title = "Pay", title = "Pay",
icon = R.drawable.core_ui_money_out icon = R.drawable.core_ui_money_out,
) { {
onPay.invoke() onPay.invoke()
} },
modifier = Modifier.weight(1f),
)
} }
} }
@Composable @Composable
fun PayCard( private fun PayCard(
modifier: Modifier,
title: String, title: String,
icon: Int, icon: Int,
onClickCard: () -> Unit onClickCard: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Card( Card(
modifier = modifier modifier = modifier
.height(144.dp) .height(144.dp)
.clickable { onClickCard.invoke() }, .clickable { onClickCard.invoke() },
border = BorderStroke(1.dp, border), border = BorderStroke(1.dp, border),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.padding(top = 20.dp, bottom = 20.dp, start = 20.dp), .padding(top = 20.dp, bottom = 20.dp, start = 20.dp),
verticalArrangement = Arrangement.SpaceBetween verticalArrangement = Arrangement.SpaceBetween,
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
.background(MaterialTheme.colorScheme.onSurface, shape = RoundedCornerShape(4.dp)), .background(
contentAlignment = Alignment.Center MaterialTheme.colorScheme.onSurface,
shape = RoundedCornerShape(4.dp),
),
contentAlignment = Alignment.Center,
) { ) {
Image( Image(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
painter = painterResource(id = icon), painter = painterResource(id = icon),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.surface) colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.surface),
) )
} }
Text(text = title) Text(text = title)
@ -251,7 +278,7 @@ fun PayCard(
@Preview(showSystemUi = true, device = "id:pixel_5") @Preview(showSystemUi = true, device = "id:pixel_5")
@Composable @Composable
fun HomeScreenPreview() { private fun HomeScreenPreview() {
HomeScreen( HomeScreen(
account = Account( account = Account(
image = "", image = "",
@ -262,9 +289,9 @@ fun HomeScreenPreview() {
currency = Currency( currency = Currency(
code = "USD", code = "USD",
displayLabel = "$", displayLabel = "$",
displaySymbol = "$" displaySymbol = "$",
), ),
productId = 1223 productId = 1223,
), ),
transactions = List(25) { index -> transactions = List(25) { index ->
Transaction( Transaction(
@ -273,24 +300,29 @@ fun HomeScreenPreview() {
currency = Currency( currency = Currency(
code = "USD", code = "USD",
displayLabel = "$", displayLabel = "$",
displaySymbol = "$" displaySymbol = "$",
), ),
transactionType = TransactionType.CREDIT transactionType = TransactionType.CREDIT,
) )
}, },
onPay = {}, onPay = {},
onRequest = {} onRequest = {},
) )
} }
@Preview @Preview
@Composable @Composable
fun PayRequestScreenPreview() { private fun PayRequestScreenPreview() {
PayRequestScreen({}, {}) PayRequestScreen({}, {})
} }
@Preview @Preview
@Composable @Composable
fun PayCardPreview() { private fun PayCardPreview() {
PayCard(Modifier.width(150.dp), "Request", R.drawable.feature_home_ic_arrow_back_black_24dp) { } PayCard(
"Request",
R.drawable.feature_home_ic_arrow_back_black_24dp,
{ },
Modifier.width(150.dp),
)
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.home package org.mifospay.feature.home
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -11,10 +20,10 @@ import kotlinx.coroutines.flow.update
import org.mifospay.core.data.base.UseCase.UseCaseCallback import org.mifospay.core.data.base.UseCase.UseCaseCallback
import org.mifospay.core.data.base.UseCaseHandler import org.mifospay.core.data.base.UseCaseHandler
import org.mifospay.core.data.domain.usecase.account.FetchAccount import org.mifospay.core.data.domain.usecase.account.FetchAccount
import org.mifospay.core.data.domain.usecase.history.HistoryContract
import org.mifospay.core.data.domain.usecase.history.TransactionsHistory
import org.mifospay.core.data.repository.local.LocalRepository import org.mifospay.core.data.repository.local.LocalRepository
import org.mifospay.core.datastore.PreferencesHelper import org.mifospay.core.datastore.PreferencesHelper
import org.mifospay.feature.HistoryContract
import org.mifospay.feature.TransactionsHistory
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -23,8 +32,8 @@ class HomeViewModel @Inject constructor(
private val localRepository: LocalRepository, private val localRepository: LocalRepository,
private val preferencesHelper: PreferencesHelper, private val preferencesHelper: PreferencesHelper,
private val fetchAccountUseCase: FetchAccount, private val fetchAccountUseCase: FetchAccount,
private val transactionsHistory: TransactionsHistory private val transactionsHistory: TransactionsHistory,
) : ViewModel(), HistoryContract.TransactionsHistoryAsync{ ) : ViewModel(), HistoryContract.TransactionsHistoryAsync {
// Expose screen UI state // Expose screen UI state
private val _homeUIState: MutableStateFlow<HomeUiState> = MutableStateFlow(HomeUiState.Loading) private val _homeUIState: MutableStateFlow<HomeUiState> = MutableStateFlow(HomeUiState.Loading)
@ -36,7 +45,8 @@ class HomeViewModel @Inject constructor(
} }
fun fetchAccountDetails() { fun fetchAccountDetails() {
useCaseHandler.execute(fetchAccountUseCase, useCaseHandler.execute(
fetchAccountUseCase,
FetchAccount.RequestValues(localRepository.clientDetails.clientId), FetchAccount.RequestValues(localRepository.clientDetails.clientId),
object : UseCaseCallback<FetchAccount.ResponseValue> { object : UseCaseCallback<FetchAccount.ResponseValue> {
override fun onSuccess(response: FetchAccount.ResponseValue) { override fun onSuccess(response: FetchAccount.ResponseValue) {
@ -44,7 +54,7 @@ class HomeViewModel @Inject constructor(
_homeUIState.update { _homeUIState.update {
HomeUiState.Success( HomeUiState.Success(
account = response.account, account = response.account,
vpa = localRepository.clientDetails.externalId vpa = localRepository.clientDetails.externalId,
) )
} }
response.account.id.let { response.account.id.let {
@ -55,7 +65,8 @@ class HomeViewModel @Inject constructor(
override fun onError(message: String) { override fun onError(message: String) {
_homeUIState.update { HomeUiState.Error } _homeUIState.update { HomeUiState.Error }
} }
}) },
)
} }
override fun onTransactionsFetchCompleted(transactions: List<Transaction>?) { override fun onTransactionsFetchCompleted(transactions: List<Transaction>?) {
@ -71,8 +82,8 @@ sealed interface HomeUiState {
data class Success( data class Success(
val account: Account? = null, val account: Account? = null,
val transactions: List<Transaction> = emptyList(), val transactions: List<Transaction> = emptyList(),
val vpa: String? = null val vpa: String? = null,
) : HomeUiState ) : HomeUiState
data object Error : HomeUiState data object Error : HomeUiState
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.home.navigation package org.mifospay.feature.home.navigation
import androidx.navigation.NavController import androidx.navigation.NavController
@ -12,12 +21,12 @@ fun NavController.navigateToHome(navOptions: NavOptions) = navigate(HOME_ROUTE,
fun NavGraphBuilder.homeScreen( fun NavGraphBuilder.homeScreen(
onRequest: (String) -> Unit, onRequest: (String) -> Unit,
onPay: () -> Unit onPay: () -> Unit,
) { ) {
composable(route = HOME_ROUTE) { composable(route = HOME_ROUTE) {
HomeRoute( HomeRoute(
onRequest = onRequest, onRequest = onRequest,
onPay = onPay onPay = onPay,
) )
} }
} }

View File

@ -1,3 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<resources> <resources>
<color name="feature_home_colorBlack87">#DE000000</color> <color name="feature_home_colorBlack87">#DE000000</color>
</resources> </resources>

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<resources> <resources>
<string name="feature_home_loading">Loading</string> <string name="feature_home_loading">Loading</string>
</resources> </resources>

View File

@ -1,16 +0,0 @@
package org.mifospay.feature.home
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
plugins { plugins {
alias(libs.plugins.mifospay.android.feature) alias(libs.plugins.mifospay.android.feature)
alias(libs.plugins.mifospay.android.library.compose) alias(libs.plugins.mifospay.android.library.compose)
@ -9,5 +18,4 @@ android {
dependencies { dependencies {
implementation(projects.core.data) implementation(projects.core.data)
implementation(projects.feature.receipt)
} }

View File

@ -1,24 +0,0 @@
package org.mifospay.invoices
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("org.mifospay.invoices.test", appContext.packageName)
}
}

View File

@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2024 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file,
You can obtain one at https://mozilla.org/MPL/2.0/.
See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest> </manifest>

View File

@ -1,6 +1,14 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.invoices package org.mifospay.feature.invoices
import android.net.Uri
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -14,7 +22,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -40,30 +47,31 @@ import org.mifospay.core.ui.ErrorScreenContent
import org.mifospay.invoices.R import org.mifospay.invoices.R
@Composable @Composable
fun InvoiceDetailScreen( internal fun InvoiceDetailScreen(
viewModel: InvoiceDetailViewModel = hiltViewModel(),
data: Uri?,
onBackPress: () -> Unit, onBackPress: () -> Unit,
navigateToReceiptScreen: (String) -> Unit navigateToReceiptScreen: (String) -> Unit,
modifier: Modifier = Modifier,
viewModel: InvoiceDetailViewModel = hiltViewModel(),
) { ) {
val invoiceDetailUiState by viewModel.invoiceDetailUiState.collectAsStateWithLifecycle() val invoiceDetailUiState by viewModel.invoiceDetailUiState.collectAsStateWithLifecycle()
InvoiceDetailScreen( InvoiceDetailScreen(
invoiceDetailUiState = invoiceDetailUiState, invoiceDetailUiState = invoiceDetailUiState,
onBackPress = onBackPress, onBackPress = onBackPress,
navigateToReceiptScreen = navigateToReceiptScreen navigateToReceiptScreen = navigateToReceiptScreen,
modifier = modifier,
) )
LaunchedEffect(key1 = true) {
viewModel.getInvoiceDetails(data)
}
} }
@Composable @Composable
fun InvoiceDetailScreen( private fun InvoiceDetailScreen(
invoiceDetailUiState: InvoiceDetailUiState, invoiceDetailUiState: InvoiceDetailUiState,
onBackPress: () -> Unit, onBackPress: () -> Unit,
navigateToReceiptScreen: (String) -> Unit navigateToReceiptScreen: (String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
MifosScaffold( MifosScaffold(
modifier = modifier,
topBarTitle = R.string.feature_invoices_invoice, topBarTitle = R.string.feature_invoices_invoice,
backPress = { onBackPress.invoke() }, backPress = { onBackPress.invoke() },
scaffoldContent = { scaffoldContent = {
@ -71,7 +79,7 @@ fun InvoiceDetailScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.padding(it) .padding(it),
) { ) {
when (invoiceDetailUiState) { when (invoiceDetailUiState) {
is InvoiceDetailUiState.Error -> { is InvoiceDetailUiState.Error -> {
@ -80,7 +88,7 @@ fun InvoiceDetailScreen(
InvoiceDetailUiState.Loading -> { InvoiceDetailUiState.Loading -> {
MfOverlayLoadingWheel( MfOverlayLoadingWheel(
contentDesc = stringResource(R.string.feature_invoices_loading) contentDesc = stringResource(R.string.feature_invoices_loading),
) )
} }
@ -89,90 +97,92 @@ fun InvoiceDetailScreen(
invoiceDetailUiState.invoice, invoiceDetailUiState.invoice,
invoiceDetailUiState.merchantId, invoiceDetailUiState.merchantId,
invoiceDetailUiState.paymentLink, invoiceDetailUiState.paymentLink,
navigateToReceiptScreen = navigateToReceiptScreen navigateToReceiptScreen = navigateToReceiptScreen,
) )
} }
} }
} }
}) },
)
} }
@Composable @Composable
@Suppress("LongMethod") @Suppress("LongMethod")
fun InvoiceDetailsContent( private fun InvoiceDetailsContent(
invoice: Invoice?, invoice: Invoice?,
merchantId: String?, merchantId: String?,
paymentLink: String?, paymentLink: String?,
navigateToReceiptScreen: (String) -> Unit navigateToReceiptScreen: (String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
Column( Column(
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp),
) { ) {
Text( Text(
text = stringResource(R.string.feature_invoices_invoice_details), text = stringResource(R.string.feature_invoices_invoice_details),
modifier = Modifier.padding(top = 16.dp) modifier = Modifier.padding(top = 16.dp),
) )
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = stringResource(R.string.feature_invoices_merchant_id), text = stringResource(R.string.feature_invoices_merchant_id),
modifier = Modifier.padding(top = 16.dp) modifier = Modifier.padding(top = 16.dp),
) )
Text( Text(
text = merchantId.toString(), text = merchantId.toString(),
modifier = Modifier.padding(top = 16.dp) modifier = Modifier.padding(top = 16.dp),
) )
} }
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = stringResource(R.string.feature_invoices_consumer_id), text = stringResource(R.string.feature_invoices_consumer_id),
modifier = Modifier.padding(top = 8.dp) modifier = Modifier.padding(top = 8.dp),
) )
Text( Text(
text = (invoice?.consumerName + " " + invoice?.consumerId), text = (invoice?.consumerName + " " + invoice?.consumerId),
modifier = Modifier.padding(top = 8.dp) modifier = Modifier.padding(top = 8.dp),
) )
} }
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = stringResource(id = R.string.feature_invoices_amount), text = stringResource(id = R.string.feature_invoices_amount),
modifier = Modifier.padding(top = 8.dp) modifier = Modifier.padding(top = 8.dp),
) )
Text( Text(
text = Constants.INR + " " + invoice?.amount + "", text = Constants.INR + " " + invoice?.amount + "",
modifier = Modifier.padding(top = 8.dp) modifier = Modifier.padding(top = 8.dp),
) )
} }
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = stringResource(id = R.string.feature_invoices_items_bought), text = stringResource(id = R.string.feature_invoices_items_bought),
modifier = Modifier.padding(top = 8.dp) modifier = Modifier.padding(top = 8.dp),
) )
Text( Text(
text = invoice?.itemsBought.toString(), text = invoice?.itemsBought.toString(),
modifier = Modifier.padding(top = 8.dp) modifier = Modifier.padding(top = 8.dp),
) )
} }
@ -180,42 +190,42 @@ fun InvoiceDetailsContent(
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = stringResource(id = R.string.feature_invoices_status), text = stringResource(id = R.string.feature_invoices_status),
modifier = Modifier.padding(top = 8.dp) modifier = Modifier.padding(top = 8.dp),
) )
Text( Text(
text = Constants.DONE, text = Constants.DONE,
modifier = Modifier.padding(top = 8.dp) modifier = Modifier.padding(top = 8.dp),
) )
} }
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = stringResource(id = R.string.feature_invoices_transaction_id), text = stringResource(id = R.string.feature_invoices_transaction_id),
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier modifier = Modifier
.padding(top = 10.dp) .padding(top = 10.dp),
) )
Text( Text(
text = invoice.transactionId ?: "", text = invoice.transactionId ?: "",
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier modifier = Modifier
.padding(top = 10.dp) .padding(top = 10.dp)
.then(Modifier.height(0.dp)) .then(Modifier.height(0.dp)),
) )
} }
Divider() Divider()
Text( Text(
text = stringResource(id = R.string.feature_invoices_unique_receipt_link), text = stringResource(id = R.string.feature_invoices_unique_receipt_link),
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
modifier = Modifier.padding(bottom = 10.dp) modifier = Modifier.padding(bottom = 10.dp),
) )
Text( Text(
text = invoice.transactionId ?: "", text = invoice.transactionId ?: "",
@ -227,33 +237,34 @@ fun InvoiceDetailsContent(
onPress = { onPress = {
invoice.transactionId?.let { it1 -> invoice.transactionId?.let { it1 ->
navigateToReceiptScreen.invoke( navigateToReceiptScreen.invoke(
it1 it1,
) )
} }
}, },
onLongPress = { onLongPress = {
clipboardManager.setText( clipboardManager.setText(
AnnotatedString( AnnotatedString(
Constants.RECEIPT_DOMAIN + invoice.transactionId Constants.RECEIPT_DOMAIN + invoice.transactionId,
) ),
) )
}) },
} )
},
) )
} }
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = stringResource(id = R.string.feature_invoices_date), text = stringResource(id = R.string.feature_invoices_date),
modifier = Modifier.padding(top = 10.dp) modifier = Modifier.padding(top = 10.dp),
) )
Text( Text(
text = DateHelper.getDateAsString(invoice!!.date), text = DateHelper.getDateAsString(invoice!!.date),
modifier = Modifier.padding(top = 10.dp) modifier = Modifier.padding(top = 10.dp),
) )
} }
@ -261,18 +272,18 @@ fun InvoiceDetailsContent(
Text( Text(
text = stringResource(id = R.string.feature_invoices_payment_options_will_be_fetched_from_upi), text = stringResource(id = R.string.feature_invoices_payment_options_will_be_fetched_from_upi),
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
modifier = Modifier.padding(vertical = 10.dp) modifier = Modifier.padding(vertical = 10.dp),
) )
Divider() Divider()
Text( Text(
text = stringResource(id = R.string.feature_invoices_unique_payment_link), text = stringResource(id = R.string.feature_invoices_unique_payment_link),
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
modifier = Modifier.padding(bottom = 10.dp) modifier = Modifier.padding(bottom = 10.dp),
) )
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = paymentLink.toString(), text = paymentLink.toString(),
@ -283,21 +294,21 @@ fun InvoiceDetailsContent(
detectTapGestures( detectTapGestures(
onLongPress = { onLongPress = {
clipboardManager.setText(AnnotatedString(paymentLink.toString())) clipboardManager.setText(AnnotatedString(paymentLink.toString()))
}) },
} )
},
) )
} }
} }
} }
@Composable @Composable
fun Divider(modifier: Modifier = Modifier) { private fun Divider(modifier: Modifier = Modifier) {
Box( Box(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.height(1.dp) .height(1.dp)
.background(Color(0x44000000)) .background(Color(0x44000000)),
) )
} }
@ -306,21 +317,21 @@ class InvoiceDetailScreenProvider : PreviewParameterProvider<InvoiceDetailUiStat
get() = sequenceOf( get() = sequenceOf(
InvoiceDetailUiState.Loading, InvoiceDetailUiState.Loading,
InvoiceDetailUiState.Error("Some Error Occurred"), InvoiceDetailUiState.Error("Some Error Occurred"),
InvoiceDetailUiState.Success(Invoice(), "", "") InvoiceDetailUiState.Success(Invoice(), "", ""),
) )
} }
@Preview(showBackground = true, showSystemUi = true) @Preview(showBackground = true, showSystemUi = true)
@Composable @Composable
fun InvoiceDetailScreenPreview( private fun InvoiceDetailScreenPreview(
@PreviewParameter(InvoiceDetailScreenProvider::class) invoiceDetailUiState: InvoiceDetailUiState @PreviewParameter(InvoiceDetailScreenProvider::class)
invoiceDetailUiState: InvoiceDetailUiState,
) { ) {
MifosTheme { MifosTheme {
InvoiceDetailScreen( InvoiceDetailScreen(
invoiceDetailUiState = invoiceDetailUiState, invoiceDetailUiState = invoiceDetailUiState,
onBackPress = {}, onBackPress = {},
navigateToReceiptScreen = {} navigateToReceiptScreen = {},
) )
} }
} }

View File

@ -1,6 +1,16 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.invoices package org.mifospay.feature.invoices
import android.net.Uri import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.mifospay.core.model.entity.Invoice import com.mifospay.core.model.entity.Invoice
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -10,34 +20,48 @@ import org.mifospay.core.data.base.UseCase
import org.mifospay.core.data.base.UseCaseHandler import org.mifospay.core.data.base.UseCaseHandler
import org.mifospay.core.data.domain.usecase.invoice.FetchInvoice import org.mifospay.core.data.domain.usecase.invoice.FetchInvoice
import org.mifospay.core.datastore.PreferencesHelper import org.mifospay.core.datastore.PreferencesHelper
import org.mifospay.feature.invoices.navigation.INVOICE_DATA_ARG
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class InvoiceDetailViewModel @Inject constructor( class InvoiceDetailViewModel @Inject constructor(
private val mUseCaseHandler: UseCaseHandler, private val mUseCaseHandler: UseCaseHandler,
private val mPreferencesHelper: PreferencesHelper, private val mPreferencesHelper: PreferencesHelper,
private val fetchInvoiceUseCase: FetchInvoice private val fetchInvoiceUseCase: FetchInvoice,
savedStateHandle: SavedStateHandle,
) : ViewModel() { ) : ViewModel() {
private val _invoiceDetailUiState = private val _invoiceDetailUiState =
MutableStateFlow<InvoiceDetailUiState>(InvoiceDetailUiState.Loading) MutableStateFlow<InvoiceDetailUiState>(InvoiceDetailUiState.Loading)
val invoiceDetailUiState: StateFlow<InvoiceDetailUiState> = _invoiceDetailUiState val invoiceDetailUiState: StateFlow<InvoiceDetailUiState> = _invoiceDetailUiState
fun getInvoiceDetails(data: Uri?) { init {
mUseCaseHandler.execute(fetchInvoiceUseCase, FetchInvoice.RequestValues(data), savedStateHandle.get<String>(INVOICE_DATA_ARG)?.let { invoiceData ->
Uri.decode(invoiceData)?.let {
getInvoiceDetails(Uri.parse(it))
}
}
}
private fun getInvoiceDetails(data: Uri?) {
mUseCaseHandler.execute(
fetchInvoiceUseCase,
FetchInvoice.RequestValues(data),
object : UseCase.UseCaseCallback<FetchInvoice.ResponseValue> { object : UseCase.UseCaseCallback<FetchInvoice.ResponseValue> {
override fun onSuccess(response: FetchInvoice.ResponseValue) { override fun onSuccess(response: FetchInvoice.ResponseValue) {
_invoiceDetailUiState.value = InvoiceDetailUiState.Success( _invoiceDetailUiState.value = InvoiceDetailUiState.Success(
response.invoices[0], response.invoices[0],
mPreferencesHelper.fullName + " " mPreferencesHelper.fullName + " " +
+ mPreferencesHelper.clientId, data.toString() mPreferencesHelper.clientId,
data.toString(),
) )
} }
override fun onError(message: String) { override fun onError(message: String) {
_invoiceDetailUiState.value = InvoiceDetailUiState.Error(message) _invoiceDetailUiState.value = InvoiceDetailUiState.Error(message)
} }
}) },
)
} }
} }
@ -46,8 +70,8 @@ sealed interface InvoiceDetailUiState {
data class Success( data class Success(
val invoice: Invoice?, val invoice: Invoice?,
val merchantId: String?, val merchantId: String?,
val paymentLink: String? val paymentLink: String?,
) : InvoiceDetailUiState ) : InvoiceDetailUiState
data class Error(val message: String) : InvoiceDetailUiState data class Error(val message: String) : InvoiceDetailUiState
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.invoices package org.mifospay.feature.invoices
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -25,81 +34,84 @@ import org.mifospay.core.designsystem.theme.grey
import org.mifospay.invoices.R import org.mifospay.invoices.R
@Composable @Composable
fun InvoiceItem( internal fun InvoiceItem(
invoiceTitle: String, invoiceTitle: String,
invoiceAmount: String, invoiceAmount: String,
invoiceStatus: String, invoiceStatus: String,
invoiceDate: String, invoiceDate: String,
invoiceId: String, invoiceId: String,
invoiceStatusIcon: Long, invoiceStatusIcon: Long,
onClick: (String) -> Unit onClick: (String) -> Unit,
modifier: Modifier = Modifier,
) { ) {
Card( Card(
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(4.dp) .padding(4.dp)
.clickable { onClick(invoiceId) }, .clickable { onClick(invoiceId) },
elevation = CardDefaults.cardElevation(4.dp), elevation = CardDefaults.cardElevation(4.dp),
colors = CardDefaults.cardColors(Color.White) colors = CardDefaults.cardColors(Color.White),
) { ) {
Column { Column {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(10.dp), .padding(10.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon( Icon(
painter = painterResource( painter = painterResource(
id = if (invoiceStatusIcon == 0L) id = if (invoiceStatusIcon == 0L) {
R.drawable.feature_invoices_ic_remove_circle_outline_black_24dp R.drawable.feature_invoices_ic_remove_circle_outline_black_24dp
else R.drawable.feature_invoices_ic_check_round_black_24dp } else {
R.drawable.feature_invoices_ic_check_round_black_24dp
},
), ),
contentDescription = "Invoice Status", contentDescription = "Invoice Status",
modifier = Modifier modifier = Modifier
.size(64.dp) .size(64.dp)
.padding(5.dp), .padding(5.dp),
tint = if (invoiceStatusIcon == 0L) Color.Yellow else Color.Blue tint = if (invoiceStatusIcon == 0L) Color.Yellow else Color.Blue,
) )
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(end = 10.dp) .padding(end = 10.dp),
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = invoiceTitle, text = invoiceTitle,
color = Color.Black, color = Color.Black,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
) )
Text( Text(
text = invoiceAmount, text = invoiceAmount,
color = Color.Black, color = Color.Black,
textAlign = TextAlign.End, textAlign = TextAlign.End,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
) )
} }
Text( Text(
text = invoiceStatus, text = invoiceStatus,
color = grey, color = grey,
modifier = Modifier.padding(top = 1.dp) modifier = Modifier.padding(top = 1.dp),
) )
Row( Row(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
) { ) {
Text( Text(
text = invoiceDate, text = invoiceDate,
color = grey, color = grey,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
) )
Text( Text(
text = invoiceId, text = invoiceId,
color = grey, color = grey,
textAlign = TextAlign.End, textAlign = TextAlign.End,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
) )
} }
Spacer( Spacer(
@ -107,7 +119,7 @@ fun InvoiceItem(
.padding(top = 10.dp) .padding(top = 10.dp)
.fillMaxWidth() .fillMaxWidth()
.height(1.dp) .height(1.dp)
.background(Color.Gray) .background(Color.Gray),
) )
} }
} }
@ -117,6 +129,14 @@ fun InvoiceItem(
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun PreviewInvoiceItem() { private fun PreviewInvoiceItem() {
InvoiceItem("Logo for Richard", "$3000", "Pending", "12/3/4", "Invoice id:12345", 0L) {} InvoiceItem(
invoiceTitle = "Logo for Richard",
invoiceAmount = "$3000",
invoiceStatus = "Pending",
invoiceDate = "12/3/4",
invoiceId = "Invoice id:12345",
invoiceStatusIcon = 0L,
onClick = {},
)
} }

View File

@ -1,3 +1,12 @@
/*
* Copyright 2024 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
*/
package org.mifospay.feature.invoices package org.mifospay.feature.invoices
import android.net.Uri import android.net.Uri
@ -25,24 +34,26 @@ import org.mifospay.invoices.R
@Composable @Composable
fun InvoiceScreenRoute( fun InvoiceScreenRoute(
navigateToInvoiceDetailScreen: (Uri) -> Unit,
modifier: Modifier = Modifier,
viewModel: InvoicesViewModel = hiltViewModel(), viewModel: InvoicesViewModel = hiltViewModel(),
navigateToInvoiceDetailScreen: (Uri) -> Unit
) { ) {
val invoiceUiState by viewModel.invoiceUiState.collectAsStateWithLifecycle() val invoiceUiState by viewModel.invoiceUiState.collectAsStateWithLifecycle()
InvoiceScreen( InvoiceScreen(
invoiceUiState = invoiceUiState, invoiceUiState = invoiceUiState,
getUniqueInvoiceLink = { viewModel.getUniqueInvoiceLink(it) }, getUniqueInvoiceLink = { viewModel.getUniqueInvoiceLink(it) },
navigateToInvoiceDetailScreen = navigateToInvoiceDetailScreen navigateToInvoiceDetailScreen = navigateToInvoiceDetailScreen,
modifier = modifier,
) )
} }
@Composable @Composable
fun InvoiceScreen( private fun InvoiceScreen(
invoiceUiState: InvoicesUiState, invoiceUiState: InvoicesUiState,
getUniqueInvoiceLink: (Long) -> Uri?, getUniqueInvoiceLink: (Long) -> Uri?,
navigateToInvoiceDetailScreen: (Uri) -> Unit navigateToInvoiceDetailScreen: (Uri) -> Unit,
modifier: Modifier = Modifier,
) { ) {
when (invoiceUiState) { when (invoiceUiState) {
is InvoicesUiState.Error -> { is InvoicesUiState.Error -> {
EmptyContentScreen( EmptyContentScreen(
@ -50,14 +61,16 @@ fun InvoiceScreen(
title = stringResource(id = R.string.feature_invoices_error_oops), title = stringResource(id = R.string.feature_invoices_error_oops),
subTitle = stringResource(id = R.string.feature_invoices_unexpected_error_subtitle), subTitle = stringResource(id = R.string.feature_invoices_unexpected_error_subtitle),
iconTint = Color.Black, iconTint = Color.Black,
iconImageVector = Info iconImageVector = Info,
) )
} }
is InvoicesUiState.InvoiceList -> { is InvoicesUiState.InvoiceList -> {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize()) { LazyColumn(modifier = Modifier.fillMaxSize()) {
items(invoiceUiState.list) { items(
items = invoiceUiState.list,
) {
InvoiceItem( InvoiceItem(
invoiceTitle = it?.title.toString(), invoiceTitle = it?.title.toString(),
invoiceAmount = it?.amount.toString(), invoiceAmount = it?.amount.toString(),
@ -70,7 +83,7 @@ fun InvoiceScreen(
invoiceUri?.let { uri -> invoiceUri?.let { uri ->
navigateToInvoiceDetailScreen.invoke(uri) navigateToInvoiceDetailScreen.invoke(uri)
} }
} },
) )
} }
} }
@ -89,7 +102,7 @@ fun InvoiceScreen(
InvoicesUiState.Loading -> { InvoicesUiState.Loading -> {
MifosLoadingWheel( MifosLoadingWheel(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
contentDesc = stringResource(R.string.feature_invoices_loading) contentDesc = stringResource(R.string.feature_invoices_loading),
) )
} }
} }
@ -101,14 +114,14 @@ class InvoicesUiStateProvider : PreviewParameterProvider<InvoicesUiState> {
InvoicesUiState.Loading, InvoicesUiState.Loading,
InvoicesUiState.Empty, InvoicesUiState.Empty,
InvoicesUiState.InvoiceList(sampleInvoiceList), InvoicesUiState.InvoiceList(sampleInvoiceList),
InvoicesUiState.Error("Some Error Occurred") InvoicesUiState.Error("Some Error Occurred"),
) )
} }
@Preview(showBackground = true, showSystemUi = true) @Preview(showBackground = true, showSystemUi = true)
@Composable @Composable
private fun InvoiceScreenPreview( private fun InvoiceScreenPreview(
@PreviewParameter(InvoicesUiStateProvider::class) invoiceUiState: InvoicesUiState @PreviewParameter(InvoicesUiStateProvider::class) invoiceUiState: InvoicesUiState,
) { ) {
MifosTheme { MifosTheme {
InvoiceScreen(invoiceUiState = invoiceUiState, getUniqueInvoiceLink = { Uri.EMPTY }, {}) InvoiceScreen(invoiceUiState = invoiceUiState, getUniqueInvoiceLink = { Uri.EMPTY }, {})
@ -125,6 +138,6 @@ val sampleInvoiceList = List(10) { index ->
transactionId = "txn_78910", transactionId = "txn_78910",
id = index.toLong(), id = index.toLong(),
title = "Stationery Purchase", title = "Stationery Purchase",
date = mutableListOf(2024, 3, 23) date = mutableListOf(2024, 3, 23),
) )
} }

Some files were not shown because too many files have changed in this diff Show More