diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index ced3aa2..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,65 +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.0 - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.3.0' - 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 }} 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 03d5e9e..60b6781 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,15 +8,15 @@ on: jobs: test: - runs-on: macos-14 + runs-on: macos-15 steps: - 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 @@ -28,9 +28,6 @@ jobs: - name: Install xcodes run: brew install xcodes - - - name: Run XcodeGen - run: swift run xcodegen - + - name: Run tests with fastlane run: bundle exec fastlane test 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 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 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..a267f9d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,25 +5,26 @@ 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-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.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 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.98.0) + aws-sdk-core (~> 3, >= 3.216.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.181.0) + aws-sdk-core (~> 3, >= 3.216.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.11.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -33,13 +34,13 @@ 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) 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) @@ -58,17 +59,17 @@ 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-net_http (1.0.1) + 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) 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) + fastimage (2.4.0) + fastlane (2.226.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -84,6 +85,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) @@ -107,8 +109,10 @@ 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) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) @@ -126,7 +130,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,34 +151,34 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.8) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.7.2) - jwt (2.8.1) + json (2.10.1) + jwt (2.10.1) 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) + plist (3.7.2) + 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) - 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) @@ -184,6 +188,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,28 +198,29 @@ 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) - xcpretty (0.3.0) - rouge (~> 2.0.7) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.0) + rouge (~> 3.28.0) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) PLATFORMS arm64-darwin-22 arm64-darwin-23 + arm64-darwin-24 x86_64-darwin-19 x86_64-darwin-20 DEPENDENCIES - fastlane (= 2.220.0) + fastlane BUNDLED WITH - 2.5.10 + 2.5.22 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/.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/Package.swift b/Modules/SharedUI/Package.swift new file mode 100644 index 0000000..2e164b8 --- /dev/null +++ b/Modules/SharedUI/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.9 + +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..c353982 --- /dev/null +++ b/Modules/SharedUI/Sources/SharedUI/Colors.swift @@ -0,0 +1,37 @@ +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) + } + + static var text: Color { + Color(.text) + } + + static var title: Color { + Color(.title) + } + + static var palletOrange: Color { + Color(.palletOrange) + } + + static var lightGray: Color { + Color(.lightGray) + } +} diff --git a/Modules/SharedUI/Sources/SharedUI/Fonts.swift b/Modules/SharedUI/Sources/SharedUI/Fonts.swift new file mode 100644 index 0000000..7e9245b --- /dev/null +++ b/Modules/SharedUI/Sources/SharedUI/Fonts.swift @@ -0,0 +1,87 @@ +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: + 18 + case .caption1: + 14 + case .caption2: + 12 + } + } +} + +private struct WrTextStyle: ViewModifier { + let fontStyle: WrFont + let textColor: Color + + @ScaledMetric + var fontSize: CGFloat + + var font: Font { + .system(size: fontSize) + } + + 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, textColor: Color = .text) -> some View { + modifier(WrTextStyle(fontStyle: font, textColor: textColor)) + } +} + +#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..4873187 --- /dev/null +++ b/Modules/SharedUI/Sources/SharedUI/Images.swift @@ -0,0 +1,7 @@ +import SwiftUI + +public extension Image { + static var logoRounded: Image { + Image("LogoRounded") + } +} diff --git a/TargetSpecificResources/WaiterRobot/Images.xcassets/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/Contents.json similarity index 100% rename from TargetSpecificResources/WaiterRobot/Images.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/WaiterRobot/Resources/Colors.xcassets/second.colorset/Contents.json b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/lightGray.colorset/Contents.json similarity index 63% rename from WaiterRobot/Resources/Colors.xcassets/second.colorset/Contents.json rename to Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/lightGray.colorset/Contents.json index c43d793..055eeee 100644 --- a/WaiterRobot/Resources/Colors.xcassets/second.colorset/Contents.json +++ b/Modules/SharedUI/Sources/SharedUI/Resources/Colors.xcassets/lightGray.colorset/Contents.json @@ -2,12 +2,12 @@ "colors" : [ { "color" : { - "color-space" : "display-p3", + "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.967", - "green" : "0.691", - "red" : "0.607" + "blue" : "0xD6", + "green" : "0xD1", + "red" : "0xD1" } }, "idiom" : "universal" 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/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..5fb9a4d --- /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" : "0x47", + "green" : "0x23", + "red" : "0x1B" + } + }, + "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/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/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/WaiterRobot/Ui/Order/Search/ProducSearchTabBarHeader.swift b/Modules/SharedUI/Sources/SharedUI/TabBarHeader.swift similarity index 80% rename from WaiterRobot/Ui/Order/Search/ProducSearchTabBarHeader.swift rename to Modules/SharedUI/Sources/SharedUI/TabBarHeader.swift index 74bfbd2..ced4f6d 100644 --- a/WaiterRobot/Ui/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 { @@ -13,7 +18,6 @@ struct ProducSearchTabBarHeader: View { Array(zip(tabBarOptions.indices, tabBarOptions)), id: \.0 ) { index, name in - Button { currentTab = index } label: { @@ -49,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/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/.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/Modules/WRCore/Package.swift b/Modules/WRCore/Package.swift new file mode 100644 index 0000000..e870c64 --- /dev/null +++ b/Modules/WRCore/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version: 5.9 + +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.7.3"), + ], + 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/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/ErrorBar.swift b/Modules/WRCore/Sources/WRCore/ErrorBar.swift new file mode 100644 index 0000000..a70dae5 --- /dev/null +++ b/Modules/WRCore/Sources/WRCore/ErrorBar.swift @@ -0,0 +1,47 @@ +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) + .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) + } + } + } + .padding(.leading, 16) + .padding(.top, 8) + .padding(.trailing, retryAction == nil ? 16 : 8) + .padding(.bottom, 8) + .background(Color.red) + .onTapGesture { + expanded.toggle() + } + .animation(.default, value: expanded) + } +} diff --git a/Modules/WRCore/Sources/WRCore/Globals.swift b/Modules/WRCore/Sources/WRCore/Globals.swift new file mode 100644 index 0000000..bd23b46 --- /dev/null +++ b/Modules/WRCore/Sources/WRCore/Globals.swift @@ -0,0 +1,82 @@ +import Foundation +import shared +import SwiftUI +import UIKit + +public var koin: IosKoinComponent { IosKoinComponent.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 + 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, + koinPlatformDeclaration: nil + ) + + let logger = koin.logger(tag: "AppDelegate") + + 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 { + 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 + } +} + +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/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..952c771 --- /dev/null +++ b/Modules/WRCore/Sources/WRCore/KotlinArrayWrapper.swift @@ -0,0 +1,19 @@ +import Foundation +import shared + +public extension Array where Element: AnyObject { + init?(_ kotlinArray: KotlinArray?) { + guard let array = kotlinArray else { + return nil + } + self.init(array) + } + + 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..45407f6 --- /dev/null +++ b/Modules/WRCore/Sources/WRCore/Mock.swift @@ -0,0 +1,77 @@ +import Foundation +import shared + +public enum Mock { + 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: colors[groupId % colors.count], + tables: (1 ... tableCount).map { + table(with: groupId * 10 + $0, hasOrders: $0 % 2 == 0, groupName: groupName) + } + ) + } + } + + public static func tableGroups(groups: Int = 1) -> [TableGroup] { + groupedTables(groups: groups).map { + TableGroup( + id: $0.id, + name: $0.name, + color: $0.color, + hidden: false + ) + } + } + + public static func table(with id: Int, hasOrders: Bool = false, groupName: String = "Hof") -> shared.Table { + shared.Table( + id: Int64(id), + number: Int32(id), + 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/WaiterRobot/Ui/Core/Navigation.swift b/Modules/WRCore/Sources/WRCore/Navigation.swift similarity index 94% rename from WaiterRobot/Ui/Core/Navigation.swift rename to Modules/WRCore/Sources/WRCore/Navigation.swift index ed0e0a7..3ae2c0f 100644 --- a/WaiterRobot/Ui/Core/Navigation.swift +++ b/Modules/WRCore/Sources/WRCore/Navigation.swift @@ -34,9 +34,9 @@ extension UIPilot { } } -extension View { +public extension View { func customBackNavigation( - title: String = localize.navigation.back(), + title: String = localize.navigation_back(), icon: String? = "chevron.left", action: @escaping () -> Void ) -> some View { @@ -57,7 +57,7 @@ extension View { } } .buttonStyle(.plain) - .foregroundStyle(.main) + .foregroundStyle(.primary) } } } @@ -73,7 +73,7 @@ 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 new file mode 100644 index 0000000..a658886 --- /dev/null +++ b/Modules/WRCore/Sources/WRCore/ObservableViewModel.swift @@ -0,0 +1,103 @@ +/// 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 ObservableTableGroupFilterViewModel: ObservableViewModel { + public init() { + super.init(viewModel: koin.tableGroupFilterVM()) + } +} + +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 ObservableProductListViewModel: ObservableViewModel { + public init() { + super.init(viewModel: koin.productListVM()) + } +} + +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/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"), ] ) diff --git a/README.md b/README.md index a06c092..760d2c6 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
@@ -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/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/WaiterRobot/Resources/Colors.xcassets/Contents.json b/Targets/Lava/Images.xcassets/Contents.json similarity index 100% rename from WaiterRobot/Resources/Colors.xcassets/Contents.json rename to Targets/Lava/Images.xcassets/Contents.json diff --git a/WaiterRobot/Resources/Images.xcassets/LaunchImage.imageset/Contents.json b/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json similarity index 53% rename from WaiterRobot/Resources/Images.xcassets/LaunchImage.imageset/Contents.json rename to Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json index 97fbe53..c7d86da 100644 --- a/WaiterRobot/Resources/Images.xcassets/LaunchImage.imageset/Contents.json +++ b/Targets/Lava/Images.xcassets/LogoRounded.imageset/Contents.json @@ -1,15 +1,12 @@ { "images" : [ { - "filename" : "LaunchImage.pdf", + "filename" : "wr-round-yellow.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true } } 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..7a9af26 --- /dev/null +++ b/Targets/Lava/Images.xcassets/LogoRounded.imageset/wr-round-yellow.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Targets/Lava/WaiterRobotLava.plist b/Targets/Lava/WaiterRobotLava.plist new file mode 100644 index 0000000..2a61768 --- /dev/null +++ b/Targets/Lava/WaiterRobotLava.plist @@ -0,0 +1,77 @@ + + + + + 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.5.0 + CFBundleVersion + 29178434 + 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 + + + UILaunchScreen + + UIImageName + LogoRounded + UIImageRespectsSafeAreaInsets + + + 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/WaiterRobot/Resources/Images.xcassets/Contents.json b/Targets/Prod/Images.xcassets/Contents.json similarity index 100% rename from WaiterRobot/Resources/Images.xcassets/Contents.json rename to Targets/Prod/Images.xcassets/Contents.json diff --git a/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json b/Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json similarity index 75% rename from WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json rename to Targets/Prod/Images.xcassets/LogoRounded.imageset/Contents.json index 92f8a3c..e4164e5 100644 --- a/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/Contents.json +++ b/Targets/Prod/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/Targets/Prod/Images.xcassets/LogoRounded.imageset/wr-round.svg b/Targets/Prod/Images.xcassets/LogoRounded.imageset/wr-round.svg new file mode 100644 index 0000000..14b173f --- /dev/null +++ b/Targets/Prod/Images.xcassets/LogoRounded.imageset/wr-round.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Targets/Prod/WaiterRobot.plist b/Targets/Prod/WaiterRobot.plist new file mode 100644 index 0000000..713b445 --- /dev/null +++ b/Targets/Prod/WaiterRobot.plist @@ -0,0 +1,77 @@ + + + + + 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.5.0 + CFBundleVersion + 1 + 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 + + + UILaunchScreen + + UIImageName + LogoRounded + UIImageRespectsSafeAreaInsets + + + 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..c42c9bb --- /dev/null +++ b/WaiterRobot.xcodeproj/project.pbxproj @@ -0,0 +1,726 @@ +// !$*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 */ + 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 = ""; }; + 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 */, + 8221B03D2DE8B448004B80AC /* ci_scripts */, + 82416FDD2D64F86900B4FCCE /* command-line-tools */, + 82416FDB2D64F86400B4FCCE /* fastlane */, + D4F4C626424188D539C8809A /* Modules */, + D20FD31A5075372C89FD743F /* Products */, + 82416F332D64F80A00B4FCCE /* Targets */, + 82416F762D64F85600B4FCCE /* WaiterRobot */, + 82416FE42D64F87800B4FCCE /* WaiterRobotTests */, + ); + 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; + }; + }; + }; + 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 = "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", + ); + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.datepollsystems.waiterrobot.beta; + PRODUCT_NAME = lava.kellner.team; + PROVISIONING_PROFILE_SPECIFIER = ""; + 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 = "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", + ); + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.datepollsystems.waiterrobot.beta; + PRODUCT_NAME = lava.kellner.team; + PROVISIONING_PROFILE_SPECIFIER = ""; + 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 = 17.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 = "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", + ); + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.datepollsystems.waiterrobot; + PRODUCT_NAME = kellner.team; + PROVISIONING_PROFILE_SPECIFIER = ""; + 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 = "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", + ); + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.datepollsystems.waiterrobot; + PRODUCT_NAME = kellner.team; + PROVISIONING_PROFILE_SPECIFIER = ""; + 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 = 17.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..4fd8fe6 --- /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" : "f2ff44dc52e8df3f4e68da4d05e27ad0b0487a33", + "version" : "1.7.6" + } + } + ], + "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/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/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/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/Features/Billing/BillingScreen.swift b/WaiterRobot/Features/Billing/BillingScreen.swift new file mode 100644 index 0000000..800de70 --- /dev/null +++ b/WaiterRobot/Features/Billing/BillingScreen.swift @@ -0,0 +1,179 @@ +import Foundation +import shared +import SwiftUI +import UIPilot +import WRCore + +struct BillingScreen: View { + @EnvironmentObject var navigator: UIPilot + + @State private var showPayDialog: Bool = false + + @StateObject private var viewModel: ObservableBillingViewModel + private let table: shared.Table + + init(table: shared.Table) { + self.table = table + _viewModel = StateObject(wrappedValue: ObservableBillingViewModel(table: table)) + } + + var body: some View { + 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 + } + + 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())") + } + + if let billItems = Array(state.billItems.data), !billItems.isEmpty { + content(billItems: billItems) + } else { + Text(localize.billing_noOrder(table.groupName, table.number.description)) + } + } + } + .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() + } + } message: { + Text(localize.billing_notSent_desc()) + } + .toolbar { + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button { + selectAll() + } label: { + Image(systemName: "checkmark") + } + + Button { + unselectAll() + } label: { + Image(systemName: "xmark") + } + } + } + } + + @ViewBuilder + private func content(billItems: [BillItem]?) -> some View { + VStack { + List { + if let billItems, !billItems.isEmpty { + Section { + ForEach(billItems, id: \.baseProductId) { item in + BillListItem( + item: item, + addOne: { + addItem(item.baseProductId, 1) + }, + addAll: { + addItem(item.baseProductId, item.ordered - item.selectedForBill) + }, + removeOne: { + addItem(item.baseProductId, -1) + }, + removeAll: { + addItem(item.baseProductId, -item.selectedForBill) + } + ) + } + } header: { + HStack { + Text("Ordered") + Spacer() + Text("Selected") + } + } + } else { + Text(localize.billing_noOrder(table.groupName, table.number.description)) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + .padding() + } + } + + HStack { + Text("\(localize.billing_total()):") + Spacer() + Text("\(state.priceSum)") + } + .font(.title2) + .padding() + .overlay(alignment: .bottom) { + Button { + paySelection(false) + } label: { + Image(systemName: "eurosign") + .font(.system(.title)) + .padding() + .tint(.white) + .offset(x: -3) + } + .background(.blue) + .mask(Circle()) + .shadow(color: Color.black.opacity(0.3), radius: 3, x: 3, y: 3) + .disabled(state.paymentState != ViewState.Idle.shared || !state.hasSelectedItems) + } + } + } +} diff --git a/WaiterRobot/Ui/Billing/PayDialog.swift b/WaiterRobot/Features/Billing/PayDialog.swift similarity index 86% rename from WaiterRobot/Ui/Billing/PayDialog.swift rename to WaiterRobot/Features/Billing/PayDialog.swift index 48b6f18..79d0e06 100644 --- a/WaiterRobot/Ui/Billing/PayDialog.swift +++ b/WaiterRobot/Features/Billing/PayDialog.swift @@ -1,6 +1,7 @@ import Combine import shared import SwiftUI +import WRCore struct PayDialog: View { @Environment(\.dismiss) private var dismiss @@ -14,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 @@ -36,7 +37,7 @@ struct PayDialog: View { ) HStack { - Text(localize.billing.change() + ":") + Text(localize.billing_change() + ":") .font(.title2) Spacer() @@ -51,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/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 96% rename from WaiterRobot/Ui/Core/ButtonStyles.swift rename to WaiterRobot/Features/Core/ButtonStyles.swift index 3fed1a9..18535c3 100644 --- a/WaiterRobot/Ui/Core/ButtonStyles.swift +++ b/WaiterRobot/Features/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.accentColor) } .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/Core/DynamicGrid.swift b/WaiterRobot/Features/Core/DynamicGrid.swift similarity index 64% rename from WaiterRobot/Ui/Core/DynamicGrid.swift rename to WaiterRobot/Features/Core/DynamicGrid.swift index 43d8a06..10f6adc 100644 --- a/WaiterRobot/Ui/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/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 83% rename from WaiterRobot/Ui/Login/LoginScannerScreen.swift rename to WaiterRobot/Features/Login/LoginScannerScreen.swift index 7434f4c..c0c884b 100644 --- a/WaiterRobot/Ui/Login/LoginScannerScreen.swift +++ b/WaiterRobot/Features/Login/LoginScannerScreen.swift @@ -2,6 +2,7 @@ import CodeScanner import shared import SwiftUI import UIPilot +import WRCore struct LoginScannerScreen: View { @EnvironmentObject var navigator: UIPilot @@ -19,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)") @@ -44,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/Ui/Login/LoginScreen.swift b/WaiterRobot/Features/Login/LoginScreen.swift similarity index 50% rename from WaiterRobot/Ui/Login/LoginScreen.swift rename to WaiterRobot/Features/Login/LoginScreen.swift index 7f1829d..d5057a3 100644 --- a/WaiterRobot/Ui/Login/LoginScreen.swift +++ b/WaiterRobot/Features/Login/LoginScreen.swift @@ -1,30 +1,28 @@ import Foundation import shared +import SharedUI import SwiftUI import UIPilot +import WRCore 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 { - 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)") } } @@ -32,16 +30,19 @@ struct LoginScreen: View { VStack { Spacer() - Image(.logoRounded) + Image.logoRounded .resizable() .scaledToFit() .frame(maxWidth: 250) .padding() - Text(localize.login.title()) + .onLongPressGesture { + showLinkInput = true + } + Text(localize.login_title()) .font(.title) .padding() - Text(localize.login.desc()) + Text(localize.login_desc()) .font(.body) .padding() .multilineTextAlignment(.center) @@ -49,13 +50,28 @@ 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_scanner_debugDialog_inputLabel(), text: $debugLoginLink) + Button(localize.dialog_cancel(), role: .cancel) { + showLinkInput = false + } + Button(localize.login_title()) { + viewModel.actual.onDebugLogin(link: debugLoginLink) + } + } .withViewModel(viewModel, navigator) } } + +#Preview { + PreviewView { + LoginScreen() + } +} diff --git a/WaiterRobot/Ui/Login/RegisterScreen.swift b/WaiterRobot/Features/Login/RegisterScreen.swift similarity index 75% rename from WaiterRobot/Ui/Login/RegisterScreen.swift rename to WaiterRobot/Features/Login/RegisterScreen.swift index 7cf48a0..39f66f2 100644 --- a/WaiterRobot/Ui/Login/RegisterScreen.swift +++ b/WaiterRobot/Features/Login/RegisterScreen.swift @@ -1,6 +1,7 @@ import shared import SwiftUI import UIPilot +import WRCore struct RegisterScreen: View { @EnvironmentObject var navigator: UIPilot @@ -20,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)") @@ -33,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() @@ -45,7 +42,7 @@ struct RegisterScreen: View { Button { viewModel.actual.cancel() } label: { - Text(localize.dialog.cancel()) + Text(localize.dialog_cancel()) } Spacer() @@ -56,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/Ui/Order/OrderListItem.swift b/WaiterRobot/Features/Order/OrderListItem.swift similarity index 99% rename from WaiterRobot/Ui/Order/OrderListItem.swift rename to WaiterRobot/Features/Order/OrderListItem.swift index dc80c88..68e757b 100644 --- a/WaiterRobot/Ui/Order/OrderListItem.swift +++ b/WaiterRobot/Features/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/Features/Order/OrderProductNoteView.swift similarity index 82% rename from WaiterRobot/Ui/Order/OrderProductNoteView.swift rename to WaiterRobot/Features/Order/OrderProductNoteView.swift index e5a5429..93498de 100644 --- a/WaiterRobot/Ui/Order/OrderProductNoteView.swift +++ b/WaiterRobot/Features/Order/OrderProductNoteView.swift @@ -1,5 +1,6 @@ import shared import SwiftUI +import WRCore struct OrderProductNoteView: View { let name: String @@ -17,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) } } @@ -25,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) } } @@ -81,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() @@ -98,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/Ui/Order/OrderScreen.swift b/WaiterRobot/Features/Order/OrderScreen.swift similarity index 76% rename from WaiterRobot/Ui/Order/OrderScreen.swift rename to WaiterRobot/Features/Order/OrderScreen.swift index 4fce01c..2066d63 100644 --- a/WaiterRobot/Ui/Order/OrderScreen.swift +++ b/WaiterRobot/Features/Order/OrderScreen.swift @@ -1,11 +1,11 @@ import shared import SwiftUI import UIPilot +import WRCore struct OrderScreen: View { @EnvironmentObject var navigator: UIPilot - @State private var productName: String = "" @State private var showProductSearch: Bool @State private var showAbortOrderConfirmationDialog = false @@ -22,54 +22,41 @@ 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) } + ) } - .withViewModel(viewModel, navigator) .animation(.default, value: viewModel.state.currentOrder) + .withViewModel(viewModel, navigator) } @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() @@ -115,7 +102,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 { @@ -124,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..32ae392 --- /dev/null +++ b/WaiterRobot/Features/Order/Search/AllProductGroupList.swift @@ -0,0 +1,39 @@ +import shared +import SharedUI +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.lightGray.frame(height: 1) + Text(productGroup.name) + Color.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/Ui/Order/ProductListItem.swift b/WaiterRobot/Features/Order/Search/ProductListItem.swift similarity index 60% rename from WaiterRobot/Ui/Order/ProductListItem.swift rename to WaiterRobot/Features/Order/Search/ProductListItem.swift index 600dd4c..50f2347 100644 --- a/WaiterRobot/Ui/Order/ProductListItem.swift +++ b/WaiterRobot/Features/Order/Search/ProductListItem.swift @@ -1,7 +1,10 @@ import shared import SwiftUI +import WRCore struct ProductListItem: View { + @Environment(\.self) var env + let product: Product let backgroundColor: Color? let onClick: () -> Void @@ -14,26 +17,30 @@ 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 { if product.soldOut { .blackWhite } else if let backgroundColor { - backgroundColor.getContentColor(lightColorScheme: .black, darkColorScheme: .white) + backgroundColor.bestContrastColor(.black, .white, in: env) } else { .blackWhite } @@ -69,22 +76,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 +85,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 new file mode 100644 index 0000000..b213e7a --- /dev/null +++ b/WaiterRobot/Features/Order/Search/ProductSearch.swift @@ -0,0 +1,62 @@ +import shared +import SwiftUI +import WRCore + +struct ProductSearch: View { + let addItem: (_ product: Product, _ amount: Int32) -> Void + + @Environment(\.dismiss) private var dismiss + + @ObservedObject private var viewModel = ObservableProductListViewModel() + + @State private var search: String = "" + @State private var selectedTab: Int = 0 + + var body: some View { + NavigationView { + 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) { + ProductTabView( + productGroups: productGroups, + addItem: { + addItem($0, $1) + dismiss() + } + ) + .searchable(text: $search, placement: .navigationBarDrawer(displayMode: .always)) + .onChange(of: search, perform: { viewModel.actual.filterProducts(filter: $0) }) + } + } + } + + private func productGroupsError(error: ResourceError>) -> some View { + Text(error.userMessage()) + } + + private func getGroupNames(_ productGroups: [GroupedProducts]) -> [String] { + var groupNames = productGroups.map { productGroup in + productGroup.name + } + groupNames.insert(localize.productSearch_groups_all(), at: 0) + return groupNames + } +} 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/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 65% rename from WaiterRobot/Ui/Settings/SettingsScreen.swift rename to WaiterRobot/Features/Settings/SettingsScreen.swift index 8d698b9..7b451c9 100644 --- a/WaiterRobot/Ui/Settings/SettingsScreen.swift +++ b/WaiterRobot/Features/Settings/SettingsScreen.swift @@ -1,6 +1,7 @@ import shared import SwiftUI import UIPilot +import WRCore struct SettingsScreen: View { @EnvironmentObject var navigator: UIPilot @@ -11,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 { @@ -59,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) { @@ -93,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 @@ -105,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() @@ -119,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() } @@ -129,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( @@ -153,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/Ui/Settings/SwitchThemeView.swift b/WaiterRobot/Features/Settings/SwitchThemeView.swift similarity index 86% rename from WaiterRobot/Ui/Settings/SwitchThemeView.swift rename to WaiterRobot/Features/Settings/SwitchThemeView.swift index fc18276..c35bd19 100644 --- a/WaiterRobot/Ui/Settings/SwitchThemeView.swift +++ b/WaiterRobot/Features/Settings/SwitchThemeView.swift @@ -1,5 +1,6 @@ import shared import SwiftUI +import WRCore struct SwitchThemeView: View { @State private var selectedTheme: AppTheme @@ -20,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/Ui/SwitchEvent/Event.swift b/WaiterRobot/Features/SwitchEvent/Event.swift similarity index 58% rename from WaiterRobot/Ui/SwitchEvent/Event.swift rename to WaiterRobot/Features/SwitchEvent/Event.swift index 3466694..c5e0597 100644 --- a/WaiterRobot/Ui/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) @@ -34,7 +51,8 @@ struct Event: View { endDate: nil, city: "Graz", organisationId: 1, - stripeSettings: shared.Event.StripeSettingsDisabled() + stripeSettings: shared.Event.StripeSettingsDisabled(), + isDemo: true ) ) } diff --git a/WaiterRobot/Ui/SwitchEvent/SwitchEventScreen.swift b/WaiterRobot/Features/SwitchEvent/SwitchEventScreen.swift similarity index 59% rename from WaiterRobot/Ui/SwitchEvent/SwitchEventScreen.swift rename to WaiterRobot/Features/SwitchEvent/SwitchEventScreen.swift index feebfbd..b138276 100644 --- a/WaiterRobot/Ui/SwitchEvent/SwitchEventScreen.swift +++ b/WaiterRobot/Features/SwitchEvent/SwitchEventScreen.swift @@ -1,6 +1,7 @@ import shared import SwiftUI import UIPilot +import WRCore struct SwitchEventScreen: View { @EnvironmentObject var navigator: UIPilot @@ -10,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() @@ -37,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: { @@ -63,9 +59,8 @@ struct SwitchEventScreen: View { } } } - } - .refreshable { - viewModel.actual.loadEvents() + } else { + ProgressView() } } } diff --git a/WaiterRobot/Ui/TableDetail/OrderedItemView.swift b/WaiterRobot/Features/TableDetail/OrderedItemView.swift similarity index 91% rename from WaiterRobot/Ui/TableDetail/OrderedItemView.swift rename to WaiterRobot/Features/TableDetail/OrderedItemView.swift index 669fc37..6413f9d 100644 --- a/WaiterRobot/Ui/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/Ui/TableDetail/TableDetailScreen.swift b/WaiterRobot/Features/TableDetail/TableDetailScreen.swift similarity index 80% rename from WaiterRobot/Ui/TableDetail/TableDetailScreen.swift rename to WaiterRobot/Features/TableDetail/TableDetailScreen.swift index 5a34d1c..ac6a337 100644 --- a/WaiterRobot/Ui/TableDetail/TableDetailScreen.swift +++ b/WaiterRobot/Features/TableDetail/TableDetailScreen.swift @@ -1,6 +1,7 @@ import shared import SwiftUI import UIPilot +import WRCore struct TableDetailScreen: View { @EnvironmentObject var navigator: UIPilot @@ -17,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() @@ -32,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) } } @@ -40,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() @@ -84,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..77e1b35 --- /dev/null +++ b/WaiterRobot/Features/TableList/TableGroupFilterSheet.swift @@ -0,0 +1,88 @@ +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: .cancellationAction) { + 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: Mock.tableGroups(groups: 10), + showAll: {}, + hideAll: {}, + onToggle: { _ in } + ) +} diff --git a/WaiterRobot/Features/TableList/TableGroupSection.swift b/WaiterRobot/Features/TableList/TableGroupSection.swift new file mode 100644 index 0000000..6bf3357 --- /dev/null +++ b/WaiterRobot/Features/TableList/TableGroupSection.swift @@ -0,0 +1,61 @@ +import shared +import SharedUI +import SwiftUI +import WRCore + +struct TableGroupSection: View { + @Environment(\.self) + private var env + + let groupedTables: GroupedTables + let onTableClick: (shared.Table) -> Void + + var body: some View { + Section { + ForEach(groupedTables.tables, id: \.id) { table in + TableView( + text: table.number.description, + hasOrders: table.hasOrders, + backgroundColor: Color(hex: groupedTables.color), + onClick: { + onTableClick(table) + } + ) + .padding(10) + } + } header: { + HStack { + if let background = Color(hex: groupedTables.color) { + title(backgroundColor: background) + } else { + title(backgroundColor: Color.lightGray) + } + + Spacer() + } + .padding(.vertical, 4) + .background(Color.whiteBlack) + } + } + + private func title(backgroundColor: Color) -> some View { + Text(groupedTables.name) + .font(.title2) + .foregroundStyle(backgroundColor.bestContrastColor(.black, .white, in: env)) + .padding(6) + .background { + RoundedRectangle(cornerRadius: 8.0) + .foregroundStyle(backgroundColor) + } + } +} + +#Preview { + LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) { + TableGroupSection( + groupedTables: Mock.groupedTables().first!, + onTableClick: { _ in } + ) + } + .padding() +} diff --git a/WaiterRobot/Ui/TableList/TableListScreen.swift b/WaiterRobot/Features/TableList/TableListScreen.swift similarity index 52% rename from WaiterRobot/Ui/TableList/TableListScreen.swift rename to WaiterRobot/Features/TableList/TableListScreen.swift index e38a070..5b55e45 100644 --- a/WaiterRobot/Ui/TableList/TableListScreen.swift +++ b/WaiterRobot/Features/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 @@ -19,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() @@ -27,11 +31,18 @@ struct TableListScreen: View { } } } - .toolbarBackground(.hidden, for: .navigationBar) } else { content() .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { + ToolbarItem(placement: .topBarLeading) { + Button { + showFilters.toggle() + } label: { + Image(systemName: "line.3.horizontal.decrease") + } + } + + ToolbarItem(placement: .topBarTrailing) { Button { viewModel.actual.openSettings() } label: { @@ -41,25 +52,50 @@ 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) + .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, + isDemoEvent: viewModel.state.isDemoEvent, + 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() @@ -79,31 +115,23 @@ struct TableListScreen: View { } } } +} - @ViewBuilder - private func tableList(data: KotlinArray) -> some View { - let tableGroups = Array(data) +struct TableListView: View { + let tableGroups: [GroupedTables] + let isDemoEvent: Bool + let onTableSelect: (shared.Table) -> Void - 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() } - ) - } - .padding() - .background(Color(UIColor.systemBackground)) - } - - Divider() + private let layout = [ + GridItem(.adaptive(minimum: 100)), + ] + var body: some View { + VStack(spacing: 0) { if tableGroups.isEmpty { Spacer() - Text(localize.tableList.noTableFound()) + Text(localize.tableList_noTableFound()) .multilineTextAlignment(.center) .frame(maxWidth: .infinity) .padding() @@ -111,39 +139,46 @@ struct TableListScreen: View { Spacer() } else { ScrollView { - LazyVGrid(columns: layout) { - ForEach(tableGroups.filter { !$0.hidden }, id: \.id) { group in + LazyVGrid( + columns: layout, + pinnedViews: [.sectionHeaders] + ) { + ForEach(tableGroups, id: \.id) { group in if !group.tables.isEmpty { TableGroupSection( - tableGroup: group, - onTableClick: { viewModel.actual.onTableClick(table: $0) } + groupedTables: group, + onTableClick: onTableSelect ) } } } .padding() } - .toolbar { - ToolbarItem(placement: .topBarLeading) { - if tableGroups.count > 1 { - Button { - showFilters.toggle() - } label: { - Image(systemName: "slider.horizontal.3") - } - } - } - } + } + + if isDemoEvent { + ErrorBar(message: localize.tableList_demoEventWarning.desc(), initialLines: 1) } } - .animation(.easeIn, value: showFilters) } } -#Preview { +#Preview("TableListScreen") { PreviewView { NavigationView { TableListScreen() } } } + +#Preview("TableListView") { + PreviewView { + NavigationView { + TableListView( + tableGroups: Mock.groupedTables(), + isDemoEvent: true, + onTableSelect: { _ in } + ) + } + } +} diff --git a/WaiterRobot/Features/TableList/TableView.swift b/WaiterRobot/Features/TableList/TableView.swift new file mode 100644 index 0000000..2622c95 --- /dev/null +++ b/WaiterRobot/Features/TableList/TableView.swift @@ -0,0 +1,59 @@ +import SharedUI +import SwiftUI + +struct TableView: View { + @Environment(\.self) + private var env + + let text: String + let hasOrders: Bool + let backgroundColor: Color + let onClick: () -> Void + + init(text: String, hasOrders: Bool, backgroundColor: Color?, onClick: @escaping () -> Void) { + self.text = text + self.hasOrders = hasOrders + self.backgroundColor = backgroundColor ?? Color.lightGray + self.onClick = onClick + } + + var body: some View { + Button(action: onClick) { + ZStack(alignment: .topTrailing) { + Text(text) + .font(.title) + .frame(maxWidth: .infinity, maxHeight: .infinity) + + if hasOrders { + Circle() + .foregroundColor(backgroundColor.bestContrastColor(Color(.darkRed), Color(.lightRed), in: env)) + .frame(width: 12, height: 12) + .padding(.top, 10) + .padding(.trailing, 10) + } + } + } + .aspectRatio(1.0, contentMode: .fit) + .background { + RoundedRectangle(cornerRadius: 20) + .foregroundColor(backgroundColor) + } + .foregroundStyle(backgroundColor.bestContrastColor(.white, .black, in: env)) + } +} + +#Preview { + VStack { + TableView(text: "1", hasOrders: true, backgroundColor: .blackWhite) {} + .frame(maxWidth: 100) + + TableView(text: "1", hasOrders: false, backgroundColor: .gray) {} + .frame(maxWidth: 100) + + TableView(text: "1", hasOrders: true, backgroundColor: .green) {} + .frame(maxWidth: 100) + + TableView(text: "2", hasOrders: true, backgroundColor: nil) {} + .frame(maxWidth: 100) + } +} diff --git a/WaiterRobot/Ui/UpdateApp/UpdateAppScreen.swift b/WaiterRobot/Features/UpdateApp/UpdateAppScreen.swift similarity index 78% rename from WaiterRobot/Ui/UpdateApp/UpdateAppScreen.swift rename to WaiterRobot/Features/UpdateApp/UpdateAppScreen.swift index 671148c..1659d94 100644 --- a/WaiterRobot/Ui/UpdateApp/UpdateAppScreen.swift +++ b/WaiterRobot/Features/UpdateApp/UpdateAppScreen.swift @@ -1,10 +1,11 @@ import shared import SwiftUI +import WRCore struct UpdateAppScreen: View { var body: some View { VStack { - Text(localize.app.forceUpdate.message()) + Text(localize.app_forceUpdate_message()) .multilineTextAlignment(.center) Button { @@ -18,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/LaunchScreen.swift b/WaiterRobot/LaunchScreen.swift index c0aa78a..43f1882 100644 --- a/WaiterRobot/LaunchScreen.swift +++ b/WaiterRobot/LaunchScreen.swift @@ -1,40 +1,32 @@ import Foundation import shared +import SharedUI 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 showProgressView = false var body: some View { ZStack { - if case .phone = device { - VStack { - Spacer() + VStack(spacing: 0) { + Image.logoRounded + .resizable() + .scaledToFit() + .frame(width: 280, height: 280) + .ignoresSafeArea() + .padding(.bottom, 23) + .transition(.slide) - Image(.launch) - .resizable() - .scaledToFit() - } - .padding(.horizontal, -2) - .ignoresSafeArea() - } else { - ZStack { - Image(.logoRounded) - .resizable() - .scaledToFit() - .frame(width: 150) + if showProgressView { + ProgressView() .padding() - - VStack { - Spacer() - - ProgressView() - .padding() - .padding(.bottom) - } + .foregroundStyle(.green) } } @@ -42,18 +34,30 @@ struct LaunchScreen: View { 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: () = WaiterRobotApp.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") } } } @@ -66,7 +70,7 @@ struct LaunchScreen: View { } #Preview { - PreviewView { - LaunchScreen() - } +// PreviewView { + LaunchScreen() +// } } diff --git a/WaiterRobot/MainView.swift b/WaiterRobot/MainView.swift index 5302b21..d7fc025 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 @@ -35,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) { @@ -97,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 } @@ -108,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 { @@ -127,7 +93,7 @@ struct MainView: View { } } } message: { - Text(localize.app.updateAvailable.message()) + Text(localize.app_updateAvailable_message()) } .onAppear { VersionChecker.shared.checkVersion { @@ -135,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 { 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/Resources/Images.xcassets/LaunchImage.imageset/LaunchImage.pdf b/WaiterRobot/Resources/Images.xcassets/LaunchImage.imageset/LaunchImage.pdf deleted file mode 100644 index 0c2fc92..0000000 Binary files a/WaiterRobot/Resources/Images.xcassets/LaunchImage.imageset/LaunchImage.pdf and /dev/null differ diff --git a/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round-full.png b/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round-full.png deleted file mode 100644 index f51ccef..0000000 Binary files a/WaiterRobot/Resources/Images.xcassets/LogoRounded.imageset/wr-round-full.png and /dev/null differ diff --git a/WaiterRobot/Ui/Billing/BillingScreen.swift b/WaiterRobot/Ui/Billing/BillingScreen.swift deleted file mode 100644 index 8fe165a..0000000 --- a/WaiterRobot/Ui/Billing/BillingScreen.swift +++ /dev/null @@ -1,144 +0,0 @@ -import Foundation -import shared -import SwiftUI -import UIPilot - -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 - - init(table: shared.Table) { - self.table = table - _viewModel = StateObject(wrappedValue: ObservableBillingViewModel(table: table)) - } - - 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() - } - } - .confirmationDialog( - localize.billing.notSent.title(), - isPresented: $showAbortConfirmation, - titleVisibility: .visible - ) { - Button(localize.dialog.closeAnyway(), role: .destructive) { - viewModel.actual.abortBill() - } - } 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") - } - } - } - } - // 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 - } - - return true - } - } - - @ViewBuilder - 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 { - Section { - ForEach(billItems, id: \.virtualId) { item in - BillListItem( - item: item, - addOne: { - viewModel.actual.addItem(virtualId: item.virtualId, amount: 1) - }, - addAll: { - viewModel.actual.addItem(virtualId: item.virtualId, amount: item.ordered - item.selectedForBill) - }, - removeOne: { - viewModel.actual.addItem(virtualId: item.virtualId, amount: -1) - }, - removeAll: { - viewModel.actual.addItem(virtualId: item.virtualId, amount: -item.selectedForBill) - } - ) - } - } header: { - HStack { - Text("Ordered") - Spacer() - Text("Selected") - } - } - } - } - - HStack { - Text("\(localize.billing.total()):") - Spacer() - Text("\(viewModel.state.priceSum)") - } - .font(.title2) - .padding() - .overlay(alignment: .bottom) { - Button { - viewModel.actual.paySelection(paymentSheetShown: false) - } label: { - Image(systemName: "eurosign") - .font(.system(.title)) - .padding() - .tint(.white) - .offset(x: -3) - } - .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) - } - } - } -} diff --git a/WaiterRobot/Ui/LoadingOverlayView.swift b/WaiterRobot/Ui/LoadingOverlayView.swift new file mode 100644 index 0000000..c9567ee --- /dev/null +++ b/WaiterRobot/Ui/LoadingOverlayView.swift @@ -0,0 +1,25 @@ +import SwiftUI + +public struct LoadingOverlayView: View { + let isLoading: Bool + let content: () -> Content + + init(isLoading: Bool, @ViewBuilder content: @escaping () -> Content) { + self.isLoading = isLoading + self.content = content + } + + public 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/Order/Search/ProductSearch.swift b/WaiterRobot/Ui/Order/Search/ProductSearch.swift deleted file mode 100644 index a0d0167..0000000 --- a/WaiterRobot/Ui/Order/Search/ProductSearch.swift +++ /dev/null @@ -1,101 +0,0 @@ -import shared -import SwiftUI - -struct ProductSearch: View { - @Environment(\.dismiss) private var dismiss - - @ObservedObject var viewModel: ObservableOrderViewModel - - @State private var search: String = "" - @State private var selectedTab: Int = 0 - - private let layout = [ - GridItem(.adaptive(minimum: 110)), - ] - - 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) - } - } - }.observeState(of: viewModel) - } - - @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 { - VStack { - ProducSearchTabBarHeader(currentTab: $selectedTab, tabBarOptions: getGroupNames(productGroups)) - - TabView(selection: $selectedTab) { - ProductSearchAllTab( - productGroups: productGroups, - columns: layout, - onProductClick: { - viewModel.actual.addItem(product: $0, amount: 1) - dismiss() - } - ) - .tag(0) - .padding() - - let enumeratedProductGroups = Array(productGroups.enumerated()) - ForEach(enumeratedProductGroups, id: \.element.id) { index, groupWithProducts 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() - } - ) - Spacer() - } - .padding() - } - .tag(index + 1) - } - } - } - .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() - } - } - } - } - } - - private func productGroupsError(error: ResourceError>) -> some View { - Text(error.userMessage) - } - - private func getGroupNames(_ productGroups: [ProductGroup]) -> [String] { - var groupNames = productGroups.map { productGroup in - productGroup.name - } - groupNames.insert(localize.productSearch.allGroups(), at: 0) - return groupNames - } -} diff --git a/WaiterRobot/Ui/Order/Search/ProductSearchAllTab.swift b/WaiterRobot/Ui/Order/Search/ProductSearchAllTab.swift deleted file mode 100644 index b41313b..0000000 --- a/WaiterRobot/Ui/Order/Search/ProductSearchAllTab.swift +++ /dev/null @@ -1,60 +0,0 @@ -import shared -import SwiftUI - -struct ProductSearchAllTab: View { - let productGroups: [ProductGroup] - 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: [ - ProductGroup( - 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/Ui/Order/Search/ProductSearchGroupList.swift b/WaiterRobot/Ui/Order/Search/ProductSearchGroupList.swift deleted file mode 100644 index effc113..0000000 --- a/WaiterRobot/Ui/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/Ui/TableList/TableGroupSection.swift b/WaiterRobot/Ui/TableList/TableGroupSection.swift deleted file mode 100644 index 66ee438..0000000 --- a/WaiterRobot/Ui/TableList/TableGroupSection.swift +++ /dev/null @@ -1,67 +0,0 @@ -import shared -import SwiftUI - -struct TableGroupSection: View { - let tableGroup: TableGroup - let onTableClick: (shared.Table) -> Void - - var body: some View { - Section { - ForEach(tableGroup.tables, id: \.id) { table in - TableView( - text: table.number.description, - hasOrders: table.hasOrders, - backgroundColor: Color(hex: tableGroup.color), - onClick: { - onTableClick(table) - } - ) - .padding(10) - } - } header: { - HStack { - if let background = Color(hex: tableGroup.color) { - title(backgroundColor: background) - } else { - title(backgroundColor: .gray.opacity(0.3)) - } - - Spacer() - } - } - } - - private func title(backgroundColor: Color) -> some View { - Text(tableGroup.name) - .font(.title2) - .foregroundStyle(backgroundColor.getContentColor(lightColorScheme: .black, darkColorScheme: .white)) - .padding(6) - .background { - RoundedRectangle(cornerRadius: 8.0) - .foregroundStyle(backgroundColor) - } - } -} - -#Preview { - LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) { - TableGroupSection( - tableGroup: TableGroup( - 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), - shared.Table(id: 3, number: 3, groupName: "Test Group", hasOrders: false), - shared.Table(id: 4, number: 4, groupName: "Test Group", hasOrders: true), - ] - ), - onTableClick: { _ in } - ) - - }.padding() -} diff --git a/WaiterRobot/Ui/TableList/TableListFilterRow.swift b/WaiterRobot/Ui/TableList/TableListFilterRow.swift deleted file mode 100644 index a3de937..0000000 --- a/WaiterRobot/Ui/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", eventId: 1, position: 1, color: nil, hidden: true, tables: []), - TableGroup(id: 2, name: "Test Group2", eventId: 1, position: 1, color: nil, hidden: false, tables: []), - ], - onToggleFilter: { _ in }, - onSelectAll: {}, - onUnselectAll: {} - ) - .padding() -} diff --git a/WaiterRobot/Ui/TableList/TableView.swift b/WaiterRobot/Ui/TableList/TableView.swift deleted file mode 100644 index cc5d227..0000000 --- a/WaiterRobot/Ui/TableList/TableView.swift +++ /dev/null @@ -1,58 +0,0 @@ -import SwiftUI - -struct TableView: View { - let text: String - let hasOrders: Bool - let backgroundColor: Color? - let onClick: () -> Void - - @Environment(\.colorScheme) - var colorScheme - - var body: some View { - Button(action: onClick) { - ZStack { - 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) - } - } - } - .aspectRatio(1.0, contentMode: .fit) - .background { - if let backgroundColor { - RoundedRectangle(cornerRadius: 20) - .foregroundColor(backgroundColor) - } else { - RoundedRectangle(cornerRadius: 20) - .foregroundColor(.gray.opacity(0.3)) - } - } - .foregroundStyle(backgroundColor?.getContentColor(lightColorScheme: .black, darkColorScheme: .white) ?? .blackWhite) - } -} - -#Preview { - VStack { - TableView(text: "1", hasOrders: false, backgroundColor: .green) {} - .frame(maxWidth: 100) - - TableView(text: "2", hasOrders: true, backgroundColor: nil) {} - .frame(maxWidth: 100) - } -} diff --git a/WaiterRobot/Ui/ViewStateOverlayView.swift b/WaiterRobot/Ui/ViewStateOverlayView.swift new file mode 100644 index 0000000..7d1520d --- /dev/null +++ b/WaiterRobot/Ui/ViewStateOverlayView.swift @@ -0,0 +1,44 @@ +import shared +import SwiftUI + +public struct ViewStateOverlayView: View { + private let state: Skie.Shared.ViewState.__Sealed + private let content: () -> Content + + init(state: ViewState, @ViewBuilder content: @escaping () -> Content) { + self.state = onEnum(of: state) + self.content = content + } + + public 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 {} diff --git a/WaiterRobot/Util/Extensions/Color.swift b/WaiterRobot/Util/Extensions/Color.swift index 1da311a..72be7b7 100644 --- a/WaiterRobot/Util/Extensions/Color.swift +++ b/WaiterRobot/Util/Extensions/Color.swift @@ -63,32 +63,47 @@ extension Color { ) } - // Adjust color based on contrast - func getContentColor(lightColorScheme: Color, darkColorScheme: Color) -> Color { - let lightContrast = contrastRatio(with: lightColorScheme) - let darkContrast = contrastRatio(with: darkColorScheme) + /// 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) } } diff --git a/WaiterRobot/WaiterRobotApp.swift b/WaiterRobot/WaiterRobotApp.swift index a695966..e9271f3 100644 --- a/WaiterRobot/WaiterRobotApp.swift +++ b/WaiterRobot/WaiterRobotApp.swift @@ -1,4 +1,4 @@ -import shared +import SharedUI import SwiftUI @main @@ -8,39 +8,4 @@ struct WaiterRobotApp: App { LaunchScreen() } } - - /// 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/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" diff --git a/fastlane/Fastfile b/fastlane/Fastfile index e827f22..3b6fee6 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,154 +1,52 @@ -# 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: "15.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.4", 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) - 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) + desc "Run all iOS unit and ui tests." + lane :test do + run_tests(scheme: "WaiterRobotLava") end - lane :setupFastlaneSecrets do |options| + desc "Send a notification to Slack" + lane :sendSlackMessage 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", + env_vars: ["SLACK_WEBHOOK_URL"] ) + + slack_webhook_url = ENV['SLACK_WEBHOOK_URL'] + version = options[:version] + environment = options[:env] + + json_body = <<-JSON + { + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "🚀 New iOS #{environment} version available on TestFlight", + "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 diff --git a/fastlane/README.md b/fastlane/README.md index 2bfe4b0..7957e41 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -23,45 +23,13 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do Run all iOS unit and ui tests. -### ios sync_certificates +### ios sendSlackMessage ```sh -[bundle exec] fastlane ios sync_certificates +[bundle exec] fastlane ios sendSlackMessage ``` -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 -``` - - +Send a notification to Slack ---- diff --git a/project.yml b/project.yml deleted file mode 100644 index 21a81a5..0000000 --- a/project.yml +++ /dev/null @@ -1,151 +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 - -packages: - shared: - url: https://github.com/DatepollSystems/WaiterRobot-Shared-Android.git - version: 1.6.9 - UIPilot: - url: https://github.com/canopas/UIPilot.git - from: 1.3.1 - CodeScanner: - url: https://github.com/twostraws/CodeScanner - from: 2.2.1 - -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: shared - - package: UIPilot - - package: CodeScanner - - info: - path: ".generated/${target_name}.plist" - properties: - CFBundleShortVersionString: "2.4.0" - # 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" - - 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 - UILaunchScreen: {} - 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: "main" - 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"