Skip to content

Commit 404f06f

Browse files
Merge branch 'master' into issue1157
2 parents 5e90e51 + 81d94d5 commit 404f06f

File tree

7 files changed

+264
-10
lines changed

7 files changed

+264
-10
lines changed

app/src/main/java/org/greenstand/android/TreeTracker/profile/ProfileScreen.kt

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ import org.greenstand.android.TreeTracker.view.LocalImage
5959
import org.greenstand.android.TreeTracker.view.ProfileField
6060
import org.greenstand.android.TreeTracker.view.TreeTrackerButton
6161
import java.io.File
62+
import org.greenstand.android.TreeTracker.utils.ValidationUtils
63+
import androidx.compose.material.MaterialTheme
64+
import androidx.compose.runtime.mutableStateOf
65+
import androidx.compose.runtime.remember
66+
import androidx.compose.runtime.setValue
67+
import androidx.compose.runtime.getValue
6268

6369
@Composable
6470
fun ProfileScreen(
@@ -79,6 +85,21 @@ fun ProfileScreen(
7985
}
8086
}
8187
}
88+
var firstNameError by remember { mutableStateOf<String?>(null) }
89+
var lastNameError by remember { mutableStateOf<String?>(null) }
90+
91+
fun validateNames(): Boolean {
92+
val firstName = state.selectedUser?.firstName ?: ""
93+
val lastName = state.selectedUser?.lastName ?: ""
94+
95+
val (firstNameValid, firstError) = ValidationUtils.validateName(firstName)
96+
val (lastNameValid, lastError) = ValidationUtils.validateName(lastName)
97+
98+
firstNameError = firstError
99+
lastNameError = lastError
100+
101+
return firstNameValid && lastNameValid
102+
}
82103
Scaffold(
83104
topBar = {
84105
ActionBar(
@@ -155,11 +176,45 @@ fun ProfileScreen(
155176
Spacer(modifier = Modifier.height(16.dp))
156177

157178
ProfileField(stringResource(id = R.string.first_name_hint), selectedUser.firstName, state.editMode) {
158-
viewModel.updateSelectedUser(firstName = it)
179+
newFirstName ->
180+
181+
val filtered = ValidationUtils.filterNameInput(newFirstName)
182+
viewModel.updateSelectedUser(firstName = filtered)
183+
if (state.editMode) {
184+
val (_, error) = ValidationUtils.validateName(filtered)
185+
firstNameError = error
186+
}
187+
}
188+
189+
if (state.editMode && firstNameError != null) {
190+
Text(
191+
text = firstNameError!!,
192+
color = MaterialTheme.colors.error,
193+
style = CustomTheme.typography.small,
194+
modifier = Modifier.padding(start = 8.dp, top = 4.dp)
195+
)
159196
}
197+
Spacer(modifier = Modifier.height(8.dp))
160198

161-
ProfileField(stringResource(id = R.string.last_name_hint), selectedUser.lastName ?: "", state.editMode) {
162-
viewModel.updateSelectedUser(lastName = it)
199+
ProfileField(
200+
stringResource(id = R.string.last_name_hint), selectedUser.lastName ?: "", state.editMode) {
201+
newLastName ->
202+
val filtered = ValidationUtils.filterNameInput(newLastName)
203+
viewModel.updateSelectedUser(lastName = filtered)
204+
205+
if (state.editMode) {
206+
val (_, error) = ValidationUtils.validateName(filtered)
207+
lastNameError = error
208+
}
209+
}
210+
211+
if (state.editMode && lastNameError != null) {
212+
Text(
213+
text = lastNameError!!,
214+
color = MaterialTheme.colors.error,
215+
style = CustomTheme.typography.small,
216+
modifier = Modifier.padding(start = 8.dp, top = 4.dp)
217+
)
163218
}
164219
if (selectedUser.wallet.contains("@")) {
165220
ProfileField(stringResource(id = R.string.email_placeholder), selectedUser.wallet ?: "", state.editMode) {
@@ -180,10 +235,16 @@ fun ProfileScreen(
180235
.align(Alignment.CenterHorizontally)
181236
.size(width = 150.dp, 60.dp),
182237
onClick = {
238+
// 验证名字
239+
val firstName = state.selectedUser?.firstName ?: ""
240+
val lastName = state.selectedUser?.lastName ?: ""
183241

184-
scope.launch {
185-
viewModel.updateUserInDatabase()
186-
viewModel.updateEditEnabled()
242+
if (ValidationUtils.validateName(firstName).first &&
243+
ValidationUtils.validateName(lastName).first) {
244+
scope.launch {
245+
viewModel.updateUserInDatabase()
246+
viewModel.updateEditEnabled()
247+
}
187248
}
188249
}
189250
) {

app/src/main/java/org/greenstand/android/TreeTracker/signup/NameEntryView.kt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ import org.greenstand.android.TreeTracker.view.ArrowButton
4949
import org.greenstand.android.TreeTracker.view.BorderedTextField
5050
import org.greenstand.android.TreeTracker.view.LanguageButton
5151
import org.greenstand.android.TreeTracker.view.TopBarTitle
52+
import org.greenstand.android.TreeTracker.utils.ValidationUtils
53+
import androidx.compose.material.MaterialTheme
54+
import androidx.compose.material.Text
55+
import androidx.compose.foundation.layout.padding
56+
import androidx.compose.ui.unit.dp
57+
import androidx.compose.foundation.layout.Spacer
58+
import androidx.compose.foundation.layout.height
5259

5360
@Composable
5461
fun NameEntryView(viewModel: SignupViewModel, state: SignUpState) {
@@ -95,8 +102,7 @@ fun NameEntryView(viewModel: SignupViewModel, state: SignUpState) {
95102
rightAction = {
96103
ArrowButton(
97104
isLeft = false,
98-
isEnabled = !state.firstName.isNullOrBlank() &&
99-
!state.lastName.isNullOrBlank()
105+
isEnabled = viewModel.isFormValid()
100106
) {
101107
cameraLauncher.launch(true)
102108
}
@@ -128,6 +134,15 @@ fun NameEntryView(viewModel: SignupViewModel, state: SignUpState) {
128134
}
129135
)
130136
)
137+
state.firstNameError?.let { error ->
138+
Text(
139+
text = error,
140+
color = MaterialTheme.colors.error,
141+
style = MaterialTheme.typography.caption,
142+
modifier = Modifier.padding(start = 8.dp, top = 4.dp)
143+
)
144+
}
145+
Spacer(modifier = Modifier.height(8.dp))
131146
BorderedTextField(
132147
value = state.lastName ?: "",
133148
padding = PaddingValues(4.dp),
@@ -146,6 +161,14 @@ fun NameEntryView(viewModel: SignupViewModel, state: SignUpState) {
146161
}
147162
)
148163
)
164+
state.lastNameError?.let { error ->
165+
Text(
166+
text = error,
167+
color = MaterialTheme.colors.error,
168+
style = MaterialTheme.typography.caption,
169+
modifier = Modifier.padding(start = 8.dp, top = 4.dp)
170+
)
171+
}
149172
}
150173
}
151174
}

app/src/main/java/org/greenstand/android/TreeTracker/signup/SignupViewModel.kt

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import org.greenstand.android.TreeTracker.models.UserRepo
2525
import org.greenstand.android.TreeTracker.models.user.User
2626
import org.greenstand.android.TreeTracker.usecases.CheckForInternetUseCase
2727
import org.greenstand.android.TreeTracker.utilities.Validation
28+
import org.greenstand.android.TreeTracker.utils.ValidationUtils
2829

2930
// Dequeue breaks equals so state will not be updated when navigating
3031
data class SignUpState(
@@ -43,6 +44,8 @@ data class SignUpState(
4344
val isTherePowerUser: Boolean? = null,
4445
val showSelfieTutorial: Boolean? = null,
4546
val showPrivacyDialog: Boolean? = true,
47+
val firstNameError: String? = null,
48+
val lastNameError: String? = null,
4649
)
4750

4851
sealed class Credential {
@@ -90,11 +93,27 @@ class SignupViewModel(
9093
}
9194

9295
fun updateFirstName(firstName: String?) {
93-
_state.value = _state.value?.copy(firstName = firstName)
96+
val filtered = firstName?.let { ValidationUtils.filterNameInput(it) }
97+
val (isValid, error) = ValidationUtils.validateName(filtered)
98+
_state.value = _state.value?.copy(
99+
firstName = filtered,
100+
firstNameError = if (filtered.isNullOrEmpty()) null else error
101+
)
94102
}
95103

96104
fun updateLastName(lastName: String?) {
97-
_state.value = _state.value?.copy(lastName = lastName)
105+
val filtered = lastName?.let { ValidationUtils.filterNameInput(it) }
106+
val (isValid, error) = ValidationUtils.validateName(filtered)
107+
_state.value = _state.value?.copy(
108+
lastName = filtered,
109+
lastNameError = if (filtered.isNullOrEmpty()) null else error
110+
)
111+
}
112+
fun isFormValid(): Boolean {
113+
val state = _state.value ?: return false
114+
val (firstNameValid, _) = ValidationUtils.validateName(state.firstName)
115+
val (lastNameValid, _) = ValidationUtils.validateName(state.lastName)
116+
return firstNameValid && lastNameValid
98117
}
99118

100119
fun setExistingUserAsPowerUser(id: Long){
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package org.greenstand.android.TreeTracker.utils
2+
3+
/**
4+
* Utility class for validating user input fields
5+
*/
6+
object ValidationUtils {
7+
8+
private val NAME_PATTERN = """^[\p{L}\s'-]+$""".toRegex()
9+
10+
private val INVALID_CHARS_PATTERN = """[0-9!@#$%^&*()_+=\[\]{};:"|<>?,./\\~`]""".toRegex()
11+
12+
/**
13+
* Validates a name field (first name or last name)
14+
* @param name The name to validate
15+
* @return Pair of (isValid, errorMessage)
16+
*/
17+
fun validateName(name: String?): Pair<Boolean, String?> {
18+
return when {
19+
name.isNullOrBlank() -> {
20+
false to "Name cannot be empty"
21+
}
22+
name.trim().length < 2 -> {
23+
false to "Name must be at least 2 characters"
24+
}
25+
name.length > 50 -> {
26+
false to "Name cannot exceed 50 characters"
27+
}
28+
INVALID_CHARS_PATTERN.containsMatchIn(name) -> {
29+
false to "Name cannot contain numbers or special characters"
30+
}
31+
!NAME_PATTERN.matches(name.trim()) -> {
32+
false to "Name can only contain letters, spaces, hyphens and apostrophes"
33+
}
34+
else -> {
35+
true to null
36+
}
37+
}
38+
}
39+
40+
/**
41+
* Filters out invalid characters from input
42+
*/
43+
fun filterNameInput(input: String): String {
44+
return input.filter { char ->
45+
char.isLetter() || char.isWhitespace() || char == '-' || char == '\''
46+
}.take(50) // Also enforce max length
47+
}
48+
}

app/src/main/res/values-sw/strings.xml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,42 @@
168168
<string name="no_gps_device_header">Hakuna GPS kwenye kifaa hiki</string>
169169
<string name="no_gps_device_content">Kifaa hiki hakina uwezo wa kuwa na GPS. Tafadhali tumia kifaa chenye GPS ili kuendelea.</string>
170170
<string name="tree_capture_review_text">bonyeza kitufe ili kuongeza taarifa</string>
171+
172+
173+
<string name="settings">Mipangilio</string>
174+
<string name="profile_title">Tazama / Hariri Wasifu</string>
175+
<string name="cancel_edit">Ghairi Hariri</string>
176+
<string name="edit_profile">Hariri Wasifu</string>
177+
<string name="save_changes">Hifadhi Mabadiliko</string>
178+
<string name="loading_user_profile">Inapakia wasifu wa mtumiaji.....</string>
179+
<string name="profile_description">Bofya hapa kutazama au kuhariri maelezo ya wasifu wako</string>
180+
<string name="privacy_title">Tazama sera ya faragha.</string>
181+
<string name="privacy_description">Bofya hapa kutazama sera ya faragha ya Greenstand kuhusu taarifa zako.</string>
182+
<string name="logout_title">Toka kwenye akaunti</string>
183+
<string name="logout_description">Toka kwenye akaunti yako</string>
184+
<string name="delete_account_title">Futa akaunti</string>
185+
<string name="account_deleted">Akaunti imefutwa</string>
186+
<string name="account_deleted_message">Taarifa zako zote zimefutwa</string>
187+
<string name="delete_account_description">Anza mchakato wa kufuta akaunti yako kabisa.</string>
188+
<string name="logout_dialog_title">Una uhakika unataka kutoka kwenye akaunto yako?</string>
189+
<string name="delete_account_dialog_title">Una uhakika unataka kufuta akauti hii?</string>
190+
<string name="delete_account_dialog_message">Una uhakika unataka kufuta akaunti yako? Ukifanya hivi, hutaweza kurudisha tena na data zako zote zitapotea. Hakikisha umeunganishwa na intaneti ili kukamilisha hatua hii.</string>
191+
<string name="logout_dialog_message">Una uhakika unataka kutoka kwenya akaunti yako?</string>
192+
<string name="announcement">Tangazo</string>
193+
<string name="click_to_write_message">Bofya kuandika ujumbe.</string>
194+
<string name="no_internet_header">Hamna intaneti.</string>
195+
<string name="no_internet_content">Tafadhali ungana na WiFi au washa data ya simu ili kufungua kiungo hiki.</string>
196+
<string name="tracking_progress_header">Ufuatiliaji unaendelea.</string>
197+
<string name="tracking_progress_message">Usisongeshe simu mpaka ufuatiliaji ukamilike.</string>
198+
<string name="privacy_policy">Sera ya faragha</string>
199+
<string name="select_tree_height_colour">Chagua rangi ya urefu wa mti.</string>
200+
<string name="select_organization">Chagua shirika.</string>
201+
<string name="no_messages_yet">Bado hakuna ujumbe.</string>
202+
<string name="survey_completed">Utafuti umekamilika.</string>
203+
<string name="upload_trees_soon_title">Pakia miti hivi karibuni.</string>
204+
<string name="upload_trees_text_content">Una zaidi ya miti 2000, tafadhali pakia haraka iwezekanavyo.</string>
205+
<string name="add_note_to_session">Ongeza kumbukumbu kwenye kikao.</string>
206+
171207
</resources>
172208

173209

app/src/main/res/values/strings.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,10 @@ Privacy Policy
204204
<string name="survey_completed">Survey Completed</string>
205205
<string name="upload_trees_soon_title">Upload Trees Soon</string>
206206
<string name="upload_trees_text_content">You have over 2000 trees, please upload as soon as you can.</string>
207+
208+
<string name="error_name_empty">Name cannot be empty</string>
209+
<string name="error_name_too_short">Name must be at least 2 characters</string>
210+
<string name="error_name_too_long">Name cannot exceed 50 characters</string>
211+
<string name="error_name_invalid">Name cannot contain numbers or special characters</string>
212+
<string name="error_name_format">Name can only contain letters, spaces, hyphens and apostrophes</string>
207213
</resources>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package org.greenstand.android.TreeTracker.utils
2+
3+
import org.junit.Assert.*
4+
import org.junit.Test
5+
6+
class ValidationUtilsTest {
7+
8+
@Test
9+
fun `valid names should pass validation`() {
10+
val validNames = listOf(
11+
"John",
12+
"Mary-Jane",
13+
"O'Connor",
14+
"José María",
15+
"李明",
16+
"François"
17+
)
18+
19+
validNames.forEach { name ->
20+
val (isValid, error) = ValidationUtils.validateName(name)
21+
assertTrue("$name should be valid", isValid)
22+
assertNull("$name should have no error", error)
23+
}
24+
}
25+
26+
@Test
27+
fun `names with numbers should fail`() {
28+
val invalidNames = listOf("John123", "4ever", "Test1")
29+
30+
invalidNames.forEach { name ->
31+
val (isValid, error) = ValidationUtils.validateName(name)
32+
assertFalse("$name should be invalid", isValid)
33+
assertEquals("Name cannot contain numbers or special characters", error)
34+
}
35+
}
36+
37+
@Test
38+
fun `names with special characters should fail`() {
39+
val invalidNames = listOf("John@Doe", "Test!", "Name#1")
40+
41+
invalidNames.forEach { name ->
42+
val (isValid, error) = ValidationUtils.validateName(name)
43+
assertFalse("$name should be invalid", isValid)
44+
assertNotNull(error)
45+
}
46+
}
47+
48+
@Test
49+
fun `empty name should fail`() {
50+
val (isValid, error) = ValidationUtils.validateName("")
51+
assertFalse(isValid)
52+
assertEquals("Name cannot be empty", error)
53+
}
54+
55+
@Test
56+
fun `filterNameInput removes invalid characters`() {
57+
assertEquals("John", ValidationUtils.filterNameInput("John123"))
58+
assertEquals("MaryJane", ValidationUtils.filterNameInput("Mary@Jane"))
59+
assertEquals("Test", ValidationUtils.filterNameInput("Test!@#"))
60+
}
61+
}

0 commit comments

Comments
 (0)