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
22 changes: 20 additions & 2 deletions NativeAppTemplate.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
0110A15F2AC816F5003EDCBA /* SendConfirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0110A15E2AC816F5003EDCBA /* SendConfirmation.swift */; };
0110A1612AC81978003EDCBA /* ResendConfirmationInstructionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0110A1602AC81978003EDCBA /* ResendConfirmationInstructionsView.swift */; };
0114F3AC2E079BD100F4A1DD /* ShopListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0114F3AB2E079BD100F4A1DD /* ShopListViewModel.swift */; };
0114F4032E07A88000F4A1DD /* ShopCreateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0114F4022E07A88000F4A1DD /* ShopCreateViewModel.swift */; };
011586122B567363005E8E8F /* SignUpOrSignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011586112B567363005E8E8F /* SignUpOrSignInView.swift */; };
011DDC21287669EA00C6C21F /* SignUpRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011DDC20287669EA00C6C21F /* SignUpRepository.swift */; };
011DDC2328766C5E00C6C21F /* SignUpService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 011DDC2228766C5D00C6C21F /* SignUpService.swift */; };
Expand Down Expand Up @@ -132,6 +133,9 @@
01B6F5AB2601F84700397E66 /* PermissionsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B6F5AA2601F84700397E66 /* PermissionsRequest.swift */; };
01B9E45228A5070D00CAC681 /* ShopkeeperSignInAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01B9E45128A5070D00CAC681 /* ShopkeeperSignInAdapter.swift */; };
01BE4F1D29CA6F8C002008BE /* TimeZoneData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01BE4F1C29CA6F8C002008BE /* TimeZoneData.swift */; };
01D85A962E07C78400A95798 /* NumberTagsWebpageListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D85A952E07C78400A95798 /* NumberTagsWebpageListViewModel.swift */; };
01D85A9A2E07C85900A95798 /* ShopBasicSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D85A992E07C85900A95798 /* ShopBasicSettingsViewModel.swift */; };
01D85A9E2E07C9BD00A95798 /* ShopSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D85A9D2E07C9BD00A95798 /* ShopSettingsViewModel.swift */; };
01D8AE8B2AB453C1009AFFBA /* ShopBasicSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D8AE8A2AB453C1009AFFBA /* ShopBasicSettingsView.swift */; };
01DCE23F298FA3B300BA311D /* ShopListCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01DCE23E298FA3B300BA311D /* ShopListCardView.swift */; };
01E0A59C25BD088600298D35 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A59125BD087E00298D35 /* SettingsView.swift */; };
Expand Down Expand Up @@ -174,6 +178,7 @@
0110A15E2AC816F5003EDCBA /* SendConfirmation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendConfirmation.swift; sourceTree = "<group>"; };
0110A1602AC81978003EDCBA /* ResendConfirmationInstructionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResendConfirmationInstructionsView.swift; sourceTree = "<group>"; };
0114F3AB2E079BD100F4A1DD /* ShopListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopListViewModel.swift; sourceTree = "<group>"; };
0114F4022E07A88000F4A1DD /* ShopCreateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopCreateViewModel.swift; sourceTree = "<group>"; };
011586112B567363005E8E8F /* SignUpOrSignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpOrSignInView.swift; sourceTree = "<group>"; };
011DDC20287669EA00C6C21F /* SignUpRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpRepository.swift; sourceTree = "<group>"; };
011DDC2228766C5D00C6C21F /* SignUpService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpService.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -288,6 +293,9 @@
01B9E45128A5070D00CAC681 /* ShopkeeperSignInAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopkeeperSignInAdapter.swift; sourceTree = "<group>"; };
01BE4F1C29CA6F8C002008BE /* TimeZoneData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZoneData.swift; sourceTree = "<group>"; };
01D19B432D4DE33500BDEAB7 /* NativeAppTemplateTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NativeAppTemplateTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
01D85A952E07C78400A95798 /* NumberTagsWebpageListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberTagsWebpageListViewModel.swift; sourceTree = "<group>"; };
01D85A992E07C85900A95798 /* ShopBasicSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopBasicSettingsViewModel.swift; sourceTree = "<group>"; };
01D85A9D2E07C9BD00A95798 /* ShopSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopSettingsViewModel.swift; sourceTree = "<group>"; };
01D8AE8A2AB453C1009AFFBA /* ShopBasicSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopBasicSettingsView.swift; sourceTree = "<group>"; };
01DCE23E298FA3B300BA311D /* ShopListCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopListCardView.swift; sourceTree = "<group>"; };
01E0A59125BD087E00298D35 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -428,9 +436,14 @@
01467355299901E50005423D /* Shop Settings */ = {
isa = PBXGroup;
children = (
017278952D7D99D100CE424F /* ItemTag Detail */,
017278992D7D99D100CE424F /* ItemTag List */,
017278912D7D99B900CE424F /* NumberTagsWebpageListView.swift */,
01D85A952E07C78400A95798 /* NumberTagsWebpageListViewModel.swift */,
01D8AE8A2AB453C1009AFFBA /* ShopBasicSettingsView.swift */,
01D85A992E07C85900A95798 /* ShopBasicSettingsViewModel.swift */,
01467356299902230005423D /* ShopSettingsView.swift */,
01D85A9D2E07C9BD00A95798 /* ShopSettingsViewModel.swift */,
);
path = "Shop Settings";
sourceTree = "<group>";
Expand All @@ -439,11 +452,10 @@
isa = PBXGroup;
children = (
013DE734284E99DF00528CC5 /* ShopCreateView.swift */,
0114F4022E07A88000F4A1DD /* ShopCreateViewModel.swift */,
01DCE23E298FA3B300BA311D /* ShopListCardView.swift */,
010F86BD2622F9C900B6C62A /* ShopListView.swift */,
0114F3AB2E079BD100F4A1DD /* ShopListViewModel.swift */,
017278952D7D99D100CE424F /* ItemTag Detail */,
017278992D7D99D100CE424F /* ItemTag List */,
);
path = "Shop List";
sourceTree = "<group>";
Expand Down Expand Up @@ -918,6 +930,8 @@
"",
"",
"",
"",
"",
);
};
/* End PBXShellScriptBuildPhase section */
Expand Down Expand Up @@ -975,6 +989,7 @@
01E0A5B625BD0FCD00298D35 /* LoadingView.swift in Sources */,
0114F3AC2E079BD100F4A1DD /* ShopListViewModel.swift in Sources */,
0172051A25AAF6C0008FD63B /* SessionsService.swift in Sources */,
01D85A962E07C78400A95798 /* NumberTagsWebpageListViewModel.swift in Sources */,
017204D125AA8479008FD63B /* DataState.swift in Sources */,
012643372B3554AD00D4E9BD /* AcceptTermsView.swift in Sources */,
0172033E25A9642E008FD63B /* Parameters.swift in Sources */,
Expand All @@ -998,6 +1013,7 @@
0172788D2D7D936E00CE424F /* CustomerScannedTag.swift in Sources */,
017278902D7D936E00CE424F /* TagView.swift in Sources */,
0106414429AA061100B46FED /* PasswordEditView.swift in Sources */,
0114F4032E07A88000F4A1DD /* ShopCreateViewModel.swift in Sources */,
0172786B2D7D840A00CE424F /* ShowTagInfoScanResult.swift in Sources */,
017204D925AA847E008FD63B /* ShopRepository.swift in Sources */,
017278612D7D83E700CE424F /* ItemTagData.swift in Sources */,
Expand All @@ -1012,9 +1028,11 @@
01E0A63025BD53FD00298D35 /* Shop.swift in Sources */,
017278072D7D4F5800CE424F /* OnboardingRepository.swift in Sources */,
0135E7192D7E33F9004AD8FA /* CompleteScanResultView.swift in Sources */,
01D85A9A2E07C85900A95798 /* ShopBasicSettingsViewModel.swift in Sources */,
0135E71A2D7E33F9004AD8FA /* ShowTagInfoScanResultView.swift in Sources */,
0135E71B2D7E33F9004AD8FA /* ScanView.swift in Sources */,
013292BE262C3EA400690B75 /* LoggedInShopkeeper.swift in Sources */,
01D85A9E2E07C9BD00A95798 /* ShopSettingsViewModel.swift in Sources */,
0172035825A9642E008FD63B /* ShopsService.swift in Sources */,
018E21CD2B36377800FFD1F6 /* MeService.swift in Sources */,
0106414029A9F2EC00B46FED /* AccountPasswordService.swift in Sources */,
Expand Down
3 changes: 2 additions & 1 deletion NativeAppTemplate/UI/App Root/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ private extension MainView {
shopRepository: dataManager.shopRepository,
itemTagRepository: dataManager.itemTagRepository,
tabViewModel: tabViewModel,
mainTab: .shops
mainTab: .shops,
messageBus: messageBus
)
return ShopListView(viewModel: viewModel)
}
Expand Down
10 changes: 7 additions & 3 deletions NativeAppTemplate/UI/Shop Detail/ShopDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,13 @@ private extension ShopDetailView {
ToolbarItem(placement: .navigationBarTrailing) {
NavigationLink(
destination: ShopSettingsView(
shopRepository: shopRepository,
itemTagRepository: itemTagRepository,
shopId: shop.wrappedValue.id
viewModel: ShopSettingsViewModel(
sessionController: sessionController,
shopRepository: shopRepository,
itemTagRepository: itemTagRepository,
messageBus: messageBus,
shopId: shop.wrappedValue.id
)
)
) {
Image(systemName: "gearshape.fill")
Expand Down
99 changes: 25 additions & 74 deletions NativeAppTemplate/UI/Shop List/ShopCreateView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,60 +9,47 @@ import SwiftUI

struct ShopCreateView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.sessionController) private var sessionController
@Environment(MessageBus.self) private var messageBus
private var shopRepository: ShopRepositoryProtocol
@State private var name = ""
@State private var description = ""
@State private var selectedTimeZone: String
@State private var isCreating = false
@State private var viewModel: ShopCreateViewModel

init(
shopRepository: ShopRepositoryProtocol
) {
self.shopRepository = shopRepository
_selectedTimeZone = State(initialValue: Utility.currentTimeZone())
init(viewModel: ShopCreateViewModel) {
self._viewModel = State(wrappedValue: viewModel)
}

private var hasInvalidData: Bool { Utility.isBlank(name) }

var body: some View {
contentView
.onChange(of: viewModel.shouldDismiss) {
if viewModel.shouldDismiss {
dismiss()
}
}
}
}

// MARK: - private
private extension ShopCreateView {
var contentView: some View {

@ViewBuilder var contentView: some View {
if isCreating {
LoadingView()
} else {
shopCreateView
}
@ViewBuilder
private var contentView: some View {
if viewModel.isCreating {
LoadingView()
} else {
shopCreateForm
}

return contentView
}

private var shopCreateView: some View {
private var shopCreateForm: some View {
NavigationStack {
Form {
Section {
TextField(String.name, text: $name)
TextField(String.name, text: $viewModel.name)
} footer: {
Text(String.shopNameIsRequired)
.foregroundStyle(Utility.isBlank(name) ? .red : .clear)
.foregroundStyle(viewModel.hasInvalidData ? .red : .clear)
}

Section {
TextField(String.descriptionString, text: $description, axis: .vertical)
TextField(String.descriptionString, text: $viewModel.description, axis: .vertical)
.lineLimit(10, reservesSpace: true)
}

Section {
Picker(String.timeZone, selection: $selectedTimeZone) {
Picker(String.timeZone, selection: $viewModel.selectedTimeZone) {
ForEach(timeZones.keys, id: \.self) { key in
Text(timeZones[key]!).tag(key)
}
Expand All @@ -72,54 +59,18 @@ private extension ShopCreateView {
.navigationTitle(String.addShop)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
createShop()
} label: {
Text(String.save)
Button(String.save) {
viewModel.createShop()
}
.disabled(hasInvalidData)
.disabled(viewModel.hasInvalidData)
}

ToolbarItem(placement: .navigationBarLeading) {
Button {
Button(String.cancel) {
dismiss()
} label: {
Text(String.cancel)
}
}
}
}
}

func createShop() {
Task { @MainActor in
isCreating = true

do {
let shop = Shop(
id: "",
name: name,
description: description,
timeZone: selectedTimeZone
)
_ = try await shopRepository.create(shop: shop)
messageBus.post(message: Message(level: .success, message: .shopCreated))
} catch {
messageBus.post(
message: Message(
level: .error,
message: error.localizedDescription,
autoDismiss: false
)
)

// e.g. Limit shopps count error
guard case NativeAppTemplateAPIError.requestFailed(_, 422, _) = error else {
try await sessionController.logout()
return
}
}

dismiss()
}
}
}
66 changes: 66 additions & 0 deletions NativeAppTemplate/UI/Shop List/ShopCreateViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// ShopCreateViewModel.swift
// NativeAppTemplate
//
// Created by Claude on 2025/06/22.
//

import Foundation
import Observation
import SwiftUI

@Observable
@MainActor
final class ShopCreateViewModel {
var name: String = ""
var description: String = ""
var selectedTimeZone: String = Utility.currentTimeZone()
var isCreating = false

private let sessionController: SessionControllerProtocol
private let shopRepository: ShopRepositoryProtocol
private(set) var messageBus: MessageBus
var shouldDismiss: Bool = false

init(
sessionController: SessionControllerProtocol,
shopRepository: ShopRepositoryProtocol,
messageBus: MessageBus
) {
self.sessionController = sessionController
self.shopRepository = shopRepository
self.messageBus = messageBus
}

var hasInvalidData: Bool {
Utility.isBlank(name)
}

func createShop() {
Task {
isCreating = true

do {
let shop = Shop(
id: "",
name: name,
description: description,
timeZone: selectedTimeZone
)
_ = try await shopRepository.create(shop: shop)
messageBus.post(message: Message(level: .success, message: .shopCreated))
shouldDismiss = true
} catch {
messageBus.post(message: Message(level: .error, message: error.localizedDescription, autoDismiss: false))

// e.g. Limit shops count error
guard case NativeAppTemplateAPIError.requestFailed(_, 422, _) = error else {
try await sessionController.logout()
return
}

shouldDismiss = true
}
}
}
}
13 changes: 11 additions & 2 deletions NativeAppTemplate/UI/Shop List/ShopListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ struct TapShopBelowTip: Tip {
}

struct ShopListView: View {
@Environment(DataManager.self) private var dataManager
@Environment(MessageBus.self) private var messageBus

@State private var viewModel: ShopListViewModel

init(viewModel: ShopListViewModel) {
self._viewModel = State(wrappedValue: viewModel)
}
Expand Down Expand Up @@ -157,7 +160,13 @@ private extension ShopListView {
onDismiss: {
viewModel.reload()
}, content: {
ShopCreateView(shopRepository: viewModel.shopRepository)
ShopCreateView(
viewModel: ShopCreateViewModel(
sessionController: dataManager.sessionController,
shopRepository: dataManager.shopRepository,
messageBus: messageBus
)
)
}
)
}
Expand Down
5 changes: 4 additions & 1 deletion NativeAppTemplate/UI/Shop List/ShopListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,22 @@ final class ShopListViewModel {
private let sessionController: SessionControllerProtocol
private let tabViewModel: TabViewModel
private let mainTab: MainTab
private let messageBus: MessageBus

init(
sessionController: SessionControllerProtocol,
shopRepository: ShopRepositoryProtocol,
itemTagRepository: ItemTagRepositoryProtocol,
tabViewModel: TabViewModel,
mainTab: MainTab
mainTab: MainTab,
messageBus: MessageBus
) {
self.sessionController = sessionController
self.shopRepository = shopRepository
self.itemTagRepository = itemTagRepository
self.tabViewModel = tabViewModel
self.mainTab = mainTab
self.messageBus = messageBus
}

func reload() {
Expand Down
Loading