From ce9a0116b2233a786002f07b6fb4ffa756da35f5 Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Sun, 23 Jun 2024 16:27:19 +0200 Subject: [PATCH 01/43] SharedUI and WRCore package - Basic SharedUI Design system - WRCore package for shared utils and core setup --- Modules/SharedUI/.gitignore | 8 ++ Modules/SharedUI/Package.swift | 28 ++++++ .../SharedUI/Sources/SharedUI/Colors.swift | 21 ++++ Modules/SharedUI/Sources/SharedUI/Fonts.swift | 84 ++++++++++++++++ .../SharedUI/Sources/SharedUI/Images.swift | 11 +++ .../Resources/Colors.xcassets/Contents.json | 0 .../accent.colorset}/Contents.json | 6 +- .../blackWhite.colorset/Contents.json | 0 .../main.colorset/Contents.json | 38 ++++++++ .../whiteBlack.colorset/Contents.json | 38 ++++++++ .../Resources/Images.xcassets/Contents.json | 0 .../LogoRounded.imageset/Contents.json | 0 .../LogoRounded.imageset/wr-round-full.png | Bin .../logoLaunch.imageset}/Contents.json | 0 .../logoLaunch.imageset}/LaunchImage.pdf | Bin .../Sources/SharedUI/SharedUIExample.swift | 20 ++++ .../Tests/SharedUITests/SharedUITests.swift | 12 +++ Modules/WRCore/.gitignore | 8 ++ Modules/WRCore/Package.swift | 33 +++++++ Modules/WRCore/Sources/WRCore/Globals.swift | 44 +++++++++ .../WRCore}/KermitLoggerExtension.swift | 2 +- .../Sources/WRCore/KotlinArrayWrapper.swift | 12 +++ Modules/WRCore/Sources/WRCore/Mock.swift | 40 ++++++++ .../WRCore/Sources/WRCore}/Navigation.swift | 4 +- .../Sources/WRCore/ObservableViewModel.swift | 91 ++++++++++++++++++ .../WRCore/Sources/WRCore}/PreviewView.swift | 8 +- .../Tests/WRCoreTests/WRCoreTests.swift | 12 +++ WaiterRobot/Core/Globals.swift | 6 -- WaiterRobot/Core/Mvi/KotlinArrayWrapper.swift | 54 ----------- .../Core/Mvi/ObservableViewModel.swift | 91 ------------------ WaiterRobot/LaunchScreen.swift | 7 +- WaiterRobot/MainView.swift | 1 + .../second.colorset/Contents.json | 20 ---- WaiterRobot/Ui/Billing/BillingScreen.swift | 1 + WaiterRobot/Ui/Billing/PayDialog.swift | 1 + WaiterRobot/Ui/Core/ButtonStyles.swift | 5 +- WaiterRobot/Ui/Login/LoginScannerScreen.swift | 1 + WaiterRobot/Ui/Login/LoginScreen.swift | 4 +- WaiterRobot/Ui/Login/RegisterScreen.swift | 1 + WaiterRobot/Ui/Order/OrderListItem.swift | 1 + .../Ui/Order/OrderProductNoteView.swift | 1 + WaiterRobot/Ui/Order/OrderScreen.swift | 3 +- .../Ui/Order/Search/ProductSearch.swift | 1 + WaiterRobot/Ui/Settings/SettingsScreen.swift | 1 + WaiterRobot/Ui/Settings/SwitchThemeView.swift | 1 + .../Ui/SwitchEvent/SwitchEventScreen.swift | 1 + .../Ui/TableDetail/TableDetailScreen.swift | 1 + .../Ui/TableList/TableGroupSection.swift | 10 +- .../Ui/TableList/TableListScreen.swift | 58 +++++++++-- WaiterRobot/Ui/TableList/TableView.swift | 3 +- .../Ui/UpdateApp/UpdateAppScreen.swift | 1 + WaiterRobot/WaiterRobotApp.swift | 38 +------- project.yml | 13 ++- 53 files changed, 605 insertions(+), 240 deletions(-) create mode 100644 Modules/SharedUI/.gitignore create mode 100644 Modules/SharedUI/Package.swift create mode 100644 Modules/SharedUI/Sources/SharedUI/Colors.swift create mode 100644 Modules/SharedUI/Sources/SharedUI/Fonts.swift create mode 100644 Modules/SharedUI/Sources/SharedUI/Images.swift rename {WaiterRobot => Modules/SharedUI/Sources/SharedUI}/Resources/Colors.xcassets/Contents.json (100%) rename {WaiterRobot/Resources/Colors.xcassets/main.colorset => Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/accent.colorset}/Contents.json (74%) rename {WaiterRobot => Modules/SharedUI/Sources/SharedUI}/Resources/Colors.xcassets/blackWhite.colorset/Contents.json (100%) create mode 100644 Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/main.colorset/Contents.json create mode 100644 Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/whiteBlack.colorset/Contents.json rename {WaiterRobot => Modules/SharedUI/Sources/SharedUI}/Resources/Images.xcassets/Contents.json (100%) rename {WaiterRobot => Modules/SharedUI/Sources/SharedUI}/Resources/Images.xcassets/LogoRounded.imageset/Contents.json (100%) rename {WaiterRobot => Modules/SharedUI/Sources/SharedUI}/Resources/Images.xcassets/LogoRounded.imageset/wr-round-full.png (100%) rename {WaiterRobot/Resources/Images.xcassets/LaunchImage.imageset => Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/logoLaunch.imageset}/Contents.json (100%) rename {WaiterRobot/Resources/Images.xcassets/LaunchImage.imageset => Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/logoLaunch.imageset}/LaunchImage.pdf (100%) create mode 100644 Modules/SharedUI/Sources/SharedUI/SharedUIExample.swift create mode 100644 Modules/SharedUI/Tests/SharedUITests/SharedUITests.swift create mode 100644 Modules/WRCore/.gitignore create mode 100644 Modules/WRCore/Package.swift create mode 100644 Modules/WRCore/Sources/WRCore/Globals.swift rename {WaiterRobot/Core => Modules/WRCore/Sources/WRCore}/KermitLoggerExtension.swift (91%) create mode 100644 Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift create mode 100644 Modules/WRCore/Sources/WRCore/Mock.swift rename {WaiterRobot/Ui/Core => Modules/WRCore/Sources/WRCore}/Navigation.swift (98%) create mode 100644 Modules/WRCore/Sources/WRCore/ObservableViewModel.swift rename {WaiterRobot/Core => Modules/WRCore/Sources/WRCore}/PreviewView.swift (78%) create mode 100644 Modules/WRCore/Tests/WRCoreTests/WRCoreTests.swift delete mode 100644 WaiterRobot/Core/Globals.swift delete mode 100644 WaiterRobot/Core/Mvi/KotlinArrayWrapper.swift delete mode 100644 WaiterRobot/Core/Mvi/ObservableViewModel.swift delete mode 100644 WaiterRobot/Resources/Colors.xcassets/second.colorset/Contents.json diff --git a/Modules/SharedUI/.gitignore b/Modules/SharedUI/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/Modules/SharedUI/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Modules/SharedUI/Package.swift b/Modules/SharedUI/Package.swift new file mode 100644 index 0000000..f92345f --- /dev/null +++ b/Modules/SharedUI/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.10 + +import PackageDescription + +let package = Package( + name: "SharedUI", + platforms: [ + .iOS(.v15), + ], + products: [ + .library( + name: "SharedUI", + targets: ["SharedUI"] + ), + ], + targets: [ + .target( + name: "SharedUI", + resources: [ + .process("Resources"), + ] + ), + .testTarget( + name: "SharedUITests", + dependencies: ["SharedUI"] + ), + ] +) diff --git a/Modules/SharedUI/Sources/SharedUI/Colors.swift b/Modules/SharedUI/Sources/SharedUI/Colors.swift new file mode 100644 index 0000000..2c806f7 --- /dev/null +++ b/Modules/SharedUI/Sources/SharedUI/Colors.swift @@ -0,0 +1,21 @@ +import SwiftUI + +public extension Color { + static var main: Color { + Color(.main) + } + + static var accent: Color { + Color(.accent) + } + + /// black in light mode, white in dark mode + static var blackWhite: Color { + Color(.blackWhite) + } + + /// white in light mode, black in dark mode + static var whiteBlack: Color { + Color(.whiteBlack) + } +} diff --git a/Modules/SharedUI/Sources/SharedUI/Fonts.swift b/Modules/SharedUI/Sources/SharedUI/Fonts.swift new file mode 100644 index 0000000..62f23d4 --- /dev/null +++ b/Modules/SharedUI/Sources/SharedUI/Fonts.swift @@ -0,0 +1,84 @@ +import SwiftUI + +public enum WrFont { + case h1 + case h2 + case h3 + case h4 + case body + case caption1 + case caption2 + + var baseSize: CGFloat { + switch self { + case .h1: + 56 + case .h2: + 48 + case .h3: + 32 + case .h4: + 24 + case .body: + 16 + case .caption1: + 14 + case .caption2: + 12 + } + } +} + +private struct WrTextStyle: ViewModifier { + let fontStyle: WrFont + + @ScaledMetric + var fontSize: CGFloat + + var font: Font { + .system(size: fontSize) + } + + init(fontStyle: WrFont) { + self.fontStyle = fontStyle + _fontSize = ScaledMetric(wrappedValue: fontStyle.baseSize) + } + + func body(content: Content) -> some View { + content + .font(font) + } +} + +public extension View { + func textStyle(_ font: WrFont) -> some View { + modifier(WrTextStyle(fontStyle: font)) + } +} + +#Preview { + ScrollView { + VStack { + Text("h1") + .textStyle(.h1) + + Text("h2") + .textStyle(.h2) + + Text("h3") + .textStyle(.h3) + + Text("h4") + .textStyle(.h4) + + Text("body") + .textStyle(.body) + + Text("caption1") + .textStyle(.caption1) + + Text("caption2") + .textStyle(.caption2) + } + } +} diff --git a/Modules/SharedUI/Sources/SharedUI/Images.swift b/Modules/SharedUI/Sources/SharedUI/Images.swift new file mode 100644 index 0000000..08cc7bb --- /dev/null +++ b/Modules/SharedUI/Sources/SharedUI/Images.swift @@ -0,0 +1,11 @@ +import SwiftUI + +public extension Image { + static var logoRounded: Image { + Image(.logoRounded) + } + + static var logoLaunch: Image { + Image(.logoLaunch) + } +} diff --git a/WaiterRobot/Resources/Colors.xcassets/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/Contents.json similarity index 100% rename from WaiterRobot/Resources/Colors.xcassets/Contents.json rename to Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/Contents.json diff --git a/WaiterRobot/Resources/Colors.xcassets/main.colorset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/accent.colorset/Contents.json similarity index 74% rename from WaiterRobot/Resources/Colors.xcassets/main.colorset/Contents.json rename to Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/accent.colorset/Contents.json index 1a06b86..aa7899f 100644 --- a/WaiterRobot/Resources/Colors.xcassets/main.colorset/Contents.json +++ b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/accent.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.491", - "red" : "0.375" + "blue" : "0xFF", + "green" : "0x7D", + "red" : "0x60" } }, "idiom" : "universal" diff --git a/WaiterRobot/Resources/Colors.xcassets/blackWhite.colorset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/blackWhite.colorset/Contents.json similarity index 100% rename from WaiterRobot/Resources/Colors.xcassets/blackWhite.colorset/Contents.json rename to Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/blackWhite.colorset/Contents.json diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/main.colorset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/main.colorset/Contents.json new file mode 100644 index 0000000..d53d1b6 --- /dev/null +++ b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/main.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xBC", + "red" : "0xAC" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x88", + "green" : "0x4C", + "red" : "0x40" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/whiteBlack.colorset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/whiteBlack.colorset/Contents.json new file mode 100644 index 0000000..0425637 --- /dev/null +++ b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/whiteBlack.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.000", + "green" : "0.000", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WaiterRobot/Resources/Images.xcassets/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/Contents.json similarity index 100% rename from WaiterRobot/Resources/Images.xcassets/Contents.json rename to Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/Contents.json diff --git a/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/Contents.json similarity index 100% rename from WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json rename to Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/Contents.json diff --git a/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round-full.png b/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/wr-round-full.png similarity index 100% rename from WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round-full.png rename to Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/wr-round-full.png diff --git a/WaiterRobot/Resources/Images.xcassets/LaunchImage.imageset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/logoLaunch.imageset/Contents.json similarity index 100% rename from WaiterRobot/Resources/Images.xcassets/LaunchImage.imageset/Contents.json rename to Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/logoLaunch.imageset/Contents.json diff --git a/WaiterRobot/Resources/Images.xcassets/LaunchImage.imageset/LaunchImage.pdf b/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/logoLaunch.imageset/LaunchImage.pdf similarity index 100% rename from WaiterRobot/Resources/Images.xcassets/LaunchImage.imageset/LaunchImage.pdf rename to Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/logoLaunch.imageset/LaunchImage.pdf diff --git a/Modules/SharedUI/Sources/SharedUI/SharedUIExample.swift b/Modules/SharedUI/Sources/SharedUI/SharedUIExample.swift new file mode 100644 index 0000000..a43a2a7 --- /dev/null +++ b/Modules/SharedUI/Sources/SharedUI/SharedUIExample.swift @@ -0,0 +1,20 @@ +import SwiftUI + +private struct SharedUIExample: View { + var body: some View { + VStack(alignment: .leading) { + Text("h1") + Text("h2") + Text("h3") + Text("h4") + Text("h5") + Text("h6") + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + } +} + +#Preview { + SharedUIExample() +} diff --git a/Modules/SharedUI/Tests/SharedUITests/SharedUITests.swift b/Modules/SharedUI/Tests/SharedUITests/SharedUITests.swift new file mode 100644 index 0000000..9c4ad5a --- /dev/null +++ b/Modules/SharedUI/Tests/SharedUITests/SharedUITests.swift @@ -0,0 +1,12 @@ +@testable import SharedUI +import XCTest + +final class SharedUITests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +} diff --git a/Modules/WRCore/.gitignore b/Modules/WRCore/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/Modules/WRCore/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Modules/WRCore/Package.swift b/Modules/WRCore/Package.swift new file mode 100644 index 0000000..1fe06d5 --- /dev/null +++ b/Modules/WRCore/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version: 5.10 + +import PackageDescription + +let package = Package( + name: "WRCore", + platforms: [ + .iOS(.v15), + ], + products: [ + .library( + name: "WRCore", + targets: ["WRCore"] + ), + ], + dependencies: [ + .package(path: "../SharedUI"), + .package(url: "https://github.com/DatepollSystems/WaiterRobot-Shared-Android.git", from: "1.6.1"), + ], + targets: [ + .target( + name: "WRCore", + dependencies: [ + .product(name: "SharedUI", package: "SharedUI"), + .product(name: "shared", package: "WaiterRobot-Shared-Android"), + ] + ), + .testTarget( + name: "WRCoreTests", + dependencies: ["WRCore"] + ), + ] +) diff --git a/Modules/WRCore/Sources/WRCore/Globals.swift b/Modules/WRCore/Sources/WRCore/Globals.swift new file mode 100644 index 0000000..a9eb3db --- /dev/null +++ b/Modules/WRCore/Sources/WRCore/Globals.swift @@ -0,0 +1,44 @@ +import Foundation +import shared +import UIKit + +public var koin: IosKoinComponent { IosKoinComponent.shared } + +public var localize: shared.L.Companion { shared.L.Companion.shared } + +public enum WRCore { + /// Setup of frameworks and all the other related stuff which is needed everywhere in the app + public static func setup() { + print("started app setup") + var appVersion = readFromInfoPlist(withKey: "CFBundleShortVersionString") + let versionSuffix = readFromInfoPlist(withKey: "VERSION_SUFFIX") + if !versionSuffix.isEmpty { + appVersion += "-\(versionSuffix)" + } + + CommonApp.shared.doInit( + appVersion: appVersion, + appBuild: Int32(readFromInfoPlist(withKey: "CFBundleVersion"))!, + phoneModel: UIDevice.current.model, + os: OS.Ios(version: UIDevice.current.systemVersion), + allowedHostsCsv: readFromInfoPlist(withKey: "ALLOWED_HOSTS"), + stripeProvider: nil + ) + + KoinKt.doInitKoinIos() + let logger = koin.logger(tag: "AppDelegate") + logger.d { "initialized Koin" } + + KMMResourcesLocalizationKt.localizationBundle = Bundle(for: shared.L.self) + logger.d { "initialized localization bundle" } + print("finished app setup") + } + + private static func readFromInfoPlist(withKey key: String) -> String { + guard let value = Bundle.main.infoDictionary?[key] as? String else { + fatalError("Could not find key '\(key)' in info.plist file.") + } + + return value + } +} diff --git a/WaiterRobot/Core/KermitLoggerExtension.swift b/Modules/WRCore/Sources/WRCore/KermitLoggerExtension.swift similarity index 91% rename from WaiterRobot/Core/KermitLoggerExtension.swift rename to Modules/WRCore/Sources/WRCore/KermitLoggerExtension.swift index e56ba20..651d510 100644 --- a/WaiterRobot/Core/KermitLoggerExtension.swift +++ b/Modules/WRCore/Sources/WRCore/KermitLoggerExtension.swift @@ -1,6 +1,6 @@ import shared -extension KermitLogger { +public extension KermitLogger { func d(message: @escaping () -> String) { d(throwable: nil, tag: tag, message: message) } diff --git a/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift b/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift new file mode 100644 index 0000000..53840b4 --- /dev/null +++ b/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift @@ -0,0 +1,12 @@ +import Foundation +import shared + +public extension Array where Element: AnyObject { + init(_ kotlinArray: KotlinArray) { + self.init() + let iterator = kotlinArray.iterator() + while iterator.hasNext() { + append(iterator.next() as! Element) + } + } +} diff --git a/Modules/WRCore/Sources/WRCore/Mock.swift b/Modules/WRCore/Sources/WRCore/Mock.swift new file mode 100644 index 0000000..1bae963 --- /dev/null +++ b/Modules/WRCore/Sources/WRCore/Mock.swift @@ -0,0 +1,40 @@ +import Foundation +import shared + +public enum Mock { + public static func tableGroups() -> [TableGroup] { + [ + tableGroup(with: 1, name: "Hof"), + tableGroup(with: 2, name: "Terasse"), + tableGroup(with: 3, name: "Zimmer A"), + ] + } + + public static func tableGroup(with id: Int64, name: String = "Hof") -> TableGroup { + TableGroup( + id: id, + name: name, + eventId: 1, + position: Int32(id), + color: "", + hidden: false, + tables: [ + table(with: 1), + table(with: 2, hasOrders: true), + table(with: 3), + table(with: 4), + table(with: 5), + table(with: 6), + ] + ) + } + + public static func table(with id: Int64, hasOrders: Bool = false) -> shared.Table { + shared.Table( + id: id, + number: Int32(id), + groupName: "Hof", + hasOrders: hasOrders + ) + } +} diff --git a/WaiterRobot/Ui/Core/Navigation.swift b/Modules/WRCore/Sources/WRCore/Navigation.swift similarity index 98% rename from WaiterRobot/Ui/Core/Navigation.swift rename to Modules/WRCore/Sources/WRCore/Navigation.swift index 08c904c..1781db5 100644 --- a/WaiterRobot/Ui/Core/Navigation.swift +++ b/Modules/WRCore/Sources/WRCore/Navigation.swift @@ -34,7 +34,7 @@ extension UIPilot { } } -extension View { +public extension View { func customBackNavigation( title: String = localize.navigation.back(), icon: String? = "chevron.left", @@ -57,7 +57,7 @@ extension View { } } .buttonStyle(.plain) - .foregroundStyle(.main) + .foregroundStyle(.primary) } } } diff --git a/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift b/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift new file mode 100644 index 0000000..8e43e28 --- /dev/null +++ b/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift @@ -0,0 +1,91 @@ +/// Base on +/// - https://johnoreilly.dev/posts/kotlinmultiplatform-swift-combine_publisher-flow/ +/// - https://proandroiddev.com/kotlin-multiplatform-mobile-sharing-the-ui-state-management-a67bd9a49882 +/// - https://github.com/orbit-mvi/orbit-swift-gradle-plugin/blob/main/src/main/resources/stateObject.swift.mustache + +import Foundation +import shared + +public class ObservableViewModel>: ObservableObject { + @Published + public private(set) var state: State + + public let actual: ViewModel + + public init(viewModel: ViewModel) { + actual = viewModel + // This is save, as the constraint is required by the generics (S must be the state of the provided VM) + state = actual.container.stateFlow.value as! State + } + + @MainActor + public func activate() async { + for await state in actual.container.refCountStateFlow { + self.state = state as! State + } + } + + deinit { + actual.onCleared() + } +} + +public class ObservableTableListViewModel: ObservableViewModel { + public init() { + super.init(viewModel: koin.tableListVM()) + } +} + +public class ObservableTableDetailViewModel: ObservableViewModel { + public init(table: Table) { + super.init(viewModel: koin.tableDetailVM(table: table)) + } +} + +public class ObservableRootViewModel: ObservableViewModel { + public init() { + super.init(viewModel: koin.rootVM()) + } +} + +public class ObservableBillingViewModel: ObservableViewModel { + public init(table: Table) { + super.init(viewModel: koin.billingVM(table: table)) + } +} + +public class ObservableOrderViewModel: ObservableViewModel { + public init(table: Table, initialItemId: KotlinLong?) { + super.init(viewModel: koin.orderVM(table: table, initialItemId: initialItemId)) + } +} + +public class ObservableLoginScannerViewModel: ObservableViewModel { + public init() { + super.init(viewModel: koin.loginScannerVM()) + } +} + +public class ObservableSettingsViewModel: ObservableViewModel { + public init() { + super.init(viewModel: koin.settingsVM()) + } +} + +public class ObservableSwitchEventViewModel: ObservableViewModel { + public init() { + super.init(viewModel: koin.switchEventVM()) + } +} + +public class ObservableRegisterViewModel: ObservableViewModel { + public init() { + super.init(viewModel: koin.registerVM()) + } +} + +public class ObservableLoginViewModel: ObservableViewModel { + public init() { + super.init(viewModel: koin.loginVM()) + } +} diff --git a/WaiterRobot/Core/PreviewView.swift b/Modules/WRCore/Sources/WRCore/PreviewView.swift similarity index 78% rename from WaiterRobot/Core/PreviewView.swift rename to Modules/WRCore/Sources/WRCore/PreviewView.swift index 77d57e9..58c0e68 100644 --- a/WaiterRobot/Core/PreviewView.swift +++ b/Modules/WRCore/Sources/WRCore/PreviewView.swift @@ -3,7 +3,7 @@ import SwiftUI import UIPilot /// Helper view which sets up everything needed for previewing content -struct PreviewView: View { +public struct PreviewView: View { @StateObject private var navigator = UIPilot(initial: CommonApp.shared.getNextRootScreen(), debug: true) @@ -12,16 +12,16 @@ struct PreviewView: View { @ViewBuilder private let content: Content - init(withUIPilot: Bool = true, @ViewBuilder content: () -> Content) { + public init(withUIPilot: Bool = true, @ViewBuilder content: () -> Content) { print("preview setup") - WaiterRobotApp.setup() + WRCore.setup() self.withUIPilot = withUIPilot self.content = content() print("preview done") } - var body: some View { + public var body: some View { ZStack { if withUIPilot { UIPilotHost(navigator) { _ in diff --git a/Modules/WRCore/Tests/WRCoreTests/WRCoreTests.swift b/Modules/WRCore/Tests/WRCoreTests/WRCoreTests.swift new file mode 100644 index 0000000..ad4e778 --- /dev/null +++ b/Modules/WRCore/Tests/WRCoreTests/WRCoreTests.swift @@ -0,0 +1,12 @@ +@testable import WRCore +import XCTest + +final class WRCoreTests: XCTestCase { + func testExample() throws { + // XCTest Documentation + // https://developer.apple.com/documentation/xctest + + // Defining Test Cases and Test Methods + // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods + } +} diff --git a/WaiterRobot/Core/Globals.swift b/WaiterRobot/Core/Globals.swift deleted file mode 100644 index 4b7160c..0000000 --- a/WaiterRobot/Core/Globals.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation -import shared - -var koin: IosKoinComponent { IosKoinComponent.shared } - -var localize: shared.L.Companion { shared.L.Companion.shared } diff --git a/WaiterRobot/Core/Mvi/KotlinArrayWrapper.swift b/WaiterRobot/Core/Mvi/KotlinArrayWrapper.swift deleted file mode 100644 index 3f481c8..0000000 --- a/WaiterRobot/Core/Mvi/KotlinArrayWrapper.swift +++ /dev/null @@ -1,54 +0,0 @@ -import Foundation -import shared - -// public class KotlinIteratorImpl: NSObject, KotlinIterator, IteratorProtocol { -// public typealias Element = T -// -// var iterator: KotlinIterator -// -// init(iterator: KotlinIterator) { -// self.iterator = iterator -// } -// -// public func next() -> Any? { -// if hasNext() { -// iterator.next() -// } else { -// nil -// } -// } -// -// public func next() -> T? { -// if hasNext() { -// (iterator.next() as! T?) -// } else { -// nil -// } -// } -// -// public func hasNext() -> Bool { -// iterator.hasNext() -// } -// } - -// extension KotlinArray where T: AnyObject { -// var array: Array { -// Array(self) -// } -// } - -// extension KotlinArray: Sequence { -// @objc public func makeIterator() -> KotlinIteratorImpl { -// KotlinIteratorImpl(iterator: iterator()) -// } -// } - -extension Array where Element: AnyObject { - init(_ kotlinArray: KotlinArray) { - self.init() - let iterator = kotlinArray.iterator() - while iterator.hasNext() { - append(iterator.next() as! Element) - } - } -} diff --git a/WaiterRobot/Core/Mvi/ObservableViewModel.swift b/WaiterRobot/Core/Mvi/ObservableViewModel.swift deleted file mode 100644 index 7804087..0000000 --- a/WaiterRobot/Core/Mvi/ObservableViewModel.swift +++ /dev/null @@ -1,91 +0,0 @@ -/// Base on -/// - https://johnoreilly.dev/posts/kotlinmultiplatform-swift-combine_publisher-flow/ -/// - https://proandroiddev.com/kotlin-multiplatform-mobile-sharing-the-ui-state-management-a67bd9a49882 -/// - https://github.com/orbit-mvi/orbit-swift-gradle-plugin/blob/main/src/main/resources/stateObject.swift.mustache - -import Foundation -import shared - -class ObservableViewModel>: ObservableObject { - @Published - public private(set) var state: State - - public let actual: ViewModel - - init(viewModel: ViewModel) { - actual = viewModel - // This is save, as the constraint is required by the generics (S must be the state of the provided VM) - state = actual.container.stateFlow.value as! State - } - - @MainActor - func activate() async { - for await state in actual.container.refCountStateFlow { - self.state = state as! State - } - } - - deinit { - actual.onCleared() - } -} - -class ObservableTableListViewModel: ObservableViewModel { - init() { - super.init(viewModel: koin.tableListVM()) - } -} - -class ObservableTableDetailViewModel: ObservableViewModel { - init(table: Table) { - super.init(viewModel: koin.tableDetailVM(table: table)) - } -} - -class ObservableRootViewModel: ObservableViewModel { - init() { - super.init(viewModel: koin.rootVM()) - } -} - -class ObservableBillingViewModel: ObservableViewModel { - init(table: Table) { - super.init(viewModel: koin.billingVM(table: table)) - } -} - -class ObservableOrderViewModel: ObservableViewModel { - init(table: Table, initialItemId: KotlinLong?) { - super.init(viewModel: koin.orderVM(table: table, initialItemId: initialItemId)) - } -} - -class ObservableLoginScannerViewModel: ObservableViewModel { - init() { - super.init(viewModel: koin.loginScannerVM()) - } -} - -class ObservableSettingsViewModel: ObservableViewModel { - init() { - super.init(viewModel: koin.settingsVM()) - } -} - -class ObservableSwitchEventViewModel: ObservableViewModel { - init() { - super.init(viewModel: koin.switchEventVM()) - } -} - -class ObservableRegisterViewModel: ObservableViewModel { - init() { - super.init(viewModel: koin.registerVM()) - } -} - -class ObservableLoginViewModel: ObservableViewModel { - init() { - super.init(viewModel: koin.loginVM()) - } -} diff --git a/WaiterRobot/LaunchScreen.swift b/WaiterRobot/LaunchScreen.swift index c0aa78a..0d6a7bf 100644 --- a/WaiterRobot/LaunchScreen.swift +++ b/WaiterRobot/LaunchScreen.swift @@ -1,6 +1,7 @@ import Foundation import shared import SwiftUI +import WRCore struct LaunchScreen: View { private let minimumOnScreenTimeSeconds = 3.0 @@ -14,7 +15,7 @@ struct LaunchScreen: View { VStack { Spacer() - Image(.launch) + Image.logoLaunch .resizable() .scaledToFit() } @@ -22,7 +23,7 @@ struct LaunchScreen: View { .ignoresSafeArea() } else { ZStack { - Image(.logoRounded) + Image.logoLaunch .resizable() .scaledToFit() .frame(width: 150) @@ -46,7 +47,7 @@ struct LaunchScreen: View { // This is needed otherwise previews will crash randomly if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] != "1" { Task { - async let setup: () = WaiterRobotApp.setup() + async let setup: () = WRCore.setup() async let delay: () = delay() await setup diff --git a/WaiterRobot/MainView.swift b/WaiterRobot/MainView.swift index 5302b21..830765e 100644 --- a/WaiterRobot/MainView.swift +++ b/WaiterRobot/MainView.swift @@ -8,6 +8,7 @@ import shared import SwiftUI import UIPilot +import WRCore struct MainView: View { @State diff --git a/WaiterRobot/Resources/Colors.xcassets/second.colorset/Contents.json b/WaiterRobot/Resources/Colors.xcassets/second.colorset/Contents.json deleted file mode 100644 index c43d793..0000000 --- a/WaiterRobot/Resources/Colors.xcassets/second.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.967", - "green" : "0.691", - "red" : "0.607" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/WaiterRobot/Ui/Billing/BillingScreen.swift b/WaiterRobot/Ui/Billing/BillingScreen.swift index ed9f6aa..f33da5b 100644 --- a/WaiterRobot/Ui/Billing/BillingScreen.swift +++ b/WaiterRobot/Ui/Billing/BillingScreen.swift @@ -2,6 +2,7 @@ import Foundation import shared import SwiftUI import UIPilot +import WRCore struct BillingScreen: View { @EnvironmentObject var navigator: UIPilot diff --git a/WaiterRobot/Ui/Billing/PayDialog.swift b/WaiterRobot/Ui/Billing/PayDialog.swift index 049ff32..82ca708 100644 --- a/WaiterRobot/Ui/Billing/PayDialog.swift +++ b/WaiterRobot/Ui/Billing/PayDialog.swift @@ -1,6 +1,7 @@ import Combine import shared import SwiftUI +import WRCore struct PayDialog: View { @Environment(\.dismiss) private var dismiss diff --git a/WaiterRobot/Ui/Core/ButtonStyles.swift b/WaiterRobot/Ui/Core/ButtonStyles.swift index 3fed1a9..1820d85 100644 --- a/WaiterRobot/Ui/Core/ButtonStyles.swift +++ b/WaiterRobot/Ui/Core/ButtonStyles.swift @@ -5,6 +5,7 @@ // Created by Alexander Kauer on 25.02.24. // +import SharedUI import SwiftUI struct WRBorderedProminentButtonStyle: ButtonStyle { @@ -16,7 +17,7 @@ struct WRBorderedProminentButtonStyle: ButtonStyle { .background( RoundedRectangle(cornerRadius: 10) .ifCondition(isEnabled) { view in - view.foregroundStyle(configuration.isPressed ? .main.opacity(0.6) : .main) + view.foregroundStyle(configuration.isPressed ? Color.main.opacity(0.6) : Color.main) } .ifCondition(!isEnabled) { view in view.foregroundStyle(.gray) @@ -40,7 +41,7 @@ struct WRSecondaryBorderedProminentButtonStyle: ButtonStyle { .background( RoundedRectangle(cornerRadius: 10) .ifCondition(isEnabled) { view in - view.foregroundStyle(configuration.isPressed ? .second : .second.opacity(0.8)) + view.foregroundStyle(configuration.isPressed ? Color.accent : Color.accent.opacity(0.8)) } .ifCondition(!isEnabled) { view in view.foregroundStyle(.gray) diff --git a/WaiterRobot/Ui/Login/LoginScannerScreen.swift b/WaiterRobot/Ui/Login/LoginScannerScreen.swift index 7434f4c..a47fe0a 100644 --- a/WaiterRobot/Ui/Login/LoginScannerScreen.swift +++ b/WaiterRobot/Ui/Login/LoginScannerScreen.swift @@ -2,6 +2,7 @@ import CodeScanner import shared import SwiftUI import UIPilot +import WRCore struct LoginScannerScreen: View { @EnvironmentObject var navigator: UIPilot diff --git a/WaiterRobot/Ui/Login/LoginScreen.swift b/WaiterRobot/Ui/Login/LoginScreen.swift index 7f1829d..ba66655 100644 --- a/WaiterRobot/Ui/Login/LoginScreen.swift +++ b/WaiterRobot/Ui/Login/LoginScreen.swift @@ -1,7 +1,9 @@ import Foundation import shared +import SharedUI import SwiftUI import UIPilot +import WRCore struct LoginScreen: View { @EnvironmentObject var navigator: UIPilot @@ -32,7 +34,7 @@ struct LoginScreen: View { VStack { Spacer() - Image(.logoRounded) + Image.logoRounded .resizable() .scaledToFit() .frame(maxWidth: 250) diff --git a/WaiterRobot/Ui/Login/RegisterScreen.swift b/WaiterRobot/Ui/Login/RegisterScreen.swift index 7cf48a0..66336cd 100644 --- a/WaiterRobot/Ui/Login/RegisterScreen.swift +++ b/WaiterRobot/Ui/Login/RegisterScreen.swift @@ -1,6 +1,7 @@ import shared import SwiftUI import UIPilot +import WRCore struct RegisterScreen: View { @EnvironmentObject var navigator: UIPilot diff --git a/WaiterRobot/Ui/Order/OrderListItem.swift b/WaiterRobot/Ui/Order/OrderListItem.swift index dc80c88..68e757b 100644 --- a/WaiterRobot/Ui/Order/OrderListItem.swift +++ b/WaiterRobot/Ui/Order/OrderListItem.swift @@ -1,5 +1,6 @@ import shared import SwiftUI +import WRCore struct OrderListItem: View { let name: String diff --git a/WaiterRobot/Ui/Order/OrderProductNoteView.swift b/WaiterRobot/Ui/Order/OrderProductNoteView.swift index e5a5429..3e01073 100644 --- a/WaiterRobot/Ui/Order/OrderProductNoteView.swift +++ b/WaiterRobot/Ui/Order/OrderProductNoteView.swift @@ -1,5 +1,6 @@ import shared import SwiftUI +import WRCore struct OrderProductNoteView: View { let name: String diff --git a/WaiterRobot/Ui/Order/OrderScreen.swift b/WaiterRobot/Ui/Order/OrderScreen.swift index 207b321..581bcd5 100644 --- a/WaiterRobot/Ui/Order/OrderScreen.swift +++ b/WaiterRobot/Ui/Order/OrderScreen.swift @@ -1,6 +1,7 @@ import shared import SwiftUI import UIPilot +import WRCore struct OrderScreen: View { @EnvironmentObject var navigator: UIPilot @@ -55,8 +56,8 @@ struct OrderScreen: View { .sheet(isPresented: $showProductSearch) { ProductSearch(viewModel: viewModel) } - .withViewModel(viewModel, navigator) .animation(.default, value: viewModel.state.currentOrder) + .withViewModel(viewModel, navigator) } @ViewBuilder diff --git a/WaiterRobot/Ui/Order/Search/ProductSearch.swift b/WaiterRobot/Ui/Order/Search/ProductSearch.swift index 0a6b1b0..8520d70 100644 --- a/WaiterRobot/Ui/Order/Search/ProductSearch.swift +++ b/WaiterRobot/Ui/Order/Search/ProductSearch.swift @@ -1,5 +1,6 @@ import shared import SwiftUI +import WRCore struct ProductSearch: View { @Environment(\.dismiss) private var dismiss diff --git a/WaiterRobot/Ui/Settings/SettingsScreen.swift b/WaiterRobot/Ui/Settings/SettingsScreen.swift index d25dd5a..59f7e8c 100644 --- a/WaiterRobot/Ui/Settings/SettingsScreen.swift +++ b/WaiterRobot/Ui/Settings/SettingsScreen.swift @@ -1,6 +1,7 @@ import shared import SwiftUI import UIPilot +import WRCore struct SettingsScreen: View { @EnvironmentObject var navigator: UIPilot diff --git a/WaiterRobot/Ui/Settings/SwitchThemeView.swift b/WaiterRobot/Ui/Settings/SwitchThemeView.swift index 42f46d7..7647244 100644 --- a/WaiterRobot/Ui/Settings/SwitchThemeView.swift +++ b/WaiterRobot/Ui/Settings/SwitchThemeView.swift @@ -1,5 +1,6 @@ import shared import SwiftUI +import WRCore struct SwitchThemeView: View { @State private var selectedTheme: AppTheme diff --git a/WaiterRobot/Ui/SwitchEvent/SwitchEventScreen.swift b/WaiterRobot/Ui/SwitchEvent/SwitchEventScreen.swift index feebfbd..b32caf3 100644 --- a/WaiterRobot/Ui/SwitchEvent/SwitchEventScreen.swift +++ b/WaiterRobot/Ui/SwitchEvent/SwitchEventScreen.swift @@ -1,6 +1,7 @@ import shared import SwiftUI import UIPilot +import WRCore struct SwitchEventScreen: View { @EnvironmentObject var navigator: UIPilot diff --git a/WaiterRobot/Ui/TableDetail/TableDetailScreen.swift b/WaiterRobot/Ui/TableDetail/TableDetailScreen.swift index 67ef363..6c548bd 100644 --- a/WaiterRobot/Ui/TableDetail/TableDetailScreen.swift +++ b/WaiterRobot/Ui/TableDetail/TableDetailScreen.swift @@ -1,6 +1,7 @@ import shared import SwiftUI import UIPilot +import WRCore struct TableDetailScreen: View { @EnvironmentObject var navigator: UIPilot diff --git a/WaiterRobot/Ui/TableList/TableGroupSection.swift b/WaiterRobot/Ui/TableList/TableGroupSection.swift index 3cfe524..eb141b2 100644 --- a/WaiterRobot/Ui/TableList/TableGroupSection.swift +++ b/WaiterRobot/Ui/TableList/TableGroupSection.swift @@ -1,5 +1,7 @@ import shared +import SharedUI import SwiftUI +import WRCore struct TableGroupSection: View { let tableGroup: TableGroup @@ -25,11 +27,13 @@ struct TableGroupSection: View { .padding(6) .background { RoundedRectangle(cornerRadius: 8.0) - .foregroundStyle(Color(.main)) + .foregroundStyle(Color.main) } Spacer() } + .padding(.vertical, 4) + .background(Color.whiteBlack) } } } @@ -53,6 +57,6 @@ struct TableGroupSection: View { ), onTableClick: { _ in } ) - - }.padding() + } + .padding() } diff --git a/WaiterRobot/Ui/TableList/TableListScreen.swift b/WaiterRobot/Ui/TableList/TableListScreen.swift index e38a070..2ff6b73 100644 --- a/WaiterRobot/Ui/TableList/TableListScreen.swift +++ b/WaiterRobot/Ui/TableList/TableListScreen.swift @@ -1,16 +1,13 @@ import shared import SwiftUI import UIPilot +import WRCore struct TableListScreen: View { @EnvironmentObject var navigator: UIPilot @StateObject private var viewModel = ObservableTableListViewModel() - private let layout = [ - GridItem(.adaptive(minimum: 100)), - ] - @State private var showFilters = false @@ -84,14 +81,38 @@ struct TableListScreen: View { private func tableList(data: KotlinArray) -> some View { let tableGroups = Array(data) + TableListView( + showFilters: $showFilters, + tableGroups: tableGroups, + onToggleFilter: { viewModel.actual.toggleFilter(tableGroup: $0) }, + onSelectAll: { viewModel.actual.showAll() }, + onUnselectAll: { viewModel.actual.hideAll() }, + onTableSelect: { viewModel.actual.onTableClick(table: $0) } + ) + } +} + +struct TableListView: View { + @Binding var showFilters: Bool + let tableGroups: [TableGroup] + let onToggleFilter: (TableGroup) -> Void + let onSelectAll: () -> Void + let onUnselectAll: () -> Void + let onTableSelect: (shared.Table) -> Void + + private let layout = [ + GridItem(.adaptive(minimum: 100)), + ] + + var body: some View { VStack(spacing: 0) { if tableGroups.count > 1, showFilters { VStack { TableListFilterRow( tableGroups: tableGroups, - onToggleFilter: { viewModel.actual.toggleFilter(tableGroup: $0) }, - onSelectAll: { viewModel.actual.showAll() }, - onUnselectAll: { viewModel.actual.hideAll() } + onToggleFilter: onToggleFilter, + onSelectAll: onSelectAll, + onUnselectAll: onUnselectAll ) } .padding() @@ -111,12 +132,15 @@ struct TableListScreen: View { Spacer() } else { ScrollView { - LazyVGrid(columns: layout) { + LazyVGrid( + columns: layout, + pinnedViews: [.sectionHeaders] + ) { ForEach(tableGroups.filter { !$0.hidden }, id: \.id) { group in if !group.tables.isEmpty { TableGroupSection( tableGroup: group, - onTableClick: { viewModel.actual.onTableClick(table: $0) } + onTableClick: onTableSelect ) } } @@ -140,10 +164,24 @@ struct TableListScreen: View { } } -#Preview { +#Preview("TableListScreen") { PreviewView { NavigationView { TableListScreen() } } } + +#Preview("TableListView") { + PreviewView { + NavigationView { + TableListView( + showFilters: .constant(false), + tableGroups: Mock.tableGroups() + ) { _ in + + } onSelectAll: {} onUnselectAll: {} onTableSelect: { _ in + } + } + } +} diff --git a/WaiterRobot/Ui/TableList/TableView.swift b/WaiterRobot/Ui/TableList/TableView.swift index 7dfaf29..1a8de46 100644 --- a/WaiterRobot/Ui/TableList/TableView.swift +++ b/WaiterRobot/Ui/TableList/TableView.swift @@ -1,3 +1,4 @@ +import SharedUI import SwiftUI struct TableView: View { @@ -9,7 +10,7 @@ struct TableView: View { Button(action: onClick) { ZStack { RoundedRectangle(cornerRadius: 20) - .stroke(.blackWhite, lineWidth: 5) + .stroke(Color.blackWhite, lineWidth: 5) Text(text) .font(.title) diff --git a/WaiterRobot/Ui/UpdateApp/UpdateAppScreen.swift b/WaiterRobot/Ui/UpdateApp/UpdateAppScreen.swift index 671148c..2132f9e 100644 --- a/WaiterRobot/Ui/UpdateApp/UpdateAppScreen.swift +++ b/WaiterRobot/Ui/UpdateApp/UpdateAppScreen.swift @@ -1,5 +1,6 @@ import shared import SwiftUI +import WRCore struct UpdateAppScreen: View { var body: some View { diff --git a/WaiterRobot/WaiterRobotApp.swift b/WaiterRobot/WaiterRobotApp.swift index a695966..4d33bb2 100644 --- a/WaiterRobot/WaiterRobotApp.swift +++ b/WaiterRobot/WaiterRobotApp.swift @@ -1,4 +1,4 @@ -import shared +import SharedUI import SwiftUI @main @@ -6,41 +6,7 @@ struct WaiterRobotApp: App { var body: some Scene { WindowGroup { LaunchScreen() + .tint(.accent) } } - - /// Setup of frameworks and all the other related stuff which is needed everywhere in the app - static func setup() { - print("started app setup") - var appVersion = readFromInfoPlist(withKey: "CFBundleShortVersionString") - let versionSuffix = readFromInfoPlist(withKey: "VERSION_SUFFIX") - if !versionSuffix.isEmpty { - appVersion += "-\(versionSuffix)" - } - - CommonApp.shared.doInit( - appVersion: appVersion, - appBuild: Int32(readFromInfoPlist(withKey: "CFBundleVersion"))!, - phoneModel: UIDevice.current.model, - os: OS.Ios(version: UIDevice.current.systemVersion), - allowedHostsCsv: readFromInfoPlist(withKey: "ALLOWED_HOSTS"), - stripeProvider: nil - ) - - KoinKt.doInitKoinIos() - let logger = koin.logger(tag: "AppDelegate") - logger.d { "initialized Koin" } - - KMMResourcesLocalizationKt.localizationBundle = Bundle(for: shared.L.self) - logger.d { "initialized localization bundle" } - print("finished app setup") - } - - private static func readFromInfoPlist(withKey key: String) -> String { - guard let value = Bundle.main.infoDictionary?[key] as? String else { - fatalError("Could not find key '\(key)' in info.plist file.") - } - - return value - } } diff --git a/project.yml b/project.yml index 15412e4..0089125 100644 --- a/project.yml +++ b/project.yml @@ -14,7 +14,14 @@ fileGroups: - install-git-hook.sh - renovate.json5 +options: + localPackagesGroup: Modules + packages: + SharedUI: + path: Modules/SharedUI + WRCore: + path: Modules/WRCore shared: url: https://github.com/DatepollSystems/WaiterRobot-Shared-Android.git version: 1.6.1 @@ -44,7 +51,9 @@ targetTemplates: - package: shared - package: UIPilot - package: CodeScanner - + - package: SharedUI + - package: WRCore + info: path: ".generated/${target_name}.plist" properties: @@ -96,7 +105,7 @@ targetTemplates: DEVELOPMENT_TEAM: "28TM58T3GZ" PRODUCT_NAME: "${displayName}" ENABLE_PREVIEWS: "YES" - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: "main" + #ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: "main" configs: RELEASE: ONLY_ACTIVE_ARCH: "NO" From 297dc317369e7c3784bf29bcfe2ea2b7a9cbf340 Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Sun, 23 Jun 2024 16:43:21 +0200 Subject: [PATCH 02/43] Ui to UI, fixed accent color --- .../Billing/BillListItem.swift | 0 .../Billing/BillingScreen.swift | 0 .../{Ui => Features}/Billing/PayDialog.swift | 0 .../Common/ConditionalViewModifier.swift | 0 .../{Ui => Features}/Core/ButtonStyles.swift | 0 .../{Ui => Features}/Core/DynamicGrid.swift | 0 .../Core/FloatingActionButton.swift | 0 .../{Ui => Features}/Core/IfCondition.swift | 0 .../{Ui => Features}/Core/PullToRefresh.swift | 0 .../Core/RefreshableScrollView.swift | 0 .../{Ui => Features}/Core/WrToolbar.swift | 0 .../Login/LoginScannerScreen.swift | 0 .../{Ui => Features}/Login/LoginScreen.swift | 0 .../Login/RegisterScreen.swift | 0 .../Order/OrderListItem.swift | 0 .../Order/OrderProductNoteView.swift | 0 .../{Ui => Features}/Order/OrderScreen.swift | 0 .../Order/ProductListItem.swift | 0 .../Search/ProducSearchTabBarHeader.swift | 0 .../Order/Search/ProductSearch.swift | 0 .../Order/Search/ProductSearchAllTab.swift | 0 .../Order/Search/ProductSearchGroupList.swift | 0 .../Settings/SettingsItem.swift | 0 .../Settings/SettingsScreen.swift | 0 .../Settings/SwitchThemeView.swift | 0 .../{Ui => Features}/SwitchEvent/Event.swift | 0 .../SwitchEvent/SwitchEventScreen.swift | 0 .../TableDetail/OrderedItemView.swift | 0 .../TableDetail/TableDetailScreen.swift | 0 .../TableList/TableGroupSection.swift | 2 +- .../TableList/TableListFilterRow.swift | 0 .../TableList/TableListScreen.swift | 0 .../TableList/TableView.swift | 0 .../UpdateApp/UpdateAppScreen.swift | 0 .../AccentColor.colorset/Contents.json | 20 +++++++++++++++++++ .../AccentColor.xcassets/Contents.json | 6 ++++++ WaiterRobot/WaiterRobotApp.swift | 1 - project.yml | 2 +- 38 files changed, 28 insertions(+), 3 deletions(-) rename WaiterRobot/{Ui => Features}/Billing/BillListItem.swift (100%) rename WaiterRobot/{Ui => Features}/Billing/BillingScreen.swift (100%) rename WaiterRobot/{Ui => Features}/Billing/PayDialog.swift (100%) rename WaiterRobot/{Ui => Features}/Common/ConditionalViewModifier.swift (100%) rename WaiterRobot/{Ui => Features}/Core/ButtonStyles.swift (100%) rename WaiterRobot/{Ui => Features}/Core/DynamicGrid.swift (100%) rename WaiterRobot/{Ui => Features}/Core/FloatingActionButton.swift (100%) rename WaiterRobot/{Ui => Features}/Core/IfCondition.swift (100%) rename WaiterRobot/{Ui => Features}/Core/PullToRefresh.swift (100%) rename WaiterRobot/{Ui => Features}/Core/RefreshableScrollView.swift (100%) rename WaiterRobot/{Ui => Features}/Core/WrToolbar.swift (100%) rename WaiterRobot/{Ui => Features}/Login/LoginScannerScreen.swift (100%) rename WaiterRobot/{Ui => Features}/Login/LoginScreen.swift (100%) rename WaiterRobot/{Ui => Features}/Login/RegisterScreen.swift (100%) rename WaiterRobot/{Ui => Features}/Order/OrderListItem.swift (100%) rename WaiterRobot/{Ui => Features}/Order/OrderProductNoteView.swift (100%) rename WaiterRobot/{Ui => Features}/Order/OrderScreen.swift (100%) rename WaiterRobot/{Ui => Features}/Order/ProductListItem.swift (100%) rename WaiterRobot/{Ui => Features}/Order/Search/ProducSearchTabBarHeader.swift (100%) rename WaiterRobot/{Ui => Features}/Order/Search/ProductSearch.swift (100%) rename WaiterRobot/{Ui => Features}/Order/Search/ProductSearchAllTab.swift (100%) rename WaiterRobot/{Ui => Features}/Order/Search/ProductSearchGroupList.swift (100%) rename WaiterRobot/{Ui => Features}/Settings/SettingsItem.swift (100%) rename WaiterRobot/{Ui => Features}/Settings/SettingsScreen.swift (100%) rename WaiterRobot/{Ui => Features}/Settings/SwitchThemeView.swift (100%) rename WaiterRobot/{Ui => Features}/SwitchEvent/Event.swift (100%) rename WaiterRobot/{Ui => Features}/SwitchEvent/SwitchEventScreen.swift (100%) rename WaiterRobot/{Ui => Features}/TableDetail/OrderedItemView.swift (100%) rename WaiterRobot/{Ui => Features}/TableDetail/TableDetailScreen.swift (100%) rename WaiterRobot/{Ui => Features}/TableList/TableGroupSection.swift (96%) rename WaiterRobot/{Ui => Features}/TableList/TableListFilterRow.swift (100%) rename WaiterRobot/{Ui => Features}/TableList/TableListScreen.swift (100%) rename WaiterRobot/{Ui => Features}/TableList/TableView.swift (100%) rename WaiterRobot/{Ui => Features}/UpdateApp/UpdateAppScreen.swift (100%) create mode 100644 WaiterRobot/Resources/AccentColor.xcassets/AccentColor.colorset/Contents.json create mode 100644 WaiterRobot/Resources/AccentColor.xcassets/Contents.json diff --git a/WaiterRobot/Ui/Billing/BillListItem.swift b/WaiterRobot/Features/Billing/BillListItem.swift similarity index 100% rename from WaiterRobot/Ui/Billing/BillListItem.swift rename to WaiterRobot/Features/Billing/BillListItem.swift diff --git a/WaiterRobot/Ui/Billing/BillingScreen.swift b/WaiterRobot/Features/Billing/BillingScreen.swift similarity index 100% rename from WaiterRobot/Ui/Billing/BillingScreen.swift rename to WaiterRobot/Features/Billing/BillingScreen.swift diff --git a/WaiterRobot/Ui/Billing/PayDialog.swift b/WaiterRobot/Features/Billing/PayDialog.swift similarity index 100% rename from WaiterRobot/Ui/Billing/PayDialog.swift rename to WaiterRobot/Features/Billing/PayDialog.swift diff --git a/WaiterRobot/Ui/Common/ConditionalViewModifier.swift b/WaiterRobot/Features/Common/ConditionalViewModifier.swift similarity index 100% rename from WaiterRobot/Ui/Common/ConditionalViewModifier.swift rename to WaiterRobot/Features/Common/ConditionalViewModifier.swift diff --git a/WaiterRobot/Ui/Core/ButtonStyles.swift b/WaiterRobot/Features/Core/ButtonStyles.swift similarity index 100% rename from WaiterRobot/Ui/Core/ButtonStyles.swift rename to WaiterRobot/Features/Core/ButtonStyles.swift diff --git a/WaiterRobot/Ui/Core/DynamicGrid.swift b/WaiterRobot/Features/Core/DynamicGrid.swift similarity index 100% rename from WaiterRobot/Ui/Core/DynamicGrid.swift rename to WaiterRobot/Features/Core/DynamicGrid.swift diff --git a/WaiterRobot/Ui/Core/FloatingActionButton.swift b/WaiterRobot/Features/Core/FloatingActionButton.swift similarity index 100% rename from WaiterRobot/Ui/Core/FloatingActionButton.swift rename to WaiterRobot/Features/Core/FloatingActionButton.swift diff --git a/WaiterRobot/Ui/Core/IfCondition.swift b/WaiterRobot/Features/Core/IfCondition.swift similarity index 100% rename from WaiterRobot/Ui/Core/IfCondition.swift rename to WaiterRobot/Features/Core/IfCondition.swift diff --git a/WaiterRobot/Ui/Core/PullToRefresh.swift b/WaiterRobot/Features/Core/PullToRefresh.swift similarity index 100% rename from WaiterRobot/Ui/Core/PullToRefresh.swift rename to WaiterRobot/Features/Core/PullToRefresh.swift diff --git a/WaiterRobot/Ui/Core/RefreshableScrollView.swift b/WaiterRobot/Features/Core/RefreshableScrollView.swift similarity index 100% rename from WaiterRobot/Ui/Core/RefreshableScrollView.swift rename to WaiterRobot/Features/Core/RefreshableScrollView.swift diff --git a/WaiterRobot/Ui/Core/WrToolbar.swift b/WaiterRobot/Features/Core/WrToolbar.swift similarity index 100% rename from WaiterRobot/Ui/Core/WrToolbar.swift rename to WaiterRobot/Features/Core/WrToolbar.swift diff --git a/WaiterRobot/Ui/Login/LoginScannerScreen.swift b/WaiterRobot/Features/Login/LoginScannerScreen.swift similarity index 100% rename from WaiterRobot/Ui/Login/LoginScannerScreen.swift rename to WaiterRobot/Features/Login/LoginScannerScreen.swift diff --git a/WaiterRobot/Ui/Login/LoginScreen.swift b/WaiterRobot/Features/Login/LoginScreen.swift similarity index 100% rename from WaiterRobot/Ui/Login/LoginScreen.swift rename to WaiterRobot/Features/Login/LoginScreen.swift diff --git a/WaiterRobot/Ui/Login/RegisterScreen.swift b/WaiterRobot/Features/Login/RegisterScreen.swift similarity index 100% rename from WaiterRobot/Ui/Login/RegisterScreen.swift rename to WaiterRobot/Features/Login/RegisterScreen.swift diff --git a/WaiterRobot/Ui/Order/OrderListItem.swift b/WaiterRobot/Features/Order/OrderListItem.swift similarity index 100% rename from WaiterRobot/Ui/Order/OrderListItem.swift rename to WaiterRobot/Features/Order/OrderListItem.swift diff --git a/WaiterRobot/Ui/Order/OrderProductNoteView.swift b/WaiterRobot/Features/Order/OrderProductNoteView.swift similarity index 100% rename from WaiterRobot/Ui/Order/OrderProductNoteView.swift rename to WaiterRobot/Features/Order/OrderProductNoteView.swift diff --git a/WaiterRobot/Ui/Order/OrderScreen.swift b/WaiterRobot/Features/Order/OrderScreen.swift similarity index 100% rename from WaiterRobot/Ui/Order/OrderScreen.swift rename to WaiterRobot/Features/Order/OrderScreen.swift diff --git a/WaiterRobot/Ui/Order/ProductListItem.swift b/WaiterRobot/Features/Order/ProductListItem.swift similarity index 100% rename from WaiterRobot/Ui/Order/ProductListItem.swift rename to WaiterRobot/Features/Order/ProductListItem.swift diff --git a/WaiterRobot/Ui/Order/Search/ProducSearchTabBarHeader.swift b/WaiterRobot/Features/Order/Search/ProducSearchTabBarHeader.swift similarity index 100% rename from WaiterRobot/Ui/Order/Search/ProducSearchTabBarHeader.swift rename to WaiterRobot/Features/Order/Search/ProducSearchTabBarHeader.swift diff --git a/WaiterRobot/Ui/Order/Search/ProductSearch.swift b/WaiterRobot/Features/Order/Search/ProductSearch.swift similarity index 100% rename from WaiterRobot/Ui/Order/Search/ProductSearch.swift rename to WaiterRobot/Features/Order/Search/ProductSearch.swift diff --git a/WaiterRobot/Ui/Order/Search/ProductSearchAllTab.swift b/WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift similarity index 100% rename from WaiterRobot/Ui/Order/Search/ProductSearchAllTab.swift rename to WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift diff --git a/WaiterRobot/Ui/Order/Search/ProductSearchGroupList.swift b/WaiterRobot/Features/Order/Search/ProductSearchGroupList.swift similarity index 100% rename from WaiterRobot/Ui/Order/Search/ProductSearchGroupList.swift rename to WaiterRobot/Features/Order/Search/ProductSearchGroupList.swift diff --git a/WaiterRobot/Ui/Settings/SettingsItem.swift b/WaiterRobot/Features/Settings/SettingsItem.swift similarity index 100% rename from WaiterRobot/Ui/Settings/SettingsItem.swift rename to WaiterRobot/Features/Settings/SettingsItem.swift diff --git a/WaiterRobot/Ui/Settings/SettingsScreen.swift b/WaiterRobot/Features/Settings/SettingsScreen.swift similarity index 100% rename from WaiterRobot/Ui/Settings/SettingsScreen.swift rename to WaiterRobot/Features/Settings/SettingsScreen.swift diff --git a/WaiterRobot/Ui/Settings/SwitchThemeView.swift b/WaiterRobot/Features/Settings/SwitchThemeView.swift similarity index 100% rename from WaiterRobot/Ui/Settings/SwitchThemeView.swift rename to WaiterRobot/Features/Settings/SwitchThemeView.swift diff --git a/WaiterRobot/Ui/SwitchEvent/Event.swift b/WaiterRobot/Features/SwitchEvent/Event.swift similarity index 100% rename from WaiterRobot/Ui/SwitchEvent/Event.swift rename to WaiterRobot/Features/SwitchEvent/Event.swift diff --git a/WaiterRobot/Ui/SwitchEvent/SwitchEventScreen.swift b/WaiterRobot/Features/SwitchEvent/SwitchEventScreen.swift similarity index 100% rename from WaiterRobot/Ui/SwitchEvent/SwitchEventScreen.swift rename to WaiterRobot/Features/SwitchEvent/SwitchEventScreen.swift diff --git a/WaiterRobot/Ui/TableDetail/OrderedItemView.swift b/WaiterRobot/Features/TableDetail/OrderedItemView.swift similarity index 100% rename from WaiterRobot/Ui/TableDetail/OrderedItemView.swift rename to WaiterRobot/Features/TableDetail/OrderedItemView.swift diff --git a/WaiterRobot/Ui/TableDetail/TableDetailScreen.swift b/WaiterRobot/Features/TableDetail/TableDetailScreen.swift similarity index 100% rename from WaiterRobot/Ui/TableDetail/TableDetailScreen.swift rename to WaiterRobot/Features/TableDetail/TableDetailScreen.swift diff --git a/WaiterRobot/Ui/TableList/TableGroupSection.swift b/WaiterRobot/Features/TableList/TableGroupSection.swift similarity index 96% rename from WaiterRobot/Ui/TableList/TableGroupSection.swift rename to WaiterRobot/Features/TableList/TableGroupSection.swift index eb141b2..fb46bfb 100644 --- a/WaiterRobot/Ui/TableList/TableGroupSection.swift +++ b/WaiterRobot/Features/TableList/TableGroupSection.swift @@ -27,7 +27,7 @@ struct TableGroupSection: View { .padding(6) .background { RoundedRectangle(cornerRadius: 8.0) - .foregroundStyle(Color.main) + .foregroundStyle(Color.accent) } Spacer() diff --git a/WaiterRobot/Ui/TableList/TableListFilterRow.swift b/WaiterRobot/Features/TableList/TableListFilterRow.swift similarity index 100% rename from WaiterRobot/Ui/TableList/TableListFilterRow.swift rename to WaiterRobot/Features/TableList/TableListFilterRow.swift diff --git a/WaiterRobot/Ui/TableList/TableListScreen.swift b/WaiterRobot/Features/TableList/TableListScreen.swift similarity index 100% rename from WaiterRobot/Ui/TableList/TableListScreen.swift rename to WaiterRobot/Features/TableList/TableListScreen.swift diff --git a/WaiterRobot/Ui/TableList/TableView.swift b/WaiterRobot/Features/TableList/TableView.swift similarity index 100% rename from WaiterRobot/Ui/TableList/TableView.swift rename to WaiterRobot/Features/TableList/TableView.swift diff --git a/WaiterRobot/Ui/UpdateApp/UpdateAppScreen.swift b/WaiterRobot/Features/UpdateApp/UpdateAppScreen.swift similarity index 100% rename from WaiterRobot/Ui/UpdateApp/UpdateAppScreen.swift rename to WaiterRobot/Features/UpdateApp/UpdateAppScreen.swift diff --git a/WaiterRobot/Resources/AccentColor.xcassets/AccentColor.colorset/Contents.json b/WaiterRobot/Resources/AccentColor.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..aa7899f --- /dev/null +++ b/WaiterRobot/Resources/AccentColor.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0x7D", + "red" : "0x60" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WaiterRobot/Resources/AccentColor.xcassets/Contents.json b/WaiterRobot/Resources/AccentColor.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/WaiterRobot/Resources/AccentColor.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WaiterRobot/WaiterRobotApp.swift b/WaiterRobot/WaiterRobotApp.swift index 4d33bb2..e9271f3 100644 --- a/WaiterRobot/WaiterRobotApp.swift +++ b/WaiterRobot/WaiterRobotApp.swift @@ -6,7 +6,6 @@ struct WaiterRobotApp: App { var body: some Scene { WindowGroup { LaunchScreen() - .tint(.accent) } } } diff --git a/project.yml b/project.yml index 0089125..0958039 100644 --- a/project.yml +++ b/project.yml @@ -105,7 +105,7 @@ targetTemplates: DEVELOPMENT_TEAM: "28TM58T3GZ" PRODUCT_NAME: "${displayName}" ENABLE_PREVIEWS: "YES" - #ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: "main" + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: "AccentColor" configs: RELEASE: ONLY_ACTIVE_ARCH: "NO" From 1423314014952bab0d955ff601bc067f9ef92196 Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Sun, 23 Jun 2024 16:45:09 +0200 Subject: [PATCH 03/43] swift 5.9 --- Modules/SharedUI/Package.swift | 2 +- Modules/WRCore/Package.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/SharedUI/Package.swift b/Modules/SharedUI/Package.swift index f92345f..2e164b8 100644 --- a/Modules/SharedUI/Package.swift +++ b/Modules/SharedUI/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.10 +// swift-tools-version: 5.9 import PackageDescription diff --git a/Modules/WRCore/Package.swift b/Modules/WRCore/Package.swift index 1fe06d5..1392efc 100644 --- a/Modules/WRCore/Package.swift +++ b/Modules/WRCore/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.10 +// swift-tools-version: 5.9 import PackageDescription From 3a27ac470d0f2c82005a0f605c7481cf52a99735 Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Sun, 23 Jun 2024 19:49:06 +0200 Subject: [PATCH 04/43] Added get started screen, more core layout --- .../xcshareddata/xcschemes/SharedUI.xcscheme | 67 +++++++++ .../SharedUI/Sources/SharedUI/Colors.swift | 12 ++ Modules/SharedUI/Sources/SharedUI/Fonts.swift | 11 +- .../palletOrange.colorset/Contents.json | 20 +++ .../text.colorset/Contents.json | 38 +++++ .../title.colorset/Contents.json | 38 +++++ .../xcshareddata/xcschemes/WRCore.xcscheme | 67 +++++++++ WaiterRobot/Features/Core/ButtonStyles.swift | 2 +- WaiterRobot/Features/Login/LoginScreen.swift | 134 ++++++++++++++---- WaiterRobot/LaunchScreen.swift | 36 ++--- WaiterRobot/MainView.swift | 76 +++++----- 11 files changed, 408 insertions(+), 93 deletions(-) create mode 100644 Modules/SharedUI/.swiftpm/xcode/xcshareddata/xcschemes/SharedUI.xcscheme create mode 100644 Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/palletOrange.colorset/Contents.json create mode 100644 Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/text.colorset/Contents.json create mode 100644 Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/title.colorset/Contents.json create mode 100644 Modules/WRCore/.swiftpm/xcode/xcshareddata/xcschemes/WRCore.xcscheme diff --git a/Modules/SharedUI/.swiftpm/xcode/xcshareddata/xcschemes/SharedUI.xcscheme b/Modules/SharedUI/.swiftpm/xcode/xcshareddata/xcschemes/SharedUI.xcscheme new file mode 100644 index 0000000..46dd249 --- /dev/null +++ b/Modules/SharedUI/.swiftpm/xcode/xcshareddata/xcschemes/SharedUI.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Modules/SharedUI/Sources/SharedUI/Colors.swift b/Modules/SharedUI/Sources/SharedUI/Colors.swift index 2c806f7..81e9e9a 100644 --- a/Modules/SharedUI/Sources/SharedUI/Colors.swift +++ b/Modules/SharedUI/Sources/SharedUI/Colors.swift @@ -18,4 +18,16 @@ public extension Color { static var whiteBlack: Color { Color(.whiteBlack) } + + static var text: Color { + Color(.text) + } + + static var title: Color { + Color(.title) + } + + static var palletOrange: Color { + Color(.palletOrange) + } } diff --git a/Modules/SharedUI/Sources/SharedUI/Fonts.swift b/Modules/SharedUI/Sources/SharedUI/Fonts.swift index 62f23d4..7e9245b 100644 --- a/Modules/SharedUI/Sources/SharedUI/Fonts.swift +++ b/Modules/SharedUI/Sources/SharedUI/Fonts.swift @@ -20,7 +20,7 @@ public enum WrFont { case .h4: 24 case .body: - 16 + 18 case .caption1: 14 case .caption2: @@ -31,6 +31,7 @@ public enum WrFont { private struct WrTextStyle: ViewModifier { let fontStyle: WrFont + let textColor: Color @ScaledMetric var fontSize: CGFloat @@ -39,20 +40,22 @@ private struct WrTextStyle: ViewModifier { .system(size: fontSize) } - init(fontStyle: WrFont) { + init(fontStyle: WrFont, textColor: Color) { self.fontStyle = fontStyle + self.textColor = textColor _fontSize = ScaledMetric(wrappedValue: fontStyle.baseSize) } func body(content: Content) -> some View { content .font(font) + .foregroundStyle(textColor) } } public extension View { - func textStyle(_ font: WrFont) -> some View { - modifier(WrTextStyle(fontStyle: font)) + func textStyle(_ font: WrFont, textColor: Color = .text) -> some View { + modifier(WrTextStyle(fontStyle: font, textColor: textColor)) } } diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/palletOrange.colorset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/palletOrange.colorset/Contents.json new file mode 100644 index 0000000..970cbee --- /dev/null +++ b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/palletOrange.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x46", + "green" : "0x96", + "red" : "0xFA" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/text.colorset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/text.colorset/Contents.json new file mode 100644 index 0000000..1ff31b2 --- /dev/null +++ b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/text.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x47", + "green" : "0x23", + "red" : "0x1B" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/title.colorset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/title.colorset/Contents.json new file mode 100644 index 0000000..f810ebf --- /dev/null +++ b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/title.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x88", + "green" : "0x4C", + "red" : "0x40" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xBC", + "red" : "0xAC" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/WRCore/.swiftpm/xcode/xcshareddata/xcschemes/WRCore.xcscheme b/Modules/WRCore/.swiftpm/xcode/xcshareddata/xcschemes/WRCore.xcscheme new file mode 100644 index 0000000..39f1807 --- /dev/null +++ b/Modules/WRCore/.swiftpm/xcode/xcshareddata/xcschemes/WRCore.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WaiterRobot/Features/Core/ButtonStyles.swift b/WaiterRobot/Features/Core/ButtonStyles.swift index 1820d85..18535c3 100644 --- a/WaiterRobot/Features/Core/ButtonStyles.swift +++ b/WaiterRobot/Features/Core/ButtonStyles.swift @@ -17,7 +17,7 @@ struct WRBorderedProminentButtonStyle: ButtonStyle { .background( RoundedRectangle(cornerRadius: 10) .ifCondition(isEnabled) { view in - view.foregroundStyle(configuration.isPressed ? Color.main.opacity(0.6) : Color.main) + view.foregroundStyle(configuration.isPressed ? Color.main.opacity(0.6) : Color.accentColor) } .ifCondition(!isEnabled) { view in view.foregroundStyle(.gray) diff --git a/WaiterRobot/Features/Login/LoginScreen.swift b/WaiterRobot/Features/Login/LoginScreen.swift index ba66655..3a366b8 100644 --- a/WaiterRobot/Features/Login/LoginScreen.swift +++ b/WaiterRobot/Features/Login/LoginScreen.swift @@ -6,58 +6,140 @@ import UIPilot import WRCore struct LoginScreen: View { + @Namespace private var loginNamespace + @EnvironmentObject var navigator: UIPilot @StateObject private var viewModel = ObservableLoginViewModel() + @State private var showGetStartedButton = false + @State private var showLogin = false + + init() { + UIPageControl.appearance().currentPageIndicatorTintColor = UIColor(.accent) + UIPageControl.appearance().pageIndicatorTintColor = UIColor(.accent).withAlphaComponent(0.2) + } + var body: some View { - switch viewModel.state.viewState { - case is ViewState.Loading: - ProgressView() - case is ViewState.Idle: - content() - case let error as ViewState.Error: - content() - .alert(isPresented: Binding.constant(true)) { - Alert( - title: Text(error.title), - message: Text(error.message), - dismissButton: .cancel(Text("OK"), action: error.onDismiss) - ) - } - default: - fatalError("Unexpected ViewState: \(viewModel.state.viewState.description)") + ZStack { + switch viewModel.state.viewState { + case is ViewState.Loading: + ProgressView() + case is ViewState.Idle: + content() + case let error as ViewState.Error: + content() + .alert(isPresented: Binding.constant(true)) { + Alert( + title: Text(error.title), + message: Text(error.message), + dismissButton: .cancel(Text("OK"), action: error.onDismiss) + ) + } + default: + fatalError("Unexpected ViewState: \(viewModel.state.viewState.description)") + } } + .animation(.spring, value: showGetStartedButton) + .animation(.spring, value: showLogin) } + @ViewBuilder private func content() -> some View { + if showLogin { + loginContent() + } else { + getStarted() + } + } + + private func getStarted() -> some View { VStack { Spacer() - Image.logoRounded - .resizable() - .scaledToFit() - .frame(maxWidth: 250) - .padding() + logo() + + Spacer() + + Text("Willkommen bei") + .textStyle(.h3) + + HStack(spacing: 0) { + Text("kellner.") + .textStyle(.h2, textColor: .title) + + Text("team") + .textStyle(.h2, textColor: .palletOrange) + } + + Spacer() + + if showGetStartedButton { + Button { + showLogin = true + } label: { + Text("Get started") + .textStyle(.body, textColor: .white) + .frame(maxWidth: .infinity) + .padding() + } + .buttonStyle(.primary) + } + } + .padding() + .task { + do { + try await Task.sleep(seconds: 2) + showGetStartedButton = true + } catch {} + } + } + + private func loginContent() -> some View { + VStack { + Spacer() + + Spacer() + + logo() + Text(localize.login.title()) - .font(.title) + .textStyle(.h3) .padding() Text(localize.login.desc()) - .font(.body) + .textStyle(.body) .padding() .multilineTextAlignment(.center) + Spacer() + Button { viewModel.actual.openScanner() } label: { Label(localize.login.withQrCode(), systemImage: "qrcode.viewfinder") - .font(.title3) + .textStyle(.body, textColor: .white) + .padding() + .frame(maxWidth: .infinity) } + .buttonStyle(.primary) .padding() - - Spacer() } .withViewModel(viewModel, navigator) } + + private func logo() -> some View { + Image.logoRounded + .resizable() + .scaledToFit() + .frame(maxWidth: 250) + .padding() + .matchedGeometryEffect(id: "logo", in: loginNamespace) + } +} + +#Preview { + PreviewView { + LoginScreen() + } } diff --git a/WaiterRobot/LaunchScreen.swift b/WaiterRobot/LaunchScreen.swift index 0d6a7bf..a2012b7 100644 --- a/WaiterRobot/LaunchScreen.swift +++ b/WaiterRobot/LaunchScreen.swift @@ -1,5 +1,6 @@ import Foundation import shared +import SharedUI import SwiftUI import WRCore @@ -8,36 +9,18 @@ struct LaunchScreen: View { private let device = UIDevice.current.userInterfaceIdiom @State private var startupFinished = false + @State private var isVisible = false var body: some View { ZStack { - if case .phone = device { - VStack { - Spacer() - - Image.logoLaunch - .resizable() - .scaledToFit() - } - .padding(.horizontal, -2) - .ignoresSafeArea() - } else { - ZStack { - Image.logoLaunch - .resizable() - .scaledToFit() - .frame(width: 150) - .padding() - - VStack { - Spacer() - - ProgressView() - .padding() - .padding(.bottom) - } - } + VStack { + Image.logoRounded + .resizable() + .scaledToFit() + .padding() + .frame(maxWidth: 300) } + .padding() if startupFinished { MainView() @@ -57,6 +40,7 @@ struct LaunchScreen: View { } } } + .animation(.spring, value: startupFinished) } private func delay() async { diff --git a/WaiterRobot/MainView.swift b/WaiterRobot/MainView.swift index 830765e..17d12e1 100644 --- a/WaiterRobot/MainView.swift +++ b/WaiterRobot/MainView.swift @@ -36,42 +36,7 @@ struct MainView: View { var body: some View { ZStack { - UIPilotHost(navigator) { route in - switch onEnum(of: route) { - case .loginScreen: - LoginScreen() - - case .tableListScreen: - TableListScreen() - - case .switchEventScreen: - SwitchEventScreen() - - case .settingsScreen: - SettingsScreen() - - case let .registerScreen(screen): - RegisterScreen(deepLink: screen.registerLink) - - case .updateApp: - UpdateAppScreen() - - case let .tableDetailScreen(screen): - TableDetailScreen(table: screen.table) - - case let .orderScreen(screen): - OrderScreen(table: screen.table, initialItemId: screen.initialItemId) - - case let .billingScreen(screen): - BillingScreen(table: screen.table) - - case .loginScannerScreen: - LoginScannerScreen() - - case .stripeInitializationScreen: - EmptyView() - } - } + resolvedView() } .preferredColorScheme(selectedScheme) .overlay(alignment: .bottom) { @@ -136,6 +101,45 @@ struct MainView: View { } } } + + private func resolvedView() -> some View { + UIPilotHost(navigator) { route in + switch onEnum(of: route) { + case .loginScreen: + LoginScreen() + + case .tableListScreen: + TableListScreen() + + case .switchEventScreen: + SwitchEventScreen() + + case .settingsScreen: + SettingsScreen() + + case let .registerScreen(screen): + RegisterScreen(deepLink: screen.registerLink) + + case .updateApp: + UpdateAppScreen() + + case let .tableDetailScreen(screen): + TableDetailScreen(table: screen.table) + + case let .orderScreen(screen): + OrderScreen(table: screen.table, initialItemId: screen.initialItemId) + + case let .billingScreen(screen): + BillingScreen(table: screen.table) + + case .loginScannerScreen: + LoginScannerScreen() + + case .stripeInitializationScreen: + EmptyView() + } + } + } } #Preview { From 49a17223710412793ca6a530c8b34c14246cf89b Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Sun, 23 Jun 2024 22:48:34 +0200 Subject: [PATCH 05/43] Rewrote some parts in the launch process --- .../title.colorset/Contents.json | 6 +- Modules/WRCore/Sources/WRCore/Globals.swift | 12 ++++ WaiterRobot/Features/Core/DynamicGrid.swift | 62 +++++++++---------- WaiterRobot/Features/Login/LoginScreen.swift | 2 +- .../Features/TableList/TableListScreen.swift | 18 +++++- WaiterRobot/LaunchScreen.swift | 57 +++++++++++------ WaiterRobot/Resources/LaunchScreen.storyboard | 47 ++++++++++++++ project.yml | 2 +- 8 files changed, 148 insertions(+), 58 deletions(-) create mode 100644 WaiterRobot/Resources/LaunchScreen.storyboard diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/title.colorset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/title.colorset/Contents.json index f810ebf..5fb9a4d 100644 --- a/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/title.colorset/Contents.json +++ b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/title.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x88", - "green" : "0x4C", - "red" : "0x40" + "blue" : "0x47", + "green" : "0x23", + "red" : "0x1B" } }, "idiom" : "universal" diff --git a/Modules/WRCore/Sources/WRCore/Globals.swift b/Modules/WRCore/Sources/WRCore/Globals.swift index a9eb3db..9966014 100644 --- a/Modules/WRCore/Sources/WRCore/Globals.swift +++ b/Modules/WRCore/Sources/WRCore/Globals.swift @@ -1,5 +1,6 @@ import Foundation import shared +import SwiftUI import UIKit public var koin: IosKoinComponent { IosKoinComponent.shared } @@ -36,9 +37,20 @@ public enum WRCore { private static func readFromInfoPlist(withKey key: String) -> String { guard let value = Bundle.main.infoDictionary?[key] as? String else { + print("ERROR") fatalError("Could not find key '\(key)' in info.plist file.") } return value } } + +public extension EnvironmentValues { + var isPreview: Bool { + #if DEBUG + return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" + #else + return false + #endif + } +} diff --git a/WaiterRobot/Features/Core/DynamicGrid.swift b/WaiterRobot/Features/Core/DynamicGrid.swift index 43d8a06..10f6adc 100644 --- a/WaiterRobot/Features/Core/DynamicGrid.swift +++ b/WaiterRobot/Features/Core/DynamicGrid.swift @@ -67,7 +67,6 @@ public struct DynamicGrid: Layout, Sendable { maxYinRow = 0 } -// print("Will place item \(index) at x:\(x) y:\(y)") subviews[index] .place( at: CGPoint(x: x, y: y), @@ -81,40 +80,37 @@ public struct DynamicGrid: Layout, Sendable { } } +@available(iOS 16.0, *) #Preview { - if #available(iOS 16, *) { - ScrollView { - VStack { - DynamicGrid(horizontalSpacing: 10, verticalSpacing: 10) { - Rectangle() - .foregroundColor(.brown) - .frame(width: 100, height: 50) - Rectangle() - .foregroundColor(.yellow) - .frame(width: 80, height: 20) - Rectangle() - .foregroundColor(.green) - .frame(width: 100, height: 60) - Rectangle() - .foregroundColor(.brown) - .frame(width: 100, height: 50) - Rectangle() - .foregroundColor(.yellow) - .frame(width: 250, height: 20) - Rectangle() - .foregroundColor(.green) - .frame(width: 100, height: 60) - Rectangle() - .foregroundColor(.blue) - .frame(width: 200, height: 50) - Rectangle() - .foregroundColor(.gray) - .frame(width: 200, height: 110) - } + ScrollView { + VStack { + DynamicGrid(horizontalSpacing: 10, verticalSpacing: 10) { + Rectangle() + .foregroundColor(.brown) + .frame(width: 100, height: 50) + Rectangle() + .foregroundColor(.yellow) + .frame(width: 80, height: 20) + Rectangle() + .foregroundColor(.green) + .frame(width: 100, height: 60) + Rectangle() + .foregroundColor(.brown) + .frame(width: 100, height: 50) + Rectangle() + .foregroundColor(.yellow) + .frame(width: 250, height: 20) + Rectangle() + .foregroundColor(.green) + .frame(width: 100, height: 60) + Rectangle() + .foregroundColor(.blue) + .frame(width: 200, height: 50) + Rectangle() + .foregroundColor(.gray) + .frame(width: 200, height: 110) } - .padding() } - } else { - EmptyView() + .padding() } } diff --git a/WaiterRobot/Features/Login/LoginScreen.swift b/WaiterRobot/Features/Login/LoginScreen.swift index 3a366b8..5223892 100644 --- a/WaiterRobot/Features/Login/LoginScreen.swift +++ b/WaiterRobot/Features/Login/LoginScreen.swift @@ -89,7 +89,7 @@ struct LoginScreen: View { .padding() .task { do { - try await Task.sleep(seconds: 2) + try await Task.sleep(seconds: 1) showGetStartedButton = true } catch {} } diff --git a/WaiterRobot/Features/TableList/TableListScreen.swift b/WaiterRobot/Features/TableList/TableListScreen.swift index 2ff6b73..28c960e 100644 --- a/WaiterRobot/Features/TableList/TableListScreen.swift +++ b/WaiterRobot/Features/TableList/TableListScreen.swift @@ -38,7 +38,23 @@ struct TableListScreen: View { } } } - .navigationTitle(CommonApp.shared.settings.eventName) + .toolbar { + ToolbarItem(placement: .principal) { + VStack { + HStack(spacing: 0) { + Text("kellner.") + .textStyle(.h4, textColor: .title) + + Text("team") + .textStyle(.h4, textColor: .palletOrange) + } + + Text(CommonApp.shared.settings.eventName) + .textStyle(.caption1) + .padding(.bottom, 6) + } + } + } .navigationBarTitleDisplayMode(.inline) .animation(.spring, value: viewModel.state.tableGroupsArray) .withViewModel(viewModel, navigator) diff --git a/WaiterRobot/LaunchScreen.swift b/WaiterRobot/LaunchScreen.swift index a2012b7..43f1882 100644 --- a/WaiterRobot/LaunchScreen.swift +++ b/WaiterRobot/LaunchScreen.swift @@ -5,42 +5,61 @@ import SwiftUI import WRCore struct LaunchScreen: View { + @Environment(\.isPreview) private var isPreview + private let minimumOnScreenTimeSeconds = 3.0 - private let device = UIDevice.current.userInterfaceIdiom @State private var startupFinished = false - @State private var isVisible = false + @State private var showProgressView = false var body: some View { ZStack { - VStack { + VStack(spacing: 0) { Image.logoRounded .resizable() .scaledToFit() - .padding() - .frame(maxWidth: 300) + .frame(width: 280, height: 280) + .ignoresSafeArea() + .padding(.bottom, 23) + .transition(.slide) + + if showProgressView { + ProgressView() + .padding() + .foregroundStyle(.green) + } } - .padding() if startupFinished { MainView() } } - .onAppear { + .animation(.spring, value: startupFinished) + .animation(.spring, value: showProgressView) + .task { + defer { + print("Show progress") + showProgressView = true + } + + do { + try await Task.sleep(seconds: 0.1) + } catch {} + } + .task { // This is needed otherwise previews will crash randomly - if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] != "1" { - Task { - async let setup: () = WRCore.setup() - async let delay: () = delay() + if !isPreview { + async let setup: () = WRCore.setup() + async let delay: () = delay() - await setup - await delay + await setup + await delay - startupFinished = true - } + startupFinished = true + } else { + print("Running from preview, skipping init") } } - .animation(.spring, value: startupFinished) } private func delay() async { @@ -51,7 +70,7 @@ struct LaunchScreen: View { } #Preview { - PreviewView { - LaunchScreen() - } +// PreviewView { + LaunchScreen() +// } } diff --git a/WaiterRobot/Resources/LaunchScreen.storyboard b/WaiterRobot/Resources/LaunchScreen.storyboard new file mode 100644 index 0000000..342bbc9 --- /dev/null +++ b/WaiterRobot/Resources/LaunchScreen.storyboard @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project.yml b/project.yml index 0958039..885b87f 100644 --- a/project.yml +++ b/project.yml @@ -76,7 +76,7 @@ targetTemplates: CFBundleName: "${target_name}" CFBundlePackageType: "$(PRODUCT_BUNDLE_PACKAGE_TYPE)" ITSAppUsesNonExemptEncryption: false - UILaunchScreen: {} + UILaunchStoryboardName: "LaunchScreen.storyboard" NSAppTransportSecurity: NSAllowsLocalNetworking: true NSCameraUsageDescription: "Camera is needed to scan QR-Codes" From 78c374fa5b87c8baa7ddf2916e1d9924e4dbe0a0 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Wed, 18 Sep 2024 17:37:32 +0200 Subject: [PATCH 06/43] chore: Bump version to 2.4.1 --- project.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project.yml b/project.yml index 21a81a5..f4efd64 100644 --- a/project.yml +++ b/project.yml @@ -48,10 +48,10 @@ targetTemplates: info: path: ".generated/${target_name}.plist" properties: - CFBundleShortVersionString: "2.4.0" + CFBundleShortVersionString: "2.4.1" # Generate VersionCode from VersionName (major * 10_000 + minor * 100 + patch, e.g. 1.2.3 -> 10203, 1.23.45 -> 12345) # Only used for prod releases. Lava uses epochMinute (same as on Android) - CFBundleVersion: "20400" + CFBundleVersion: "20401" VERSION_SUFFIX: "${versionSuffix}" ALLOWED_HOSTS: "${allowedHosts}" From 19cee61cb9961a152e8cb867a838b7c150153e13 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Sat, 28 Sep 2024 16:03:39 +0200 Subject: [PATCH 07/43] WR-36: Add Sentry --- project.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/project.yml b/project.yml index f4efd64..8b58cf7 100644 --- a/project.yml +++ b/project.yml @@ -24,6 +24,9 @@ packages: CodeScanner: url: https://github.com/twostraws/CodeScanner from: 2.2.1 + Sentry: + url: https://github.com/getsentry/sentry-cocoa.git + version: 8.36.0 # must be updated together with the "Sentry-Dynamic.xcframework" in the shared project settings: base: @@ -44,6 +47,8 @@ targetTemplates: - package: shared - package: UIPilot - package: CodeScanner + - package: Sentry + product: Sentry-Dynamic info: path: ".generated/${target_name}.plist" From 81e8e30dc7fae377df8bd7dafa2f52beca9a4003 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Sat, 28 Sep 2024 16:58:13 +0200 Subject: [PATCH 08/43] WR-36: Refactor CommonApp initialization --- WaiterRobot/WaiterRobotApp.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/WaiterRobot/WaiterRobotApp.swift b/WaiterRobot/WaiterRobotApp.swift index a695966..ba7df08 100644 --- a/WaiterRobot/WaiterRobotApp.swift +++ b/WaiterRobot/WaiterRobotApp.swift @@ -24,12 +24,11 @@ struct WaiterRobotApp: App { phoneModel: UIDevice.current.model, os: OS.Ios(version: UIDevice.current.systemVersion), allowedHostsCsv: readFromInfoPlist(withKey: "ALLOWED_HOSTS"), - stripeProvider: nil + stripeProvider: nil, + koinPlatformDeclaration: nil ) - KoinKt.doInitKoinIos() let logger = koin.logger(tag: "AppDelegate") - logger.d { "initialized Koin" } KMMResourcesLocalizationKt.localizationBundle = Bundle(for: shared.L.self) logger.d { "initialized localization bundle" } From 62fab7aba487441b6d76d7db1be4fca92d21a72c Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Sat, 28 Sep 2024 17:11:11 +0200 Subject: [PATCH 09/43] WR-26: Add debug login dialog --- WaiterRobot/Ui/Login/LoginScreen.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/WaiterRobot/Ui/Login/LoginScreen.swift b/WaiterRobot/Ui/Login/LoginScreen.swift index 7f1829d..c2350c9 100644 --- a/WaiterRobot/Ui/Login/LoginScreen.swift +++ b/WaiterRobot/Ui/Login/LoginScreen.swift @@ -7,6 +7,8 @@ struct LoginScreen: View { @EnvironmentObject var navigator: UIPilot @StateObject private var viewModel = ObservableLoginViewModel() + @State private var showLinkInput = false + @State private var debugLoginLink = "" var body: some View { switch viewModel.state.viewState { @@ -37,6 +39,9 @@ struct LoginScreen: View { .scaledToFit() .frame(maxWidth: 250) .padding() + .onLongPressGesture { + showLinkInput = true + } Text(localize.login.title()) .font(.title) .padding() @@ -56,6 +61,15 @@ struct LoginScreen: View { Spacer() } + .alert(localize.login.title(), isPresented: $showLinkInput) { + TextField(localize.login.debugDialog.inputLabel(), text: $debugLoginLink) + Button(localize.dialog.cancel(), role: .cancel) { + showLinkInput = false + } + Button(localize.login.title()) { + viewModel.actual.onDebugLogin(link: debugLoginLink) + } + } .withViewModel(viewModel, navigator) } } From 46907c7f0d69a97436fc07967f3e8e9b4e55a5c1 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Sat, 9 Nov 2024 16:30:30 +0100 Subject: [PATCH 10/43] WR-36: Increment shared version --- project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.yml b/project.yml index 8b58cf7..ecfc1a1 100644 --- a/project.yml +++ b/project.yml @@ -17,7 +17,7 @@ fileGroups: packages: shared: url: https://github.com/DatepollSystems/WaiterRobot-Shared-Android.git - version: 1.6.9 + version: 1.6.10 UIPilot: url: https://github.com/canopas/UIPilot.git from: 1.3.1 From b6291a6276d9d5934fc2f72585194c7c3d94a584 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Sat, 9 Nov 2024 17:09:15 +0100 Subject: [PATCH 11/43] Add Slack release Notification --- .github/workflows/publish.yml | 1 + fastlane/Fastfile | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ced3aa2..e61f212 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -63,3 +63,4 @@ jobs: FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }} MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index e827f22..0b0e801 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -95,6 +95,7 @@ platform :ios do version_tag = version + "-lava-" + build_number.to_s add_git_tag(tag: version_tag) push_git_tags(tag: version_tag) + sendSlackMessage(version: version_tag) end desc "Push a new prod build to TestFlight" @@ -123,6 +124,7 @@ platform :ios do # e.g. 2.0.2 add_git_tag(tag: version) push_git_tags(tag: version) + sendSlackMessage(version: version) end lane :setupFastlaneSecrets do |options| @@ -151,4 +153,43 @@ platform :ios do key_filepath: "./fastlane/.keys/api_key.p8", ) end + + desc "Send a notification to Slack" + lande :sendSlackMessage do |options| + ensure_env_vars( + env_vars: ["SLACK_WEBHOOK_URL"] + ) + + slack_webhook_url = ENV['SLACK_WEB_HOOK_URL'] + version = options[:version] + + json_body = <<-JSON + { + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "🚀 New iOS release available", + "emoji": true + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Version:* `#{version}`" + } + }, + ] + } + JSON + + sh <<-EOS + curl -X POST \ + --header 'Content-type: application/json' \ + --url "#{slack_webhook_url}" \ + --data '#{json_body}' + EOS + end end From e48ff5af67731ad47cb74ac94f5edfbefe35e205 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Sat, 9 Nov 2024 17:12:30 +0100 Subject: [PATCH 12/43] Fix typo --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0b0e801..1f7a9fe 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -155,7 +155,7 @@ platform :ios do end desc "Send a notification to Slack" - lande :sendSlackMessage do |options| + lane :sendSlackMessage do |options| ensure_env_vars( env_vars: ["SLACK_WEBHOOK_URL"] ) From 5ab3199027c83cf9d43e4385e8e8417ad783ca21 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Mon, 11 Nov 2024 09:01:23 +0100 Subject: [PATCH 13/43] Add environment to message --- fastlane/Fastfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 1f7a9fe..735bfd6 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -95,7 +95,7 @@ platform :ios do version_tag = version + "-lava-" + build_number.to_s add_git_tag(tag: version_tag) push_git_tags(tag: version_tag) - sendSlackMessage(version: version_tag) + sendSlackMessage(version: version_tag, env: "lava") end desc "Push a new prod build to TestFlight" @@ -124,7 +124,7 @@ platform :ios do # e.g. 2.0.2 add_git_tag(tag: version) push_git_tags(tag: version) - sendSlackMessage(version: version) + sendSlackMessage(version: version, env: "production") end lane :setupFastlaneSecrets do |options| @@ -162,6 +162,7 @@ platform :ios do slack_webhook_url = ENV['SLACK_WEB_HOOK_URL'] version = options[:version] + environment = options[:env] json_body = <<-JSON { @@ -170,7 +171,7 @@ platform :ios do "type": "header", "text": { "type": "plain_text", - "text": "🚀 New iOS release available", + "text": "🚀 New iOS #{environment} version available on TestFlight", "emoji": true } }, From 03326fa18b1e17282a3ba522acc3856eeef43784 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Mon, 11 Nov 2024 09:15:28 +0100 Subject: [PATCH 14/43] Fix Slack Notification --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 735bfd6..c390fe4 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -160,7 +160,7 @@ platform :ios do env_vars: ["SLACK_WEBHOOK_URL"] ) - slack_webhook_url = ENV['SLACK_WEB_HOOK_URL'] + slack_webhook_url = ENV['SLACK_WEBHOOK_URL'] version = options[:version] environment = options[:env] From f105c670db19625a790858f0f85577fb37503170 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Mon, 11 Nov 2024 12:14:18 +0100 Subject: [PATCH 15/43] Fix README.md image link --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a06c092..899d38c 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@

kellner.team

Lightning fast and simple gastronomy

- - Download on the App Store + + Download on the App Store
From 242722fe43b958547ca308f0d9498ddb7fd36b09 Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Mon, 11 Nov 2024 21:08:48 +0100 Subject: [PATCH 16/43] Update ruby and fastlane --- .github/workflows/publish.yml | 6 +-- .github/workflows/test.yml | 4 +- .ruby-version | 2 +- Gemfile | 2 +- Gemfile.lock | 70 ++++++++++++++++++----------------- 5 files changed, 44 insertions(+), 40 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e61f212..090accf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,12 +23,12 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 - - name: Setup Ruby 3.3.0 + - name: Setup Ruby 3.3.4 uses: ruby/setup-ruby@v1 with: - ruby-version: '3.3.0' + ruby-version: '3.3.4' bundler-cache: true - + - name: Netrc maven.pkg.github.com uses: extractions/netrc@v2 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 03d5e9e..2eada67 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,10 +13,10 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 - - name: Setup Ruby 3.3.0 + - name: Setup Ruby 3.3.4 uses: ruby/setup-ruby@v1 with: - ruby-version: '3.3.0' + ruby-version: '3.3.4' bundler-cache: true - name: Netrc api.github.com diff --git a/.ruby-version b/.ruby-version index bea438e..a0891f5 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.3.1 +3.3.4 diff --git a/Gemfile b/Gemfile index d5defb3..a627214 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source 'https://rubygems.org' -gem 'fastlane', '2.220.0' +gem 'fastlane' diff --git a/Gemfile.lock b/Gemfile.lock index de6a2cb..22b7cc4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,25 +5,25 @@ GEM base64 nkf rexml - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.926.0) - aws-sdk-core (3.194.2) + aws-partitions (1.1004.0) + aws-sdk-core (3.212.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.80.0) - aws-sdk-core (~> 3, >= 3.193.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.149.1) - aws-sdk-core (~> 3, >= 3.194.0) + aws-sdk-kms (1.95.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.170.1) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -38,8 +38,8 @@ GEM domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.110.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -60,15 +60,15 @@ GEM faraday-httpclient (1.0.1) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) - fastlane (2.220.0) + fastlane (2.225.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -84,6 +84,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -109,6 +110,8 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) @@ -126,7 +129,7 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.0) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) @@ -147,31 +150,31 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.7) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.7.2) - jwt (2.8.1) + json (2.8.1) + jwt (2.9.3) base64 - mini_magick (4.12.0) + mini_magick (4.13.2) mini_mime (1.1.5) multi_json (1.15.0) multipart-post (2.4.1) - nanaimo (0.3.0) + nanaimo (0.4.0) naturally (2.2.1) nkf (0.2.0) - optparse (0.5.0) + optparse (0.6.0) os (1.1.4) plist (3.7.1) - public_suffix (5.0.5) + public_suffix (6.0.1) rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.6) + rexml (3.3.9) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -184,6 +187,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -193,15 +197,15 @@ GEM tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.24.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (~> 3.2.4) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) @@ -214,7 +218,7 @@ PLATFORMS x86_64-darwin-20 DEPENDENCIES - fastlane (= 2.220.0) + fastlane BUNDLED WITH - 2.5.10 + 2.5.19 From 85dd35b3a3a87eaf2c4a7fa14f655b1ae27a75c3 Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Mon, 11 Nov 2024 21:14:00 +0100 Subject: [PATCH 17/43] Update to ruby 3.3.6 --- .github/workflows/publish.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- .ruby-version | 2 +- Gemfile.lock | 3 ++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 090accf..16b6177 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,10 +23,10 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 - - name: Setup Ruby 3.3.4 + - name: Setup Ruby 3.3.6 uses: ruby/setup-ruby@v1 with: - ruby-version: '3.3.4' + ruby-version: '3.3.6' bundler-cache: true - name: Netrc maven.pkg.github.com diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2eada67..d945e76 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,10 +13,10 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 - - name: Setup Ruby 3.3.4 + - name: Setup Ruby 3.3.6 uses: ruby/setup-ruby@v1 with: - ruby-version: '3.3.4' + ruby-version: '3.3.6' bundler-cache: true - name: Netrc api.github.com diff --git a/.ruby-version b/.ruby-version index a0891f5..9c25013 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.3.4 +3.3.6 diff --git a/Gemfile.lock b/Gemfile.lock index 22b7cc4..144eb2c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -214,6 +214,7 @@ GEM PLATFORMS arm64-darwin-22 arm64-darwin-23 + arm64-darwin-24 x86_64-darwin-19 x86_64-darwin-20 @@ -221,4 +222,4 @@ DEPENDENCIES fastlane BUNDLED WITH - 2.5.19 + 2.5.22 From 2ff73c38bd365bb6eb9ba62306986f66c3f82332 Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Mon, 11 Nov 2024 21:19:41 +0100 Subject: [PATCH 18/43] Updated CLI Swift Packages --- Package.resolved | 21 ++++++--------------- Package.swift | 4 ++-- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/Package.resolved b/Package.resolved index 95ed5e2..174fd27 100644 --- a/Package.resolved +++ b/Package.resolved @@ -9,15 +9,6 @@ "version" : "4.6.1" } }, - { - "identity" : "graphviz", - "kind" : "remoteSourceControl", - "location" : "https://github.com/SwiftDocOrg/GraphViz.git", - "state" : { - "revision" : "70bebcf4597b9ce33e19816d6bbd4ba9b7bdf038", - "version" : "0.2.0" - } - }, { "identity" : "jsonutilities", "kind" : "remoteSourceControl", @@ -68,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nicklockwood/SwiftFormat", "state" : { - "revision" : "ab238886b8b50f8b678b251f3c28c0c887305407", - "version" : "0.53.8" + "revision" : "86ed20990585f478c0daf309af645c2a528b59d8", + "version" : "0.54.6" } }, { @@ -86,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/yonaskolb/XcodeGen.git", "state" : { - "revision" : "9816466703aede482c7436fddc6535684a7a9168", - "version" : "2.40.1" + "revision" : "82c6ab9bbd5b6075fc0887d897733fc0c4ffc9ab", + "version" : "2.42.0" } }, { @@ -95,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tuist/XcodeProj.git", "state" : { - "revision" : "6e60fb55271c80f83a186c9b1b4982fd991cfc0a", - "version" : "8.13.0" + "revision" : "447c159b0c5fb047a024fd8d942d4a76cf47dde0", + "version" : "8.16.0" } }, { diff --git a/Package.swift b/Package.swift index 3a07dcb..fa00b5e 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,7 @@ let package = Package( .macOS(.v10_15), ], dependencies: [ - .package(url: "https://github.com/yonaskolb/XcodeGen.git", from: "2.40.0"), - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.53.8"), + .package(url: "https://github.com/yonaskolb/XcodeGen.git", from: "2.42.0"), + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.54.6"), ] ) From 55e9ac9fcae8c1f2de3e52b897f4104d3b9da636 Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Sat, 16 Nov 2024 23:05:34 +0100 Subject: [PATCH 19/43] Removed shared repo from app target --- project.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/project.yml b/project.yml index aeac622..72ee0da 100644 --- a/project.yml +++ b/project.yml @@ -22,9 +22,6 @@ packages: path: Modules/SharedUI WRCore: path: Modules/WRCore - shared: - url: https://github.com/DatepollSystems/WaiterRobot-Shared-Android.git - version: 1.6.10 UIPilot: url: https://github.com/canopas/UIPilot.git from: 1.3.1 @@ -51,7 +48,6 @@ targetTemplates: group: "WaiterRobot/Resources" dependencies: - - package: shared - package: UIPilot - package: CodeScanner - package: SharedUI From aec5d1dde69b8e512c8b603e300dcb0130a8dfee Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Sun, 17 Nov 2024 08:32:09 +0100 Subject: [PATCH 20/43] Update to ruby 3.3.6 --- .github/workflows/publish.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- .ruby-version | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e61f212..3a6a10c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,10 +23,10 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 - - name: Setup Ruby 3.3.0 + - name: Setup Ruby 3.3.6 uses: ruby/setup-ruby@v1 with: - ruby-version: '3.3.0' + ruby-version: '3.3.6' bundler-cache: true - name: Netrc maven.pkg.github.com diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 03d5e9e..d945e76 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,10 +13,10 @@ jobs: - name: Checkout repo uses: actions/checkout@v4 - - name: Setup Ruby 3.3.0 + - name: Setup Ruby 3.3.6 uses: ruby/setup-ruby@v1 with: - ruby-version: '3.3.0' + ruby-version: '3.3.6' bundler-cache: true - name: Netrc api.github.com diff --git a/.ruby-version b/.ruby-version index bea438e..9c25013 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.3.1 +3.3.6 From aba4c8af89d93db6b8d23fded7a687ba2ef890db Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Tue, 18 Feb 2025 18:26:57 +0100 Subject: [PATCH 21/43] fixed gitignore --- .gitignore | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 5b17b5a..2b28786 100644 --- a/.gitignore +++ b/.gitignore @@ -244,14 +244,11 @@ Temporary Items # End of https://www.toptal.com/developers/gitignore/api/fastlane,xcode,intellij,appcode,macos -# Xcodegen -.generated/ -WaiterRobot.xcodeproj/* -!WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm - .keys* .env.* !.env.example .idea fastlane/.env + +WaiterRobot.xcodeproj/project.xcworkspace/xcuserdata From 1ae79b5e82077035663a0561c183ffbb7221203e Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Tue, 18 Feb 2025 18:33:13 +0100 Subject: [PATCH 22/43] Track project file again, fixed build issues, applied folder structure in Xocde --- .gitignore | 1 + .../AppIcon.appiconset/100.png | Bin .../AppIcon.appiconset/1024.png | Bin .../AppIcon.appiconset/114.png | Bin .../AppIcon.appiconset/120.png | Bin .../AppIcon.appiconset/144.png | Bin .../AppIcon.appiconset/152.png | Bin .../AppIcon.appiconset/167.png | Bin .../AppIcon.appiconset/180.png | Bin .../Images.xcassets/AppIcon.appiconset/20.png | Bin .../Images.xcassets/AppIcon.appiconset/29.png | Bin .../Images.xcassets/AppIcon.appiconset/40.png | Bin .../Images.xcassets/AppIcon.appiconset/50.png | Bin .../Images.xcassets/AppIcon.appiconset/57.png | Bin .../Images.xcassets/AppIcon.appiconset/58.png | Bin .../Images.xcassets/AppIcon.appiconset/60.png | Bin .../Images.xcassets/AppIcon.appiconset/72.png | Bin .../Images.xcassets/AppIcon.appiconset/76.png | Bin .../Images.xcassets/AppIcon.appiconset/80.png | Bin .../Images.xcassets/AppIcon.appiconset/87.png | Bin .../AppIcon.appiconset/Contents.json | 0 Targets/Lava/WaiterRobotLava.plist | 72 ++ .../AppIcon.appiconset/100.png | Bin .../AppIcon.appiconset/1024.png | Bin .../AppIcon.appiconset/114.png | Bin .../AppIcon.appiconset/120.png | Bin .../AppIcon.appiconset/144.png | Bin .../AppIcon.appiconset/152.png | Bin .../AppIcon.appiconset/167.png | Bin .../AppIcon.appiconset/180.png | Bin .../Images.xcassets/AppIcon.appiconset/20.png | Bin .../Images.xcassets/AppIcon.appiconset/29.png | Bin .../Images.xcassets/AppIcon.appiconset/40.png | Bin .../Images.xcassets/AppIcon.appiconset/50.png | Bin .../Images.xcassets/AppIcon.appiconset/57.png | Bin .../Images.xcassets/AppIcon.appiconset/58.png | Bin .../Images.xcassets/AppIcon.appiconset/60.png | Bin .../Images.xcassets/AppIcon.appiconset/72.png | Bin .../Images.xcassets/AppIcon.appiconset/76.png | Bin .../Images.xcassets/AppIcon.appiconset/80.png | Bin .../Images.xcassets/AppIcon.appiconset/87.png | Bin .../AppIcon.appiconset/Contents.json | 0 .../Prod}/Images.xcassets/Contents.json | 0 Targets/Prod/WaiterRobot.plist | 72 ++ WaiterRobot.xcodeproj/project.pbxproj | 724 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 41 + .../xcschemes/WaiterRobot.xcscheme | 87 +++ .../xcschemes/WaiterRobotLava.xcscheme | 98 +++ project.yml | 161 ---- 51 files changed, 1110 insertions(+), 161 deletions(-) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/100.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/1024.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/114.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/120.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/144.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/152.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/167.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/180.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/20.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/29.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/40.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/50.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/57.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/58.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/60.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/72.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/76.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/80.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/87.png (100%) rename {TargetSpecificResources/WaiterRobotLava => Targets/Lava}/Images.xcassets/AppIcon.appiconset/Contents.json (100%) create mode 100644 Targets/Lava/WaiterRobotLava.plist rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/100.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/1024.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/114.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/120.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/144.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/152.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/167.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/180.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/20.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/29.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/40.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/50.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/57.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/58.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/60.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/72.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/76.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/80.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/87.png (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/AppIcon.appiconset/Contents.json (100%) rename {TargetSpecificResources/WaiterRobot => Targets/Prod}/Images.xcassets/Contents.json (100%) create mode 100644 Targets/Prod/WaiterRobot.plist create mode 100644 WaiterRobot.xcodeproj/project.pbxproj create mode 100644 WaiterRobot.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 WaiterRobot.xcodeproj/xcshareddata/xcschemes/WaiterRobot.xcscheme create mode 100644 WaiterRobot.xcodeproj/xcshareddata/xcschemes/WaiterRobotLava.xcscheme delete mode 100644 project.yml diff --git a/.gitignore b/.gitignore index 2b28786..b6dfbce 100644 --- a/.gitignore +++ b/.gitignore @@ -252,3 +252,4 @@ Temporary Items fastlane/.env WaiterRobot.xcodeproj/project.xcworkspace/xcuserdata +WaiterRobot.xcodeproj/xcuserdata diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/100.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/100.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/100.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/100.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/1024.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/1024.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/1024.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/1024.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/114.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/114.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/114.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/114.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/120.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/120.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/120.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/120.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/144.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/144.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/144.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/144.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/152.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/152.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/152.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/152.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/167.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/167.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/167.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/167.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/180.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/180.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/180.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/180.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/20.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/20.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/20.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/20.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/29.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/29.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/29.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/29.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/40.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/40.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/40.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/40.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/50.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/50.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/50.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/50.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/57.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/57.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/57.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/57.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/58.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/58.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/58.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/58.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/60.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/60.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/60.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/60.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/72.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/72.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/72.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/72.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/76.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/76.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/76.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/76.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/80.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/80.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/80.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/80.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/87.png b/Targets/Lava/Images.xcassets/AppIcon.appiconset/87.png similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/87.png rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/87.png diff --git a/TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/Contents.json b/Targets/Lava/Images.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from TargetSpecificResources/WaiterRobotLava/Images.xcassets/AppIcon.appiconset/Contents.json rename to Targets/Lava/Images.xcassets/AppIcon.appiconset/Contents.json diff --git a/Targets/Lava/WaiterRobotLava.plist b/Targets/Lava/WaiterRobotLava.plist new file mode 100644 index 0000000..321fb5f --- /dev/null +++ b/Targets/Lava/WaiterRobotLava.plist @@ -0,0 +1,72 @@ + + + + + ALLOWED_HOSTS + * + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + lava.kellner.team + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + org.datepollsystems.waiterrobot.beta + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLocalizations + + en + de + + CFBundleName + WaiterRobotLava + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 2.4.1 + CFBundleVersion + 28998325 + ITSAppUsesNonExemptEncryption + + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + NSBluetoothAlwaysUsageDescription + We don't use bluetooth + NSCameraUsageDescription + Camera is needed to scan QR-Codes + NSContactsUsageDescription + We don't use your contacts + NSLocationWhenInUseUsageDescription + We don't use your location + NSMotionUsageDescription + We don't use your motion sensors + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchStoryboardName + LaunchScreen.storyboard + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + VERSION_SUFFIX + lava + + diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/100.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/100.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/100.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/100.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/1024.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/1024.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/1024.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/1024.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/114.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/114.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/114.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/114.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/120.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/120.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/120.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/120.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/144.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/144.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/144.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/144.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/152.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/152.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/152.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/152.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/167.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/167.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/167.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/167.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/180.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/180.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/180.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/180.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/20.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/20.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/20.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/20.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/29.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/29.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/29.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/29.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/40.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/40.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/40.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/40.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/50.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/50.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/50.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/50.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/57.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/57.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/57.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/57.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/58.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/58.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/58.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/58.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/60.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/60.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/60.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/60.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/72.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/72.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/72.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/72.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/76.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/76.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/76.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/76.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/80.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/80.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/80.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/80.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/87.png b/Targets/Prod/Images.xcassets/AppIcon.appiconset/87.png similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/87.png rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/87.png diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/Contents.json b/Targets/Prod/Images.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/AppIcon.appiconset/Contents.json rename to Targets/Prod/Images.xcassets/AppIcon.appiconset/Contents.json diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/Contents.json b/Targets/Prod/Images.xcassets/Contents.json similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.xcassets/Contents.json rename to Targets/Prod/Images.xcassets/Contents.json diff --git a/Targets/Prod/WaiterRobot.plist b/Targets/Prod/WaiterRobot.plist new file mode 100644 index 0000000..82cfd6b --- /dev/null +++ b/Targets/Prod/WaiterRobot.plist @@ -0,0 +1,72 @@ + + + + + ALLOWED_HOSTS + my.kellner.team + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + kellner.team + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + org.datepollsystems.waiterrobot + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLocalizations + + en + de + + CFBundleName + WaiterRobot + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 2.4.1 + CFBundleVersion + 20401 + ITSAppUsesNonExemptEncryption + + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + NSBluetoothAlwaysUsageDescription + We don't use bluetooth + NSCameraUsageDescription + Camera is needed to scan QR-Codes + NSContactsUsageDescription + We don't use your contacts + NSLocationWhenInUseUsageDescription + We don't use your location + NSMotionUsageDescription + We don't use your motion sensors + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchStoryboardName + LaunchScreen.storyboard + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + VERSION_SUFFIX + + + diff --git a/WaiterRobot.xcodeproj/project.pbxproj b/WaiterRobot.xcodeproj/project.pbxproj new file mode 100644 index 0000000..82e04f7 --- /dev/null +++ b/WaiterRobot.xcodeproj/project.pbxproj @@ -0,0 +1,724 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 70; + objects = { + +/* Begin PBXBuildFile section */ + 2518F8E494B3AF68133FE7A8 /* Sentry-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 9603F5DF319E397835ED07CF /* Sentry-Dynamic */; }; + 604A97194CDC4C491AB8FC8B /* SharedUI in Frameworks */ = {isa = PBXBuildFile; productRef = BABDBDDD081BD349A058BE89 /* SharedUI */; }; + 61A313900669DC487EA6863D /* SharedUI in Frameworks */ = {isa = PBXBuildFile; productRef = 55C201C10992ADF9DCF1AB1D /* SharedUI */; }; + 70671FD5C7F6B2CFD5B3C81B /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 9DEDC56556C4308CCDECB2F9 /* CodeScanner */; }; + 73FF2191C23572A5DFF9E957 /* UIPilot in Frameworks */ = {isa = PBXBuildFile; productRef = C109840ED6C429035266718E /* UIPilot */; }; + 7936C5E94AE48188F2ADFA8A /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 37CBF231C369CD35ECC4385E /* CodeScanner */; }; + 7D707FDB5B95C572C974EC5E /* Sentry-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 43EED5F7BB605AA2D75C1887 /* Sentry-Dynamic */; }; + C1135F54B24A6B86BDC6F769 /* WRCore in Frameworks */ = {isa = PBXBuildFile; productRef = B7E489280D66E5F0DD389817 /* WRCore */; }; + DC97745CEB90B555448FA95F /* WRCore in Frameworks */ = {isa = PBXBuildFile; productRef = 169FFF39D770347AD66BA7D7 /* WRCore */; }; + E8FE7DAD449CC3CE8386FA59 /* UIPilot in Frameworks */ = {isa = PBXBuildFile; productRef = 31066418D1485DF4475A6079 /* UIPilot */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + C9B0B7C31B94895B59E15AC6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 52CBC7F10D93BB13D5065932 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C201714A3A531C2B18B55E0C; + remoteInfo = WaiterRobotLava; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 150FCA59370ECA6E035CC817 /* WaiterRobotTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WaiterRobotTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3051906575D9F78EC5C723A3 /* install-git-hook.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "install-git-hook.sh"; sourceTree = ""; }; + 36AB5A225A4846F5C481DF79 /* SharedUI */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SharedUI; path = Modules/SharedUI; sourceTree = SOURCE_ROOT; }; + 42B9ECF0CFED88271A5AF4A0 /* .swiftformat */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftformat; sourceTree = ""; }; + 6819CC6EBD52B1CEAB4D92C1 /* kellner.team.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = kellner.team.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 6F409B484C54EA38AF4A71B3 /* Gemfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Gemfile; sourceTree = ""; }; + A546B3D407374B188009CF24 /* renovate.json5 */ = {isa = PBXFileReference; lastKnownFileType = text; path = renovate.json5; sourceTree = ""; }; + A8204F732B8FD93EBB7E360E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + A952F4BB224D5237592F4D35 /* Package.resolved */ = {isa = PBXFileReference; lastKnownFileType = text; path = Package.resolved; sourceTree = ""; }; + AF07F3A2F61F5C483A0AFE65 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; + C499D63CB90F1FFC844F330E /* lava.kellner.team.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = lava.kellner.team.app; sourceTree = BUILT_PRODUCTS_DIR; }; + CA5239AA380E7A56C54E19AC /* WRCore */ = {isa = PBXFileReference; lastKnownFileType = folder; name = WRCore; path = Modules/WRCore; sourceTree = SOURCE_ROOT; }; + F87396495BC0BE8285940A2F /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 82416F362D64F84C00B4FCCE /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Prod/Images.xcassets, + Prod/WaiterRobot.plist, + ); + target = F343603AC32E990B90EB0750 /* WaiterRobot */; + }; + 82416F372D64F84E00B4FCCE /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Lava/Images.xcassets, + Lava/WaiterRobotLava.plist, + ); + target = C201714A3A531C2B18B55E0C /* WaiterRobotLava */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 82416F332D64F80A00B4FCCE /* Targets */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (82416F362D64F84C00B4FCCE /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 82416F372D64F84E00B4FCCE /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Targets; sourceTree = ""; }; + 82416F762D64F85600B4FCCE /* WaiterRobot */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = WaiterRobot; sourceTree = ""; }; + 82416FDB2D64F86400B4FCCE /* fastlane */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = fastlane; sourceTree = ""; }; + 82416FDD2D64F86900B4FCCE /* command-line-tools */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = "command-line-tools"; sourceTree = ""; }; + 82416FE22D64F87200B4FCCE /* .github */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = .github; sourceTree = ""; }; + 82416FE42D64F87800B4FCCE /* WaiterRobotTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = WaiterRobotTests; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 849BE04D5C6F481ED261FC99 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E8FE7DAD449CC3CE8386FA59 /* UIPilot in Frameworks */, + 7936C5E94AE48188F2ADFA8A /* CodeScanner in Frameworks */, + 604A97194CDC4C491AB8FC8B /* SharedUI in Frameworks */, + DC97745CEB90B555448FA95F /* WRCore in Frameworks */, + 2518F8E494B3AF68133FE7A8 /* Sentry-Dynamic in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EE8E9FC485BFB11435986301 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 73FF2191C23572A5DFF9E957 /* UIPilot in Frameworks */, + 70671FD5C7F6B2CFD5B3C81B /* CodeScanner in Frameworks */, + 61A313900669DC487EA6863D /* SharedUI in Frameworks */, + C1135F54B24A6B86BDC6F769 /* WRCore in Frameworks */, + 7D707FDB5B95C572C974EC5E /* Sentry-Dynamic in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 16C4C84F786E4BF6ABEEB65D = { + isa = PBXGroup; + children = ( + AF07F3A2F61F5C483A0AFE65 /* .gitignore */, + 42B9ECF0CFED88271A5AF4A0 /* .swiftformat */, + 6F409B484C54EA38AF4A71B3 /* Gemfile */, + A546B3D407374B188009CF24 /* renovate.json5 */, + A8204F732B8FD93EBB7E360E /* README.md */, + A952F4BB224D5237592F4D35 /* Package.resolved */, + 3051906575D9F78EC5C723A3 /* install-git-hook.sh */, + F87396495BC0BE8285940A2F /* Package.swift */, + 82416FE22D64F87200B4FCCE /* .github */, + 82416FDD2D64F86900B4FCCE /* command-line-tools */, + 82416FDB2D64F86400B4FCCE /* fastlane */, + D4F4C626424188D539C8809A /* Modules */, + 82416F332D64F80A00B4FCCE /* Targets */, + 82416F762D64F85600B4FCCE /* WaiterRobot */, + 82416FE42D64F87800B4FCCE /* WaiterRobotTests */, + D20FD31A5075372C89FD743F /* Products */, + ); + sourceTree = ""; + }; + D20FD31A5075372C89FD743F /* Products */ = { + isa = PBXGroup; + children = ( + 6819CC6EBD52B1CEAB4D92C1 /* kellner.team.app */, + C499D63CB90F1FFC844F330E /* lava.kellner.team.app */, + 150FCA59370ECA6E035CC817 /* WaiterRobotTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + D4F4C626424188D539C8809A /* Modules */ = { + isa = PBXGroup; + children = ( + 36AB5A225A4846F5C481DF79 /* SharedUI */, + CA5239AA380E7A56C54E19AC /* WRCore */, + ); + path = Modules; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4E481FB65E091F7A1A12A5DD /* WaiterRobotTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7B7EA4A39EE1196FB614A98 /* Build configuration list for PBXNativeTarget "WaiterRobotTests" */; + buildPhases = ( + FA85AD508C602CDF6620D3F3 /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + CEACA342507672FF87FB2792 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 82416FE42D64F87800B4FCCE /* WaiterRobotTests */, + ); + name = WaiterRobotTests; + productName = WaiterRobotTests; + productReference = 150FCA59370ECA6E035CC817 /* WaiterRobotTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + C201714A3A531C2B18B55E0C /* WaiterRobotLava */ = { + isa = PBXNativeTarget; + buildConfigurationList = 60349513182458A51949DF23 /* Build configuration list for PBXNativeTarget "WaiterRobotLava" */; + buildPhases = ( + 3373788ECCEFE183F0C29B83 /* Set BuildNumber to epochMinute */, + ADBAC7ADCDC436C4DA348DF2 /* Sources */, + 40C1E9D09B43297FF0E234CC /* Resources */, + EE8E9FC485BFB11435986301 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 82416F762D64F85600B4FCCE /* WaiterRobot */, + ); + name = WaiterRobotLava; + packageProductDependencies = ( + C109840ED6C429035266718E /* UIPilot */, + 9DEDC56556C4308CCDECB2F9 /* CodeScanner */, + 55C201C10992ADF9DCF1AB1D /* SharedUI */, + B7E489280D66E5F0DD389817 /* WRCore */, + 43EED5F7BB605AA2D75C1887 /* Sentry-Dynamic */, + ); + productName = WaiterRobotLava; + productReference = C499D63CB90F1FFC844F330E /* lava.kellner.team.app */; + productType = "com.apple.product-type.application"; + }; + F343603AC32E990B90EB0750 /* WaiterRobot */ = { + isa = PBXNativeTarget; + buildConfigurationList = 84B53686DDD2A6289194F3F2 /* Build configuration list for PBXNativeTarget "WaiterRobot" */; + buildPhases = ( + CC0150DC6397C15A620ADF26 /* Sources */, + 35A540AA506C44AA0778E09E /* Resources */, + 849BE04D5C6F481ED261FC99 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 82416F762D64F85600B4FCCE /* WaiterRobot */, + ); + name = WaiterRobot; + packageProductDependencies = ( + 31066418D1485DF4475A6079 /* UIPilot */, + 37CBF231C369CD35ECC4385E /* CodeScanner */, + BABDBDDD081BD349A058BE89 /* SharedUI */, + 169FFF39D770347AD66BA7D7 /* WRCore */, + 9603F5DF319E397835ED07CF /* Sentry-Dynamic */, + ); + productName = WaiterRobot; + productReference = 6819CC6EBD52B1CEAB4D92C1 /* kellner.team.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 52CBC7F10D93BB13D5065932 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + TargetAttributes = { + 4E481FB65E091F7A1A12A5DD = { + TestTargetID = C201714A3A531C2B18B55E0C; + }; + C201714A3A531C2B18B55E0C = { + DevelopmentTeam = 28TM58T3GZ; + ProvisioningStyle = Manual; + }; + F343603AC32E990B90EB0750 = { + DevelopmentTeam = 28TM58T3GZ; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 1A0E05B4BC1CB0EF27BAC2DF /* Build configuration list for PBXProject "WaiterRobot" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = 16C4C84F786E4BF6ABEEB65D; + packageReferences = ( + 035550925EEDC82A97EDAA4E /* XCRemoteSwiftPackageReference "CodeScanner" */, + 41C3BBF4176E4ED04740B917 /* XCRemoteSwiftPackageReference "sentry-cocoa" */, + C238B2EE28985222F9A6AB7A /* XCRemoteSwiftPackageReference "UIPilot" */, + ED2DCE1705F5455A52D62F47 /* XCLocalSwiftPackageReference "Modules/SharedUI" */, + F7A9B4B06DB5FDEC25E63741 /* XCLocalSwiftPackageReference "Modules/WRCore" */, + ); + projectDirPath = ""; + projectRoot = ""; + targets = ( + F343603AC32E990B90EB0750 /* WaiterRobot */, + C201714A3A531C2B18B55E0C /* WaiterRobotLava */, + 4E481FB65E091F7A1A12A5DD /* WaiterRobotTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 35A540AA506C44AA0778E09E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 40C1E9D09B43297FF0E234CC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3373788ECCEFE183F0C29B83 /* Set BuildNumber to epochMinute */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Set BuildNumber to epochMinute"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Lava uses the epochMinute as buildNumber\n/usr/libexec/PlistBuddy -c \"Set CFBundleVersion $(($(date +\"%s\")/60))\" \"Targets/Lava/WaiterRobotLava.plist\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + ADBAC7ADCDC436C4DA348DF2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CC0150DC6397C15A620ADF26 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FA85AD508C602CDF6620D3F3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + CEACA342507672FF87FB2792 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C201714A3A531C2B18B55E0C /* WaiterRobotLava */; + targetProxy = C9B0B7C31B94895B59E15AC6 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 236DC7989F17AC6D76D6D63B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = WaiterRobot/Entitlements/WaiterRobotLava.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = 28TM58T3GZ; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = Targets/Lava/WaiterRobotLava.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.datepollsystems.waiterrobot.beta; + PRODUCT_NAME = lava.kellner.team; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.datepollsystems.waiterrobot.beta"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2B983EAC56498844F3713062 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = WaiterRobot/Entitlements/WaiterRobotLava.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = 28TM58T3GZ; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = Targets/Lava/WaiterRobotLava.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.datepollsystems.waiterrobot.beta; + PRODUCT_NAME = lava.kellner.team; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore org.datepollsystems.waiterrobot.beta"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 38A908BC2BC6B2D6B3ECCB9E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 402914793E48DD4051D47D26 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/lava.kellner.team.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/lava.kellner.team"; + TEST_TARGET_NAME = WaiterRobotLava; + }; + name = Release; + }; + 44B7CE69862540345ADF321A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = WaiterRobot/Entitlements/WaiterRobot.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = 28TM58T3GZ; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = Targets/Prod/WaiterRobot.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.datepollsystems.waiterrobot; + PRODUCT_NAME = kellner.team; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore org.datepollsystems.waiterrobot"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + BA1FEF364D4297DFC0649B54 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + GENERATE_INFOPLIST_FILE = YES; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/lava.kellner.team.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/lava.kellner.team"; + TEST_TARGET_NAME = WaiterRobotLava; + }; + name = Debug; + }; + BC262E433F206CA7F894B5C3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = WaiterRobot/Entitlements/WaiterRobot.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = 28TM58T3GZ; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = Targets/Prod/WaiterRobot.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.datepollsystems.waiterrobot; + PRODUCT_NAME = kellner.team; + PROVISIONING_PROFILE_SPECIFIER = "match Development org.datepollsystems.waiterrobot"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + EE115F820C751485D8ED5368 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1A0E05B4BC1CB0EF27BAC2DF /* Build configuration list for PBXProject "WaiterRobot" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EE115F820C751485D8ED5368 /* Debug */, + 38A908BC2BC6B2D6B3ECCB9E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 60349513182458A51949DF23 /* Build configuration list for PBXNativeTarget "WaiterRobotLava" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 236DC7989F17AC6D76D6D63B /* Debug */, + 2B983EAC56498844F3713062 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 84B53686DDD2A6289194F3F2 /* Build configuration list for PBXNativeTarget "WaiterRobot" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BC262E433F206CA7F894B5C3 /* Debug */, + 44B7CE69862540345ADF321A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + F7B7EA4A39EE1196FB614A98 /* Build configuration list for PBXNativeTarget "WaiterRobotTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BA1FEF364D4297DFC0649B54 /* Debug */, + 402914793E48DD4051D47D26 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + ED2DCE1705F5455A52D62F47 /* XCLocalSwiftPackageReference "Modules/SharedUI" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Modules/SharedUI; + }; + F7A9B4B06DB5FDEC25E63741 /* XCLocalSwiftPackageReference "Modules/WRCore" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Modules/WRCore; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 035550925EEDC82A97EDAA4E /* XCRemoteSwiftPackageReference "CodeScanner" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/twostraws/CodeScanner"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.2.1; + }; + }; + 41C3BBF4176E4ED04740B917 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/getsentry/sentry-cocoa.git"; + requirement = { + kind = exactVersion; + version = 8.36.0; + }; + }; + C238B2EE28985222F9A6AB7A /* XCRemoteSwiftPackageReference "UIPilot" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/canopas/UIPilot.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.3.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 169FFF39D770347AD66BA7D7 /* WRCore */ = { + isa = XCSwiftPackageProductDependency; + productName = WRCore; + }; + 31066418D1485DF4475A6079 /* UIPilot */ = { + isa = XCSwiftPackageProductDependency; + package = C238B2EE28985222F9A6AB7A /* XCRemoteSwiftPackageReference "UIPilot" */; + productName = UIPilot; + }; + 37CBF231C369CD35ECC4385E /* CodeScanner */ = { + isa = XCSwiftPackageProductDependency; + package = 035550925EEDC82A97EDAA4E /* XCRemoteSwiftPackageReference "CodeScanner" */; + productName = CodeScanner; + }; + 43EED5F7BB605AA2D75C1887 /* Sentry-Dynamic */ = { + isa = XCSwiftPackageProductDependency; + package = 41C3BBF4176E4ED04740B917 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; + productName = "Sentry-Dynamic"; + }; + 55C201C10992ADF9DCF1AB1D /* SharedUI */ = { + isa = XCSwiftPackageProductDependency; + productName = SharedUI; + }; + 9603F5DF319E397835ED07CF /* Sentry-Dynamic */ = { + isa = XCSwiftPackageProductDependency; + package = 41C3BBF4176E4ED04740B917 /* XCRemoteSwiftPackageReference "sentry-cocoa" */; + productName = "Sentry-Dynamic"; + }; + 9DEDC56556C4308CCDECB2F9 /* CodeScanner */ = { + isa = XCSwiftPackageProductDependency; + package = 035550925EEDC82A97EDAA4E /* XCRemoteSwiftPackageReference "CodeScanner" */; + productName = CodeScanner; + }; + B7E489280D66E5F0DD389817 /* WRCore */ = { + isa = XCSwiftPackageProductDependency; + productName = WRCore; + }; + BABDBDDD081BD349A058BE89 /* SharedUI */ = { + isa = XCSwiftPackageProductDependency; + productName = SharedUI; + }; + C109840ED6C429035266718E /* UIPilot */ = { + isa = XCSwiftPackageProductDependency; + package = C238B2EE28985222F9A6AB7A /* XCRemoteSwiftPackageReference "UIPilot" */; + productName = UIPilot; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 52CBC7F10D93BB13D5065932 /* Project object */; +} diff --git a/WaiterRobot.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/WaiterRobot.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/WaiterRobot.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..3ecc5f8 --- /dev/null +++ b/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,41 @@ +{ + "pins" : [ + { + "identity" : "codescanner", + "kind" : "remoteSourceControl", + "location" : "https://github.com/twostraws/CodeScanner", + "state" : { + "revision" : "34da57fb63b47add20de8a85da58191523ccce57", + "version" : "2.5.0" + } + }, + { + "identity" : "sentry-cocoa", + "kind" : "remoteSourceControl", + "location" : "https://github.com/getsentry/sentry-cocoa.git", + "state" : { + "revision" : "5575af93efb776414f243e93d6af9f6258dc539a", + "version" : "8.36.0" + } + }, + { + "identity" : "uipilot", + "kind" : "remoteSourceControl", + "location" : "https://github.com/canopas/UIPilot.git", + "state" : { + "revision" : "ff7e5f93897b1f639cd249b34ba49aec5c692c0f", + "version" : "1.3.1" + } + }, + { + "identity" : "waiterrobot-shared-android", + "kind" : "remoteSourceControl", + "location" : "https://github.com/DatepollSystems/WaiterRobot-Shared-Android.git", + "state" : { + "revision" : "43ef7e427e6cfa46c81c526b5e0e291ca227f845", + "version" : "1.6.10" + } + } + ], + "version" : 2 +} diff --git a/WaiterRobot.xcodeproj/xcshareddata/xcschemes/WaiterRobot.xcscheme b/WaiterRobot.xcodeproj/xcshareddata/xcschemes/WaiterRobot.xcscheme new file mode 100644 index 0000000..1cc4a2e --- /dev/null +++ b/WaiterRobot.xcodeproj/xcshareddata/xcschemes/WaiterRobot.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WaiterRobot.xcodeproj/xcshareddata/xcschemes/WaiterRobotLava.xcscheme b/WaiterRobot.xcodeproj/xcshareddata/xcschemes/WaiterRobotLava.xcscheme new file mode 100644 index 0000000..4978de7 --- /dev/null +++ b/WaiterRobot.xcodeproj/xcshareddata/xcschemes/WaiterRobotLava.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project.yml b/project.yml deleted file mode 100644 index 72ee0da..0000000 --- a/project.yml +++ /dev/null @@ -1,161 +0,0 @@ -name: WaiterRobot - -fileGroups: - - .gitignore - - .github - - README.md - - project.yml - - Package.swift - - Package.resolved - - fastlane - - Gemfile - - command-line-tools - - .swiftformat - - install-git-hook.sh - - renovate.json5 - -options: - localPackagesGroup: Modules - -packages: - SharedUI: - path: Modules/SharedUI - WRCore: - path: Modules/WRCore - UIPilot: - url: https://github.com/canopas/UIPilot.git - from: 1.3.1 - CodeScanner: - url: https://github.com/twostraws/CodeScanner - from: 2.2.1 - Sentry: - url: https://github.com/getsentry/sentry-cocoa.git - version: 8.36.0 # must be updated together with the "Sentry-Dynamic.xcframework" in the shared project - -settings: - base: - IPHONEOS_DEPLOYMENT_TARGET: '15.0' - ENABLE_USER_SCRIPT_SANDBOXING: false - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS: true - -targetTemplates: - WaiterRobot: - type: application - platform: iOS - sources: - - path: "WaiterRobot" - - path: "TargetSpecificResources/${target_name}" - group: "WaiterRobot/Resources" - - dependencies: - - package: UIPilot - - package: CodeScanner - - package: SharedUI - - package: WRCore - - package: Sentry - product: Sentry-Dynamic - - info: - path: ".generated/${target_name}.plist" - properties: - CFBundleShortVersionString: "2.4.1" - # Generate VersionCode from VersionName (major * 10_000 + minor * 100 + patch, e.g. 1.2.3 -> 10203, 1.23.45 -> 12345) - # Only used for prod releases. Lava uses epochMinute (same as on Android) - CFBundleVersion: "20401" - - VERSION_SUFFIX: "${versionSuffix}" - ALLOWED_HOSTS: "${allowedHosts}" - - CFBundleDevelopmentRegion: "$(DEVELOPMENT_LANGUAGE)" - CFBundleDisplayName: "${displayName}" - CFBundleExecutable: "$(EXECUTABLE_NAME)" - CFBundleIdentifier: "${identifier}" - CFBundleInfoDictionaryVersion: "6.0" - CFBundleLocalizations: - - en - - de - CFBundleName: "${target_name}" - CFBundlePackageType: "$(PRODUCT_BUNDLE_PACKAGE_TYPE)" - ITSAppUsesNonExemptEncryption: false - UILaunchStoryboardName: "LaunchScreen.storyboard" - NSAppTransportSecurity: - NSAllowsLocalNetworking: true - NSCameraUsageDescription: "Camera is needed to scan QR-Codes" - NSContactsUsageDescription: "We don't use your contacts" - NSMotionUsageDescription: "We don't use your motion sensors" - NSLocationWhenInUseUsageDescription: "We don't use your location" - NSBluetoothAlwaysUsageDescription: "We don't use bluetooth" - UIApplicationSceneManifest: - UIApplicationSupportsMultipleScenes: false - UIRequiredDeviceCapabilities: - - armv7 - UISupportedInterfaceOrientations: - - UIInterfaceOrientationPortrait - UISupportedInterfaceOrientations~ipad: - - UIInterfaceOrientationPortrait - - UIInterfaceOrientationPortraitUpsideDown - - UIInterfaceOrientationLandscapeLeft - - UIInterfaceOrientationLandscapeRight - - settings: - base: - PRODUCT_BUNDLE_IDENTIFIER: "${identifier}" - INFOPLIST_FILE: ".generated/${target_name}.plist" - CODE_SIGN_STYLE: "Manual" - CODE_SIGN_ENTITLEMENTS: "WaiterRobot/Entitlements/${target_name}.entitlements" - DEVELOPMENT_TEAM: "28TM58T3GZ" - PRODUCT_NAME: "${displayName}" - ENABLE_PREVIEWS: "YES" - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: "AccentColor" - configs: - RELEASE: - ONLY_ACTIVE_ARCH: "NO" - PROVISIONING_PROFILE_SPECIFIER: "match AppStore ${identifier}" - CODE_SIGN_IDENTITY: "iPhone Distribution" - DEBUG: - ONLY_ACTIVE_ARCH: "YES" - PROVISIONING_PROFILE_SPECIFIER: "match Development ${identifier}" - CODE_SIGN_IDENTITY: "iPhone Developer" - -targets: - WaiterRobot: - templates: - - WaiterRobot - scheme: {} - templateAttributes: - identifier: "org.datepollsystems.waiterrobot" - displayName: "kellner.team" - versionSuffix: "" - allowedHosts: "my.kellner.team" - - WaiterRobotLava: - templates: - - WaiterRobot - scheme: - testTargets: - - name: WaiterRobotTests - randomExecutionOrder: true - templateAttributes: - identifier: "org.datepollsystems.waiterrobot.beta" - displayName: "lava.kellner.team" - versionSuffix: "lava" - allowedHosts: "*" - preBuildScripts: - - name: Set BuildNumber to epochMinute - basedOnDependencyAnalysis: false # run for each build - script: | - # Lava uses the epochMinute as buildNumber - /usr/libexec/PlistBuddy -c "Set CFBundleVersion $(($(date +"%s")/60))" ".generated/WaiterRobotLava.plist" - - WaiterRobotTests: - type: bundle.unit-test - platform: iOS - sources: - - WaiterRobotTests - dependencies: - - target: WaiterRobotLava - settings: - base: - GENERATE_INFOPLIST_FILE: true - TEST_TARGET_NAME: WaiterRobotLava - TEST_HOST: "$(BUILT_PRODUCTS_DIR)/lava.kellner.team.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/lava.kellner.team" From 42c8bd16c1d497846a76bd513b0ce94af14e04d9 Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Tue, 18 Feb 2025 18:37:07 +0100 Subject: [PATCH 23/43] Deleted publish lane of fastlane --- .github/workflows/publish.yml | 66 ----------------------------------- 1 file changed, 66 deletions(-) delete mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 16b6177..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Publish to TestFlight -concurrency: # Cancel currently running releases when a new one is started - group: publish-android-${{ github.ref_name }} - cancel-in-progress: true -on: - workflow_dispatch: - push: - branches: - - main - - develop - paths: - - 'project.yml' - - 'WaiterRobot/**' - - 'TargetSpecificResources/**' - - 'fastlane/**' - - 'Gemfile' - - 'Gemfile.lock' - -jobs: - publish: - runs-on: macos-14 - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Setup Ruby 3.3.6 - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3.6' - bundler-cache: true - - - name: Netrc maven.pkg.github.com - uses: extractions/netrc@v2 - with: - machine: maven.pkg.github.com - username: cirunner - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Run XcodeGen - run: swift run xcodegen - - - name: Install xcodes - run: brew install xcodes - - - name: Run tests with fastlane - run: bundle exec fastlane test - - - name: Extract secrets - run: | - cd fastlane - echo "${{ secrets.KEYS_TAR_ASC }}" > .keys.tar.gz.asc - gpg -d --passphrase "${{ secrets.KEYS_PASSPHRASE }}" --batch .keys.tar.gz.asc > .keys.tar.gz - tar xzf .keys.tar.gz - chmod 600 .keys/github-deploy-key - cd .. - - - name: Publish to TestFlight - run: bundle exec fastlane releaseWaiterRobot_${{ github.ref_name }} - env: - FASTLANE_APPLE_ID: ${{ secrets.FASTLANE_APPLE_ID }} - FASTLANE_CERTIFICATES_GIT_URL: ${{ secrets.FASTLANE_CERTIFICATES_GIT_URL }} - FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }} - FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }} - MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} From d2d2d2a409cc79a854279c08585d18d2bee2e86d Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Tue, 18 Feb 2025 18:41:16 +0100 Subject: [PATCH 24/43] Fix for tests --- .github/workflows/test.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d945e76..c59e950 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,11 +26,5 @@ jobs: username: cirunner password: ${{ secrets.GITHUB_TOKEN }} - - name: Install xcodes - run: brew install xcodes - - - name: Run XcodeGen - run: swift run xcodegen - - name: Run tests with fastlane run: bundle exec fastlane test From d97780a8ac4e9c132be79c6f23eeb09f22ff283e Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Tue, 18 Feb 2025 18:45:02 +0100 Subject: [PATCH 25/43] Select Xcode 16.2 --- .github/workflows/test.yml | 3 +++ fastlane/Fastfile | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c59e950..2e09af4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,5 +26,8 @@ jobs: username: cirunner password: ${{ secrets.GITHUB_TOKEN }} + - name: Install xcodes + run: brew install xcodes + - name: Run tests with fastlane run: bundle exec fastlane test diff --git a/fastlane/Fastfile b/fastlane/Fastfile index c390fe4..bdaf90f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -17,7 +17,7 @@ default_platform(:ios) platform :ios do before_all do - xcodes(version: "15.2", select_for_current_build_only: true) + xcodes(version: "16.2", select_for_current_build_only: true) end desc "Run all iOS unit and ui tests." From a3ec29fd650be05175ebc3619e36a72110eeaa2b Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Tue, 18 Feb 2025 18:49:46 +0100 Subject: [PATCH 26/43] Use macos-15 --- .github/workflows/swiftformat.yml | 2 +- .github/workflows/test.yml | 2 +- Targets/Lava/WaiterRobotLava.plist | 2 +- fastlane/README.md | 8 ++++++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/swiftformat.yml b/.github/workflows/swiftformat.yml index e87a1f5..9ce1745 100644 --- a/.github/workflows/swiftformat.yml +++ b/.github/workflows/swiftformat.yml @@ -9,7 +9,7 @@ on: jobs: format: - runs-on: macos-latest + runs-on: macos-15 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2e09af4..60b6781 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ on: jobs: test: - runs-on: macos-14 + runs-on: macos-15 steps: - name: Checkout repo uses: actions/checkout@v4 diff --git a/Targets/Lava/WaiterRobotLava.plist b/Targets/Lava/WaiterRobotLava.plist index 321fb5f..24f0872 100644 --- a/Targets/Lava/WaiterRobotLava.plist +++ b/Targets/Lava/WaiterRobotLava.plist @@ -26,7 +26,7 @@ CFBundleShortVersionString 2.4.1 CFBundleVersion - 28998325 + 28998348 ITSAppUsesNonExemptEncryption NSAppTransportSecurity diff --git a/fastlane/README.md b/fastlane/README.md index 2bfe4b0..df9528c 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -63,6 +63,14 @@ Push a new prod build to TestFlight +### ios sendSlackMessage + +```sh +[bundle exec] fastlane ios sendSlackMessage +``` + +Send a notification to Slack + ---- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. From 57edfebdfa395809dcbf61bde6ab293ced69e25c Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Tue, 18 Feb 2025 19:03:03 +0100 Subject: [PATCH 27/43] Update fastlane, use Xcode 16.1 --- Gemfile.lock | 47 ++++----- Targets/Lava/WaiterRobotLava.plist | 2 +- fastlane/Fastfile | 154 +---------------------------- fastlane/README.md | 40 -------- 4 files changed, 30 insertions(+), 213 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 144eb2c..a267f9d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,21 +9,22 @@ GEM public_suffix (>= 2.0.2, < 7.0) artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.3.0) - aws-partitions (1.1004.0) - aws-sdk-core (3.212.0) + aws-eventstream (1.3.1) + aws-partitions (1.1051.0) + aws-sdk-core (3.218.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) + base64 jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.95.0) - aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-kms (1.98.0) + aws-sdk-core (~> 3, >= 3.216.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.170.1) - aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-s3 (1.181.0) + aws-sdk-core (~> 3, >= 3.216.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.10.1) + aws-sigv4 (1.11.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -33,7 +34,7 @@ GEM commander (4.6.0) highline (~> 2.0.0) declarative (0.0.20) - digest-crc (0.6.5) + digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) domain_name (0.6.20240107) dotenv (2.8.1) @@ -58,8 +59,8 @@ GEM faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) + faraday-multipart (1.1.0) + multipart-post (~> 2.0) faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) @@ -67,8 +68,8 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.3.1) - fastlane (2.225.0) + fastimage (2.4.0) + fastlane (2.226.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -108,7 +109,7 @@ GEM tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.3.0) + xcpretty (~> 0.4.0) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-sirp (1.0.0) sysrandom (~> 1.0) @@ -150,12 +151,12 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.7) + http-cookie (1.0.8) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.8.1) - jwt (2.9.3) + json (2.10.1) + jwt (2.10.1) base64 mini_magick (4.13.2) mini_mime (1.1.5) @@ -166,7 +167,7 @@ GEM nkf (0.2.0) optparse (0.6.0) os (1.1.4) - plist (3.7.1) + plist (3.7.2) public_suffix (6.0.1) rake (13.2.1) representable (3.2.0) @@ -174,10 +175,10 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.9) - rouge (2.0.7) + rexml (3.4.1) + rouge (3.28.0) ruby2_keywords (0.0.5) - rubyzip (2.3.2) + rubyzip (2.4.1) security (0.1.5) signet (0.19.0) addressable (~> 2.8) @@ -206,8 +207,8 @@ GEM colored2 (~> 3.1) nanaimo (~> 0.4.0) rexml (>= 3.3.6, < 4.0) - xcpretty (0.3.0) - rouge (~> 2.0.7) + xcpretty (0.4.0) + rouge (~> 3.28.0) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) diff --git a/Targets/Lava/WaiterRobotLava.plist b/Targets/Lava/WaiterRobotLava.plist index 24f0872..3edd4f1 100644 --- a/Targets/Lava/WaiterRobotLava.plist +++ b/Targets/Lava/WaiterRobotLava.plist @@ -26,7 +26,7 @@ CFBundleShortVersionString 2.4.1 CFBundleVersion - 28998348 + 28998359 ITSAppUsesNonExemptEncryption NSAppTransportSecurity diff --git a/fastlane/Fastfile b/fastlane/Fastfile index bdaf90f..48f3c40 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,157 +1,13 @@ -# This file contains the fastlane.tools configuration -# You can find the documentation at https://docs.fastlane.tools -# -# For a list of all available actions, check out -# -# https://docs.fastlane.tools/actions -# -# For a list of all available plugins, check out -# -# https://docs.fastlane.tools/plugins/available-plugins -# - -# Uncomment the line if you want fastlane to automatically update itself -# update_fastlane - default_platform(:ios) platform :ios do - before_all do - xcodes(version: "16.2", select_for_current_build_only: true) - end - - desc "Run all iOS unit and ui tests." - lane :test do - run_tests(scheme: "WaiterRobotLava") - end - - desc "Sync certificates" - lane :sync_certificates do - if ENV["CI"] - match( - type: "appstore", - git_private_key: "./fastlane/.keys/github-deploy-key", - keychain_name: "WaiterRobot_iOS_keychain", - keychain_password: ENV["KEYCHAIN_PASSWORD"] - ) - match( - type: "development", - git_private_key: "./fastlane/.keys/github-deploy-key", - keychain_name: "WaiterRobot_iOS_keychain", - keychain_password: ENV["KEYCHAIN_PASSWORD"] - ) - else - match( - type: "appstore", - git_private_key: "./fastlane/.keys/github-deploy-key", - ) - match( - type: "development", - git_private_key: "./fastlane/.keys/github-deploy-key", - ) + before_all do + xcodes(version: "16.1", select_for_current_build_only: true) end - end - - desc "Renew certificates and profiles" - lane :renew_certificates do - setupFastlaneSecrets - - match( - type: "appstore", - force: true - ) - match( - type: "development", - force: true - ) - end - - desc "Push a new lava build to TestFlight" - lane :releaseWaiterRobot_develop do |options| - setupFastlaneSecrets - sync_certificates - - build_app( - project: "WaiterRobot.xcodeproj", - scheme: "WaiterRobotLava", - output_name: "WaiterRobotLava.ipa", - output_directory: "./build/", - export_method: "app-store" - ) - - upload_to_testflight( - ipa: "./build/WaiterRobotLava.ipa", - skip_waiting_for_build_processing: true - ) - - version = get_version_number( - xcodeproj:"WaiterRobot.xcodeproj", - target:"WaiterRobotLava" - ) - # Build number gets set to epochMinute by a preBuildScript (see project.yml) - build_number = get_ipa_info_plist_value(ipa: "build/WaiterRobotLava.ipa", key: "CFBundleVersion") - - # e.g. 2.0.2-lava-27943760 - version_tag = version + "-lava-" + build_number.to_s - add_git_tag(tag: version_tag) - push_git_tags(tag: version_tag) - sendSlackMessage(version: version_tag, env: "lava") - end - desc "Push a new prod build to TestFlight" - lane :releaseWaiterRobot_main do |options| - setupFastlaneSecrets - sync_certificates - - build_app( - project: "WaiterRobot.xcodeproj", - scheme: "WaiterRobot", - output_name: "WaiterRobot.ipa", - output_directory: "./build/", - export_method: "app-store" - ) - - upload_to_testflight( - ipa: "./build/WaiterRobot.ipa", - skip_waiting_for_build_processing: true - ) - - version = get_version_number( - xcodeproj:"WaiterRobot.xcodeproj", - target:"WaiterRobot" - ) - - # e.g. 2.0.2 - add_git_tag(tag: version) - push_git_tags(tag: version) - sendSlackMessage(version: version, env: "production") - end - - lane :setupFastlaneSecrets do |options| - ensure_env_vars( - env_vars: ["FASTLANE_APPLE_ID", "FASTLANE_CERTIFICATES_GIT_URL", "FASTLANE_KEY_ID", "FASTLANE_ISSUER_ID"] - ) - - if ENV["CI"] - ensure_env_vars( - env_vars: ["KEYCHAIN_PASSWORD"] - ) - - create_keychain( - name: "WaiterRobot_iOS_keychain", - password: ENV["KEYCHAIN_PASSWORD"], - default_keychain: true, - unlock: true, - timeout: 3600, - lock_when_sleeps: false - ) - end - - app_store_connect_api_key( - key_id: ENV["FASTLANE_KEY_ID"], - issuer_id: ENV["FASTLANE_ISSUER_ID"], - key_filepath: "./fastlane/.keys/api_key.p8", - ) + desc "Run all iOS unit and ui tests." + lane :test do + run_tests(scheme: "WaiterRobotLava") end desc "Send a notification to Slack" diff --git a/fastlane/README.md b/fastlane/README.md index df9528c..7957e41 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -23,46 +23,6 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do Run all iOS unit and ui tests. -### ios sync_certificates - -```sh -[bundle exec] fastlane ios sync_certificates -``` - -Sync certificates - -### ios renew_certificates - -```sh -[bundle exec] fastlane ios renew_certificates -``` - -Renew certificates and profiles - -### ios releaseWaiterRobot_develop - -```sh -[bundle exec] fastlane ios releaseWaiterRobot_develop -``` - -Push a new lava build to TestFlight - -### ios releaseWaiterRobot_main - -```sh -[bundle exec] fastlane ios releaseWaiterRobot_main -``` - -Push a new prod build to TestFlight - -### ios setupFastlaneSecrets - -```sh -[bundle exec] fastlane ios setupFastlaneSecrets -``` - - - ### ios sendSlackMessage ```sh From 58a3a8865afac872a157919693bdd506fcb7901a Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Tue, 18 Feb 2025 19:24:09 +0100 Subject: [PATCH 28/43] Enabled automatic code signing --- Targets/Lava/WaiterRobotLava.plist | 4 +-- Targets/Prod/WaiterRobot.plist | 4 +-- WaiterRobot.xcodeproj/project.pbxproj | 40 +++++++++++++-------------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Targets/Lava/WaiterRobotLava.plist b/Targets/Lava/WaiterRobotLava.plist index 3edd4f1..0150156 100644 --- a/Targets/Lava/WaiterRobotLava.plist +++ b/Targets/Lava/WaiterRobotLava.plist @@ -24,9 +24,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 2.4.1 + 2.5.0 CFBundleVersion - 28998359 + 28998383 ITSAppUsesNonExemptEncryption NSAppTransportSecurity diff --git a/Targets/Prod/WaiterRobot.plist b/Targets/Prod/WaiterRobot.plist index 82cfd6b..eeb7ee5 100644 --- a/Targets/Prod/WaiterRobot.plist +++ b/Targets/Prod/WaiterRobot.plist @@ -24,9 +24,9 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 2.4.1 + 2.5.0 CFBundleVersion - 20401 + 1 ITSAppUsesNonExemptEncryption NSAppTransportSecurity diff --git a/WaiterRobot.xcodeproj/project.pbxproj b/WaiterRobot.xcodeproj/project.pbxproj index 82e04f7..2232d45 100644 --- a/WaiterRobot.xcodeproj/project.pbxproj +++ b/WaiterRobot.xcodeproj/project.pbxproj @@ -231,14 +231,6 @@ 4E481FB65E091F7A1A12A5DD = { TestTargetID = C201714A3A531C2B18B55E0C; }; - C201714A3A531C2B18B55E0C = { - DevelopmentTeam = 28TM58T3GZ; - ProvisioningStyle = Manual; - }; - F343603AC32E990B90EB0750 = { - DevelopmentTeam = 28TM58T3GZ; - ProvisioningStyle = Manual; - }; }; }; buildConfigurationList = 1A0E05B4BC1CB0EF27BAC2DF /* Build configuration list for PBXProject "WaiterRobot" */; @@ -345,11 +337,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = WaiterRobot/Entitlements/WaiterRobotLava.entitlements; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 28TM58T3GZ; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Targets/Lava/WaiterRobotLava.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Lava kellner.team"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -357,7 +351,7 @@ ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.datepollsystems.waiterrobot.beta; PRODUCT_NAME = lava.kellner.team; - PROVISIONING_PROFILE_SPECIFIER = "match Development org.datepollsystems.waiterrobot.beta"; + PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -369,11 +363,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = WaiterRobot/Entitlements/WaiterRobotLava.entitlements; - CODE_SIGN_IDENTITY = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 28TM58T3GZ; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Targets/Lava/WaiterRobotLava.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Lava kellner.team"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -381,7 +377,7 @@ ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = org.datepollsystems.waiterrobot.beta; PRODUCT_NAME = lava.kellner.team; - PROVISIONING_PROFILE_SPECIFIER = "match AppStore org.datepollsystems.waiterrobot.beta"; + PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -468,11 +464,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = WaiterRobot/Entitlements/WaiterRobot.entitlements; - CODE_SIGN_IDENTITY = "iPhone Distribution"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 28TM58T3GZ; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Targets/Prod/WaiterRobot.plist; + INFOPLIST_KEY_CFBundleDisplayName = kellner.team; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -480,7 +478,7 @@ ONLY_ACTIVE_ARCH = NO; PRODUCT_BUNDLE_IDENTIFIER = org.datepollsystems.waiterrobot; PRODUCT_NAME = kellner.team; - PROVISIONING_PROFILE_SPECIFIER = "match AppStore org.datepollsystems.waiterrobot"; + PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -509,11 +507,13 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = WaiterRobot/Entitlements/WaiterRobot.entitlements; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 28TM58T3GZ; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Targets/Prod/WaiterRobot.plist; + INFOPLIST_KEY_CFBundleDisplayName = kellner.team; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -521,7 +521,7 @@ ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = org.datepollsystems.waiterrobot; PRODUCT_NAME = kellner.team; - PROVISIONING_PROFILE_SPECIFIER = "match Development org.datepollsystems.waiterrobot"; + PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; From 1a1d3542533a76f265e518ca48308c0ac8c7f03c Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Thu, 29 May 2025 16:52:28 +0200 Subject: [PATCH 29/43] Update .gitignore for xcode cloud --- .gitignore | 1 - .../xcschemes/xcschememanagement.plist | 19 +++++++++++++++++++ .../Search/ProducSearchTabBarHeader.swift | 1 - .../Features/TableList/TableListScreen.swift | 1 - 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 WaiterRobot.xcodeproj/xcuserdata/alexpriv.xcuserdatad/xcschemes/xcschememanagement.plist diff --git a/.gitignore b/.gitignore index b6dfbce..2b28786 100644 --- a/.gitignore +++ b/.gitignore @@ -252,4 +252,3 @@ Temporary Items fastlane/.env WaiterRobot.xcodeproj/project.xcworkspace/xcuserdata -WaiterRobot.xcodeproj/xcuserdata diff --git a/WaiterRobot.xcodeproj/xcuserdata/alexpriv.xcuserdatad/xcschemes/xcschememanagement.plist b/WaiterRobot.xcodeproj/xcuserdata/alexpriv.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..9f01855 --- /dev/null +++ b/WaiterRobot.xcodeproj/xcuserdata/alexpriv.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,19 @@ + + + + + SchemeUserState + + WaiterRobot.xcscheme_^#shared#^_ + + orderHint + 2 + + WaiterRobotLava.xcscheme_^#shared#^_ + + orderHint + 3 + + + + diff --git a/WaiterRobot/Features/Order/Search/ProducSearchTabBarHeader.swift b/WaiterRobot/Features/Order/Search/ProducSearchTabBarHeader.swift index 74bfbd2..cc76630 100644 --- a/WaiterRobot/Features/Order/Search/ProducSearchTabBarHeader.swift +++ b/WaiterRobot/Features/Order/Search/ProducSearchTabBarHeader.swift @@ -13,7 +13,6 @@ struct ProducSearchTabBarHeader: View { Array(zip(tabBarOptions.indices, tabBarOptions)), id: \.0 ) { index, name in - Button { currentTab = index } label: { diff --git a/WaiterRobot/Features/TableList/TableListScreen.swift b/WaiterRobot/Features/TableList/TableListScreen.swift index 28c960e..012956e 100644 --- a/WaiterRobot/Features/TableList/TableListScreen.swift +++ b/WaiterRobot/Features/TableList/TableListScreen.swift @@ -195,7 +195,6 @@ struct TableListView: View { showFilters: .constant(false), tableGroups: Mock.tableGroups() ) { _ in - } onSelectAll: {} onUnselectAll: {} onTableSelect: { _ in } } From d0f48b5b025620ccaea0f14219a8cb1257c1f2ec Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Thu, 29 May 2025 17:34:02 +0200 Subject: [PATCH 30/43] Use env vars for dependencies --- WaiterRobot.xcodeproj/project.pbxproj | 4 +++- ci_scripts/ci_post_clone.sh | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100755 ci_scripts/ci_post_clone.sh diff --git a/WaiterRobot.xcodeproj/project.pbxproj b/WaiterRobot.xcodeproj/project.pbxproj index 2232d45..cd41971 100644 --- a/WaiterRobot.xcodeproj/project.pbxproj +++ b/WaiterRobot.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + 8221B03D2DE8B448004B80AC /* ci_scripts */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = ci_scripts; sourceTree = ""; }; 82416F332D64F80A00B4FCCE /* Targets */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (82416F362D64F84C00B4FCCE /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 82416F372D64F84E00B4FCCE /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Targets; sourceTree = ""; }; 82416F762D64F85600B4FCCE /* WaiterRobot */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = WaiterRobot; sourceTree = ""; }; 82416FDB2D64F86400B4FCCE /* fastlane */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = fastlane; sourceTree = ""; }; @@ -113,13 +114,14 @@ 3051906575D9F78EC5C723A3 /* install-git-hook.sh */, F87396495BC0BE8285940A2F /* Package.swift */, 82416FE22D64F87200B4FCCE /* .github */, + 8221B03D2DE8B448004B80AC /* ci_scripts */, 82416FDD2D64F86900B4FCCE /* command-line-tools */, 82416FDB2D64F86400B4FCCE /* fastlane */, D4F4C626424188D539C8809A /* Modules */, + D20FD31A5075372C89FD743F /* Products */, 82416F332D64F80A00B4FCCE /* Targets */, 82416F762D64F85600B4FCCE /* WaiterRobot */, 82416FE42D64F87800B4FCCE /* WaiterRobotTests */, - D20FD31A5075372C89FD743F /* Products */, ); sourceTree = ""; }; diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh new file mode 100755 index 0000000..4341e5f --- /dev/null +++ b/ci_scripts/ci_post_clone.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# Define the credentials +netrc_user=$netrc_user +netrc_password=$netrc_password + +# Create or overwrite the .netrc file +cat < ~/.netrc +machine maven.pkg.github.com +login $netrc_user +password $netrc_password +EOF + +# Set secure permissions +chmod 600 ~/.netrc + +echo ".netrc file created for maven.pkg.github.com" From 2c45c888463b245a2a6a2323cb7bce2a0b268350 Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Thu, 29 May 2025 18:30:14 +0200 Subject: [PATCH 31/43] Removed Launchscreen --- WaiterRobot/Resources/LaunchScreen.storyboard | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 WaiterRobot/Resources/LaunchScreen.storyboard diff --git a/WaiterRobot/Resources/LaunchScreen.storyboard b/WaiterRobot/Resources/LaunchScreen.storyboard deleted file mode 100644 index 342bbc9..0000000 --- a/WaiterRobot/Resources/LaunchScreen.storyboard +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From b590798b671f9c9e3b7fa183dc432177c6a5621c Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Mon, 2 Jun 2025 09:49:19 +0200 Subject: [PATCH 32/43] Fixed launchscreen --- .../SharedUI/Sources/SharedUI/Images.swift | 4 --- .../LogoRounded.imageset/Contents.json | 2 +- .../LogoRounded.imageset/wr-round-full.png | Bin 105926 -> 0 bytes .../LogoRounded.imageset/wr-round.svg | 34 ++++++++++++++++++ .../logoLaunch.imageset/LaunchImage.pdf | Bin 45643 -> 0 bytes Targets/Lava/Images.xcassets/Contents.json | 6 ++++ Targets/Lava/WaiterRobotLava.plist | 7 ++++ Targets/Prod/WaiterRobot.plist | 7 ++++ .../Resources/Images.xcassets/Contents.json | 6 ++++ .../LogoRounded.imageset}/Contents.json | 5 +-- .../LogoRounded.imageset/wr-round.svg | 34 ++++++++++++++++++ 11 files changed, 96 insertions(+), 9 deletions(-) delete mode 100644 Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/wr-round-full.png create mode 100644 Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg delete mode 100644 Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/logoLaunch.imageset/LaunchImage.pdf create mode 100644 Targets/Lava/Images.xcassets/Contents.json create mode 100644 WaiterRobot/Resources/Images.xcassets/Contents.json rename {Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/logoLaunch.imageset => WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset}/Contents.json (53%) create mode 100644 WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg diff --git a/Modules/SharedUI/Sources/SharedUI/Images.swift b/Modules/SharedUI/Sources/SharedUI/Images.swift index 08cc7bb..bcfe6f5 100644 --- a/Modules/SharedUI/Sources/SharedUI/Images.swift +++ b/Modules/SharedUI/Sources/SharedUI/Images.swift @@ -4,8 +4,4 @@ public extension Image { static var logoRounded: Image { Image(.logoRounded) } - - static var logoLaunch: Image { - Image(.logoLaunch) - } } diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/Contents.json index 92f8a3c..e4164e5 100644 --- a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/Contents.json +++ b/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "wr-round-full.png", + "filename" : "wr-round.svg", "idiom" : "universal" } ], diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/wr-round-full.png b/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/wr-round-full.png deleted file mode 100644 index f51ccefa9523fa4afd7a61983e3d1ff0df1db52a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 105926 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°tik=u^Kbe}{ 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 z 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 diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg b/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg new file mode 100644 index 0000000..14b173f --- /dev/null +++ b/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/logoLaunch.imageset/LaunchImage.pdf b/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/logoLaunch.imageset/LaunchImage.pdf deleted file mode 100644 index 0c2fc92912cacb5b265ea6c4833642430e6c421c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45643 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 c$BqopmzOun)BDHZ!{9UIApplicationSupportsMultipleScenes + UILaunchScreen + + UIImageName + LogoRounded + UIImageRespectsSafeAreaInsets + + UILaunchStoryboardName LaunchScreen.storyboard UIRequiredDeviceCapabilities diff --git a/Targets/Prod/WaiterRobot.plist b/Targets/Prod/WaiterRobot.plist index eeb7ee5..8dfafcc 100644 --- a/Targets/Prod/WaiterRobot.plist +++ b/Targets/Prod/WaiterRobot.plist @@ -49,6 +49,13 @@ UIApplicationSupportsMultipleScenes + UILaunchScreen + + UIImageName + LogoRounded + UIImageRespectsSafeAreaInsets + + UILaunchStoryboardName LaunchScreen.storyboard UIRequiredDeviceCapabilities diff --git a/WaiterRobot/Resources/Images.xcassets/Contents.json b/WaiterRobot/Resources/Images.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/WaiterRobot/Resources/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/logoLaunch.imageset/Contents.json b/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json similarity index 53% rename from Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/logoLaunch.imageset/Contents.json rename to WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json index 97fbe53..e4164e5 100644 --- a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/logoLaunch.imageset/Contents.json +++ b/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json @@ -1,15 +1,12 @@ { "images" : [ { - "filename" : "LaunchImage.pdf", + "filename" : "wr-round.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true } } diff --git a/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg b/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg new file mode 100644 index 0000000..14b173f --- /dev/null +++ b/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From e7c7bd3e7dcfd84762cfca20b7aa71bd43581b69 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Mon, 2 Jun 2025 22:02:25 +0200 Subject: [PATCH 33/43] WIP: Refactor architecture --- Modules/WRCore/Package.swift | 2 +- Modules/WRCore/Sources/WRCore/Alert.swift | 21 +++ Modules/WRCore/Sources/WRCore/Globals.swift | 33 +++- .../Sources/WRCore/KotlinArrayWrapper.swift | 7 +- Modules/WRCore/Sources/WRCore/Mock.swift | 10 -- .../WRCore/Sources/WRCore/Navigation.swift | 4 +- .../Sources/WRCore/ObservableViewModel.swift | 6 + .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Features/Billing/BillingScreen.swift | 168 +++++++++++------- WaiterRobot/Features/Login/LoginScreen.swift | 30 ++-- .../Features/Login/RegisterScreen.swift | 16 +- .../Features/Order/Search/ProductSearch.swift | 55 +++--- .../Order/Search/ProductSearchAllTab.swift | 4 +- WaiterRobot/Features/SwitchEvent/Event.swift | 3 +- .../TableDetail/OrderedItemView.swift | 3 +- .../TableList/TableGroupSection.swift | 14 +- .../TableList/TableListFilterRow.swift | 4 +- .../Features/UpdateApp/UpdateAppScreen.swift | 6 +- WaiterRobot/Ui/LoadingOverlayView.swift | 25 +++ WaiterRobot/Ui/ViewStateOverlayView.swift | 44 +++++ 20 files changed, 301 insertions(+), 158 deletions(-) create mode 100644 Modules/WRCore/Sources/WRCore/Alert.swift create mode 100644 WaiterRobot/Ui/LoadingOverlayView.swift create mode 100644 WaiterRobot/Ui/ViewStateOverlayView.swift diff --git a/Modules/WRCore/Package.swift b/Modules/WRCore/Package.swift index 1392efc..e870c64 100644 --- a/Modules/WRCore/Package.swift +++ b/Modules/WRCore/Package.swift @@ -15,7 +15,7 @@ let package = Package( ], dependencies: [ .package(path: "../SharedUI"), - .package(url: "https://github.com/DatepollSystems/WaiterRobot-Shared-Android.git", from: "1.6.1"), + .package(url: "https://github.com/DatepollSystems/WaiterRobot-Shared-Android.git", from: "1.7.3"), ], targets: [ .target( diff --git a/Modules/WRCore/Sources/WRCore/Alert.swift b/Modules/WRCore/Sources/WRCore/Alert.swift new file mode 100644 index 0000000..01a2306 --- /dev/null +++ b/Modules/WRCore/Sources/WRCore/Alert.swift @@ -0,0 +1,21 @@ +import shared +import SwiftUI + +public extension Alert { + init(_ dialog: DialogState) { + if let secondaryButton = dialog.secondaryButton { + self.init( + title: Text(dialog.title.localized()), + message: Text(dialog.text.localized()), + primaryButton: .default(Text(dialog.primaryButton.text.localized()), action: dialog.primaryButton.action), + secondaryButton: .cancel(Text(secondaryButton.text.localized()), action: secondaryButton.action) + ) + } else { + self.init( + title: Text(dialog.title.localized()), + message: Text(dialog.text.localized()), + dismissButton: .default(Text(dialog.primaryButton.text.localized()), action: dialog.primaryButton.action) + ) + } + } +} diff --git a/Modules/WRCore/Sources/WRCore/Globals.swift b/Modules/WRCore/Sources/WRCore/Globals.swift index 85f1812..bd23b46 100644 --- a/Modules/WRCore/Sources/WRCore/Globals.swift +++ b/Modules/WRCore/Sources/WRCore/Globals.swift @@ -4,8 +4,7 @@ import SwiftUI import UIKit public var koin: IosKoinComponent { IosKoinComponent.shared } - -public var localize: shared.L.Companion { shared.L.Companion.shared } +public var localize: shared.MR.strings { shared.MR.strings() } public enum WRCore { /// Setup of frameworks and all the other related stuff which is needed everywhere in the app @@ -29,7 +28,6 @@ public enum WRCore { let logger = koin.logger(tag: "AppDelegate") - KMMResourcesLocalizationKt.localizationBundle = Bundle(for: shared.L.self) logger.d { "initialized localization bundle" } print("finished app setup") } @@ -53,3 +51,32 @@ public extension EnvironmentValues { #endif } } + +public extension StringResource { + func callAsFunction() -> String { + desc().localized() + } + + func callAsFunction(_ args: String...) -> String { + format(args: args).localized() + } +} + +public extension StringDesc { + func callAsFunction() -> String { + localized() + } +} + +public extension Skie.Shared.Resource.__Sealed { + var data: T? { + switch self { + case let .loading(resource): + resource.data + case let .error(resource): + resource.data + case let .success(resource): + resource.data + } + } +} diff --git a/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift b/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift index 53840b4..a9e5964 100644 --- a/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift +++ b/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift @@ -2,9 +2,12 @@ import Foundation import shared public extension Array where Element: AnyObject { - init(_ kotlinArray: KotlinArray) { + init?(_ kotlinArray: KotlinArray?) { + guard let array = kotlinArray else { + return nil + } self.init() - let iterator = kotlinArray.iterator() + let iterator = array.iterator() while iterator.hasNext() { append(iterator.next() as! Element) } diff --git a/Modules/WRCore/Sources/WRCore/Mock.swift b/Modules/WRCore/Sources/WRCore/Mock.swift index 1bae963..c9d19db 100644 --- a/Modules/WRCore/Sources/WRCore/Mock.swift +++ b/Modules/WRCore/Sources/WRCore/Mock.swift @@ -14,18 +14,8 @@ public enum Mock { TableGroup( id: id, name: name, - eventId: 1, - position: Int32(id), color: "", hidden: false, - tables: [ - table(with: 1), - table(with: 2, hasOrders: true), - table(with: 3), - table(with: 4), - table(with: 5), - table(with: 6), - ] ) } diff --git a/Modules/WRCore/Sources/WRCore/Navigation.swift b/Modules/WRCore/Sources/WRCore/Navigation.swift index c0072ce..3ae2c0f 100644 --- a/Modules/WRCore/Sources/WRCore/Navigation.swift +++ b/Modules/WRCore/Sources/WRCore/Navigation.swift @@ -36,7 +36,7 @@ extension UIPilot { public extension View { func customBackNavigation( - title: String = localize.navigation.back(), + title: String = localize.navigation_back(), icon: String? = "chevron.left", action: @escaping () -> Void ) -> some View { @@ -73,7 +73,7 @@ public extension View { logger.d { "Got sideEffect: \(sideEffect)" } switch onEnum(of: sideEffect as! NavOrViewModelEffect) { case let .navEffect(navEffect): - await navigator.navigate(navEffect.action) + navigator.navigate(navEffect.action) case let .vMEffect(effect): if handler?(effect.effect) != true { logger.w { "Side effect \(effect.effect) was not handled." } diff --git a/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift b/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift index 8e43e28..89dae0b 100644 --- a/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift +++ b/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift @@ -60,6 +60,12 @@ public class ObservableOrderViewModel: ObservableViewModel { + public init() { + super.init(viewModel: koin.getProductListVM()) + } +} + public class ObservableLoginScannerViewModel: ObservableViewModel { public init() { super.init(viewModel: koin.loginScannerVM()) diff --git a/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3ecc5f8..55894a6 100644 --- a/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DatepollSystems/WaiterRobot-Shared-Android.git", "state" : { - "revision" : "43ef7e427e6cfa46c81c526b5e0e291ca227f845", - "version" : "1.6.10" + "revision" : "c01f764d92dcd7df8ee4ad4ecba55a9200ed7a00", + "version" : "1.7.3" } } ], diff --git a/WaiterRobot/Features/Billing/BillingScreen.swift b/WaiterRobot/Features/Billing/BillingScreen.swift index 1c63843..cd0e690 100644 --- a/WaiterRobot/Features/Billing/BillingScreen.swift +++ b/WaiterRobot/Features/Billing/BillingScreen.swift @@ -8,7 +8,6 @@ struct BillingScreen: View { @EnvironmentObject var navigator: UIPilot @State private var showPayDialog: Bool = false - @State private var showAbortConfirmation = false @StateObject private var viewModel: ObservableBillingViewModel private let table: shared.Table @@ -19,92 +18,122 @@ struct BillingScreen: View { } var body: some View { - let billItems = Array(viewModel.state.billItemsArray) - - content(billItems: billItems) - .navigationTitle(localize.billing.title(value0: table.groupName, value1: table.number.description)) - .navigationBarTitleDisplayMode(.inline) - .customBackNavigation( - title: localize.dialog.cancel(), - icon: nil - ) { - if viewModel.state.hasCustomSelection { - showAbortConfirmation = true - } else { - viewModel.actual.abortBill() - } + BillingScreenView( + table: table, + state: viewModel.state, + abortBill: { viewModel.actual.abortBill() }, + selectAll: { viewModel.actual.selectAll() }, + unselectAll: { viewModel.actual.unselectAll() }, + addItem: { viewModel.actual.addItem(baseProductId: $0, amount: $1) }, + paySelection: { viewModel.actual.paySelection(paymentSheetShown: $0) } + ) + // TODO: make only half screen when ios 15 is dropped + .sheet(isPresented: $showPayDialog) { + PayDialog(viewModel: viewModel) + } + .withViewModel(viewModel, navigator) { effect in + switch onEnum(of: effect) { + case .showPaymentSheet: + showPayDialog = true + case .toast: + break // TODO: add "toast" support } - .confirmationDialog( - localize.billing.notSent.title(), - isPresented: $showAbortConfirmation, - titleVisibility: .visible - ) { - Button(localize.dialog.closeAnyway(), role: .destructive) { - viewModel.actual.abortBill() + + return true + } + } +} + +private struct BillingScreenView: View { + @State private var showPayDialog: Bool = false + @State private var showAbortConfirmation = false + + let table: shared.Table + let state: BillingState + let abortBill: () -> Void + let selectAll: () -> Void + let unselectAll: () -> Void + let addItem: (_ baseProductId: Int64, _ amount: Int32) -> Void + let paySelection: (_ paymentSheetShown: Bool) -> Void + + var body: some View { + ViewStateOverlayView(state: state.paymentState) { + let billItemsState = onEnum(of: state.billItems_) + + if case let .loading(ressource) = billItemsState, ressource.data == nil { + ProgressView() + } else { + if case let .error(resource) = billItemsState { + Text("Error \(resource.userMessage())") } - } message: { - Text(localize.billing.notSent.desc()) - } - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - if !billItems.isEmpty { - Button { - viewModel.actual.selectAll() - } label: { - Image(systemName: "checkmark") - } - } - if !billItems.isEmpty { - Button { - viewModel.actual.unselectAll() - } label: { - Image(systemName: "xmark") - } - } + if let billItems = Array(state.billItems_.data), !billItems.isEmpty { + content(billItems: billItems) + } else { + Text(localize.billing_noOrder(table.groupName, table.number.description)) } } - // TODO: make only half screen when ios 15 is dropped - .sheet(isPresented: $showPayDialog) { - PayDialog(viewModel: viewModel) + } + .navigationTitle(localize.billing_title(table.groupName, table.number.description)) + .navigationBarTitleDisplayMode(.inline) + .customBackNavigation( + title: localize.dialog_cancel(), + icon: nil + ) { + if state.hasCustomSelection { + showAbortConfirmation = true + } else { + abortBill() + } + } + .confirmationDialog( + localize.billing_notSent_title(), + isPresented: $showAbortConfirmation, + titleVisibility: .visible + ) { + Button(localize.dialog_closeAnyway(), role: .destructive) { + abortBill() } - .withViewModel(viewModel, navigator) { effect in - switch onEnum(of: effect) { - case .showPaymentSheet: - showPayDialog = true - case .toast: - break // TODO: add "toast" support + } message: { + Text(localize.billing_notSent_desc()) + } + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { + selectAll() + } label: { + Image(systemName: "checkmark") } - return true + Button { + unselectAll() + } label: { + Image(systemName: "xmark") + } } + } } @ViewBuilder - private func content(billItems: [BillItem]) -> some View { + private func content(billItems: [BillItem]?) -> some View { VStack { List { - if billItems.isEmpty { - Text(localize.billing.noOpenBill(value0: table.groupName, value1: table.number.description)) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .padding() - } else { + if let billItems, !billItems.isEmpty { Section { - ForEach(billItems, id: \.virtualId) { item in + ForEach(billItems, id: \.baseProductId) { item in BillListItem( item: item, addOne: { - viewModel.actual.addItem(virtualId: item.virtualId, amount: 1) + addItem(item.baseProductId, 1) }, addAll: { - viewModel.actual.addItem(virtualId: item.virtualId, amount: item.ordered - item.selectedForBill) + addItem(item.baseProductId, item.ordered - item.selectedForBill) }, removeOne: { - viewModel.actual.addItem(virtualId: item.virtualId, amount: -1) + addItem(item.baseProductId, -1) }, removeAll: { - viewModel.actual.addItem(virtualId: item.virtualId, amount: -item.selectedForBill) + addItem(item.baseProductId, -item.selectedForBill) } ) } @@ -115,19 +144,24 @@ struct BillingScreen: View { Text("Selected") } } + } else { + Text(localize.billing_noOrder(table.groupName, table.number.description)) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding() } } HStack { - Text("\(localize.billing.total()):") + Text("\(localize.billing_total()):") Spacer() - Text("\(viewModel.state.priceSum)") + Text("\(state.priceSum)") } .font(.title2) .padding() .overlay(alignment: .bottom) { Button { - viewModel.actual.paySelection(paymentSheetShown: false) + paySelection(false) } label: { Image(systemName: "eurosign") .font(.system(.title)) @@ -138,7 +172,7 @@ struct BillingScreen: View { .background(.blue) .mask(Circle()) .shadow(color: Color.black.opacity(0.3), radius: 3, x: 3, y: 3) - .disabled(viewModel.state.viewState != ViewState.Idle.shared || !viewModel.state.hasSelectedItems) + .disabled(state.paymentState != ViewState.Idle.shared || !state.hasSelectedItems) } } } diff --git a/WaiterRobot/Features/Login/LoginScreen.swift b/WaiterRobot/Features/Login/LoginScreen.swift index 8b08669..d5057a3 100644 --- a/WaiterRobot/Features/Login/LoginScreen.swift +++ b/WaiterRobot/Features/Login/LoginScreen.swift @@ -13,22 +13,16 @@ struct LoginScreen: View { @State private var debugLoginLink = "" var body: some View { - switch viewModel.state.viewState { - case is ViewState.Loading: + switch onEnum(of: viewModel.state.viewState) { + case .loading: ProgressView() - case is ViewState.Idle: + case .idle: content() - case let error as ViewState.Error: + case let .error(error): content() .alert(isPresented: Binding.constant(true)) { - Alert( - title: Text(error.title), - message: Text(error.message), - dismissButton: .cancel(Text("OK"), action: error.onDismiss) - ) + Alert(error.dialog) } - default: - fatalError("Unexpected ViewState: \(viewModel.state.viewState.description)") } } @@ -44,11 +38,11 @@ struct LoginScreen: View { .onLongPressGesture { showLinkInput = true } - Text(localize.login.title()) + Text(localize.login_title()) .font(.title) .padding() - Text(localize.login.desc()) + Text(localize.login_desc()) .font(.body) .padding() .multilineTextAlignment(.center) @@ -56,19 +50,19 @@ struct LoginScreen: View { Button { viewModel.actual.openScanner() } label: { - Label(localize.login.withQrCode(), systemImage: "qrcode.viewfinder") + Label(localize.login_withQrCode(), systemImage: "qrcode.viewfinder") .font(.title3) } .padding() Spacer() } - .alert(localize.login.title(), isPresented: $showLinkInput) { - TextField(localize.login.debugDialog.inputLabel(), text: $debugLoginLink) - Button(localize.dialog.cancel(), role: .cancel) { + .alert(localize.login_title(), isPresented: $showLinkInput) { + TextField(localize.login_scanner_debugDialog_inputLabel(), text: $debugLoginLink) + Button(localize.dialog_cancel(), role: .cancel) { showLinkInput = false } - Button(localize.login.title()) { + Button(localize.login_title()) { viewModel.actual.onDebugLogin(link: debugLoginLink) } } diff --git a/WaiterRobot/Features/Login/RegisterScreen.swift b/WaiterRobot/Features/Login/RegisterScreen.swift index 66336cd..39f66f2 100644 --- a/WaiterRobot/Features/Login/RegisterScreen.swift +++ b/WaiterRobot/Features/Login/RegisterScreen.swift @@ -21,11 +21,7 @@ struct RegisterScreen: View { case let error as ViewState.Error: content() .alert(isPresented: Binding.constant(true)) { - Alert( - title: Text(error.title), - message: Text(error.message), - dismissButton: .cancel(Text("OK"), action: error.onDismiss) - ) + Alert(error.dialog) } default: fatalError("Unexpected ViewState: \(viewModel.state.viewState.description)") @@ -34,10 +30,10 @@ struct RegisterScreen: View { private func content() -> some View { VStack { - Text(localize.register.name.desc()) + Text(localize.register_name_desc()) .font(.body) - TextField(localize.register.name.title(), text: $name) + TextField(localize.register_name_title(), text: $name) .font(.body) .fixedSize() .padding() @@ -46,7 +42,7 @@ struct RegisterScreen: View { Button { viewModel.actual.cancel() } label: { - Text(localize.dialog.cancel()) + Text(localize.dialog_cancel()) } Spacer() @@ -57,12 +53,12 @@ struct RegisterScreen: View { registerLink: deepLink ) } label: { - Text(localize.register.login()) + Text(localize.register_login()) } } .padding() - Label(localize.register.alreadyRegisteredInfo(), systemImage: "info.circle.fill") + Label(localize.register_alreadyRegisteredInfo(), systemImage: "info.circle.fill") } .padding() .navigationBarHidden(true) diff --git a/WaiterRobot/Features/Order/Search/ProductSearch.swift b/WaiterRobot/Features/Order/Search/ProductSearch.swift index 21d8b3c..8dde89b 100644 --- a/WaiterRobot/Features/Order/Search/ProductSearch.swift +++ b/WaiterRobot/Features/Order/Search/ProductSearch.swift @@ -5,7 +5,8 @@ import WRCore struct ProductSearch: View { @Environment(\.dismiss) private var dismiss - @ObservedObject var viewModel: ObservableOrderViewModel + @ObservedObject var viewModel: ObservableProductListViewModel + let addItem: (_ product: Product, _ amount: Int32) -> Void @State private var search: String = "" @State private var selectedTab: Int = 0 @@ -30,16 +31,8 @@ struct ProductSearch: View { } @ViewBuilder - private func productsGroupsList(productGroups: KotlinArray) -> some View { - let productGroups = Array(productGroups) - - if productGroups.isEmpty { - Text(localize.productSearch.noProductFound()) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .padding() - - } else { + private func productsGroupsList(productGroups: KotlinArray) -> some View { + if let productGroups = Array(productGroups), !productGroups.isEmpty { VStack { ProducSearchTabBarHeader(currentTab: $selectedTab, tabBarOptions: getGroupNames(productGroups)) @@ -48,7 +41,7 @@ struct ProductSearch: View { productGroups: productGroups, columns: layout, onProductClick: { - viewModel.actual.addItem(product: $0, amount: 1) + addItem($0, 1) dismiss() } ) @@ -56,17 +49,10 @@ struct ProductSearch: View { .padding() let enumeratedProductGroups = Array(productGroups.enumerated()) - ForEach(enumeratedProductGroups, id: \.element.id) { index, groupWithProducts in + ForEach(enumeratedProductGroups, id: \.element.id) { index, _ in ScrollView { LazyVGrid(columns: layout, spacing: 0) { - ProductSearchGroupList( - products: groupWithProducts.products, - backgroundColor: Color(hex: groupWithProducts.color), - onProductClick: { - viewModel.actual.addItem(product: $0, amount: 1) - dismiss() - } - ) + productGroup(groupedProducts: groupedProducts) Spacer() } .padding() @@ -80,23 +66,40 @@ struct ProductSearch: View { .onChange(of: search, perform: { viewModel.actual.filterProducts(filter: $0) }) .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button(localize.dialog.cancel()) { + Button(localize.dialog_cancel()) { dismiss() } } } + } else { + Text(localize.productSearch_noProductFound()) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding() } } - private func productGroupsError(error: ResourceError>) -> some View { - Text(error.userMessage) + @ViewBuilder + private func productGroup(groupedProducts: GroupedProducts) -> some View { + ProductSearchGroupList( + products: groupedProducts.products, + backgroundColor: Color(hex: groupedProducts.color), + onProductClick: { + addItem($0, 1) + dismiss() + } + ) + } + + private func productGroupsError(error: ResourceError>) -> some View { + Text(error.userMessage()) } - private func getGroupNames(_ productGroups: [ProductGroup]) -> [String] { + private func getGroupNames(_ productGroups: [GroupedProducts]) -> [String] { var groupNames = productGroups.map { productGroup in productGroup.name } - groupNames.insert(localize.productSearch.allGroups(), at: 0) + groupNames.insert(localize.productSearch_groups_all(), at: 0) return groupNames } } diff --git a/WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift b/WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift index b41313b..66fad7b 100644 --- a/WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift +++ b/WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift @@ -2,7 +2,7 @@ import shared import SwiftUI struct ProductSearchAllTab: View { - let productGroups: [ProductGroup] + let productGroups: [GroupedProducts] let columns: [GridItem] let onProductClick: (Product) -> Void @@ -35,7 +35,7 @@ struct ProductSearchAllTab: View { #Preview { ProductSearchAllTab( productGroups: [ - ProductGroup( + GroupedProducts( id: 1, name: "Test Group 1", position: 1, diff --git a/WaiterRobot/Features/SwitchEvent/Event.swift b/WaiterRobot/Features/SwitchEvent/Event.swift index 3466694..6289dd6 100644 --- a/WaiterRobot/Features/SwitchEvent/Event.swift +++ b/WaiterRobot/Features/SwitchEvent/Event.swift @@ -34,7 +34,8 @@ struct Event: View { endDate: nil, city: "Graz", organisationId: 1, - stripeSettings: shared.Event.StripeSettingsDisabled() + stripeSettings: shared.Event.StripeSettingsDisabled(), + isDemo: false ) ) } diff --git a/WaiterRobot/Features/TableDetail/OrderedItemView.swift b/WaiterRobot/Features/TableDetail/OrderedItemView.swift index 669fc37..6413f9d 100644 --- a/WaiterRobot/Features/TableDetail/OrderedItemView.swift +++ b/WaiterRobot/Features/TableDetail/OrderedItemView.swift @@ -26,7 +26,8 @@ struct OrderedItemView: View { baseProductId: 1, name: "Test", amount: 1, - virtualId: 2 + virtualId: 2, + note: "" ), tabbed: {} ) diff --git a/WaiterRobot/Features/TableList/TableGroupSection.swift b/WaiterRobot/Features/TableList/TableGroupSection.swift index d994504..3952346 100644 --- a/WaiterRobot/Features/TableList/TableGroupSection.swift +++ b/WaiterRobot/Features/TableList/TableGroupSection.swift @@ -4,16 +4,16 @@ import SwiftUI import WRCore struct TableGroupSection: View { - let tableGroup: TableGroup + let groupedTables: GroupedTables let onTableClick: (shared.Table) -> Void var body: some View { Section { - ForEach(tableGroup.tables, id: \.id) { table in + ForEach(groupedTables.tables, id: \.id) { table in TableView( text: table.number.description, hasOrders: table.hasOrders, - backgroundColor: Color(hex: tableGroup.color), + backgroundColor: Color(hex: groupedTables.color), onClick: { onTableClick(table) } @@ -22,7 +22,7 @@ struct TableGroupSection: View { } } header: { HStack { - if let background = Color(hex: tableGroup.color) { + if let background = Color(hex: groupedTables.color) { title(backgroundColor: background) } else { title(backgroundColor: .gray.opacity(0.3)) @@ -36,7 +36,7 @@ struct TableGroupSection: View { } private func title(backgroundColor: Color) -> some View { - Text(tableGroup.name) + Text(groupedTables.name) .font(.title2) .foregroundStyle(backgroundColor.getContentColor(lightColorScheme: .black, darkColorScheme: .white)) .padding(6) @@ -50,13 +50,11 @@ struct TableGroupSection: View { #Preview { LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) { TableGroupSection( - tableGroup: TableGroup( + groupedTables: GroupedTables( id: 1, name: "Test Group", eventId: 1, - position: 1, color: nil, - hidden: false, tables: [ shared.Table(id: 1, number: 1, groupName: "Test Group", hasOrders: true), shared.Table(id: 2, number: 2, groupName: "Test Group", hasOrders: false), diff --git a/WaiterRobot/Features/TableList/TableListFilterRow.swift b/WaiterRobot/Features/TableList/TableListFilterRow.swift index a3de937..a8e7aa3 100644 --- a/WaiterRobot/Features/TableList/TableListFilterRow.swift +++ b/WaiterRobot/Features/TableList/TableListFilterRow.swift @@ -106,8 +106,8 @@ struct TableListFilterRow: View { #Preview { TableListFilterRow( tableGroups: [ - TableGroup(id: 1, name: "Test Group1", eventId: 1, position: 1, color: nil, hidden: true, tables: []), - TableGroup(id: 2, name: "Test Group2", eventId: 1, position: 1, color: nil, hidden: false, tables: []), + TableGroup(id: 1, name: "Test Group1", color: nil, hidden: true), + TableGroup(id: 2, name: "Test Group2", color: nil, hidden: false), ], onToggleFilter: { _ in }, onSelectAll: {}, diff --git a/WaiterRobot/Features/UpdateApp/UpdateAppScreen.swift b/WaiterRobot/Features/UpdateApp/UpdateAppScreen.swift index 2132f9e..1659d94 100644 --- a/WaiterRobot/Features/UpdateApp/UpdateAppScreen.swift +++ b/WaiterRobot/Features/UpdateApp/UpdateAppScreen.swift @@ -5,7 +5,7 @@ import WRCore struct UpdateAppScreen: View { var body: some View { VStack { - Text(localize.app.forceUpdate.message()) + Text(localize.app_forceUpdate_message()) .multilineTextAlignment(.center) Button { @@ -19,11 +19,11 @@ struct UpdateAppScreen: View { UIApplication.shared.open(url, options: [:], completionHandler: nil) } } label: { - Text(localize.app.forceUpdate.openStore(value0: "App Store")) + Text(localize.app_forceUpdate_openStore("App Store")) }.padding() } .padding() - .navigationTitle(localize.app.forceUpdate.title()) + .navigationTitle(localize.app_forceUpdate_title()) .navigationBarTitleDisplayMode(.inline) } } diff --git a/WaiterRobot/Ui/LoadingOverlayView.swift b/WaiterRobot/Ui/LoadingOverlayView.swift new file mode 100644 index 0000000..c478bec --- /dev/null +++ b/WaiterRobot/Ui/LoadingOverlayView.swift @@ -0,0 +1,25 @@ +import SwiftUI + +struct LoadingOverlayView: View { + let isLoading: Bool + let content: () -> Content + + init(isLoading: Bool, @ViewBuilder content: @escaping () -> Content) { + self.isLoading = isLoading + self.content = content + } + + var body: some View { + ZStack { + content() + .opacity(isLoading ? 0.5 : 1.0) + + if isLoading { + Color.black.opacity(0.2) + .edgesIgnoringSafeArea(.all) + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + } + } + } +} diff --git a/WaiterRobot/Ui/ViewStateOverlayView.swift b/WaiterRobot/Ui/ViewStateOverlayView.swift new file mode 100644 index 0000000..1c6a49f --- /dev/null +++ b/WaiterRobot/Ui/ViewStateOverlayView.swift @@ -0,0 +1,44 @@ +import shared +import SwiftUI + +struct ViewStateOverlayView: View { + let state: Skie.Shared.ViewState.__Sealed + let content: () -> Content + + init(state: ViewState, @ViewBuilder content: @escaping () -> Content) { + self.state = onEnum(of: state) + self.content = content + } + + var body: some View { + ZStack { + LoadingOverlayView(isLoading: isLoading) { + VStack(alignment: .leading) { + content() + } + } + } + .alert(item: Binding( + get: { dialogState }, + set: { _ in dialogState?.onDismiss() } + )) { dialog in + Alert(dialog) + } + } + + private var isLoading: Bool { + if case .loading = state { + return true + } + return false + } + + private var dialogState: DialogState? { + if case let .error(error) = state { + return error.dialog + } + return nil + } +} + +extension DialogState: Identifiable {} From 64d0fd15d9aefaa677cf33b871a287ba2d7ebb13 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Mon, 9 Jun 2025 16:50:57 +0200 Subject: [PATCH 34/43] Refactor architecture: First working version --- .../SharedUI/Sources/SharedUI/Images.swift | 2 +- .../Resources/Images.xcassets/Contents.json | 6 - .../LogoRounded.imageset/Contents.json | 12 -- Modules/WRCore/Sources/WRCore/ErrorBar.swift | 51 ++++++++ .../Sources/WRCore/KotlinArrayWrapper.swift | 6 +- Modules/WRCore/Sources/WRCore/Mock.swift | 32 +++-- .../Sources/WRCore/ObservableViewModel.swift | 8 +- .../LogoRounded.imageset/Contents.json | 14 +++ .../LogoRounded.imageset/wr-round-yellow.svg | 56 +++++++++ Targets/Lava/WaiterRobotLava.plist | 6 +- .../LogoRounded.imageset/Contents.json | 14 +++ .../LogoRounded.imageset/wr-round.svg | 0 Targets/Prod/WaiterRobot.plist | 12 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Features/Billing/BillingScreen.swift | 4 +- WaiterRobot/Features/Billing/PayDialog.swift | 10 +- .../Features/Login/LoginScannerScreen.swift | 10 +- .../Features/Order/OrderProductNoteView.swift | 14 +-- WaiterRobot/Features/Order/OrderScreen.swift | 37 ++---- .../Features/Order/Search/ProductSearch.swift | 59 +++++---- .../Features/Settings/SettingsScreen.swift | 64 ++++------ .../Features/Settings/SwitchThemeView.swift | 4 +- .../SwitchEvent/SwitchEventScreen.swift | 50 ++++---- .../TableDetail/TableDetailScreen.swift | 13 +- .../TableList/TableGroupFilterSheet.swift | 91 ++++++++++++++ .../TableList/TableListFilterRow.swift | 117 ------------------ .../Features/TableList/TableListScreen.swift | 93 +++++--------- WaiterRobot/MainView.swift | 10 +- .../Resources/Images.xcassets/Contents.json | 6 - .../LogoRounded.imageset/Contents.json | 12 -- .../LogoRounded.imageset/wr-round.svg | 34 ----- WaiterRobot/Ui/LoadingOverlayView.swift | 4 +- WaiterRobot/Ui/ViewStateOverlayView.swift | 4 +- 33 files changed, 426 insertions(+), 433 deletions(-) delete mode 100644 Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/Contents.json delete mode 100644 Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/Contents.json create mode 100644 Modules/WRCore/Sources/WRCore/ErrorBar.swift create mode 100644 Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json create mode 100644 Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg create mode 100644 Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json rename {Modules/SharedUI/Sources/SharedUI/Resources => Targets/Prod}/Images.xcassets/LogoRounded.imageset/wr-round.svg (100%) create mode 100644 WaiterRobot/Features/TableList/TableGroupFilterSheet.swift delete mode 100644 WaiterRobot/Features/TableList/TableListFilterRow.swift delete mode 100644 WaiterRobot/Resources/Images.xcassets/Contents.json delete mode 100644 WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json delete mode 100644 WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg diff --git a/Modules/SharedUI/Sources/SharedUI/Images.swift b/Modules/SharedUI/Sources/SharedUI/Images.swift index bcfe6f5..4873187 100644 --- a/Modules/SharedUI/Sources/SharedUI/Images.swift +++ b/Modules/SharedUI/Sources/SharedUI/Images.swift @@ -2,6 +2,6 @@ import SwiftUI public extension Image { static var logoRounded: Image { - Image(.logoRounded) + Image("LogoRounded") } } diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/Contents.json deleted file mode 100644 index e4164e5..0000000 --- a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "wr-round.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Modules/WRCore/Sources/WRCore/ErrorBar.swift b/Modules/WRCore/Sources/WRCore/ErrorBar.swift new file mode 100644 index 0000000..e4d7cbb --- /dev/null +++ b/Modules/WRCore/Sources/WRCore/ErrorBar.swift @@ -0,0 +1,51 @@ +import shared +import SharedUI +import SwiftUI + +public struct ErrorBar: View { + let message: StringDesc + let initialLines: Int + let retryAction: (() -> Void)? + + @State private var expanded = false + + public init(message: StringDesc, initialLines: Int = 2, retryAction: (() -> Void)? = nil) { + self.message = message + self.initialLines = initialLines + self.retryAction = retryAction + } + + public var body: some View { + HStack(alignment: .center) { + Text(message()) + .lineLimit(expanded ? nil : initialLines) + .multilineTextAlignment(.leading) + // .foregroundColor(Color.onErrorContainer) + .frame(maxWidth: .infinity, alignment: .leading) + + if retryAction != nil { + Spacer().frame(width: 16) + Button(action: { + retryAction?() + }) { + Text(localize.exceptions_retry()) + .bold() + .multilineTextAlignment(.center) + .lineLimit(expanded ? nil : initialLines) + } + // .foregroundColor(Color.onErrorContainer) + } + } + .padding(.leading, 16) + .padding(.top, 8) + .padding(.trailing, retryAction == nil ? 16 : 8) + .padding(.bottom, 8) + // .background(Color.errorContainer) + .onTapGesture { + withAnimation { + expanded.toggle() + } + } + .animation(.default, value: expanded) + } +} diff --git a/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift b/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift index a9e5964..952c771 100644 --- a/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift +++ b/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift @@ -6,8 +6,12 @@ public extension Array where Element: AnyObject { guard let array = kotlinArray else { return nil } + self.init(array) + } + + init(_ kotlinArray: KotlinArray) { self.init() - let iterator = array.iterator() + let iterator = kotlinArray.iterator() while iterator.hasNext() { append(iterator.next() as! Element) } diff --git a/Modules/WRCore/Sources/WRCore/Mock.swift b/Modules/WRCore/Sources/WRCore/Mock.swift index c9d19db..55a6fd4 100644 --- a/Modules/WRCore/Sources/WRCore/Mock.swift +++ b/Modules/WRCore/Sources/WRCore/Mock.swift @@ -2,21 +2,31 @@ import Foundation import shared public enum Mock { - public static func tableGroups() -> [TableGroup] { + public static func groupedTables() -> [GroupedTables] { [ - tableGroup(with: 1, name: "Hof"), - tableGroup(with: 2, name: "Terasse"), - tableGroup(with: 3, name: "Zimmer A"), + GroupedTables( + id: 1, + name: "Hof", + eventId: 1, + color: nil, + tables: [ + table(with: 1), + table(with: 2), + table(with: 3), + ] + ), ] } - public static func tableGroup(with id: Int64, name: String = "Hof") -> TableGroup { - TableGroup( - id: id, - name: name, - color: "", - hidden: false, - ) + public static func tableGroups() -> [TableGroup] { + groupedTables().map { + TableGroup( + id: $0.id, + name: $0.name, + color: $0.color, + hidden: false, + ) + } } public static func table(with id: Int64, hasOrders: Bool = false) -> shared.Table { diff --git a/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift b/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift index 89dae0b..a658886 100644 --- a/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift +++ b/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift @@ -36,6 +36,12 @@ public class ObservableTableListViewModel: ObservableViewModel { + public init() { + super.init(viewModel: koin.tableGroupFilterVM()) + } +} + public class ObservableTableDetailViewModel: ObservableViewModel { public init(table: Table) { super.init(viewModel: koin.tableDetailVM(table: table)) @@ -62,7 +68,7 @@ public class ObservableOrderViewModel: ObservableViewModel { public init() { - super.init(viewModel: koin.getProductListVM()) + super.init(viewModel: koin.productListVM()) } } diff --git a/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json b/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json new file mode 100644 index 0000000..e576538 --- /dev/null +++ b/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json @@ -0,0 +1,14 @@ +{ + "images": + [ + { + "filename": "wr-round-yellow.svg", + "idiom": "universal" + } + ], + "info": + { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg b/Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg new file mode 100644 index 0000000..1a14ba8 --- /dev/null +++ b/Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Targets/Lava/WaiterRobotLava.plist b/Targets/Lava/WaiterRobotLava.plist index b35ff2a..cb19726 100644 --- a/Targets/Lava/WaiterRobotLava.plist +++ b/Targets/Lava/WaiterRobotLava.plist @@ -26,7 +26,7 @@ CFBundleShortVersionString 2.5.0 CFBundleVersion - 28998383 + 29158000 ITSAppUsesNonExemptEncryption NSAppTransportSecurity @@ -54,10 +54,8 @@ UIImageName LogoRounded UIImageRespectsSafeAreaInsets - + - UILaunchStoryboardName - LaunchScreen.storyboard UIRequiredDeviceCapabilities armv7 diff --git a/Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json b/Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json new file mode 100644 index 0000000..988ec93 --- /dev/null +++ b/Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json @@ -0,0 +1,14 @@ +{ + "images": + [ + { + "filename": "wr-round.svg", + "idiom": "universal" + } + ], + "info": + { + "author": "xcode", + "version": 1 + } +} \ No newline at end of file diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg b/Targets/Prod/Images.xcassets/LogoRounded.imageset/wr-round.svg similarity index 100% rename from Modules/SharedUI/Sources/SharedUI/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg rename to Targets/Prod/Images.xcassets/LogoRounded.imageset/wr-round.svg diff --git a/Targets/Prod/WaiterRobot.plist b/Targets/Prod/WaiterRobot.plist index 8dfafcc..713b445 100644 --- a/Targets/Prod/WaiterRobot.plist +++ b/Targets/Prod/WaiterRobot.plist @@ -35,15 +35,15 @@ NSBluetoothAlwaysUsageDescription - We don't use bluetooth + We don't use bluetooth NSCameraUsageDescription Camera is needed to scan QR-Codes NSContactsUsageDescription - We don't use your contacts + We don't use your contacts NSLocationWhenInUseUsageDescription - We don't use your location + We don't use your location NSMotionUsageDescription - We don't use your motion sensors + We don't use your motion sensors UIApplicationSceneManifest UIApplicationSupportsMultipleScenes @@ -54,10 +54,8 @@ UIImageName LogoRounded UIImageRespectsSafeAreaInsets - + - UILaunchStoryboardName - LaunchScreen.storyboard UIRequiredDeviceCapabilities armv7 diff --git a/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 55894a6..4fd8fe6 100644 --- a/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/WaiterRobot.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DatepollSystems/WaiterRobot-Shared-Android.git", "state" : { - "revision" : "c01f764d92dcd7df8ee4ad4ecba55a9200ed7a00", - "version" : "1.7.3" + "revision" : "f2ff44dc52e8df3f4e68da4d05e27ad0b0487a33", + "version" : "1.7.6" } } ], diff --git a/WaiterRobot/Features/Billing/BillingScreen.swift b/WaiterRobot/Features/Billing/BillingScreen.swift index cd0e690..800de70 100644 --- a/WaiterRobot/Features/Billing/BillingScreen.swift +++ b/WaiterRobot/Features/Billing/BillingScreen.swift @@ -58,7 +58,7 @@ private struct BillingScreenView: View { var body: some View { ViewStateOverlayView(state: state.paymentState) { - let billItemsState = onEnum(of: state.billItems_) + let billItemsState = onEnum(of: state.billItems) if case let .loading(ressource) = billItemsState, ressource.data == nil { ProgressView() @@ -67,7 +67,7 @@ private struct BillingScreenView: View { Text("Error \(resource.userMessage())") } - if let billItems = Array(state.billItems_.data), !billItems.isEmpty { + if let billItems = Array(state.billItems.data), !billItems.isEmpty { content(billItems: billItems) } else { Text(localize.billing_noOrder(table.groupName, table.number.description)) diff --git a/WaiterRobot/Features/Billing/PayDialog.swift b/WaiterRobot/Features/Billing/PayDialog.swift index ae3048a..79d0e06 100644 --- a/WaiterRobot/Features/Billing/PayDialog.swift +++ b/WaiterRobot/Features/Billing/PayDialog.swift @@ -15,14 +15,14 @@ struct PayDialog: View { NavigationView { VStack { HStack { - Text(localize.billing.total() + ":") + Text(localize.billing_total() + ":") .font(.title2) Spacer() Text(viewModel.state.priceSum.description) .font(.title2) } - TextField(localize.billing.given(), text: $moneyGiven) + TextField(localize.billing_given(), text: $moneyGiven) .font(.title) .keyboardType(.numbersAndPunctuation) .onChange(of: moneyGiven) { value in @@ -37,7 +37,7 @@ struct PayDialog: View { ) HStack { - Text(localize.billing.change() + ":") + Text(localize.billing_change() + ":") .font(.title2) Spacer() @@ -52,14 +52,14 @@ struct PayDialog: View { .padding() .toolbar { ToolbarItem(placement: .navigationBarLeading) { - Button(localize.dialog.cancel()) { + Button(localize.dialog_cancel()) { dismiss() } } } .toolbar { ToolbarItem(placement: .confirmationAction) { - Button(localize.billing.pay()) { + Button(localize.billing_pay_cash()) { viewModel.actual.paySelection(paymentSheetShown: true) dismiss() } diff --git a/WaiterRobot/Features/Login/LoginScannerScreen.swift b/WaiterRobot/Features/Login/LoginScannerScreen.swift index a47fe0a..c0c884b 100644 --- a/WaiterRobot/Features/Login/LoginScannerScreen.swift +++ b/WaiterRobot/Features/Login/LoginScannerScreen.swift @@ -20,11 +20,7 @@ struct LoginScannerScreen: View { case let error as ViewState.Error: content() .alert(isPresented: Binding.constant(true)) { - Alert( - title: Text(error.title), - message: Text(error.message), - dismissButton: .cancel(Text("OK"), action: error.onDismiss) - ) + Alert(error.dialog) } default: fatalError("Unexpected ViewState: \(viewModel.state.viewState.description)") @@ -45,14 +41,14 @@ struct LoginScannerScreen: View { } } - Text(localize.login.scanner.desc()) + Text(localize.login_scanner_desc()) .padding() .multilineTextAlignment(.center) Button { viewModel.actual.goBack() } label: { - Text(localize.dialog.cancel()) + Text(localize.dialog_cancel()) } } .withViewModel(viewModel, navigator) diff --git a/WaiterRobot/Features/Order/OrderProductNoteView.swift b/WaiterRobot/Features/Order/OrderProductNoteView.swift index 3e01073..93498de 100644 --- a/WaiterRobot/Features/Order/OrderProductNoteView.swift +++ b/WaiterRobot/Features/Order/OrderProductNoteView.swift @@ -18,7 +18,7 @@ struct OrderProductNoteView: View { var body: some View { NavigationView { content() - .navigationTitle(localize.order.addNoteDialog.title(value0: name)) + .navigationTitle(localize.order_add_note_title(name)) .navigationBarTitleDisplayMode(.inline) } } @@ -26,16 +26,16 @@ struct OrderProductNoteView: View { @ViewBuilder private func content() -> some View { VStack { - Text(localize.order.addNoteDialog.inputLabel()) + Text(localize.order_add_note_input_label()) Group { if #available(iOS 16, *) { - TextField(localize.order.addNoteDialog.inputPlaceholder(), text: $noteText, axis: .vertical) + TextField(localize.order_add_note_input_placeholder(), text: $noteText, axis: .vertical) .lineLimit(5, reservesSpace: true) .toolbarBackground(.visible, for: .bottomBar) } else { // TODO: Maybe change to TextEditor - TextField(localize.order.addNoteDialog.inputPlaceholder(), text: $noteText) + TextField(localize.order_add_note_input_placeholder(), text: $noteText) .lineLimit(5) } } @@ -82,14 +82,14 @@ struct OrderProductNoteView: View { @ViewBuilder private func cancelButton() -> some View { - Button(localize.dialog.cancel(), role: .cancel) { + Button(localize.dialog_cancel(), role: .cancel) { dismiss() } } @ViewBuilder private func clearButton() -> some View { - Button(localize.dialog.clear(), role: .destructive) { + Button(localize.dialog_clear(), role: .destructive) { noteText = "" onSaveNote(nil) dismiss() @@ -99,7 +99,7 @@ struct OrderProductNoteView: View { @ViewBuilder private func saveButton() -> some View { - Button(localize.dialog.save()) { + Button(localize.dialog_save()) { onSaveNote(noteText) dismiss() } diff --git a/WaiterRobot/Features/Order/OrderScreen.swift b/WaiterRobot/Features/Order/OrderScreen.swift index 8e5ed52..e4189c8 100644 --- a/WaiterRobot/Features/Order/OrderScreen.swift +++ b/WaiterRobot/Features/Order/OrderScreen.swift @@ -23,38 +23,27 @@ struct OrderScreen: View { } var body: some View { - VStack { - switch onEnum(of: viewModel.state.currentOrder) { - case .loading: - ProgressView() - - case let .error(error): - Text(error.userMessage) - .foregroundStyle(.red) - .padding(.horizontal) - - currentOder(error.data) - - case let .success(resource): - currentOder(resource.data) - } + ViewStateOverlayView(state: viewModel.state.orderingState) { + currentOder(Array(viewModel.state.currentOrder)) } - .navigationTitle(localize.order.title(value0: table.groupName, value1: table.number.description)) + .navigationTitle(localize.order_title(table.groupName, table.number.description)) .navigationBarTitleDisplayMode(.large) .navigationBarBackButtonHidden() .confirmationDialog( - localize.order.notSent.title(), + localize.order_notSent_title(), isPresented: $showAbortOrderConfirmationDialog, titleVisibility: .visible ) { - Button(localize.dialog.closeAnyway(), role: .destructive) { + Button(localize.dialog_closeAnyway(), role: .destructive) { viewModel.actual.abortOrder() } } message: { - Text(localize.order.notSent.desc()) + Text(localize.order_notSent_desc()) } .sheet(isPresented: $showProductSearch) { - ProductSearch(viewModel: viewModel) + ProductSearch( + addItem: { viewModel.actual.addItem(product: $0, amount: $1) } + ) } .animation(.default, value: viewModel.state.currentOrder) .withViewModel(viewModel, navigator) @@ -62,15 +51,13 @@ struct OrderScreen: View { @ViewBuilder private func currentOder( - _ currentOrderArray: KotlinArray? + _ currentOrder: [OrderItem] ) -> some View { - let currentOrder = currentOrderArray.map { Array($0) } ?? Array() - VStack(spacing: 0) { if currentOrder.isEmpty { Spacer() - Text(localize.order.addProduct()) + Text(localize.order_product_add()) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .padding() @@ -116,7 +103,7 @@ struct OrderScreen: View { } .buttonStyle(.primary) } - .customBackNavigation(title: localize.dialog.cancel(), icon: "chevron.backward") { + .customBackNavigation(title: localize.dialog_cancel(), icon: "chevron.backward") { if currentOrder.isEmpty { viewModel.actual.abortOrder() } else { diff --git a/WaiterRobot/Features/Order/Search/ProductSearch.swift b/WaiterRobot/Features/Order/Search/ProductSearch.swift index 8dde89b..2ad9869 100644 --- a/WaiterRobot/Features/Order/Search/ProductSearch.swift +++ b/WaiterRobot/Features/Order/Search/ProductSearch.swift @@ -3,10 +3,11 @@ import SwiftUI import WRCore struct ProductSearch: View { + let addItem: (_ product: Product, _ amount: Int32) -> Void + @Environment(\.dismiss) private var dismiss - @ObservedObject var viewModel: ObservableProductListViewModel - let addItem: (_ product: Product, _ amount: Int32) -> Void + @ObservedObject private var viewModel = ObservableProductListViewModel() @State private var search: String = "" @State private var selectedTab: Int = 0 @@ -17,22 +18,40 @@ struct ProductSearch: View { var body: some View { NavigationView { - switch onEnum(of: viewModel.state.productGroups) { - case .loading: - ProgressView() - case let .error(resource): - productGroupsError(error: resource) - case let .success(resource): - if let productGroups = resource.data { - productsGroupsList(productGroups: productGroups) + content() + .observeState(of: viewModel) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button(localize.dialog_cancel()) { + dismiss() + } + } } + } + } + + @ViewBuilder + private func content() -> some View { + switch onEnum(of: viewModel.state.productGroups) { + case .loading: + ProgressView() + case let .error(resource): + productGroupsError(error: resource) + case let .success(resource): + if let productGroups = Array(resource.data) { + productsGroupsList(productGroups: productGroups) } - }.observeState(of: viewModel) + } } @ViewBuilder - private func productsGroupsList(productGroups: KotlinArray) -> some View { - if let productGroups = Array(productGroups), !productGroups.isEmpty { + private func productsGroupsList(productGroups: [GroupedProducts]) -> some View { + if productGroups.isEmpty { + Text(localize.productSearch_noProductFound()) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding() + } else { VStack { ProducSearchTabBarHeader(currentTab: $selectedTab, tabBarOptions: getGroupNames(productGroups)) @@ -49,7 +68,7 @@ struct ProductSearch: View { .padding() let enumeratedProductGroups = Array(productGroups.enumerated()) - ForEach(enumeratedProductGroups, id: \.element.id) { index, _ in + ForEach(enumeratedProductGroups, id: \.element.id) { index, groupedProducts in ScrollView { LazyVGrid(columns: layout, spacing: 0) { productGroup(groupedProducts: groupedProducts) @@ -64,18 +83,6 @@ struct ProductSearch: View { .tabViewStyle(.page(indexDisplayMode: .never)) .searchable(text: $search, placement: .navigationBarDrawer(displayMode: .always)) .onChange(of: search, perform: { viewModel.actual.filterProducts(filter: $0) }) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button(localize.dialog_cancel()) { - dismiss() - } - } - } - } else { - Text(localize.productSearch_noProductFound()) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .padding() } } diff --git a/WaiterRobot/Features/Settings/SettingsScreen.swift b/WaiterRobot/Features/Settings/SettingsScreen.swift index 20d9a7b..7b451c9 100644 --- a/WaiterRobot/Features/Settings/SettingsScreen.swift +++ b/WaiterRobot/Features/Settings/SettingsScreen.swift @@ -12,44 +12,24 @@ struct SettingsScreen: View { @StateObject private var viewModel = ObservableSettingsViewModel() var body: some View { - switch viewModel.state.viewState { - case is ViewState.Loading: - ProgressView() - case is ViewState.Idle: - content() - case let error as ViewState.Error: - content() - .alert(isPresented: Binding.constant(true)) { - Alert( - title: Text(error.title), - message: Text(error.message), - dismissButton: .cancel(Text("OK"), action: error.onDismiss) - ) - } - default: - fatalError("Unexpected ViewState: \(viewModel.state.viewState.description)") - } - } - - private func content() -> some View { List { general() payment() - Section(header: Text(localize.settings.about.title())) { - Link(localize.settings.about.privacyPolicy(), destination: URL(string: CommonApp.shared.privacyPolicyUrl)!) + Section(header: Text(localize.settings_about_title())) { + Link(localize.settings_about_privacyPolicy(), destination: URL(string: CommonApp.shared.privacyPolicyUrl)!) } HStack { Spacer() - Text(viewModel.state.versionString) + Text(viewModel.state.versionString()) .font(.footnote) Spacer() } .listRowBackground(Color.clear) } - .navigationTitle(localize.settings.title()) + .navigationTitle(localize.settings_title()) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { @@ -60,28 +40,28 @@ struct SettingsScreen: View { } } .confirmationDialog( - localize.settings.general.logout.title(value0: CommonApp.shared.settings.organisationName), + localize.settings_general_logout_title(CommonApp.shared.settings.organisationName), isPresented: $showConfirmLogout, titleVisibility: .visible ) { - Button(localize.settings.general.logout.action(), role: .destructive, action: { viewModel.actual.logout() }) - Button(localize.settings.general.keepLoggedIn(), role: .cancel, action: { showConfirmLogout = false }) + Button(localize.settings_general_logout_action(), role: .destructive, action: { viewModel.actual.logout() }) + Button(localize.settings_general_logout_cancel(), role: .cancel, action: { showConfirmLogout = false }) } message: { - Text(localize.settings.general.logout.desc(value0: CommonApp.shared.settings.organisationName)) + Text(localize.settings_general_logout_desc(CommonApp.shared.settings.organisationName)) } .confirmationDialog( - localize.settings.payment.skipMoneyBackDialog.title(), + localize.settings_payment_skipMoneyBackDialog_title(), isPresented: $showConfirmSkipMoneyBackDialog, titleVisibility: .visible ) { - Button(localize.settings.payment.skipMoneyBackDialog.confirmAction(), role: .destructive) { + Button(localize.settings_payment_skipMoneyBackDialog_confirm_action(), role: .destructive) { viewModel.actual.toggleSkipMoneyBackDialog(value: true, confirmed: true) } - Button(localize.dialog.cancel(), role: .cancel) { + Button(localize.dialog_cancel(), role: .cancel) { showConfirmSkipMoneyBackDialog = false } } message: { - Text(localize.settings.payment.skipMoneyBackDialog.confirmDesc()) + Text(localize.settings_payment_skipMoneyBackDialog_confirm_desc()) } .withViewModel(viewModel, navigator) { effect in switch onEnum(of: effect) { @@ -94,10 +74,10 @@ struct SettingsScreen: View { } private func general() -> some View { - Section(header: Text(localize.settings.general.title())) { + Section(header: Text(localize.settings_general_title())) { SettingsItem( icon: "rectangle.portrait.and.arrow.right", - title: localize.settings.general.logout.action(), + title: localize.settings_general_logout_action(), subtitle: "\"\(CommonApp.shared.settings.organisationName)\" / \"\(CommonApp.shared.settings.waiterName)\"", onClick: { showConfirmLogout = true @@ -106,7 +86,7 @@ struct SettingsScreen: View { SettingsItem( icon: "person.3", - title: localize.switchEvent.title(), + title: localize.switchEvent_title(), subtitle: CommonApp.shared.settings.eventName, onClick: { viewModel.actual.switchEvent() @@ -120,8 +100,8 @@ struct SettingsScreen: View { SettingsItem( icon: "arrow.triangle.2.circlepath", - title: localize.settings.general.refresh.title(), - subtitle: localize.settings.general.refresh.desc(), + title: localize.settings_general_refresh_title(), + subtitle: localize.settings_general_refresh_desc(), onClick: { viewModel.actual.refreshAll() } @@ -130,11 +110,11 @@ struct SettingsScreen: View { } private func payment() -> some View { - Section(header: Text(localize.settings.payment.title())) { + Section(header: Text(localize.settings_payment_title())) { SettingsItem( icon: "dollarsign.arrow.circlepath", - title: localize.settings.payment.skipMoneyBackDialog.title(), - subtitle: localize.settings.payment.skipMoneyBackDialog.desc(), + title: localize.settings_payment_skipMoneyBackDialog_title(), + subtitle: localize.settings_payment_skipMoneyBackDialog_desc(), action: { Toggle( isOn: .init( @@ -154,8 +134,8 @@ struct SettingsScreen: View { SettingsItem( icon: "checkmark.square", - title: localize.settings.payment.selectAllProductsByDefault.title(), - subtitle: localize.settings.payment.selectAllProductsByDefault.desc(), + title: localize.settings_payment_selectAllProductsByDefault_title(), + subtitle: localize.settings_payment_selectAllProductsByDefault_desc(), action: { Toggle( isOn: .init( diff --git a/WaiterRobot/Features/Settings/SwitchThemeView.swift b/WaiterRobot/Features/Settings/SwitchThemeView.swift index 623e0a5..c35bd19 100644 --- a/WaiterRobot/Features/Settings/SwitchThemeView.swift +++ b/WaiterRobot/Features/Settings/SwitchThemeView.swift @@ -21,9 +21,9 @@ struct SwitchThemeView: View { .padding(.trailing) .foregroundColor(.blue) - Picker(localize.settings.general.darkMode.title(), selection: $selectedTheme) { + Picker(localize.settings_general_darkMode_title(), selection: $selectedTheme) { ForEach(AppTheme.companion.valueList(), id: \.name) { theme in - Text(theme.settingsText()).tag(theme) + Text(theme.settingsText().localized()).tag(theme) } } .onChange(of: selectedTheme, perform: onChange) diff --git a/WaiterRobot/Features/SwitchEvent/SwitchEventScreen.swift b/WaiterRobot/Features/SwitchEvent/SwitchEventScreen.swift index b32caf3..b138276 100644 --- a/WaiterRobot/Features/SwitchEvent/SwitchEventScreen.swift +++ b/WaiterRobot/Features/SwitchEvent/SwitchEventScreen.swift @@ -11,26 +11,6 @@ struct SwitchEventScreen: View { @State private var selectedEvent: Event? var body: some View { - VStack { - switch onEnum(of: viewModel.state.viewState) { - case .loading: - ProgressView() - case .idle: - content() - case let .error(error): - content() - .alert(isPresented: Binding.constant(true)) { - Alert( - title: Text(error.title), - message: Text(error.message), - dismissButton: .cancel(Text("OK"), action: error.onDismiss) - ) - } - } - }.withViewModel(viewModel, navigator) - } - - private func content() -> some View { VStack { Image(systemName: "person.3") .resizable() @@ -38,22 +18,37 @@ struct SwitchEventScreen: View { .frame(maxHeight: 100) .padding() - Text(localize.switchEvent.desc()) + Text(localize.switchEvent_desc()) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .padding() Divider() - ScrollView { - if viewModel.state.events.isEmpty { - Text(localize.switchEvent.noEventFound()) + content(viewModel.state.events) + .refreshable { + try? await viewModel.actual.loadEvents().join() + } + + }.withViewModel(viewModel, navigator) + } + + private func content(_ eventResource: shared.Resource>) -> some View { + ScrollView { + let resource = onEnum(of: eventResource) + + if case let .error(error) = resource { + ErrorBar(message: error.userMessage, retryAction: { viewModel.actual.loadEvents() }) + } + if let events = Array(resource.data) { + if events.isEmpty { + Text(localize.switchEvent_noEventFound()) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .padding() } else { LazyVStack { - ForEach(viewModel.state.events, id: \.id) { event in + ForEach(events, id: \.id) { event in Button { viewModel.actual.onEventSelected(event: event) } label: { @@ -64,9 +59,8 @@ struct SwitchEventScreen: View { } } } - } - .refreshable { - viewModel.actual.loadEvents() + } else { + ProgressView() } } } diff --git a/WaiterRobot/Features/TableDetail/TableDetailScreen.swift b/WaiterRobot/Features/TableDetail/TableDetailScreen.swift index 17d00d2..ac6a337 100644 --- a/WaiterRobot/Features/TableDetail/TableDetailScreen.swift +++ b/WaiterRobot/Features/TableDetail/TableDetailScreen.swift @@ -18,14 +18,14 @@ struct TableDetailScreen: View { var body: some View { content() - .navigationTitle(localize.tableDetail.title(value0: table.groupName, value1: table.number.description)) + .navigationTitle(localize.tableDetail_title(table.groupName, table.number.description)) .withViewModel(viewModel, navigator) } // TODO: add refreshing and loading indicator (also check android) private func content() -> some View { VStack { - switch onEnum(of: viewModel.state.orderedItemsResource) { + switch onEnum(of: viewModel.state.orderedItems) { case .loading: ProgressView() @@ -33,7 +33,7 @@ struct TableDetailScreen: View { tableDetailsError(error) case let .success(resource): - if let orderedItems = resource.data as? [OrderedItem] { + if let orderedItems = Array(resource.data) { tableDetails(orderedItems: orderedItems) } } @@ -41,12 +41,11 @@ struct TableDetailScreen: View { } private func tableDetails(orderedItems: [OrderedItem]) -> some View { - // TODO: we need KotlinArray here in shared VStack { if orderedItems.isEmpty { Spacer() - Text(localize.tableDetail.noOrder(value0: table.groupName, value1: table.number.description)) + Text(localize.tableDetail_noOrder(table.groupName, table.number.description)) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .padding() @@ -85,7 +84,7 @@ struct TableDetailScreen: View { } } - private func tableDetailsError(_ error: ResourceError) -> some View { - Text(error.userMessage) + private func tableDetailsError(_ error: ResourceError>) -> some View { + Text(error.userMessage()) } } diff --git a/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift b/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift new file mode 100644 index 0000000..924422d --- /dev/null +++ b/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift @@ -0,0 +1,91 @@ +import shared +import SwiftUI +import UIPilot +import WRCore + +struct TableGroupFilterSheet: View { + @Environment(\.dismiss) private var dismiss + + @StateObject private var viewModel = ObservableTableGroupFilterViewModel() + + var body: some View { + NavigationView { + content() + .observeState(of: viewModel) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button(localize.dialog_cancel()) { + dismiss() + } + } + } + } + } + + @ViewBuilder + private func content() -> some View { + switch onEnum(of: viewModel.state.groups) { + case .loading: + ProgressView() + case let .error(resource): + Text(resource.userMessage()) + case let .success(resource): + TableGroupFilter( + groups: Array(resource.data) ?? [], + showAll: { viewModel.actual.showAll() }, + hideAll: { viewModel.actual.hideAll() }, + onToggle: { viewModel.actual.toggleFilter(tableGroup: $0) } + ) + } + } +} + +private struct TableGroupFilter: View { + let groups: [TableGroup] + let showAll: () -> Void + let hideAll: () -> Void + let onToggle: (TableGroup) -> Void + + var body: some View { + if groups.isEmpty { + // Should not happen as open filter is only shown when there are groups + Text(localize.tableList_noTableFound()) + } else { + ScrollView { + LazyVStack(alignment: .leading) { + ForEach(groups, id: \.id) { group in + HStack { + Circle() + .fill(Color(hex: group.color) ?? Color.gray.opacity(0.3)) + .frame(height: 40) + + Text(group.name) + + Spacer() + + Toggle( + isOn: .init( + get: { !group.hidden }, + set: { _ in onToggle(group) } + ), + label: {} + ).labelsHidden() + }.padding(.horizontal) + } + } + } + } + } +} + +#Preview { + TableGroupFilter( + groups: [ + shared.TableGroup(id: 1, name: "Group 1", color: "ffaaff", hidden: false), + shared.TableGroup(id: 2, name: "Group 2", color: "aaffaa", hidden: false), + ], + showAll: {}, + hideAll: {}, + onToggle: { _ in } + ) +} diff --git a/WaiterRobot/Features/TableList/TableListFilterRow.swift b/WaiterRobot/Features/TableList/TableListFilterRow.swift deleted file mode 100644 index a8e7aa3..0000000 --- a/WaiterRobot/Features/TableList/TableListFilterRow.swift +++ /dev/null @@ -1,117 +0,0 @@ -import shared -import SwiftUI - -struct TableListFilterRow: View { - let tableGroups: [TableGroup] - let onToggleFilter: (TableGroup) -> Void - let onSelectAll: () -> Void - let onUnselectAll: () -> Void - - var body: some View { - if #available(iOS 16, *) { - newFilter() - } else { - oldFilter() - } - } - - @available(iOS 16, *) - private func newFilter() -> some View { - VStack(spacing: 20) { - DynamicGrid( - horizontalSpacing: 5, - verticalSpacing: 5 - ) { - ForEach(tableGroups, id: \.id) { group in - if group.hidden { - Button { - onToggleFilter(group) - } label: { - Text(group.name) - .padding() - } - .buttonStyle(.gray) - } else { - Button { - onToggleFilter(group) - } label: { - Text(group.name) - .padding() - } - .buttonStyle(.primary) - } - } - } - - HStack { - Button { - onSelectAll() - } label: { - Image(systemName: "rectangle.badge.checkmark") - .imageScale(.large) - .padding(8) - } - .buttonStyle(.primary) - - Button { - onUnselectAll() - } label: { - Image(systemName: "rectangle.badge.xmark") - .imageScale(.large) - .padding(8) - } - .buttonStyle(.gray) - } - .frame(maxWidth: .infinity) - } - } - - private func oldFilter() -> some View { - HStack { - ScrollView(.horizontal) { - HStack { - ForEach(tableGroups, id: \.id) { group in - Button { - onToggleFilter(group) // viewModel.actual.toggleFilter(tableGroup: group) - } label: { - Text(group.name) - } - .buttonStyle(.bordered) - .tint(group.hidden ? .primary : .blue) - } - } - .padding(.horizontal) - } - .padding(.bottom, 4) - - Button { - onSelectAll() - } label: { - Image(systemName: "checkmark") - } - .padding(.trailing) - .disabled(tableGroups.allSatisfy { !$0.hidden }) - - Button { - onUnselectAll() - } label: { - Image(systemName: "xmark") - } - .padding(.trailing) - .disabled(tableGroups.allSatisfy(\.hidden)) - } - } -} - -#Preview { - TableListFilterRow( - tableGroups: [ - TableGroup(id: 1, name: "Test Group1", color: nil, hidden: true), - TableGroup(id: 2, name: "Test Group2", color: nil, hidden: false), - ], - onToggleFilter: { _ in }, - onSelectAll: {}, - onUnselectAll: {} - ) - .padding() -} diff --git a/WaiterRobot/Features/TableList/TableListScreen.swift b/WaiterRobot/Features/TableList/TableListScreen.swift index 012956e..b4d2c43 100644 --- a/WaiterRobot/Features/TableList/TableListScreen.swift +++ b/WaiterRobot/Features/TableList/TableListScreen.swift @@ -16,6 +16,13 @@ struct TableListScreen: View { if #available(iOS 16.0, *) { content() .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + showFilters.toggle() + } label: { + Image(systemName: "line.3.horizontal.decrease") + } + } ToolbarItem(placement: .topBarTrailing) { Button { viewModel.actual.openSettings() @@ -28,6 +35,13 @@ struct TableListScreen: View { } else { content() .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + showFilters.toggle() + } label: { + Image(systemName: "line.3.horizontal.decrease") + } + } ToolbarItem(placement: .navigationBarTrailing) { Button { viewModel.actual.openSettings() @@ -56,23 +70,31 @@ struct TableListScreen: View { } } .navigationBarTitleDisplayMode(.inline) - .animation(.spring, value: viewModel.state.tableGroupsArray) + .animation(.spring, value: viewModel.state.tableGroups) + .sheet(isPresented: $showFilters) { + TableGroupFilterSheet() + } .withViewModel(viewModel, navigator) } private func content() -> some View { ZStack { - if let data = viewModel.state.tableGroupsArray.data { - tableList(data: data) + if let tableGroups = Array(viewModel.state.tableGroups.data) { + TableListView( + tableGroups: tableGroups, + onTableSelect: { viewModel.actual.onTableClick(table: $0) } + ) + } else { + ProgressView() } - switch onEnum(of: viewModel.state.tableGroupsArray) { + switch onEnum(of: viewModel.state.tableGroups) { case let .error(resource): VStack { Spacer() HStack { - Text(resource.userMessage) + Text(resource.userMessage()) .padding() Spacer() @@ -92,28 +114,10 @@ struct TableListScreen: View { } } } - - @ViewBuilder - private func tableList(data: KotlinArray) -> some View { - let tableGroups = Array(data) - - TableListView( - showFilters: $showFilters, - tableGroups: tableGroups, - onToggleFilter: { viewModel.actual.toggleFilter(tableGroup: $0) }, - onSelectAll: { viewModel.actual.showAll() }, - onUnselectAll: { viewModel.actual.hideAll() }, - onTableSelect: { viewModel.actual.onTableClick(table: $0) } - ) - } } struct TableListView: View { - @Binding var showFilters: Bool - let tableGroups: [TableGroup] - let onToggleFilter: (TableGroup) -> Void - let onSelectAll: () -> Void - let onUnselectAll: () -> Void + let tableGroups: [GroupedTables] let onTableSelect: (shared.Table) -> Void private let layout = [ @@ -122,25 +126,10 @@ struct TableListView: View { var body: some View { VStack(spacing: 0) { - if tableGroups.count > 1, showFilters { - VStack { - TableListFilterRow( - tableGroups: tableGroups, - onToggleFilter: onToggleFilter, - onSelectAll: onSelectAll, - onUnselectAll: onUnselectAll - ) - } - .padding() - .background(Color(UIColor.systemBackground)) - } - - Divider() - if tableGroups.isEmpty { Spacer() - Text(localize.tableList.noTableFound()) + Text(localize.tableList_noTableFound()) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .padding() @@ -152,10 +141,10 @@ struct TableListView: View { columns: layout, pinnedViews: [.sectionHeaders] ) { - ForEach(tableGroups.filter { !$0.hidden }, id: \.id) { group in + ForEach(tableGroups, id: \.id) { group in if !group.tables.isEmpty { TableGroupSection( - tableGroup: group, + groupedTables: group, onTableClick: onTableSelect ) } @@ -163,20 +152,8 @@ struct TableListView: View { } .padding() } - .toolbar { - ToolbarItem(placement: .topBarLeading) { - if tableGroups.count > 1 { - Button { - showFilters.toggle() - } label: { - Image(systemName: "slider.horizontal.3") - } - } - } - } } } - .animation(.easeIn, value: showFilters) } } @@ -192,11 +169,9 @@ struct TableListView: View { PreviewView { NavigationView { TableListView( - showFilters: .constant(false), - tableGroups: Mock.tableGroups() - ) { _ in - } onSelectAll: {} onUnselectAll: {} onTableSelect: { _ in - } + tableGroups: Mock.groupedTables(), + onTableSelect: { _ in } + ) } } } diff --git a/WaiterRobot/MainView.swift b/WaiterRobot/MainView.swift index 17d12e1..d7fc025 100644 --- a/WaiterRobot/MainView.swift +++ b/WaiterRobot/MainView.swift @@ -63,7 +63,7 @@ struct MainView: View { .withViewModel(viewModel, navigator) { effect in switch onEnum(of: effect) { case let .showSnackBar(snackBar): - snackBarMessage = snackBar.message + snackBarMessage = snackBar.message() DispatchQueue.main.asyncAfter(deadline: .now() + 5) { snackBarMessage = nil } @@ -74,14 +74,14 @@ struct MainView: View { viewModel.actual.onDeepLink(url: url.absoluteString) } .alert( - localize.app.updateAvailable.title(), + localize.app_updateAvailable_title(), isPresented: $showUpdateAvailableAlert ) { - Button(localize.dialog.cancel(), role: .cancel) { + Button(localize.dialog_cancel(), role: .cancel) { showUpdateAvailableAlert = false } - Button(localize.app.forceUpdate.openStore(value0: "App Store")) { + Button(localize.app_forceUpdate_openStore("App Store")) { guard let storeUrl = VersionChecker.shared.storeUrl, let url = URL(string: storeUrl) else { @@ -93,7 +93,7 @@ struct MainView: View { } } } message: { - Text(localize.app.updateAvailable.message()) + Text(localize.app_updateAvailable_message()) } .onAppear { VersionChecker.shared.checkVersion { diff --git a/WaiterRobot/Resources/Images.xcassets/Contents.json b/WaiterRobot/Resources/Images.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/WaiterRobot/Resources/Images.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json b/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json deleted file mode 100644 index e4164e5..0000000 --- a/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "wr-round.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg b/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg deleted file mode 100644 index 14b173f..0000000 --- a/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WaiterRobot/Ui/LoadingOverlayView.swift b/WaiterRobot/Ui/LoadingOverlayView.swift index c478bec..c9567ee 100644 --- a/WaiterRobot/Ui/LoadingOverlayView.swift +++ b/WaiterRobot/Ui/LoadingOverlayView.swift @@ -1,6 +1,6 @@ import SwiftUI -struct LoadingOverlayView: View { +public struct LoadingOverlayView: View { let isLoading: Bool let content: () -> Content @@ -9,7 +9,7 @@ struct LoadingOverlayView: View { self.content = content } - var body: some View { + public var body: some View { ZStack { content() .opacity(isLoading ? 0.5 : 1.0) diff --git a/WaiterRobot/Ui/ViewStateOverlayView.swift b/WaiterRobot/Ui/ViewStateOverlayView.swift index 1c6a49f..4772e7d 100644 --- a/WaiterRobot/Ui/ViewStateOverlayView.swift +++ b/WaiterRobot/Ui/ViewStateOverlayView.swift @@ -1,7 +1,7 @@ import shared import SwiftUI -struct ViewStateOverlayView: View { +public struct ViewStateOverlayView: View { let state: Skie.Shared.ViewState.__Sealed let content: () -> Content @@ -10,7 +10,7 @@ struct ViewStateOverlayView: View { self.content = content } - var body: some View { + public var body: some View { ZStack { LoadingOverlayView(isLoading: isLoading) { VStack(alignment: .leading) { From 7f6adaf0b027ae15988c8db26cb8f88838a8e65f Mon Sep 17 00:00:00 2001 From: Alexander Kauer Date: Mon, 23 Jun 2025 21:15:11 +0200 Subject: [PATCH 35/43] Fixed launch logo and readme --- README.md | 6 +- .../LogoRounded.imageset/Contents.json | 20 ++-- .../LogoRounded.imageset/wr-round-yellow.svg | 92 ++++++++----------- Targets/Lava/WaiterRobotLava.plist | 2 +- .../LogoRounded.imageset/Contents.json | 20 ++-- .../Features/TableList/TableListScreen.swift | 5 +- 6 files changed, 61 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 899d38c..760d2c6 100644 --- a/README.md +++ b/README.md @@ -20,16 +20,14 @@ The KMM module is integrated as a Swift-Package (shared). This project uses XcodeGen for generating the Xcode project. -1. Xcodegen +1. Gems Run in your terminal: ```bash -swift run xcodegen +bundle install ``` -> This command must also be run after switching branches and it's advisable to also run it after a `git pull` - 2. Git pre-commit hook To have unified formatting, we use SwiftFormat. The pre-commit hook can be installed if the code should be formatted automatically before every commit. Execute following command in your terminal: diff --git a/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json b/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json index e576538..c7d86da 100644 --- a/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json +++ b/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json @@ -1,14 +1,12 @@ { - "images": - [ - { - "filename": "wr-round-yellow.svg", - "idiom": "universal" - } - ], - "info": + "images" : [ { - "author": "xcode", - "version": 1 + "filename" : "wr-round-yellow.svg", + "idiom" : "universal" } -} \ No newline at end of file + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg b/Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg index 1a14ba8..7a9af26 100644 --- a/Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg +++ b/Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg @@ -1,56 +1,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Targets/Lava/WaiterRobotLava.plist b/Targets/Lava/WaiterRobotLava.plist index cb19726..2a61768 100644 --- a/Targets/Lava/WaiterRobotLava.plist +++ b/Targets/Lava/WaiterRobotLava.plist @@ -26,7 +26,7 @@ CFBundleShortVersionString 2.5.0 CFBundleVersion - 29158000 + 29178434 ITSAppUsesNonExemptEncryption NSAppTransportSecurity diff --git a/Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json b/Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json index 988ec93..e4164e5 100644 --- a/Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json +++ b/Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json @@ -1,14 +1,12 @@ { - "images": - [ - { - "filename": "wr-round.svg", - "idiom": "universal" - } - ], - "info": + "images" : [ { - "author": "xcode", - "version": 1 + "filename" : "wr-round.svg", + "idiom" : "universal" } -} \ No newline at end of file + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WaiterRobot/Features/TableList/TableListScreen.swift b/WaiterRobot/Features/TableList/TableListScreen.swift index b4d2c43..75013d5 100644 --- a/WaiterRobot/Features/TableList/TableListScreen.swift +++ b/WaiterRobot/Features/TableList/TableListScreen.swift @@ -35,14 +35,15 @@ struct TableListScreen: View { } else { content() .toolbar { - ToolbarItem(placement: .topBarTrailing) { + ToolbarItem(placement: .topBarLeading) { Button { showFilters.toggle() } label: { Image(systemName: "line.3.horizontal.decrease") } } - ToolbarItem(placement: .navigationBarTrailing) { + + ToolbarItem(placement: .topBarTrailing) { Button { viewModel.actual.openSettings() } label: { From 8a20317d4108f7480b56799d72cdfdfca36e302e Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Tue, 24 Jun 2025 22:08:45 +0200 Subject: [PATCH 36/43] Reafactor architecture: Cleanup --- .../Sources/SharedUI/TabBarHeader.swift | 16 +++- Modules/WRCore/Sources/WRCore/Mock.swift | 73 ++++++++++++++----- Targets/Lava/WaiterRobotLava.plist | 2 +- WaiterRobot/Features/Order/OrderScreen.swift | 7 +- .../Order/Search/AllProductGroupList.swift | 38 ++++++++++ .../Order/Search/ProductGroupList.swift | 40 ++++++++++ .../Order/{ => Search}/ProductListItem.swift | 47 +++--------- .../Features/Order/Search/ProductSearch.swift | 66 ++--------------- .../Order/Search/ProductSearchAllTab.swift | 60 --------------- .../Order/Search/ProductSearchGroupList.swift | 38 ---------- .../Order/Search/ProductTabView.swift | 62 ++++++++++++++++ .../TableList/TableGroupFilterSheet.swift | 5 +- .../TableList/TableGroupSection.swift | 13 +--- .../Features/TableList/TableView.swift | 29 +++++--- WaiterRobot/Util/Extensions/Color.swift | 5 +- 15 files changed, 256 insertions(+), 245 deletions(-) rename WaiterRobot/Features/Order/Search/ProducSearchTabBarHeader.swift => Modules/SharedUI/Sources/SharedUI/TabBarHeader.swift (80%) create mode 100644 WaiterRobot/Features/Order/Search/AllProductGroupList.swift create mode 100644 WaiterRobot/Features/Order/Search/ProductGroupList.swift rename WaiterRobot/Features/Order/{ => Search}/ProductListItem.swift (63%) delete mode 100644 WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift delete mode 100644 WaiterRobot/Features/Order/Search/ProductSearchGroupList.swift create mode 100644 WaiterRobot/Features/Order/Search/ProductTabView.swift diff --git a/WaiterRobot/Features/Order/Search/ProducSearchTabBarHeader.swift b/Modules/SharedUI/Sources/SharedUI/TabBarHeader.swift similarity index 80% rename from WaiterRobot/Features/Order/Search/ProducSearchTabBarHeader.swift rename to Modules/SharedUI/Sources/SharedUI/TabBarHeader.swift index cc76630..ced4f6d 100644 --- a/WaiterRobot/Features/Order/Search/ProducSearchTabBarHeader.swift +++ b/Modules/SharedUI/Sources/SharedUI/TabBarHeader.swift @@ -1,11 +1,16 @@ import SwiftUI -struct ProducSearchTabBarHeader: View { +public struct TabBarHeader: View { @Namespace var namespace @Binding var currentTab: Int var tabBarOptions: [String] - var body: some View { + public init(currentTab: Binding, tabBarOptions: [String]) { + _currentTab = currentTab + self.tabBarOptions = tabBarOptions + } + + public var body: some View { VStack(spacing: 0) { ScrollView(.horizontal) { HStack { @@ -48,8 +53,11 @@ struct ProducSearchTabBarHeader: View { } } +@available(iOS 17.0, *) #Preview { - ProducSearchTabBarHeader( - currentTab: .constant(4), tabBarOptions: ["All", "Food", "Drinks", "more", "One more"] + @Previewable @State var currentTab = 3 + TabBarHeader( + currentTab: $currentTab, + tabBarOptions: ["All", "Food", "Drinks", "more", "One more"] ) } diff --git a/Modules/WRCore/Sources/WRCore/Mock.swift b/Modules/WRCore/Sources/WRCore/Mock.swift index 55a6fd4..af7fc79 100644 --- a/Modules/WRCore/Sources/WRCore/Mock.swift +++ b/Modules/WRCore/Sources/WRCore/Mock.swift @@ -2,24 +2,26 @@ import Foundation import shared public enum Mock { - public static func groupedTables() -> [GroupedTables] { - [ - GroupedTables( - id: 1, - name: "Hof", + public static func groupedTables(groups: Int = 1) -> [GroupedTables] { + let colors = ["ffaaee", "ffeeaa", "eeaaff", nil] + return (1 ... groups).map { groupId in + let tableCount = groupId % 3 == 0 ? 4 : 3 + let groupName = "Table Group \(groupId)" + + return GroupedTables( + id: Int64(groupId), + name: groupName, eventId: 1, - color: nil, - tables: [ - table(with: 1), - table(with: 2), - table(with: 3), - ] - ), - ] + color: colors[groupId % colors.count], + tables: (1 ... tableCount).map { + table(with: groupId * 10 + $0, hasOrders: $0 % 2 == 0, groupName: groupName) + } + ) + } } - public static func tableGroups() -> [TableGroup] { - groupedTables().map { + public static func tableGroups(groups: Int = 1) -> [TableGroup] { + groupedTables(groups: groups).map { TableGroup( id: $0.id, name: $0.name, @@ -29,12 +31,47 @@ public enum Mock { } } - public static func table(with id: Int64, hasOrders: Bool = false) -> shared.Table { + public static func table(with id: Int, hasOrders: Bool = false, groupName: String = "Hof") -> shared.Table { shared.Table( - id: id, + id: Int64(id), number: Int32(id), - groupName: "Hof", + groupName: groupName, hasOrders: hasOrders ) } + + public static func product(with id: Int, soldOut: Bool = false, color: String? = nil, allergens: Set = []) -> Product { + Product( + id: Int64(id), + name: "Product \(id)", + price: Money(cents: Int32(id * 10)), + soldOut: soldOut, + color: color, + allergens: allergens.enumerated().map { index, shortName in + Allergen(id: Int64(index), name: shortName.description, shortName: shortName.description) + }.filter { $0.shortName.isEmpty == false }, + position: Int32(id), + ) + } + + public static func productGroups(groups: Int = 1) -> [GroupedProducts] { + let colors = ["ffaaee", "ffeeaa", "eeaaff", nil].shuffled() + let allergenList = "ABCDEFG " + return (1 ... groups).map { groupId in + let productCount = groupId % 3 == 0 ? 4 : 3 + let groupName = "Product Group \(groupId)" + return GroupedProducts( + id: Int64(groupId), + name: groupName, + position: Int32(groupId), + color: colors[groupId % colors.count], + products: (1 ... productCount).map { + let allergens = (0 ... ($0 % 3)).map { _ in + allergenList.randomElement()! + } + return product(with: groupId * 10 + $0, soldOut: $0 % 5 == 2, allergens: Set(allergens)) + }, + ) + } + } } diff --git a/Targets/Lava/WaiterRobotLava.plist b/Targets/Lava/WaiterRobotLava.plist index cb19726..812752f 100644 --- a/Targets/Lava/WaiterRobotLava.plist +++ b/Targets/Lava/WaiterRobotLava.plist @@ -26,7 +26,7 @@ CFBundleShortVersionString 2.5.0 CFBundleVersion - 29158000 + 29174937 ITSAppUsesNonExemptEncryption NSAppTransportSecurity diff --git a/WaiterRobot/Features/Order/OrderScreen.swift b/WaiterRobot/Features/Order/OrderScreen.swift index e4189c8..2066d63 100644 --- a/WaiterRobot/Features/Order/OrderScreen.swift +++ b/WaiterRobot/Features/Order/OrderScreen.swift @@ -6,7 +6,6 @@ import WRCore struct OrderScreen: View { @EnvironmentObject var navigator: UIPilot - @State private var productName: String = "" @State private var showProductSearch: Bool @State private var showAbortOrderConfirmationDialog = false @@ -112,3 +111,9 @@ struct OrderScreen: View { } } } + +#Preview { + PreviewView { + OrderScreen(table: Mock.table(with: 1), initialItemId: 1) + } +} diff --git a/WaiterRobot/Features/Order/Search/AllProductGroupList.swift b/WaiterRobot/Features/Order/Search/AllProductGroupList.swift new file mode 100644 index 0000000..e54823e --- /dev/null +++ b/WaiterRobot/Features/Order/Search/AllProductGroupList.swift @@ -0,0 +1,38 @@ +import shared +import SwiftUI +import WRCore + +struct AllProductGroupList: View { + let productGroups: [GroupedProducts] + let onProductClick: (Product) -> Void + + var body: some View { + ScrollView { + ForEach(productGroups, id: \.id) { productGroup in + if !productGroup.products.isEmpty { + Section { + ProductGroupList( + products: productGroup.products, + backgroundColor: Color(hex: productGroup.color), + onProductClick: onProductClick + ) + } header: { + HStack { + Color(UIColor.lightGray).frame(height: 1) + Text(productGroup.name) + Color(UIColor.lightGray).frame(height: 1) + } + } + } + } + } + } +} + +#Preview { + AllProductGroupList( + productGroups: Mock.productGroups(groups: 3), + onProductClick: { _ in } + ) + .padding() +} diff --git a/WaiterRobot/Features/Order/Search/ProductGroupList.swift b/WaiterRobot/Features/Order/Search/ProductGroupList.swift new file mode 100644 index 0000000..bb1eb58 --- /dev/null +++ b/WaiterRobot/Features/Order/Search/ProductGroupList.swift @@ -0,0 +1,40 @@ +import shared +import SwiftUI +import WRCore + +struct ProductGroupList: View { + let products: [Product] + let backgroundColor: Color? + let onProductClick: (Product) -> Void + + private let layout = [ + GridItem(.adaptive(minimum: 110)), + ] + + var body: some View { + ScrollView { + LazyVGrid(columns: layout, spacing: 0) { + ForEach(products, id: \.id) { product in + ProductListItem(product: product, backgroundColor: backgroundColor) { + onProductClick(product) + } + .foregroundColor(.blackWhite) + .padding(10) + } + } + } + } +} + +#Preview { + ProductGroupList( + products: [ + Mock.product(with: 1), + Mock.product(with: 2, soldOut: true, allergens: ["A"]), + Mock.product(with: 3, color: "ffaa00", allergens: ["A"]), + Mock.product(with: 4, soldOut: true, color: "ffaa00"), + ], + backgroundColor: .yellow, + onProductClick: { _ in } + ) +} diff --git a/WaiterRobot/Features/Order/ProductListItem.swift b/WaiterRobot/Features/Order/Search/ProductListItem.swift similarity index 63% rename from WaiterRobot/Features/Order/ProductListItem.swift rename to WaiterRobot/Features/Order/Search/ProductListItem.swift index 600dd4c..d0aaf81 100644 --- a/WaiterRobot/Features/Order/ProductListItem.swift +++ b/WaiterRobot/Features/Order/Search/ProductListItem.swift @@ -1,5 +1,6 @@ import shared import SwiftUI +import WRCore struct ProductListItem: View { let product: Product @@ -14,19 +15,23 @@ struct ProductListItem: View { onClick: @escaping () -> Void ) { self.product = product - self.backgroundColor = backgroundColor - self.onClick = onClick + if let color = product.color { + self.backgroundColor = Color(hex: color) + } else { + self.backgroundColor = backgroundColor + } var allergens = "" for allergen in self.product.allergens { allergens += "\(allergen.shortName), " } - if allergens.count > 2 { self.allergens = String(allergens.prefix(allergens.count - 2)) } else { self.allergens = "" } + + self.onClick = onClick } var foregroundColor: Color { @@ -69,22 +74,8 @@ struct ProductListItem: View { #Preview { ProductListItem( - product: Product( - id: 2, - name: "Wine", - price: Money(cents: 290), - soldOut: true, - color: nil, - allergens: [ - Allergen(id: 1, name: "Egg", shortName: "E"), - Allergen(id: 2, name: "Egg2", shortName: "A"), - Allergen(id: 3, name: "Egg3", shortName: "B"), - Allergen(id: 4, name: "Egg4", shortName: "C"), - Allergen(id: 5, name: "Egg5", shortName: "D"), - ], - position: 1 - ), - backgroundColor: .yellow, + product: Mock.product(with: 1, soldOut: false, color: "ffaaee", allergens: ["A"]), + backgroundColor: .red, onClick: {} ) .frame(maxWidth: 100, maxHeight: 100) @@ -92,22 +83,8 @@ struct ProductListItem: View { #Preview { ProductListItem( - product: Product( - id: 2, - name: "Wine", - price: Money(cents: 290), - soldOut: false, - color: nil, - allergens: [ - Allergen(id: 1, name: "Egg", shortName: "E"), - Allergen(id: 2, name: "Egg2", shortName: "A"), - Allergen(id: 3, name: "Egg3", shortName: "B"), - Allergen(id: 4, name: "Egg4", shortName: "C"), - Allergen(id: 5, name: "Egg5", shortName: "D"), - ], - position: 1 - ), - backgroundColor: .yellow, + product: Mock.product(with: 1, soldOut: true, color: "ffaaee", allergens: ["A", "B"]), + backgroundColor: .red, onClick: {} ) .frame(maxWidth: 100, maxHeight: 100) diff --git a/WaiterRobot/Features/Order/Search/ProductSearch.swift b/WaiterRobot/Features/Order/Search/ProductSearch.swift index 2ad9869..b213e7a 100644 --- a/WaiterRobot/Features/Order/Search/ProductSearch.swift +++ b/WaiterRobot/Features/Order/Search/ProductSearch.swift @@ -12,10 +12,6 @@ struct ProductSearch: View { @State private var search: String = "" @State private var selectedTab: Int = 0 - private let layout = [ - GridItem(.adaptive(minimum: 110)), - ] - var body: some View { NavigationView { content() @@ -39,65 +35,19 @@ struct ProductSearch: View { productGroupsError(error: resource) case let .success(resource): if let productGroups = Array(resource.data) { - productsGroupsList(productGroups: productGroups) - } - } - } - - @ViewBuilder - private func productsGroupsList(productGroups: [GroupedProducts]) -> some View { - if productGroups.isEmpty { - Text(localize.productSearch_noProductFound()) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .padding() - } else { - VStack { - ProducSearchTabBarHeader(currentTab: $selectedTab, tabBarOptions: getGroupNames(productGroups)) - - TabView(selection: $selectedTab) { - ProductSearchAllTab( - productGroups: productGroups, - columns: layout, - onProductClick: { - addItem($0, 1) - dismiss() - } - ) - .tag(0) - .padding() - - let enumeratedProductGroups = Array(productGroups.enumerated()) - ForEach(enumeratedProductGroups, id: \.element.id) { index, groupedProducts in - ScrollView { - LazyVGrid(columns: layout, spacing: 0) { - productGroup(groupedProducts: groupedProducts) - Spacer() - } - .padding() - } - .tag(index + 1) + ProductTabView( + productGroups: productGroups, + addItem: { + addItem($0, $1) + dismiss() } - } + ) + .searchable(text: $search, placement: .navigationBarDrawer(displayMode: .always)) + .onChange(of: search, perform: { viewModel.actual.filterProducts(filter: $0) }) } - .tabViewStyle(.page(indexDisplayMode: .never)) - .searchable(text: $search, placement: .navigationBarDrawer(displayMode: .always)) - .onChange(of: search, perform: { viewModel.actual.filterProducts(filter: $0) }) } } - @ViewBuilder - private func productGroup(groupedProducts: GroupedProducts) -> some View { - ProductSearchGroupList( - products: groupedProducts.products, - backgroundColor: Color(hex: groupedProducts.color), - onProductClick: { - addItem($0, 1) - dismiss() - } - ) - } - private func productGroupsError(error: ResourceError>) -> some View { Text(error.userMessage()) } diff --git a/WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift b/WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift deleted file mode 100644 index 66fad7b..0000000 --- a/WaiterRobot/Features/Order/Search/ProductSearchAllTab.swift +++ /dev/null @@ -1,60 +0,0 @@ -import shared -import SwiftUI - -struct ProductSearchAllTab: View { - let productGroups: [GroupedProducts] - let columns: [GridItem] - let onProductClick: (Product) -> Void - - var body: some View { - ScrollView { - LazyVGrid(columns: columns) { - ForEach(productGroups, id: \.id) { productGroup in - if !productGroup.products.isEmpty { - Section { - ProductSearchGroupList( - products: productGroup.products, - backgroundColor: Color(hex: productGroup.color), - onProductClick: onProductClick - ) - } header: { - HStack { - Color(UIColor.lightGray).frame(height: 1) - Text(productGroup.name) - Color(UIColor.lightGray).frame(height: 1) - } - } - } - } - Spacer() - } - } - } -} - -#Preview { - ProductSearchAllTab( - productGroups: [ - GroupedProducts( - id: 1, - name: "Test Group 1", - position: 1, - color: "", - products: [ - Product( - id: 1, - name: "Beer", - price: Money(cents: 450), - soldOut: false, - color: nil, - allergens: [], - position: 1 - ), - ] - ), - ], - columns: [GridItem(.adaptive(minimum: 110))], - onProductClick: { _ in } - ) - .padding() -} diff --git a/WaiterRobot/Features/Order/Search/ProductSearchGroupList.swift b/WaiterRobot/Features/Order/Search/ProductSearchGroupList.swift deleted file mode 100644 index effc113..0000000 --- a/WaiterRobot/Features/Order/Search/ProductSearchGroupList.swift +++ /dev/null @@ -1,38 +0,0 @@ -import shared -import SwiftUI - -struct ProductSearchGroupList: View { - let products: [Product] - let backgroundColor: Color? - let onProductClick: (Product) -> Void - - var body: some View { - ForEach(products, id: \.id) { product in - ProductListItem(product: product, backgroundColor: backgroundColor) { - onProductClick(product) - } - .foregroundColor(.blackWhite) - .padding(10) - } - } -} - -#Preview { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 110))]) { - ProductSearchGroupList( - products: [ - Product( - id: 1, - name: "Beer", - price: Money(cents: 450), - soldOut: false, - color: nil, - allergens: [], - position: 1 - ), - ], - backgroundColor: .yellow, - onProductClick: { _ in } - ) - } -} diff --git a/WaiterRobot/Features/Order/Search/ProductTabView.swift b/WaiterRobot/Features/Order/Search/ProductTabView.swift new file mode 100644 index 0000000..e2e5204 --- /dev/null +++ b/WaiterRobot/Features/Order/Search/ProductTabView.swift @@ -0,0 +1,62 @@ +import shared +import SharedUI +import SwiftUI +import WRCore + +struct ProductTabView: View { + let productGroups: [GroupedProducts] + let addItem: (_ product: Product, _ amount: Int32) -> Void + + @State private var selectedTab: Int = 0 + + var body: some View { + if productGroups.isEmpty { + Text(localize.productSearch_noProductFound()) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding() + } else { + VStack { + TabBarHeader( + currentTab: $selectedTab, + tabBarOptions: getGroupNames(productGroups) + ) + + TabView(selection: $selectedTab) { + AllProductGroupList( + productGroups: productGroups, + onProductClick: { addItem($0, 1) } + ) + .tag(0) + .padding() + + let enumeratedProductGroups = Array(productGroups.enumerated()) + ForEach(enumeratedProductGroups, id: \.element.id) { index, groupedProducts in + ProductGroupList( + products: groupedProducts.products, + backgroundColor: Color(hex: groupedProducts.color), + onProductClick: { addItem($0, 1) } + ).padding() + .tag(index + 1) + } + } + } + .tabViewStyle(.page(indexDisplayMode: .never)) + } + } + + private func getGroupNames(_ productGroups: [GroupedProducts]) -> [String] { + var groupNames = productGroups.map { productGroup in + productGroup.name + } + groupNames.insert(localize.productSearch_groups_all(), at: 0) + return groupNames + } +} + +#Preview { + ProductTabView( + productGroups: Mock.productGroups(groups: 3), + addItem: { _, _ in } + ) +} diff --git a/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift b/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift index 924422d..6ea82a2 100644 --- a/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift +++ b/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift @@ -80,10 +80,7 @@ private struct TableGroupFilter: View { #Preview { TableGroupFilter( - groups: [ - shared.TableGroup(id: 1, name: "Group 1", color: "ffaaff", hidden: false), - shared.TableGroup(id: 2, name: "Group 2", color: "aaffaa", hidden: false), - ], + groups: Mock.tableGroups(groups: 10), showAll: {}, hideAll: {}, onToggle: { _ in } diff --git a/WaiterRobot/Features/TableList/TableGroupSection.swift b/WaiterRobot/Features/TableList/TableGroupSection.swift index 3952346..89ec6a4 100644 --- a/WaiterRobot/Features/TableList/TableGroupSection.swift +++ b/WaiterRobot/Features/TableList/TableGroupSection.swift @@ -50,18 +50,7 @@ struct TableGroupSection: View { #Preview { LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) { TableGroupSection( - groupedTables: GroupedTables( - id: 1, - name: "Test Group", - eventId: 1, - color: nil, - tables: [ - shared.Table(id: 1, number: 1, groupName: "Test Group", hasOrders: true), - shared.Table(id: 2, number: 2, groupName: "Test Group", hasOrders: false), - shared.Table(id: 3, number: 3, groupName: "Test Group", hasOrders: false), - shared.Table(id: 4, number: 4, groupName: "Test Group", hasOrders: true), - ] - ), + groupedTables: Mock.groupedTables().first!, onTableClick: { _ in } ) } diff --git a/WaiterRobot/Features/TableList/TableView.swift b/WaiterRobot/Features/TableList/TableView.swift index 43fa12a..761ee38 100644 --- a/WaiterRobot/Features/TableList/TableView.swift +++ b/WaiterRobot/Features/TableList/TableView.swift @@ -4,11 +4,15 @@ import SwiftUI struct TableView: View { let text: String let hasOrders: Bool - let backgroundColor: Color? + let backgroundColor: Color let onClick: () -> Void - @Environment(\.colorScheme) - var colorScheme + init(text: String, hasOrders: Bool, backgroundColor: Color?, onClick: @escaping () -> Void) { + self.text = text + self.hasOrders = hasOrders + self.backgroundColor = backgroundColor ?? .gray.opacity(0.3) + self.onClick = onClick + } var body: some View { Button(action: onClick) { @@ -23,7 +27,7 @@ struct TableView: View { Spacer() Circle() - .foregroundColor(backgroundColor?.getContentColor(lightColorScheme: Color(.darkRed), darkColorScheme: Color(.lightRed))) + .foregroundColor(backgroundColor.getContentColor(lightColorScheme: Color(.darkRed), darkColorScheme: Color(.lightRed))) .frame(width: 12) } @@ -36,20 +40,21 @@ struct TableView: View { } .aspectRatio(1.0, contentMode: .fit) .background { - if let backgroundColor { - RoundedRectangle(cornerRadius: 20) - .foregroundColor(backgroundColor) - } else { - RoundedRectangle(cornerRadius: 20) - .foregroundColor(.gray.opacity(0.3)) - } + RoundedRectangle(cornerRadius: 20) + .foregroundColor(backgroundColor) } - .foregroundStyle(backgroundColor?.getContentColor(lightColorScheme: .black, darkColorScheme: .white) ?? .blackWhite) + .foregroundStyle(backgroundColor.getContentColor(lightColorScheme: .white, darkColorScheme: .black)) } } #Preview { VStack { + TableView(text: "1", hasOrders: false, backgroundColor: .black) {} + .frame(maxWidth: 100) + + TableView(text: "1", hasOrders: false, backgroundColor: .gray.opacity(0.1)) {} + .frame(maxWidth: 100) + TableView(text: "1", hasOrders: false, backgroundColor: .green) {} .frame(maxWidth: 100) diff --git a/WaiterRobot/Util/Extensions/Color.swift b/WaiterRobot/Util/Extensions/Color.swift index 1da311a..c877195 100644 --- a/WaiterRobot/Util/Extensions/Color.swift +++ b/WaiterRobot/Util/Extensions/Color.swift @@ -64,9 +64,10 @@ extension Color { } // Adjust color based on contrast + // TODO: this does not respect opacity func getContentColor(lightColorScheme: Color, darkColorScheme: Color) -> Color { - let lightContrast = contrastRatio(with: lightColorScheme) - let darkContrast = contrastRatio(with: darkColorScheme) + let lightContrast = lightColorScheme.contrastRatio(with: self) + let darkContrast = darkColorScheme.contrastRatio(with: self) return lightContrast > darkContrast ? lightColorScheme : darkColorScheme } From 8d5e0b15d4529ffe8cb3858aab9b6b15bb04b79e Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Sun, 20 Jul 2025 12:16:11 +0200 Subject: [PATCH 37/43] Fix bestContrastColor --- WaiterRobot.xcodeproj/project.pbxproj | 4 +- .../Order/Search/ProductListItem.swift | 4 +- .../TableList/TableGroupSection.swift | 6 +- .../Features/TableList/TableView.swift | 32 ++++------ WaiterRobot/Util/Extensions/Color.swift | 60 ++++++++++++------- 5 files changed, 61 insertions(+), 45 deletions(-) diff --git a/WaiterRobot.xcodeproj/project.pbxproj b/WaiterRobot.xcodeproj/project.pbxproj index cd41971..c42c9bb 100644 --- a/WaiterRobot.xcodeproj/project.pbxproj +++ b/WaiterRobot.xcodeproj/project.pbxproj @@ -432,7 +432,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -582,7 +582,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; diff --git a/WaiterRobot/Features/Order/Search/ProductListItem.swift b/WaiterRobot/Features/Order/Search/ProductListItem.swift index d0aaf81..50f2347 100644 --- a/WaiterRobot/Features/Order/Search/ProductListItem.swift +++ b/WaiterRobot/Features/Order/Search/ProductListItem.swift @@ -3,6 +3,8 @@ import SwiftUI import WRCore struct ProductListItem: View { + @Environment(\.self) var env + let product: Product let backgroundColor: Color? let onClick: () -> Void @@ -38,7 +40,7 @@ struct ProductListItem: View { if product.soldOut { .blackWhite } else if let backgroundColor { - backgroundColor.getContentColor(lightColorScheme: .black, darkColorScheme: .white) + backgroundColor.bestContrastColor(.black, .white, in: env) } else { .blackWhite } diff --git a/WaiterRobot/Features/TableList/TableGroupSection.swift b/WaiterRobot/Features/TableList/TableGroupSection.swift index 89ec6a4..ba35708 100644 --- a/WaiterRobot/Features/TableList/TableGroupSection.swift +++ b/WaiterRobot/Features/TableList/TableGroupSection.swift @@ -4,6 +4,8 @@ import SwiftUI import WRCore struct TableGroupSection: View { + @Environment(\.self) var env + let groupedTables: GroupedTables let onTableClick: (shared.Table) -> Void @@ -25,7 +27,7 @@ struct TableGroupSection: View { if let background = Color(hex: groupedTables.color) { title(backgroundColor: background) } else { - title(backgroundColor: .gray.opacity(0.3)) + title(backgroundColor: .lightGray) } Spacer() @@ -38,7 +40,7 @@ struct TableGroupSection: View { private func title(backgroundColor: Color) -> some View { Text(groupedTables.name) .font(.title2) - .foregroundStyle(backgroundColor.getContentColor(lightColorScheme: .black, darkColorScheme: .white)) + .foregroundStyle(backgroundColor.bestContrastColor(.black, .white, in: env)) .padding(6) .background { RoundedRectangle(cornerRadius: 8.0) diff --git a/WaiterRobot/Features/TableList/TableView.swift b/WaiterRobot/Features/TableList/TableView.swift index 761ee38..0d0c50a 100644 --- a/WaiterRobot/Features/TableList/TableView.swift +++ b/WaiterRobot/Features/TableList/TableView.swift @@ -2,6 +2,8 @@ import SharedUI import SwiftUI struct TableView: View { + @Environment(\.self) var env + let text: String let hasOrders: Bool let backgroundColor: Color @@ -10,31 +12,23 @@ struct TableView: View { init(text: String, hasOrders: Bool, backgroundColor: Color?, onClick: @escaping () -> Void) { self.text = text self.hasOrders = hasOrders - self.backgroundColor = backgroundColor ?? .gray.opacity(0.3) + self.backgroundColor = backgroundColor ?? .lightGray self.onClick = onClick } var body: some View { Button(action: onClick) { - ZStack { + ZStack(alignment: .topTrailing) { Text(text) .font(.title) .frame(maxWidth: .infinity, maxHeight: .infinity) if hasOrders { - VStack(alignment: .trailing) { - HStack { - Spacer() - - Circle() - .foregroundColor(backgroundColor.getContentColor(lightColorScheme: Color(.darkRed), darkColorScheme: Color(.lightRed))) - .frame(width: 12) - } - - Spacer() - } - .padding(.top, 10) - .padding(.trailing, 10) + Circle() + .foregroundColor(backgroundColor.bestContrastColor(Color(.darkRed), Color(.lightRed), in: env)) + .frame(width: 12, height: 12) + .padding(.top, 10) + .padding(.trailing, 10) } } } @@ -43,19 +37,19 @@ struct TableView: View { RoundedRectangle(cornerRadius: 20) .foregroundColor(backgroundColor) } - .foregroundStyle(backgroundColor.getContentColor(lightColorScheme: .white, darkColorScheme: .black)) + .foregroundStyle(backgroundColor.bestContrastColor(.white, .black, in: env)) } } #Preview { VStack { - TableView(text: "1", hasOrders: false, backgroundColor: .black) {} + TableView(text: "1", hasOrders: true, backgroundColor: .blackWhite) {} .frame(maxWidth: 100) - TableView(text: "1", hasOrders: false, backgroundColor: .gray.opacity(0.1)) {} + TableView(text: "1", hasOrders: false, backgroundColor: .gray) {} .frame(maxWidth: 100) - TableView(text: "1", hasOrders: false, backgroundColor: .green) {} + TableView(text: "1", hasOrders: true, backgroundColor: .green) {} .frame(maxWidth: 100) TableView(text: "2", hasOrders: true, backgroundColor: nil) {} diff --git a/WaiterRobot/Util/Extensions/Color.swift b/WaiterRobot/Util/Extensions/Color.swift index c877195..8a258a7 100644 --- a/WaiterRobot/Util/Extensions/Color.swift +++ b/WaiterRobot/Util/Extensions/Color.swift @@ -8,6 +8,10 @@ import SwiftUI extension Color { + static var lightGray: Color { + Color(hex: "#D1D1D6") + } + init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int: UInt64 = 0 @@ -63,33 +67,47 @@ extension Color { ) } - // Adjust color based on contrast - // TODO: this does not respect opacity - func getContentColor(lightColorScheme: Color, darkColorScheme: Color) -> Color { - let lightContrast = lightColorScheme.contrastRatio(with: self) - let darkContrast = darkColorScheme.contrastRatio(with: self) + /// Adjust color based on contrast + func bestContrastColor(_ color1: Color, _ color2: Color, in env: EnvironmentValues) -> Color { + let backgroundResolved = resolve(in: env) + let color1Resolved = color1.resolve(in: env) + let color2Resolved = color2.resolve(in: env) - return lightContrast > darkContrast ? lightColorScheme : darkColorScheme - } + let contrast1 = Color.Resolved.contrastRatio(foreground: color1Resolved, background: backgroundResolved) + let contrast2 = Color.Resolved.contrastRatio(foreground: color2Resolved, background: backgroundResolved) - // Calculate contrast ratio - private func contrastRatio(with other: Color) -> Double { - let l1 = luminance() - let l2 = other.luminance() - return (max(l1, l2) + 0.05) / (min(l1, l2) + 0.05) + return contrast1 > contrast2 ? color1 : color2 } +} - // Calculate luminance - private func luminance() -> Double { - let components = cgColor?.components ?? [0, 0, 0, 1] - let red = Color.convertSRGBToLinear(components[0]) - let green = Color.convertSRGBToLinear(components[1]) - let blue = Color.convertSRGBToLinear(components[2]) +extension Color.Resolved { + static func contrastRatio(foreground: Color.Resolved, background: Color.Resolved) -> Float { + #if DEBUG + if background.opacity != 1 { + fatalError("Background can not be translucent") + } + #endif + let lum1 = foreground.composite(on: background).luminance() // calculate the luminance when composed on top of background to account for alpha + let lum2 = background.luminance() + let lighter = max(lum1, lum2) + let darker = min(lum1, lum2) + return (lighter + 0.05) / (darker + 0.05) + } - return 0.2126 * red + 0.7152 * green + 0.0722 * blue + func luminance() -> Float { + 0.2126 * linearRed + 0.7152 * linearGreen + 0.0722 * linearBlue } - private static func convertSRGBToLinear(_ component: CGFloat) -> Double { - component <= 0.03928 ? Double(component) / 12.92 : pow((Double(component) + 0.055) / 1.055, 2.4) + private func composite(on background: Color.Resolved) -> Color.Resolved { + if opacity == 1 { return self } + if opacity == 0 { return self } + + let alpha = opacity + background.opacity * (1 - opacity) + + let r = (red * opacity + background.red * background.opacity * (1 - opacity)) / alpha + let g = (green * opacity + background.green * background.opacity * (1 - opacity)) / alpha + let b = (blue * opacity + background.blue * background.opacity * (1 - opacity)) / alpha + + return Color.Resolved(red: r, green: g, blue: b, opacity: alpha) } } From 93e73abea77f01a9bf92736c91a6ac0b6c76ed35 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Sun, 20 Jul 2025 12:17:34 +0200 Subject: [PATCH 38/43] Fix toolbar background --- WaiterRobot/Features/TableList/TableListScreen.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/WaiterRobot/Features/TableList/TableListScreen.swift b/WaiterRobot/Features/TableList/TableListScreen.swift index 75013d5..abcee87 100644 --- a/WaiterRobot/Features/TableList/TableListScreen.swift +++ b/WaiterRobot/Features/TableList/TableListScreen.swift @@ -31,7 +31,6 @@ struct TableListScreen: View { } } } - .toolbarBackground(.hidden, for: .navigationBar) } else { content() .toolbar { From 1218532ce5a53e91d59f6e9c33d259133a1626d8 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Sun, 20 Jul 2025 12:52:48 +0200 Subject: [PATCH 39/43] Indicate Demo events --- Modules/WRCore/Sources/WRCore/ErrorBar.swift | 2 +- WaiterRobot/Features/SwitchEvent/Event.swift | 21 +++++++++++++++++-- .../Features/TableList/TableListScreen.swift | 7 +++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Modules/WRCore/Sources/WRCore/ErrorBar.swift b/Modules/WRCore/Sources/WRCore/ErrorBar.swift index e4d7cbb..21f3d17 100644 --- a/Modules/WRCore/Sources/WRCore/ErrorBar.swift +++ b/Modules/WRCore/Sources/WRCore/ErrorBar.swift @@ -40,7 +40,7 @@ public struct ErrorBar: View { .padding(.top, 8) .padding(.trailing, retryAction == nil ? 16 : 8) .padding(.bottom, 8) - // .background(Color.errorContainer) + .background(Color.red) .onTapGesture { withAnimation { expanded.toggle() diff --git a/WaiterRobot/Features/SwitchEvent/Event.swift b/WaiterRobot/Features/SwitchEvent/Event.swift index 6289dd6..c5e0597 100644 --- a/WaiterRobot/Features/SwitchEvent/Event.swift +++ b/WaiterRobot/Features/SwitchEvent/Event.swift @@ -1,12 +1,29 @@ import shared import SwiftUI +import WRCore struct Event: View { let event: shared.Event var body: some View { VStack(alignment: .leading) { - Text(event.name) + HStack { + Text(event.name) + + if event.isDemo { + Spacer() + + Text(localize.switchEvent_demoEvent()) + .font(.caption) + .foregroundColor(.white) + .padding(.horizontal, 10) + .padding(.vertical, 2) + .background { + Capsule() + .fill(.darkRed) + } + } + } HStack { Text(event.city) @@ -35,7 +52,7 @@ struct Event: View { city: "Graz", organisationId: 1, stripeSettings: shared.Event.StripeSettingsDisabled(), - isDemo: false + isDemo: true ) ) } diff --git a/WaiterRobot/Features/TableList/TableListScreen.swift b/WaiterRobot/Features/TableList/TableListScreen.swift index abcee87..5b55e45 100644 --- a/WaiterRobot/Features/TableList/TableListScreen.swift +++ b/WaiterRobot/Features/TableList/TableListScreen.swift @@ -82,6 +82,7 @@ struct TableListScreen: View { if let tableGroups = Array(viewModel.state.tableGroups.data) { TableListView( tableGroups: tableGroups, + isDemoEvent: viewModel.state.isDemoEvent, onTableSelect: { viewModel.actual.onTableClick(table: $0) } ) } else { @@ -118,6 +119,7 @@ struct TableListScreen: View { struct TableListView: View { let tableGroups: [GroupedTables] + let isDemoEvent: Bool let onTableSelect: (shared.Table) -> Void private let layout = [ @@ -153,6 +155,10 @@ struct TableListView: View { .padding() } } + + if isDemoEvent { + ErrorBar(message: localize.tableList_demoEventWarning.desc(), initialLines: 1) + } } } } @@ -170,6 +176,7 @@ struct TableListView: View { NavigationView { TableListView( tableGroups: Mock.groupedTables(), + isDemoEvent: true, onTableSelect: { _ in } ) } From 4b1613eaf3805b814695ba9b2fca7943c664152f Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Sun, 20 Jul 2025 13:26:48 +0200 Subject: [PATCH 40/43] Possible fix --- Modules/WRCore/Sources/WRCore/Mock.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/WRCore/Sources/WRCore/Mock.swift b/Modules/WRCore/Sources/WRCore/Mock.swift index af7fc79..45407f6 100644 --- a/Modules/WRCore/Sources/WRCore/Mock.swift +++ b/Modules/WRCore/Sources/WRCore/Mock.swift @@ -26,7 +26,7 @@ public enum Mock { id: $0.id, name: $0.name, color: $0.color, - hidden: false, + hidden: false ) } } @@ -50,7 +50,7 @@ public enum Mock { allergens: allergens.enumerated().map { index, shortName in Allergen(id: Int64(index), name: shortName.description, shortName: shortName.description) }.filter { $0.shortName.isEmpty == false }, - position: Int32(id), + position: Int32(id) ) } @@ -70,7 +70,7 @@ public enum Mock { allergenList.randomElement()! } return product(with: groupId * 10 + $0, soldOut: $0 % 5 == 2, allergens: Set(allergens)) - }, + } ) } } From 4c55dabf86f2cc5bf11d2ae01f807f0527bc02d4 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Mon, 11 Aug 2025 18:04:57 +0200 Subject: [PATCH 41/43] Apply suggestions from code review Co-authored-by: Alexander Kauer --- WaiterRobot/Features/TableList/TableGroupSection.swift | 3 ++- WaiterRobot/Features/TableList/TableView.swift | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/WaiterRobot/Features/TableList/TableGroupSection.swift b/WaiterRobot/Features/TableList/TableGroupSection.swift index ba35708..240f366 100644 --- a/WaiterRobot/Features/TableList/TableGroupSection.swift +++ b/WaiterRobot/Features/TableList/TableGroupSection.swift @@ -4,7 +4,8 @@ import SwiftUI import WRCore struct TableGroupSection: View { - @Environment(\.self) var env + @Environment(\.self) + private var env let groupedTables: GroupedTables let onTableClick: (shared.Table) -> Void diff --git a/WaiterRobot/Features/TableList/TableView.swift b/WaiterRobot/Features/TableList/TableView.swift index 0d0c50a..ce90771 100644 --- a/WaiterRobot/Features/TableList/TableView.swift +++ b/WaiterRobot/Features/TableList/TableView.swift @@ -2,7 +2,8 @@ import SharedUI import SwiftUI struct TableView: View { - @Environment(\.self) var env + @Environment(\.self) + private var env let text: String let hasOrders: Bool From 84dc18c684e92c0a5237bea7388a195005664e16 Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Mon, 11 Aug 2025 18:19:57 +0200 Subject: [PATCH 42/43] Requested PR changes --- .../SharedUI/Sources/SharedUI/Colors.swift | 4 ++++ .../lightGray.colorset/Contents.json | 20 +++++++++++++++++++ Modules/WRCore/Sources/WRCore/ErrorBar.swift | 6 +----- .../Order/Search/AllProductGroupList.swift | 5 +++-- .../TableList/TableGroupFilterSheet.swift | 2 +- .../TableList/TableGroupSection.swift | 4 ++-- .../Features/TableList/TableView.swift | 4 ++-- WaiterRobot/Ui/ViewStateOverlayView.swift | 4 ++-- WaiterRobot/Util/Extensions/Color.swift | 4 ---- 9 files changed, 35 insertions(+), 18 deletions(-) create mode 100644 Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/lightGray.colorset/Contents.json diff --git a/Modules/SharedUI/Sources/SharedUI/Colors.swift b/Modules/SharedUI/Sources/SharedUI/Colors.swift index 81e9e9a..c353982 100644 --- a/Modules/SharedUI/Sources/SharedUI/Colors.swift +++ b/Modules/SharedUI/Sources/SharedUI/Colors.swift @@ -30,4 +30,8 @@ public extension Color { static var palletOrange: Color { Color(.palletOrange) } + + static var lightGray: Color { + Color(.lightGray) + } } diff --git a/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/lightGray.colorset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/lightGray.colorset/Contents.json new file mode 100644 index 0000000..055eeee --- /dev/null +++ b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/lightGray.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xD6", + "green" : "0xD1", + "red" : "0xD1" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/WRCore/Sources/WRCore/ErrorBar.swift b/Modules/WRCore/Sources/WRCore/ErrorBar.swift index 21f3d17..a70dae5 100644 --- a/Modules/WRCore/Sources/WRCore/ErrorBar.swift +++ b/Modules/WRCore/Sources/WRCore/ErrorBar.swift @@ -20,7 +20,6 @@ public struct ErrorBar: View { Text(message()) .lineLimit(expanded ? nil : initialLines) .multilineTextAlignment(.leading) - // .foregroundColor(Color.onErrorContainer) .frame(maxWidth: .infinity, alignment: .leading) if retryAction != nil { @@ -33,7 +32,6 @@ public struct ErrorBar: View { .multilineTextAlignment(.center) .lineLimit(expanded ? nil : initialLines) } - // .foregroundColor(Color.onErrorContainer) } } .padding(.leading, 16) @@ -42,9 +40,7 @@ public struct ErrorBar: View { .padding(.bottom, 8) .background(Color.red) .onTapGesture { - withAnimation { - expanded.toggle() - } + expanded.toggle() } .animation(.default, value: expanded) } diff --git a/WaiterRobot/Features/Order/Search/AllProductGroupList.swift b/WaiterRobot/Features/Order/Search/AllProductGroupList.swift index e54823e..32ae392 100644 --- a/WaiterRobot/Features/Order/Search/AllProductGroupList.swift +++ b/WaiterRobot/Features/Order/Search/AllProductGroupList.swift @@ -1,4 +1,5 @@ import shared +import SharedUI import SwiftUI import WRCore @@ -18,9 +19,9 @@ struct AllProductGroupList: View { ) } header: { HStack { - Color(UIColor.lightGray).frame(height: 1) + Color.lightGray.frame(height: 1) Text(productGroup.name) - Color(UIColor.lightGray).frame(height: 1) + Color.lightGray.frame(height: 1) } } } diff --git a/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift b/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift index 6ea82a2..77e1b35 100644 --- a/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift +++ b/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift @@ -13,7 +13,7 @@ struct TableGroupFilterSheet: View { content() .observeState(of: viewModel) .toolbar { - ToolbarItem(placement: .topBarLeading) { + ToolbarItem(placement: .cancellationAction) { Button(localize.dialog_cancel()) { dismiss() } diff --git a/WaiterRobot/Features/TableList/TableGroupSection.swift b/WaiterRobot/Features/TableList/TableGroupSection.swift index 240f366..6bf3357 100644 --- a/WaiterRobot/Features/TableList/TableGroupSection.swift +++ b/WaiterRobot/Features/TableList/TableGroupSection.swift @@ -4,7 +4,7 @@ import SwiftUI import WRCore struct TableGroupSection: View { - @Environment(\.self) + @Environment(\.self) private var env let groupedTables: GroupedTables @@ -28,7 +28,7 @@ struct TableGroupSection: View { if let background = Color(hex: groupedTables.color) { title(backgroundColor: background) } else { - title(backgroundColor: .lightGray) + title(backgroundColor: Color.lightGray) } Spacer() diff --git a/WaiterRobot/Features/TableList/TableView.swift b/WaiterRobot/Features/TableList/TableView.swift index ce90771..2622c95 100644 --- a/WaiterRobot/Features/TableList/TableView.swift +++ b/WaiterRobot/Features/TableList/TableView.swift @@ -2,7 +2,7 @@ import SharedUI import SwiftUI struct TableView: View { - @Environment(\.self) + @Environment(\.self) private var env let text: String @@ -13,7 +13,7 @@ struct TableView: View { init(text: String, hasOrders: Bool, backgroundColor: Color?, onClick: @escaping () -> Void) { self.text = text self.hasOrders = hasOrders - self.backgroundColor = backgroundColor ?? .lightGray + self.backgroundColor = backgroundColor ?? Color.lightGray self.onClick = onClick } diff --git a/WaiterRobot/Ui/ViewStateOverlayView.swift b/WaiterRobot/Ui/ViewStateOverlayView.swift index 4772e7d..7d1520d 100644 --- a/WaiterRobot/Ui/ViewStateOverlayView.swift +++ b/WaiterRobot/Ui/ViewStateOverlayView.swift @@ -2,8 +2,8 @@ import shared import SwiftUI public struct ViewStateOverlayView: View { - let state: Skie.Shared.ViewState.__Sealed - let content: () -> Content + private let state: Skie.Shared.ViewState.__Sealed + private let content: () -> Content init(state: ViewState, @ViewBuilder content: @escaping () -> Content) { self.state = onEnum(of: state) diff --git a/WaiterRobot/Util/Extensions/Color.swift b/WaiterRobot/Util/Extensions/Color.swift index 8a258a7..72be7b7 100644 --- a/WaiterRobot/Util/Extensions/Color.swift +++ b/WaiterRobot/Util/Extensions/Color.swift @@ -8,10 +8,6 @@ import SwiftUI extension Color { - static var lightGray: Color { - Color(hex: "#D1D1D6") - } - init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int: UInt64 = 0 From 85bf66d72062aeac38bedde9bcd52d391ab1b2fb Mon Sep 17 00:00:00 2001 From: Fabian Schedler Date: Mon, 11 Aug 2025 19:15:25 +0200 Subject: [PATCH 43/43] Increase fastlane xcode version --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 48f3c40..3b6fee6 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -2,7 +2,7 @@ default_platform(:ios) platform :ios do before_all do - xcodes(version: "16.1", select_for_current_build_only: true) + xcodes(version: "16.4", select_for_current_build_only: true) end desc "Run all iOS unit and ui tests."