Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 2.0.5 #8

Merged
merged 10 commits into from
Jun 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ on:
branches:
- main
- develop
paths:
- 'project.yml'
- 'WaiterRobot/**'
- 'TargetSpecificResources/**'
- '.github/workflows/publish.yml'

jobs:
publish:
Expand All @@ -26,18 +31,11 @@ jobs:
tar xzf .keys.tar.gz
chmod 600 .keys/github-deploy-key
cd ..

- name: Netrc github.com
uses: extractions/netrc@v1
with:
machine: github.com
username: ${{ secrets.NETRC_USERNAME }}
password: ${{ secrets.NETRC_TOKEN }}


- name: Netrc api.github.com
uses: extractions/netrc@v1
with:
machine: api.github.com
machine: maven.pkg.github.com
username: ${{ secrets.NETRC_USERNAME }}
password: ${{ secrets.NETRC_TOKEN }}

Expand All @@ -55,4 +53,4 @@ jobs:
FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }}
FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
# WaiterRobot iOS
<p align="center">
<img src="documentation/wr-square-rounded.png" style="width:200px; border-radius: 15px;"/>
</p>
<h1 align="center">WaiterRobot</h1>
<p align="center">Lightning fast and simple gastronomy</p>

# iOS

This Repository includes the iOS version of the WaiterRobot App. It is based on a shared Kotlin-Multiplatform (KMM)
module, which can be found [here](https://github.com/DatepollSystems/waiterrobot-mobile_android-shared) (there you can
Expand All @@ -25,6 +31,16 @@ This command must also be run after switching branches and it's advisable to als

3. Open the `WaiterRobot.xcodeproj` in Xcode and start coding :)

> If the Build fails with an exception that the binaries for the shared library couldn't be downloaded, you need to add
> the following to your `~/.netrc` file (create the file if it doesn't exist) to allow accessing the GitHub API.
> The personal access token can be created under [Settings -> Developer settings -> Personal access tokens -> Fine-grained tokens](https://github.com/settings/tokens?type=beta). "Public Repositories (read-only)" permission should be enougth.

```
machine maven.pkg.github.com
login [github username]
password [your new personal access token]
```

## Dev with local KMM module version

For a guide to use a local version of the KMM module
Expand All @@ -38,10 +54,25 @@ see [KMMBridge local dev spm](https://touchlab.github.io/KMMBridge/spm/IOS_LOCAL
4. When finished delete folder, make sure to select "Remove References"!!! (otherwise the whole KMM
project will be deleted locally)

## Releasing

Production release is triggered on push to main. The CI then builds the app and deploys it to
TestFlight. After testing the app then must be released manually from there. A tag in the form of
`major.minor.patch` (e.g. android-1.0.0) is created. (see [publish.yml](.github/workflows/publish.yml))

> Do not forget to bump the iOS app version ([project.yml](project.yml), CFBundleShortVersionString & CFBundleVersion)
> on the dev branch after a production release was made.

On each push to develop also a lava (dev) build is triggered and published to TestFlight of
the WaiterRobot Lava app. A tag in the form of `major.minor.patch-lava-epochMinutes` is created
(e.g. android-1.0.1-lava-27935730). (see [publish.yml](.github/workflows/publish.yml))

# Language, libraries and tools

- [Swift](https://www.apple.com/de/swift/)
- [XcodeGen](https://yonaskolb.github.io/XcodeGen/)
- [SwiftUI](https://developer.apple.com/xcode/swiftui/)
- [CodeScanner](https://github.com/twostraws/CodeScanner) QR-Code scanner
- [UIPilot](https://canopas.github.io/UIPilot/) SwiftUI navigation
- [Fastlane](https://docs.fastlane.tools/)
- [Match](https://docs.fastlane.tools/actions/match/)
2 changes: 1 addition & 1 deletion WaiterRobot/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
let logger = koin.logger(tag: "AppDelegate")
logger.d { "initialized Koin" }

KMMResourcesLocalizationKt.localizationBundle = Bundle(for: L.self)
KMMResourcesLocalizationKt.localizationBundle = Bundle(for: shared.L.self)
logger.d { "initialized localization bundle" }

return true
Expand Down
9 changes: 7 additions & 2 deletions WaiterRobot/Core/Globals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ var koin: IosKoinComponent {
}

// Shortcut for localization
var S: L.Companion {
get { L.Companion.shared }
@available(*, deprecated, renamed: "L")
var S: shared.L.Companion {
get { shared.L.Companion.shared }
}

var L: shared.L.Companion {
get { shared.L.Companion.shared }
}
4 changes: 2 additions & 2 deletions WaiterRobot/Core/Mvi/ObservableViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import shared
@MainActor
class ObservableViewModel<S: ViewModelState, E: ViewModelEffect, VM: AbstractViewModel<S, E>>: ObservableObject {
@Published public private(set) var state: S
public private(set) var sideEffect: AnyPublisher<E, Never>
public private(set) var sideEffect: AnyPublisher<NavOrViewModelEffect<E>, Never>

public let actual: VM

init(vm: VM) {
self.actual = vm
// This is save, as the constraint is required by the generics (S must be the state of the provided VM)
self.state = actual.container.stateFlow.value as! S
self.sideEffect = actual.container.sideEffectFlow.asPublisher() as AnyPublisher<E, Never>
self.sideEffect = actual.container.sideEffectFlow.asPublisher() as AnyPublisher<NavOrViewModelEffect<E>, Never>

(actual.container.stateFlow.asPublisher() as AnyPublisher<S, Never>)
.receive(on: RunLoop.main)
Expand Down
30 changes: 12 additions & 18 deletions WaiterRobot/Ui/Billing/BillingScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct BillingScreen: View {
VStack {
List {
if vm.state.billItems.isEmpty {
Text(S.billing.noOpenBill(value0: table.number.description))
Text(L.billing.noOpenBill(value0: table.number.description, value1: table.groupName))
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
.padding()
Expand Down Expand Up @@ -59,15 +59,15 @@ struct BillingScreen: View {
}

HStack {
Text("\(S.billing.total()):")
Text("\(L.billing.total()):")
Spacer()
Text("\(vm.state.priceSum)")
}
.font(.title2)
.padding()
.overlay(alignment: .bottom) {
Button {
vm.actual.paySelection()
showPayDialog = true
} label: {
Image(systemName: "dollarsign")
.font(.system(.title))
Expand All @@ -81,14 +81,14 @@ struct BillingScreen: View {
}
}
}
.navigationTitle(S.billing.title(value0: table.number.description))
.navigationTitle(L.billing.title(value0: table.number.description, value1: table.groupName))
.navigationBarTitleDisplayMode(.inline)
.customBackNavigation(title: S.dialog.cancel(), icon: nil, action: vm.actual.goBack) // TODO
.confirmationDialog(S.billing.notSent.title(), isPresented: Binding.constant(vm.state.showConfirmationDialog), titleVisibility: .visible) {
Button(S.dialog.closeAnyway(), role: .destructive, action: vm.actual.abortBill)
Button(S.dialog.cancel(), role: .cancel, action: vm.actual.keepBill)
.customBackNavigation(title: L.dialog.cancel(), icon: nil, action: vm.actual.goBack) // TODO
.confirmationDialog(L.billing.notSent.title(), isPresented: Binding.constant(vm.state.showConfirmationDialog), titleVisibility: .visible) {
Button(L.dialog.closeAnyway(), role: .destructive, action: vm.actual.abortBill)
Button(L.dialog.cancel(), role: .cancel, action: vm.actual.keepBill)
} message: {
Text(S.billing.notSent.desc())
Text(L.billing.notSent.desc())
}
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Expand All @@ -105,15 +105,9 @@ struct BillingScreen: View {
}
}
}
.onReceive(vm.sideEffect) { effect in
switch effect {
case let navEffect as NavigationEffect:
handleNavigation(navEffect.action, navigator)
default:
koin.logger(tag: "BillingScreen").w {
"No action defined for sideEffect \(effect.self.description)"
}
}
.sheet(isPresented: $showPayDialog) {
PayDialog(vm: vm)
}
.handleSideEffects(of: vm, navigator)
}
}
65 changes: 65 additions & 0 deletions WaiterRobot/Ui/Billing/PayDialog.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import SwiftUI
import Combine
import shared

struct PayDialog: View {
@Environment(\.dismiss) private var dismiss

@ObservedObject var vm: ObservableViewModel<BillingState, BillingEffect, BillingViewModel>

@State private var moneyGiven: String = ""

var body: some View {
let range = NSRange(location: 0, length: moneyGiven.utf16.count)
let regex = try! NSRegularExpression(pattern: "^(\\d+([.,]\\d{0,2})?)?$")
let isInputInvalid = vm.state.changeText == "NaN" || vm.state.changeText.hasPrefix("-") || regex.firstMatch(in: moneyGiven, options: [], range: range) == nil

NavigationView {
VStack{
HStack {
Text(L.billing.total() + ":")
.font(.title2)
Spacer()
Text(vm.state.priceSum.description)
.font(.title2)
}

TextField(L.billing.given(), text: $moneyGiven)
.font(.title)
.keyboardType(.numbersAndPunctuation)
.onChange(of: moneyGiven, perform: vm.actual.moneyGiven)
.frame(height: 48)
.padding(EdgeInsets(top: 0, leading: 6, bottom: 0, trailing: 6))
.cornerRadius(5)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(isInputInvalid ? .red : .secondary, lineWidth: 2.0)
)

HStack {
Text(L.billing.change() + ":")
.font(.title2)
Spacer()
Text(vm.state.changeText)
.font(.title2)
}

Spacer()
}
.padding()
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(L.dialog.cancel()) {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button(L.billing.pay()) {
vm.actual.paySelection()
dismiss()
}
}
}
}
}
}
49 changes: 34 additions & 15 deletions WaiterRobot/Ui/Core/Navigation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@ import SwiftUI
import shared
import UIPilot

func handleNavigation(_ navAction: NavAction, _ navigator: UIPilot<Screen>) {
koin.logger(tag: "Navigation").d { "Handle navigation: \(navAction.description)" }
switch navAction {
case is NavAction.Pop:
navigator.pop()
case let nav as NavAction.Push:
navigator.push(nav.screen)
case let nav as NavAction.PopUpTo:
navigator.popTo(nav.screen, inclusive: nav.inclusive)
case let nav as NavAction.PopUpAndPush:
navigator.popTo(nav.popUpTo, inclusive: nav.inclusive)
navigator.push(nav.screen)
default:
koin.logger(tag: "Navigation").e {
"No nav action for nav effect \(navAction.self.description)"
extension UIPilot<Screen> {
func navigate(_ navAction: NavAction) {
koin.logger(tag: "Navigation").d { "Handle navigation: \(navAction.description)" }
switch navAction {
case is NavAction.Pop:
pop()
case let nav as NavAction.Push:
push(nav.screen)
case let nav as NavAction.PopUpTo:
popTo(nav.screen, inclusive: nav.inclusive)
case let nav as NavAction.PopUpAndPush:
popTo(nav.popUpTo, inclusive: nav.inclusive)
push(nav.screen)
default:
koin.logger(tag: "Navigation").e {
"No nav action for nav effect \(navAction.self.description)"
}
}
}
}
Expand All @@ -35,4 +37,21 @@ extension View {
}
}
}

@MainActor
func handleSideEffects<S, E, VM, OVM>(of vm: OVM, _ navigator: UIPilot<Screen>, handler: ((E) -> Bool)? = nil) -> some View where S : ViewModelState, E: ViewModelEffect, VM: AbstractViewModel<S, E>, OVM: ObservableViewModel<S, E, VM> {
onReceive(vm.sideEffect) { effect in
debugPrint("Got Sideeffect \(effect)")
switch effect {
case let navEffect as NavOrViewModelEffectNavEffect<E>:
navigator.navigate(navEffect.action)
case let sideEffect as NavOrViewModelEffectVMEffect<E>:
if handler?(sideEffect.effect) != true {
koin.logger(tag: "handleSideEffects").w { "Side effect \(sideEffect.effect) was not handled." }
}
default:
koin.logger(tag: "handleSideEffects").w { "Unhandled effect type \(effect)." }
}
}
}
}
13 changes: 2 additions & 11 deletions WaiterRobot/Ui/Login/LoginScannerScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct LoginScannerScreen: View {
VStack {
CodeScannerView(
codeTypes: [.qr],
simulatedData: ""
simulatedData: "https://my.kellner.team/ml/signIn?token=gj8TeJ4eQ0oRhD5yw8THx5OFhjQ&purpose=SIGN_IN"
) { result in
switch result {
case .success(let result):
Expand All @@ -36,16 +36,7 @@ struct LoginScannerScreen: View {
}
}
}
.onReceive(vm.sideEffect) { effect in
switch effect {
case let navEffect as NavigationEffect:
handleNavigation(navEffect.action, navigator)
default:
koin.logger(tag: "LoginScannerScreen").w {
"No action defined for sideEffect \(effect.self.description)"
}
}
}
.handleSideEffects(of: vm, navigator)
}
}

Expand Down
11 changes: 1 addition & 10 deletions WaiterRobot/Ui/Login/LoginScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,6 @@ struct LoginScreen: View {
Spacer()
}
}
.onReceive(vm.sideEffect) { effect in
switch effect {
case let navEffect as NavigationEffect:
handleNavigation(navEffect.action, navigator)
default:
koin.logger(tag: "LoginScreen").w {
"No action defined for sideEffect \(effect.self.description)"
}
}
}
.handleSideEffects(of: vm, navigator)
}
}
11 changes: 1 addition & 10 deletions WaiterRobot/Ui/Login/RegisterScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,7 @@ struct RegisterScreen: View {
.padding()
}
.navigationBarHidden(true)
.onReceive(vm.sideEffect) { effect in
switch effect {
case let navEffect as NavigationEffect:
handleNavigation(navEffect.action, navigator)
default:
koin.logger(tag: "LoginScreen").w {
"No action defined for sideEffect \(effect.self.description)"
}
}
}
.handleSideEffects(of: vm, navigator)
}
}

Expand Down
Loading