Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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()!!

Expand All @@ -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)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@
<string name="shops">Shops</string>
<string name="shop_name">Shop Name</string>
<string name="shop_name_is_required">Shop name is required.</string>
<string name="shop_name_is_invalid">Shop name is invalid.</string>
<string name="shop_description_is_invalid">Shop description is invalid.</string>
<string name="shop_name_help">Name must be 1-%1$d characters.</string>
<string name="shop_description_help">Description must be 0-%1$d characters.</string>
<string name="add_shop_description">Add a new shop.</string>
<string name="tap_shop_below">Tap a shop below</string>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"
Expand Down
Loading