Skip to content

Commit 3992e6f

Browse files
authored
Fix Client and Merchant registration error (#1870)
1 parent b3c9b3f commit 3992e6f

File tree

4 files changed

+94
-55
lines changed

4 files changed

+94
-55
lines changed

core/common/src/commonMain/kotlin/org/mifospay/core/common/utils/StringExtensions.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,11 @@ fun maskString(input: String, maskChar: Char = '*'): String {
3131
fun String.capitalizeWords(): String = split(" ").joinToString(" ") { it ->
3232
it.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
3333
}
34+
35+
fun String.hasSpaces(): Boolean {
36+
return this.contains(" ")
37+
}
38+
39+
fun String.hasConsecutiveRepetitions(): Boolean {
40+
return Regex("(.)\\1").containsMatchIn(this)
41+
}

core/ui/src/commonMain/kotlin/org/mifospay/core/ui/PasswordStrengthIndicator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ private fun MinimumCharacterCount(
138138
} else {
139139
MaterialTheme.colorScheme.surfaceDim
140140
},
141-
label = "minmumCharacterCountColor",
141+
label = "minimumCharacterCountColor",
142142
)
143143
Row(
144144
modifier = modifier,

core/ui/src/commonMain/kotlin/org/mifospay/core/ui/utils/PasswordChecker.kt

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

12+
import org.mifospay.core.common.utils.hasConsecutiveRepetitions
13+
import org.mifospay.core.common.utils.hasSpaces
1214
import kotlin.math.log2
1315
import kotlin.math.pow
1416

1517
object PasswordChecker {
16-
private const val MIN_PASSWORD_LENGTH = 8
17-
private const val STRONG_PASSWORD_LENGTH = 12
18-
private const val MIN_ENTROPY_BITS = 60.0
19-
private const val MAX_PASSWORD_LENGTH = 128
18+
private const val MIN_PASSWORD_LENGTH = 12
19+
private const val STRONG_PASSWORD_LENGTH = 15
20+
private const val MIN_ENTROPY_BITS = 100.0
21+
private const val MAX_PASSWORD_LENGTH = 50
2022

2123
fun getPasswordStrengthResult(password: String): PasswordStrengthResult {
22-
when {
23-
password.isEmpty() -> return PasswordStrengthResult.Error("Password cannot be empty.")
24-
password.length > MAX_PASSWORD_LENGTH -> {
25-
return PasswordStrengthResult.Error(
26-
"Password is too long. Maximum length is $MAX_PASSWORD_LENGTH characters.",
27-
)
28-
}
24+
val errors = buildList {
25+
if (password.isEmpty()) add("Password cannot be empty.")
26+
if (password.length > MAX_PASSWORD_LENGTH) add("Password is too long. Maximum length is $MAX_PASSWORD_LENGTH characters.")
27+
if (password.hasSpaces()) add("Password must not contain spaces.")
28+
if (password.hasConsecutiveRepetitions()) add("Password must not contain consecutive repetitive characters.")
29+
}
30+
31+
if (errors.isNotEmpty()) {
32+
return PasswordStrengthResult.Error(errors.joinToString("\n"))
2933
}
3034

3135
val result = getPasswordStrength(password)
3236

3337
return PasswordStrengthResult.Success(result)
3438
}
3539

36-
fun getPasswordStrength(password: String): PasswordStrength {
40+
private fun getPasswordStrength(password: String): PasswordStrength {
3741
val length = password.length
3842
val hasUpperCase = password.any { it.isUpperCase() }
3943
val hasLowerCase = password.any { it.isLowerCase() }
@@ -47,10 +51,10 @@ object PasswordChecker {
4751
return when {
4852
length < MIN_PASSWORD_LENGTH -> PasswordStrength.LEVEL_0
4953
numTypesPresent == 1 -> PasswordStrength.LEVEL_1
50-
numTypesPresent == 2 -> PasswordStrength.LEVEL_2
51-
numTypesPresent == 3 && length >= STRONG_PASSWORD_LENGTH -> PasswordStrength.LEVEL_4
54+
numTypesPresent == 2 || numTypesPresent == 3 -> PasswordStrength.LEVEL_2
5255
numTypesPresent == 4 && length >= STRONG_PASSWORD_LENGTH &&
5356
entropyBits >= MIN_ENTROPY_BITS -> PasswordStrength.LEVEL_5
57+
numTypesPresent == 4 && length >= STRONG_PASSWORD_LENGTH -> PasswordStrength.LEVEL_4
5458

5559
else -> PasswordStrength.LEVEL_3
5660
}
@@ -82,6 +86,12 @@ object PasswordChecker {
8286
if (password.length < STRONG_PASSWORD_LENGTH) {
8387
feedback.add("For a stronger password, use at least $STRONG_PASSWORD_LENGTH characters.")
8488
}
89+
if (password.hasConsecutiveRepetitions()) {
90+
feedback.add("Remove consecutive repeating characters.")
91+
}
92+
if (password.hasSpaces()) {
93+
feedback.add("Remove spaces.")
94+
}
8595

8696
return feedback
8797
}

feature/auth/src/commonMain/kotlin/org/mifospay/feature/auth/signup/SignupViewModel.kt

Lines changed: 61 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ package org.mifospay.feature.auth.signup
1212
import androidx.lifecycle.SavedStateHandle
1313
import androidx.lifecycle.viewModelScope
1414
import kotlinx.coroutines.Job
15+
import kotlinx.coroutines.async
16+
import kotlinx.coroutines.awaitAll
1517
import kotlinx.coroutines.flow.update
1618
import kotlinx.coroutines.launch
1719
import org.mifospay.core.common.DataState
@@ -34,7 +36,8 @@ import org.mifospay.core.ui.utils.PasswordStrengthResult
3436
import org.mifospay.feature.auth.signup.SignUpAction.Internal.ReceivePasswordStrengthResult
3537

3638
private const val KEY_STATE = "signup_state"
37-
private const val MIN_PASSWORD_LENGTH = 8
39+
private const val MIN_PASSWORD_LENGTH = 12
40+
private const val MAX_PASSWORD_LENGTH = 50
3841

3942
class SignupViewModel(
4043
private val userRepository: UserRepository,
@@ -172,7 +175,7 @@ class SignupViewModel(
172175

173176
private fun handlePasswordInput(action: SignUpAction.PasswordInputChange) {
174177
// Update input:
175-
mutableStateFlow.update { it.copy(passwordInput = action.password) }
178+
mutableStateFlow.update { it.copy(passwordInput = action.password, passwordError = null) }
176179
// Update password strength:
177180
passwordStrengthJob.cancel()
178181
if (action.password.isEmpty()) {
@@ -203,7 +206,11 @@ class SignupViewModel(
203206
}
204207
}
205208

206-
is PasswordStrengthResult.Error -> {}
209+
is PasswordStrengthResult.Error -> {
210+
mutableStateFlow.update {
211+
it.copy(passwordError = result.message.toString(), passwordStrengthState = PasswordStrengthState.NONE)
212+
}
213+
}
207214
}
208215
}
209216

@@ -288,15 +295,31 @@ class SignupViewModel(
288295
}
289296
}
290297

298+
state.passwordInput.length > MAX_PASSWORD_LENGTH -> {
299+
mutableStateFlow.update {
300+
it.copy(
301+
dialogState = SignUpDialog.Error(
302+
"Password must be less than $MAX_PASSWORD_LENGTH characters long.",
303+
),
304+
)
305+
}
306+
}
307+
291308
!state.isPasswordMatch -> {
292309
mutableStateFlow.update {
293310
it.copy(dialogState = SignUpDialog.Error("Passwords do not match."))
294311
}
295312
}
296313

297314
!state.isPasswordStrong -> {
315+
val errorMessage = state.passwordError?.takeIf { it.isNotBlank() }
316+
?: "Please ensure password contains :" +
317+
"\n- At least one uppercase character" +
318+
"\n- At least one lowercase character" +
319+
"\n- At least one numeric digit" +
320+
"\n- At least one special character"
298321
mutableStateFlow.update {
299-
it.copy(dialogState = SignUpDialog.Error("Password is weak."))
322+
it.copy(dialogState = SignUpDialog.Error(errorMessage.lines().joinToString("\n") { "- $it" }))
300323
}
301324
}
302325

@@ -347,50 +370,47 @@ class SignupViewModel(
347370
it.copy(dialogState = SignUpDialog.Loading)
348371
}
349372

350-
// 0. Unique Mobile Number (checked in MOBILE VERIFICATION ACTIVITY)
351-
// 1. Check for unique external id and username
352-
// 2. Create user
353-
// 3. Create Client
354-
// 4. Update User and connect client with user
355-
checkForUsernameExists(state.userNameInput)
373+
val fieldsToCheck = mapOf(
374+
"Username" to state.userNameInput,
375+
"Mobile Number" to state.mobileNumberInput,
376+
)
377+
checkUniqueFields(fieldsToCheck)
356378
}
357379

358-
private fun checkForUsernameExists(username: String) {
380+
private fun checkUniqueFields(fields: Map<String, String>) {
359381
viewModelScope.launch {
360-
val result = searchRepository.searchResources(
361-
username,
362-
Constants.CLIENTS,
363-
false,
364-
)
365-
366-
when (result) {
367-
is DataState.Error -> {
368-
val message = result.exception.message.toString()
369-
mutableStateFlow.update {
370-
it.copy(dialogState = SignUpDialog.Error(message))
371-
}
382+
val results = fields.map { (label, value) ->
383+
async {
384+
val result = searchRepository.searchResources(value, Constants.CLIENTS, false)
385+
label to result
372386
}
387+
}.awaitAll()
373388

374-
is DataState.Success -> {
375-
if (result.data.isEmpty()) {
376-
// Username is unique
377-
val newUser = NewUser(
378-
state.userNameInput,
379-
state.firstNameInput,
380-
state.lastNameInput,
381-
state.emailInput,
382-
state.passwordInput,
383-
)
384-
385-
createUser(newUser)
386-
} else {
387-
mutableStateFlow.update {
388-
it.copy(dialogState = SignUpDialog.Error("Username already exists."))
389-
}
389+
val errorMessages = results.mapNotNull { (label, result) ->
390+
when (result) {
391+
is DataState.Success -> {
392+
if (result.data.isNotEmpty()) "$label already exists." else null
390393
}
394+
is DataState.Error ->
395+
result.exception.message
396+
?: "Error checking $label."
397+
else -> null
391398
}
399+
}
392400

393-
is DataState.Loading -> Unit
401+
if (errorMessages.isNotEmpty()) {
402+
mutableStateFlow.update {
403+
it.copy(dialogState = SignUpDialog.Error(errorMessages.joinToString("\n")))
404+
}
405+
} else {
406+
val newUser = NewUser(
407+
state.userNameInput,
408+
state.firstNameInput,
409+
state.lastNameInput,
410+
state.emailInput,
411+
state.passwordInput,
412+
)
413+
createUser(newUser)
394414
}
395415
}
396416
}
@@ -509,6 +529,7 @@ data class SignUpState(
509529
val businessNameInput: String = "",
510530
val dialogState: SignUpDialog? = null,
511531
val passwordStrengthState: PasswordStrengthState = PasswordStrengthState.NONE,
532+
val passwordError: String? = null,
512533
) : Parcelable {
513534
@IgnoredOnParcel
514535
val isPasswordStrong: Boolean

0 commit comments

Comments
 (0)