diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/NatConstants.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/NatConstants.kt
index 3a11a1c..4ee6899 100644
--- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/NatConstants.kt
+++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/NatConstants.kt
@@ -9,6 +9,8 @@ object NatConstants {
const val TERMS_OF_USE_URL: String = "https://nativeapptemplate.com/terms"
const val MINIMUM_PASSWORD_LENGTH: Int = 8
+ const val MAXIMUM_SHOP_NAME_LENGTH: Int = 100
+ const val MAXIMUM_SHOP_DESCRIPTION_LENGTH: Int = 1_000
const val MAXIMUM_ITEM_TAG_NAME_LENGTH: Int = 100
const val MAXIMUM_ITEM_TAG_DESCRIPTION_LENGTH: Int = 1_000
diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsView.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsView.kt
index 2907cc8..c29759c 100644
--- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsView.kt
+++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsView.kt
@@ -7,7 +7,7 @@ import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
@@ -154,11 +154,18 @@ fun ShopBasicSettingsContentView(
value = uiState.name,
onValueChange = { viewModel.updateName(it) },
supportingText = {
- Text(
- text = stringResource(id = R.string.shop_name_is_required),
- style = MaterialTheme.typography.bodyLarge,
- color = if (uiState.name.isBlank()) Color.Red else Color.Transparent,
- )
+ Column {
+ Text(
+ text = stringResource(R.string.shop_name_help, uiState.maximumNameLength),
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Text(
+ text = stringResource(R.string.shop_name_is_invalid),
+ style = MaterialTheme.typography.bodyLarge,
+ color = if (viewModel.hasInvalidDataName()) Color.Red else Color.Transparent,
+ )
+ }
},
modifier = Modifier
.fillMaxWidth(),
@@ -172,9 +179,24 @@ fun ShopBasicSettingsContentView(
},
value = uiState.description,
onValueChange = { viewModel.updateDescription(it) },
+ supportingText = {
+ Column {
+ Text(
+ text = stringResource(R.string.shop_description_help, uiState.maximumDescriptionLength),
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Text(
+ text = stringResource(R.string.shop_description_is_invalid),
+ style = MaterialTheme.typography.bodyLarge,
+ color = if (viewModel.hasInvalidDataDescription()) Color.Red else Color.Transparent,
+ )
+ }
+ },
+ minLines = 4,
modifier = Modifier
.fillMaxWidth()
- .height(128.dp),
+ .heightIn(min = 120.dp),
)
ExposedDropdownMenuBox(
diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsViewModel.kt
index 87519bf..2c49468 100644
--- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsViewModel.kt
+++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsViewModel.kt
@@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
+import com.nativeapptemplate.nativeapptemplatefree.NatConstants
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
import com.nativeapptemplate.nativeapptemplatefree.data.shop.ShopRepository
import com.nativeapptemplate.nativeapptemplatefree.model.Shop
@@ -27,6 +28,8 @@ data class ShopBasicSettingsUiState(
val name: String = "",
val description: String = "",
val timeZone: String = TimeZones.DEFAULT_TIME_ZONE,
+ val maximumNameLength: Int = NatConstants.MAXIMUM_SHOP_NAME_LENGTH,
+ val maximumDescriptionLength: Int = NatConstants.MAXIMUM_SHOP_DESCRIPTION_LENGTH,
val isLoading: Boolean = true,
val success: Boolean = false,
@@ -127,7 +130,8 @@ class ShopBasicSettingsViewModel @Inject constructor(
}
fun hasInvalidData(): Boolean {
- if (uiState.value.name.isBlank()) return true
+ if (hasInvalidDataName()) return true
+ if (hasInvalidDataDescription()) return true
val shopData = uiState.value.shop.getData()!!
@@ -136,15 +140,29 @@ class ShopBasicSettingsViewModel @Inject constructor(
shopData.getTimeZone() == uiState.value.timeZone
}
+ fun hasInvalidDataName(): Boolean {
+ val name = uiState.value.name
+ val maximumNameLength = uiState.value.maximumNameLength
+ return name.isBlank() || name.length > maximumNameLength
+ }
+
+ fun hasInvalidDataDescription(): Boolean {
+ return uiState.value.description.length > uiState.value.maximumDescriptionLength
+ }
+
fun updateName(newName: String) {
- _uiState.update {
- it.copy(name = newName)
+ if (newName.length <= uiState.value.maximumNameLength) {
+ _uiState.update {
+ it.copy(name = newName)
+ }
}
}
fun updateDescription(newDescription: String) {
- _uiState.update {
- it.copy(description = newDescription)
+ if (newDescription.length <= uiState.value.maximumDescriptionLength) {
+ _uiState.update {
+ it.copy(description = newDescription)
+ }
}
}
diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateView.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateView.kt
index 7ca88d3..596fd45 100644
--- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateView.kt
+++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateView.kt
@@ -8,7 +8,7 @@ import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
@@ -145,11 +145,18 @@ fun ShopCreateContentView(
value = uiState.name,
onValueChange = { viewModel.updateName(it) },
supportingText = {
- Text(
- text = stringResource(id = R.string.shop_name_is_required),
- style = MaterialTheme.typography.bodyLarge,
- color = if (uiState.name.isBlank()) Color.Red else Color.Transparent,
- )
+ Column {
+ Text(
+ text = stringResource(R.string.shop_name_help, uiState.maximumNameLength),
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Text(
+ text = stringResource(R.string.shop_name_is_invalid),
+ style = MaterialTheme.typography.bodyLarge,
+ color = if (viewModel.hasInvalidDataName()) Color.Red else Color.Transparent,
+ )
+ }
},
modifier = Modifier
.fillMaxWidth(),
@@ -163,9 +170,24 @@ fun ShopCreateContentView(
},
value = uiState.description,
onValueChange = { viewModel.updateDescription(it) },
+ supportingText = {
+ Column {
+ Text(
+ text = stringResource(R.string.shop_description_help, uiState.maximumDescriptionLength),
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ Text(
+ text = stringResource(R.string.shop_description_is_invalid),
+ style = MaterialTheme.typography.bodyLarge,
+ color = if (viewModel.hasInvalidDataDescription()) Color.Red else Color.Transparent,
+ )
+ }
+ },
+ minLines = 4,
modifier = Modifier
.fillMaxWidth()
- .height(128.dp),
+ .heightIn(min = 120.dp),
)
ExposedDropdownMenuBox(
diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateViewModel.kt
index 31cbd69..ddc29bd 100644
--- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateViewModel.kt
+++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateViewModel.kt
@@ -2,6 +2,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.shops
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.nativeapptemplate.nativeapptemplatefree.NatConstants
import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription
import com.nativeapptemplate.nativeapptemplatefree.data.shop.ShopRepository
import com.nativeapptemplate.nativeapptemplatefree.model.Shop
@@ -22,6 +23,8 @@ data class ShopCreateUiState(
val name: String = "",
val description: String = "",
val timeZone: String = TimeZones.currentTimeZoneKey(),
+ val maximumNameLength: Int = NatConstants.MAXIMUM_SHOP_NAME_LENGTH,
+ val maximumDescriptionLength: Int = NatConstants.MAXIMUM_SHOP_DESCRIPTION_LENGTH,
val isLoading: Boolean = false,
val isCreated: Boolean = false,
@@ -72,18 +75,32 @@ class ShopCreateViewModel @Inject constructor(
}
fun hasInvalidData(): Boolean {
- return uiState.value.name.isBlank()
+ return hasInvalidDataName() || hasInvalidDataDescription()
+ }
+
+ fun hasInvalidDataName(): Boolean {
+ val name = uiState.value.name
+ val maximumNameLength = uiState.value.maximumNameLength
+ return name.isBlank() || name.length > maximumNameLength
+ }
+
+ fun hasInvalidDataDescription(): Boolean {
+ return uiState.value.description.length > uiState.value.maximumDescriptionLength
}
fun updateName(newName: String) {
- _uiState.update {
- it.copy(name = newName)
+ if (newName.length <= uiState.value.maximumNameLength) {
+ _uiState.update {
+ it.copy(name = newName)
+ }
}
}
fun updateDescription(newDescription: String) {
- _uiState.update {
- it.copy(description = newDescription)
+ if (newDescription.length <= uiState.value.maximumDescriptionLength) {
+ _uiState.update {
+ it.copy(description = newDescription)
+ }
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f17c929..e956061 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -86,6 +86,10 @@
Shops
Shop Name
Shop name is required.
+ Shop name is invalid.
+ Shop description is invalid.
+ Name must be 1-%1$d characters.
+ Description must be 0-%1$d characters.
Add a new shop.
Tap a shop below
diff --git a/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsViewModelTest.kt b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsViewModelTest.kt
index fd8f5a2..659d07a 100644
--- a/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsViewModelTest.kt
+++ b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsViewModelTest.kt
@@ -2,6 +2,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.shop_settings
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.testing.invoke
+import com.nativeapptemplate.nativeapptemplatefree.NatConstants
import com.nativeapptemplate.nativeapptemplatefree.model.Attributes
import com.nativeapptemplate.nativeapptemplatefree.model.Data
import com.nativeapptemplate.nativeapptemplatefree.model.Shop
@@ -115,6 +116,73 @@ class ShopBasicSettingsViewModelTest {
assertTrue(viewModel.hasInvalidData())
}
+
+ @Test
+ fun maximumNameLength_matchesConstant() = runTest {
+ assertEquals(
+ NatConstants.MAXIMUM_SHOP_NAME_LENGTH,
+ viewModel.uiState.value.maximumNameLength,
+ )
+ }
+
+ @Test
+ fun maximumDescriptionLength_matchesConstant() = runTest {
+ assertEquals(
+ NatConstants.MAXIMUM_SHOP_DESCRIPTION_LENGTH,
+ viewModel.uiState.value.maximumDescriptionLength,
+ )
+ }
+
+ @Test
+ fun nameAtMaximumLength_isValid() = runTest {
+ backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
+
+ shopRepository.sendShop(testInputShop)
+ viewModel.reload()
+
+ viewModel.updateName("a".repeat(100))
+
+ assertFalse(viewModel.hasInvalidDataName())
+ }
+
+ @Test
+ fun nameAboveMaximumLength_isRejectedByUpdater() = runTest {
+ backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
+
+ shopRepository.sendShop(testInputShop)
+ viewModel.reload()
+
+ val previous = viewModel.uiState.value.name
+ viewModel.updateName("a".repeat(101))
+
+ // updater clamps; value should remain unchanged from the loaded shop name
+ assertEquals(previous, viewModel.uiState.value.name)
+ }
+
+ @Test
+ fun descriptionAtMaximumLength_isValid() = runTest {
+ backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
+
+ shopRepository.sendShop(testInputShop)
+ viewModel.reload()
+
+ viewModel.updateDescription("x".repeat(1_000))
+
+ assertFalse(viewModel.hasInvalidDataDescription())
+ }
+
+ @Test
+ fun descriptionAboveMaximumLength_isRejectedByUpdater() = runTest {
+ backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
+
+ shopRepository.sendShop(testInputShop)
+ viewModel.reload()
+
+ val previous = viewModel.uiState.value.description
+ viewModel.updateDescription("x".repeat(1_001))
+
+ assertEquals(previous, viewModel.uiState.value.description)
+ }
}
private const val SHOP_TYPE = "shop"
diff --git a/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateViewModelTest.kt b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateViewModelTest.kt
index bd93e03..07dc796 100644
--- a/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateViewModelTest.kt
+++ b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateViewModelTest.kt
@@ -1,5 +1,6 @@
package com.nativeapptemplate.nativeapptemplatefree.ui.shops
+import com.nativeapptemplate.nativeapptemplatefree.NatConstants
import com.nativeapptemplate.nativeapptemplatefree.model.Attributes
import com.nativeapptemplate.nativeapptemplatefree.model.Data
import com.nativeapptemplate.nativeapptemplatefree.model.Shop
@@ -80,6 +81,56 @@ class ShopCreateViewModelTest {
assertTrue(viewModel.hasInvalidData())
}
+
+ @Test
+ fun maximumNameLength_matchesConstant() = runTest {
+ assertEquals(NatConstants.MAXIMUM_SHOP_NAME_LENGTH, viewModel.uiState.value.maximumNameLength)
+ }
+
+ @Test
+ fun maximumDescriptionLength_matchesConstant() = runTest {
+ assertEquals(
+ NatConstants.MAXIMUM_SHOP_DESCRIPTION_LENGTH,
+ viewModel.uiState.value.maximumDescriptionLength,
+ )
+ }
+
+ @Test
+ fun nameAtMaximumLength_isValid() = runTest {
+ backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
+
+ viewModel.updateName("a".repeat(100))
+
+ assertFalse(viewModel.hasInvalidDataName())
+ }
+
+ @Test
+ fun nameAboveMaximumLength_isRejectedByUpdater() = runTest {
+ backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
+
+ viewModel.updateName("a".repeat(101))
+
+ // updater clamps; value should remain blank (initial)
+ assertEquals("", viewModel.uiState.value.name)
+ }
+
+ @Test
+ fun descriptionAtMaximumLength_isValid() = runTest {
+ backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
+
+ viewModel.updateDescription("x".repeat(1_000))
+
+ assertFalse(viewModel.hasInvalidDataDescription())
+ }
+
+ @Test
+ fun descriptionAboveMaximumLength_isRejectedByUpdater() = runTest {
+ backgroundScope.launch(UnconfinedTestDispatcher()) { viewModel.uiState.collect() }
+
+ viewModel.updateDescription("x".repeat(1_001))
+
+ assertEquals("", viewModel.uiState.value.description)
+ }
}
private const val SHOP_TYPE = "shop"