Skip to content

Commit d6039a0

Browse files
authored
Merge pull request #50 from nativeapptemplate/substrate-v2--shop-name-description-caps
Add client-side length caps + truncation for Shop name/description
2 parents 8d7d2f6 + 64f8b0c commit d6039a0

7 files changed

Lines changed: 327 additions & 10 deletions

File tree

NativeAppTemplate/Constants.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ enum NativeAppTemplateConstants {
7272
static let shadowRadius: CGFloat = 8
7373
}
7474

75+
// MARK: - Shop
76+
77+
static let maximumShopNameLength = 100
78+
static let maximumShopDescriptionLength = 1_000
79+
7580
// MARK: - ItemTag
7681

7782
static let maximumItemTagNameLength = 100
@@ -146,6 +151,17 @@ extension String {
146151
static let addShopDescription = "Add a new shop."
147152
static let deleteShop = "Delete Shop"
148153
static let shopNameIsRequired = "Shop name is required."
154+
static let shopNameIsInvalid = "Shop name is invalid."
155+
static let shopDescriptionIsInvalid = "Shop description is too long."
156+
157+
static func shopNameHelp(maximumLength: Int) -> String {
158+
"Name must be 1–\(maximumLength) characters."
159+
}
160+
161+
static func shopDescriptionHelp(maximumLength: Int) -> String {
162+
"Description can be up to \(maximumLength) characters."
163+
}
164+
149165
static let timeZone = "Time Zone"
150166
static let createShopsLabel = "Create shops"
151167
static let tapShopBelow = "Tap a shop below."

NativeAppTemplate/UI/Shop List/ShopCreateView.swift

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,37 @@ struct ShopCreateView: View {
3636
Form {
3737
Section {
3838
TextField(String.name, text: $viewModel.name)
39+
.onChange(of: viewModel.name) {
40+
viewModel.validateNameLength()
41+
}
42+
} header: {
43+
Text(String.shopName)
3944
} footer: {
40-
Text(String.shopNameIsRequired)
41-
.foregroundStyle(viewModel.hasInvalidData ? .validationError : .clear)
45+
VStack(alignment: .leading) {
46+
Text(String.shopNameHelp(maximumLength: viewModel.maximumNameLength))
47+
.font(.uiFootnote)
48+
Text(String.shopNameIsInvalid)
49+
.font(.uiFootnote)
50+
.foregroundStyle(viewModel.hasInvalidDataName ? .validationError : .clear)
51+
}
4252
}
4353

4454
Section {
4555
TextField(String.descriptionString, text: $viewModel.description, axis: .vertical)
4656
.lineLimit(10, reservesSpace: true)
57+
.onChange(of: viewModel.description) {
58+
viewModel.validateDescriptionLength()
59+
}
60+
} header: {
61+
Text(String.descriptionString)
62+
} footer: {
63+
VStack(alignment: .leading) {
64+
Text(String.shopDescriptionHelp(maximumLength: viewModel.maximumDescriptionLength))
65+
.font(.uiFootnote)
66+
Text(String.shopDescriptionIsInvalid)
67+
.font(.uiFootnote)
68+
.foregroundStyle(viewModel.hasInvalidDataDescription ? .validationError : .clear)
69+
}
4770
}
4871

4972
Section {

NativeAppTemplate/UI/Shop List/ShopCreateViewModel.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,37 @@ final class ShopCreateViewModel {
3131
}
3232

3333
var hasInvalidData: Bool {
34-
Utility.isBlank(name)
34+
hasInvalidDataName || hasInvalidDataDescription
35+
}
36+
37+
var hasInvalidDataName: Bool {
38+
if Utility.isBlank(name) {
39+
return true
40+
}
41+
if name.count > maximumNameLength {
42+
return true
43+
}
44+
return false
45+
}
46+
47+
var hasInvalidDataDescription: Bool {
48+
description.count > maximumDescriptionLength
49+
}
50+
51+
var maximumNameLength: Int {
52+
NativeAppTemplateConstants.maximumShopNameLength
53+
}
54+
55+
var maximumDescriptionLength: Int {
56+
NativeAppTemplateConstants.maximumShopDescriptionLength
57+
}
58+
59+
func validateNameLength() {
60+
name = String(name.prefix(maximumNameLength))
61+
}
62+
63+
func validateDescriptionLength() {
64+
description = String(description.prefix(maximumDescriptionLength))
3565
}
3666

3767
func createShop() {

NativeAppTemplate/UI/Shop Settings/ShopBasicSettingsView.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,37 @@ private extension ShopBasicSettingsView {
4545
Form {
4646
Section {
4747
TextField(String.shopName, text: $viewModel.name)
48+
.onChange(of: viewModel.name) {
49+
viewModel.validateNameLength()
50+
}
4851
} header: {
4952
Text(String.shopName)
5053
} footer: {
51-
Text(String.shopNameIsRequired)
52-
.font(.uiFootnote)
53-
.foregroundStyle(Utility.isBlank(viewModel.name) ? .validationError : .clear)
54+
VStack(alignment: .leading) {
55+
Text(String.shopNameHelp(maximumLength: viewModel.maximumNameLength))
56+
.font(.uiFootnote)
57+
Text(String.shopNameIsInvalid)
58+
.font(.uiFootnote)
59+
.foregroundStyle(viewModel.hasInvalidDataName ? .validationError : .clear)
60+
}
5461
}
5562

5663
Section {
5764
TextField(String.descriptionString, text: $viewModel.description, axis: .vertical)
5865
.lineLimit(10, reservesSpace: true)
66+
.onChange(of: viewModel.description) {
67+
viewModel.validateDescriptionLength()
68+
}
5969
} header: {
6070
Text(String.descriptionString)
71+
} footer: {
72+
VStack(alignment: .leading) {
73+
Text(String.shopDescriptionHelp(maximumLength: viewModel.maximumDescriptionLength))
74+
.font(.uiFootnote)
75+
Text(String.shopDescriptionIsInvalid)
76+
.font(.uiFootnote)
77+
.foregroundStyle(viewModel.hasInvalidDataDescription ? .validationError : .clear)
78+
}
6179
}
6280

6381
Section {

NativeAppTemplate/UI/Shop Settings/ShopBasicSettingsViewModel.swift

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ final class ShopBasicSettingsViewModel {
3939
}
4040

4141
var hasInvalidData: Bool {
42-
if Utility.isBlank(name) {
42+
if hasInvalidDataName {
43+
return true
44+
}
45+
46+
if hasInvalidDataDescription {
4347
return true
4448
}
4549

@@ -54,6 +58,36 @@ final class ShopBasicSettingsViewModel {
5458
return false
5559
}
5660

61+
var hasInvalidDataName: Bool {
62+
if Utility.isBlank(name) {
63+
return true
64+
}
65+
if name.count > maximumNameLength {
66+
return true
67+
}
68+
return false
69+
}
70+
71+
var hasInvalidDataDescription: Bool {
72+
description.count > maximumDescriptionLength
73+
}
74+
75+
var maximumNameLength: Int {
76+
NativeAppTemplateConstants.maximumShopNameLength
77+
}
78+
79+
var maximumDescriptionLength: Int {
80+
NativeAppTemplateConstants.maximumShopDescriptionLength
81+
}
82+
83+
func validateNameLength() {
84+
name = String(name.prefix(maximumNameLength))
85+
}
86+
87+
func validateDescriptionLength() {
88+
description = String(description.prefix(maximumDescriptionLength))
89+
}
90+
5791
func reload() {
5892
Task { @MainActor in
5993
isFetching = true

NativeAppTemplateTests/UI/Shop List/ShopCreateViewModelTest.swift

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,91 @@ struct ShopCreateViewModelTest {
2727
#expect(viewModel.isCreating == false)
2828
}
2929

30-
@Test("Has invalid data", arguments: ["", "Shop Name 1"])
31-
func hasInvalidData(name: String) {
30+
@Test
31+
func maximumNameLength() {
32+
let viewModel = ShopCreateViewModel(
33+
sessionController: sessionController,
34+
shopRepository: shopRepository,
35+
messageBus: messageBus
36+
)
37+
38+
#expect(viewModel.maximumNameLength == 100)
39+
}
40+
41+
@Test
42+
func maximumDescriptionLength() {
43+
let viewModel = ShopCreateViewModel(
44+
sessionController: sessionController,
45+
shopRepository: shopRepository,
46+
messageBus: messageBus
47+
)
48+
49+
#expect(viewModel.maximumDescriptionLength == 1_000)
50+
}
51+
52+
@Test("Name validation", arguments: [
53+
("", true), // blank → invalid
54+
("a", false), // 1 char → valid
55+
("Shop Name 1", false), // normal → valid
56+
(String(repeating: "a", count: 100), false), // exactly 100 → valid
57+
(String(repeating: "a", count: 101), true) // 101 → invalid
58+
])
59+
func nameValidation(name: String, shouldBeInvalid: Bool) {
3260
let viewModel = ShopCreateViewModel(
3361
sessionController: sessionController,
3462
shopRepository: shopRepository,
3563
messageBus: messageBus
3664
)
3765

3866
viewModel.name = name
39-
#expect(viewModel.hasInvalidData == (name == "" ? true : false))
67+
68+
#expect(viewModel.hasInvalidDataName == shouldBeInvalid)
69+
}
70+
71+
@Test("Description validation", arguments: [
72+
("", false), // empty → valid
73+
("Short note.", false), // short → valid
74+
(String(repeating: "x", count: 1000), false), // exactly 1000 → valid
75+
(String(repeating: "x", count: 1001), true) // 1001 → invalid
76+
])
77+
func descriptionValidation(description: String, shouldBeInvalid: Bool) {
78+
let viewModel = ShopCreateViewModel(
79+
sessionController: sessionController,
80+
shopRepository: shopRepository,
81+
messageBus: messageBus
82+
)
83+
84+
viewModel.description = description
85+
86+
#expect(viewModel.hasInvalidDataDescription == shouldBeInvalid)
87+
}
88+
89+
@Test
90+
func validateNameLengthTruncatesCorrectly() {
91+
let viewModel = ShopCreateViewModel(
92+
sessionController: sessionController,
93+
shopRepository: shopRepository,
94+
messageBus: messageBus
95+
)
96+
97+
viewModel.name = String(repeating: "a", count: 100) + "EXTRA"
98+
viewModel.validateNameLength()
99+
100+
#expect(viewModel.name == String(repeating: "a", count: 100))
101+
}
102+
103+
@Test
104+
func validateDescriptionLengthTruncatesCorrectly() {
105+
let viewModel = ShopCreateViewModel(
106+
sessionController: sessionController,
107+
shopRepository: shopRepository,
108+
messageBus: messageBus
109+
)
110+
111+
viewModel.description = String(repeating: "x", count: 1500)
112+
viewModel.validateDescriptionLength()
113+
114+
#expect(viewModel.description.count == 1_000)
40115
}
41116

42117
@Test

0 commit comments

Comments
 (0)