Skip to content

Commit ff171dd

Browse files
authored
Feat: Migrated Saved Card Module to KMP (#1800)
1 parent 3135f66 commit ff171dd

File tree

73 files changed

+4222
-1124
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+4222
-1124
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ that can be used as a dependency in any other wallet based project. It is develo
4848
| :feature:qr | Not started ||||||
4949
| :feature:receipt | Not started ||||||
5050
| :feature:request-money | Not started ||||||
51-
| :feature:saved-cards | Not started | | | | | |
51+
| :feature:saved-cards | Done | | | | | |
5252
| :feature:search | Not started ||||||
5353
| :feature:send-money | Not started ||||||
5454
| :feature:standing-instruction | Not started ||||||

core/common/src/commonMain/kotlin/org/mifospay/core/common/CreditCardUtils.kt

Lines changed: 0 additions & 39 deletions
This file was deleted.

core/data/src/commonMain/kotlin/org/mifospay/core/data/repository/SavedCardRepository.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ package org.mifospay.core.data.repository
1111

1212
import kotlinx.coroutines.flow.Flow
1313
import org.mifospay.core.common.DataState
14-
import org.mifospay.core.network.model.GenericResponse
15-
import org.mifospay.core.network.model.entity.savedcards.Card
14+
import org.mifospay.core.model.savedcards.CardPayload
15+
import org.mifospay.core.model.savedcards.SavedCard
1616

1717
interface SavedCardRepository {
18-
suspend fun getSavedCards(clientId: Int): Flow<DataState<List<Card>>>
18+
fun getSavedCards(clientId: Long): Flow<DataState<List<SavedCard>>>
1919

20-
suspend fun addSavedCard(clientId: Int, card: Card): Flow<DataState<GenericResponse>>
20+
fun getSavedCard(clientId: Long, cardId: Long): Flow<DataState<SavedCard>>
2121

22-
suspend fun deleteCard(clientId: Int, cardId: Int): Flow<DataState<GenericResponse>>
22+
suspend fun addSavedCard(clientId: Long, card: CardPayload): DataState<String>
2323

24-
suspend fun updateCard(clientId: Int, cardId: Int, card: Card): Flow<DataState<GenericResponse>>
24+
suspend fun deleteCard(clientId: Long, cardId: Long): DataState<String>
25+
26+
suspend fun updateCard(clientId: Long, cardId: Long, card: CardPayload): DataState<String>
2527
}

core/data/src/commonMain/kotlin/org/mifospay/core/data/repositoryImp/SavedCardRepositoryImpl.kt

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,76 @@ package org.mifospay.core.data.repositoryImp
1111

1212
import kotlinx.coroutines.CoroutineDispatcher
1313
import kotlinx.coroutines.flow.Flow
14+
import kotlinx.coroutines.flow.catch
1415
import kotlinx.coroutines.flow.flowOn
16+
import kotlinx.coroutines.flow.map
17+
import kotlinx.coroutines.flow.onStart
18+
import kotlinx.coroutines.withContext
1519
import org.mifospay.core.common.DataState
1620
import org.mifospay.core.common.asDataStateFlow
1721
import org.mifospay.core.data.repository.SavedCardRepository
22+
import org.mifospay.core.model.savedcards.CardPayload
23+
import org.mifospay.core.model.savedcards.SavedCard
1824
import org.mifospay.core.network.FineractApiManager
19-
import org.mifospay.core.network.model.GenericResponse
20-
import org.mifospay.core.network.model.entity.savedcards.Card
2125

2226
class SavedCardRepositoryImpl(
2327
private val apiManager: FineractApiManager,
2428
private val ioDispatcher: CoroutineDispatcher,
2529
) : SavedCardRepository {
26-
override suspend fun getSavedCards(clientId: Int): Flow<DataState<List<Card>>> {
27-
return apiManager.savedCardApi.getSavedCards(clientId).asDataStateFlow().flowOn(ioDispatcher)
30+
override fun getSavedCards(clientId: Long): Flow<DataState<List<SavedCard>>> {
31+
return apiManager.savedCardApi
32+
.getSavedCards(clientId)
33+
.catch { DataState.Error(it, null) }
34+
.onStart { DataState.Loading }
35+
.asDataStateFlow().flowOn(ioDispatcher)
2836
}
2937

30-
override suspend fun addSavedCard(clientId: Int, card: Card): Flow<DataState<GenericResponse>> {
31-
return apiManager.savedCardApi.addSavedCard(clientId, card).asDataStateFlow().flowOn(ioDispatcher)
38+
override fun getSavedCard(clientId: Long, cardId: Long): Flow<DataState<SavedCard>> {
39+
return apiManager.savedCardApi
40+
.getSavedCard(clientId, cardId)
41+
.catch { DataState.Error(it, null) }
42+
.onStart { DataState.Loading }
43+
.map { it.first() }
44+
.asDataStateFlow().flowOn(ioDispatcher)
3245
}
3346

34-
override suspend fun deleteCard(clientId: Int, cardId: Int): Flow<DataState<GenericResponse>> {
35-
return apiManager.savedCardApi.deleteCard(clientId, cardId).asDataStateFlow().flowOn(ioDispatcher)
47+
override suspend fun addSavedCard(clientId: Long, card: CardPayload): DataState<String> {
48+
return try {
49+
withContext(ioDispatcher) {
50+
apiManager.savedCardApi.addSavedCard(clientId, card)
51+
}
52+
53+
DataState.Success("Card added successfully")
54+
} catch (e: Exception) {
55+
DataState.Error(e, null)
56+
}
57+
}
58+
59+
override suspend fun deleteCard(clientId: Long, cardId: Long): DataState<String> {
60+
return try {
61+
withContext(ioDispatcher) {
62+
apiManager.savedCardApi.deleteCard(clientId, cardId)
63+
}
64+
65+
DataState.Success("Card deleted successfully")
66+
} catch (e: Exception) {
67+
DataState.Error(e, null)
68+
}
3669
}
3770

3871
override suspend fun updateCard(
39-
clientId: Int,
40-
cardId: Int,
41-
card: Card,
42-
): Flow<DataState<GenericResponse>> {
43-
return apiManager.savedCardApi
44-
.updateCard(clientId, cardId, card)
45-
.asDataStateFlow().flowOn(ioDispatcher)
72+
clientId: Long,
73+
cardId: Long,
74+
card: CardPayload,
75+
): DataState<String> {
76+
return try {
77+
withContext(ioDispatcher) {
78+
apiManager.savedCardApi.updateCard(clientId, cardId, card)
79+
}
80+
81+
DataState.Success("Card updated successfully")
82+
} catch (e: Exception) {
83+
DataState.Error(e, null)
84+
}
4685
}
4786
}

core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/MifosScaffold.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import androidx.compose.foundation.layout.Box
1313
import androidx.compose.foundation.layout.PaddingValues
1414
import androidx.compose.foundation.layout.RowScope
1515
import androidx.compose.foundation.layout.WindowInsets
16+
import androidx.compose.foundation.layout.fillMaxSize
17+
import androidx.compose.foundation.layout.imePadding
1618
import androidx.compose.foundation.layout.navigationBarsPadding
1719
import androidx.compose.foundation.layout.padding
1820
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -86,7 +88,10 @@ fun MifosScaffold(
8688
)
8789
}
8890
},
89-
modifier = modifier,
91+
modifier = modifier
92+
.fillMaxSize()
93+
.navigationBarsPadding()
94+
.imePadding(),
9095
)
9196
}
9297

@@ -106,7 +111,10 @@ fun MifosScaffold(
106111
content: @Composable (PaddingValues) -> Unit,
107112
) {
108113
Scaffold(
109-
modifier = modifier,
114+
modifier = modifier
115+
.fillMaxSize()
116+
.navigationBarsPadding()
117+
.imePadding(),
110118
topBar = topBar,
111119
bottomBar = bottomBar,
112120
snackbarHost = { SnackbarHost(snackbarHostState) },

core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/component/TextField.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ fun MifosOutlinedTextField(
5757
showClearIcon: Boolean = true,
5858
readOnly: Boolean = false,
5959
clearIcon: ImageVector = MifosIcons.Close,
60-
onClickClearIcon: () -> Unit = {},
60+
onClickClearIcon: () -> Unit = { onValueChange("") },
6161
onKeyboardActions: (() -> Unit)? = null,
6262
leadingIcon: @Composable (() -> Unit)? = null,
6363
trailingIcon: @Composable (() -> Unit)? = null,
@@ -111,7 +111,7 @@ fun MifosTextField(
111111
showClearIcon: Boolean = true,
112112
readOnly: Boolean = false,
113113
clearIcon: ImageVector = MifosIcons.Close,
114-
onClickClearIcon: () -> Unit = {},
114+
onClickClearIcon: () -> Unit = { onValueChange("") },
115115
textStyle: TextStyle = LocalTextStyle.current,
116116
visualTransformation: VisualTransformation = VisualTransformation.None,
117117
keyboardActions: KeyboardActions = KeyboardActions.Default,

core/designsystem/src/commonMain/kotlin/org/mifospay/core/designsystem/utils/ModifierExt.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99
*/
1010
package org.mifospay.core.designsystem.utils
1111

12+
import androidx.compose.foundation.Indication
13+
import androidx.compose.foundation.clickable
14+
import androidx.compose.foundation.interaction.MutableInteractionSource
1215
import androidx.compose.runtime.Composable
1316
import androidx.compose.runtime.Stable
17+
import androidx.compose.runtime.remember
1418
import androidx.compose.ui.Modifier
19+
import androidx.compose.ui.composed
1520
import androidx.compose.ui.draw.scale
1621
import androidx.compose.ui.focus.FocusDirection
1722
import androidx.compose.ui.input.key.Key
@@ -22,6 +27,8 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent
2227
import androidx.compose.ui.input.key.type
2328
import androidx.compose.ui.platform.LocalFocusManager
2429
import androidx.compose.ui.platform.LocalLayoutDirection
30+
import androidx.compose.ui.platform.debugInspectorInfo
31+
import androidx.compose.ui.semantics.Role
2532
import androidx.compose.ui.unit.LayoutDirection
2633

2734
@Stable
@@ -52,3 +59,27 @@ fun Modifier.tabNavigation(): Modifier {
5259
}
5360
}
5461
}
62+
63+
fun Modifier.onClick(
64+
indication: Indication? = null,
65+
enabled: Boolean = true,
66+
onClickLabel: String? = null,
67+
role: Role? = null,
68+
onClick: () -> Unit,
69+
) = this.composed(
70+
inspectorInfo = debugInspectorInfo {
71+
name = "onClickModifier"
72+
value = enabled
73+
},
74+
) {
75+
val interactionSource = remember { MutableInteractionSource() }
76+
clickable(
77+
indication = indication,
78+
interactionSource = interactionSource,
79+
enabled = enabled,
80+
onClickLabel = onClickLabel,
81+
role = role,
82+
) {
83+
onClick.invoke()
84+
}
85+
}

core/model/src/commonMain/kotlin/org/mifospay/core/model/notification/Notification.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ package org.mifospay.core.model.notification
1111

1212
import kotlinx.serialization.Serializable
1313
import org.mifospay.core.common.DateHelper.toFormattedDateTime
14+
import org.mifospay.core.common.IgnoredOnParcel
1415
import org.mifospay.core.common.Parcelable
1516
import org.mifospay.core.common.Parcelize
1617

@@ -27,5 +28,6 @@ data class Notification(
2728
val isSystemGenerated: Boolean,
2829
val createdAt: String,
2930
) : Parcelable {
31+
@IgnoredOnParcel
3032
val formattedDate = createdAt.toFormattedDateTime()
3133
}

core/network/src/commonMain/kotlin/org/mifospay/core/network/model/entity/savedcards/Card.kt renamed to core/model/src/commonMain/kotlin/org/mifospay/core/model/savedcards/CardPayload.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77
*
88
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
99
*/
10-
package org.mifospay.core.network.model.entity.savedcards
10+
package org.mifospay.core.model.savedcards
1111

1212
import kotlinx.serialization.Serializable
1313

1414
@Serializable
15-
data class Card(
16-
val cardNumber: String = "",
17-
val cvv: String = "",
18-
val expiryDate: String = "",
19-
val firstName: String = "",
20-
val lastName: String = "",
21-
val id: Int = 0,
15+
data class CardPayload(
16+
val firstName: String,
17+
val lastName: String,
18+
val cardNumber: String,
19+
val cvv: String,
20+
val expiryDate: String,
21+
val backgroundColor: String? = null,
2222
)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2024 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
9+
*/
10+
package org.mifospay.core.model.savedcards
11+
12+
import kotlinx.serialization.SerialName
13+
import kotlinx.serialization.Serializable
14+
import org.mifospay.core.common.IgnoredOnParcel
15+
import org.mifospay.core.common.Parcelable
16+
import org.mifospay.core.common.Parcelize
17+
18+
@Serializable
19+
@Parcelize
20+
data class SavedCard(
21+
val id: Long = 0,
22+
@SerialName("client_id")
23+
val clientId: Long,
24+
val firstName: String,
25+
val lastName: String,
26+
val cardNumber: String,
27+
val cvv: String,
28+
val expiryDate: String,
29+
val backgroundColor: String,
30+
@SerialName("created_at")
31+
val createdAt: List<Long>,
32+
@SerialName("updated_at")
33+
val updatedAt: List<Long>,
34+
) : Parcelable {
35+
@IgnoredOnParcel
36+
val fullName = "$firstName $lastName"
37+
38+
@IgnoredOnParcel
39+
val formattedExpiryDate: String
40+
get() = "${expiryDate.substring(0, 2)}/${expiryDate.substring(2, 4)}"
41+
42+
@IgnoredOnParcel
43+
val maskedCvv: String
44+
get() = "*".repeat(cvv.length)
45+
}

0 commit comments

Comments
 (0)