Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
case .pointOfSaleOrdersi2:
return buildConfig == .localDeveloper || buildConfig == .alpha
case .pointOfSaleBarcodeScanningi2:
return false
return buildConfig == .localDeveloper || buildConfig == .alpha
default:
return true
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import SwiftUI

@available(iOS 17.0, *)
struct PointOfSaleBarcodeScannerSetup: View {
@Binding var isPresented: Bool
@State private var flowManager: PointOfSaleBarcodeScannerSetupFlowManager

init(isPresented: Binding<Bool>) {
self._isPresented = isPresented
self.flowManager = PointOfSaleBarcodeScannerSetupFlowManager(isPresented: isPresented)
}

var body: some View {
VStack(spacing: POSSpacing.xxLarge) {
// Header
PointOfSaleModalHeader(isPresented: $isPresented,
title: .constant(AttributedString(currentTitle)))

VStack {
currentContent
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe wrapping the content within the TabView will help with transition animations, although it could be tricky, especially if we don't know how many steps we have until the initial selection is made. Nevertheless, I would consider something like this here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes... that's definitely worth trying, though I do want to keep it open to branching flows in future – e.g. additional steps after a failure for troubleshooting, going back to an arbitrary point in the flow.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

though I do want to keep it open to branching flows in future – e.g. additional steps after a failure for troubleshooting, going back to an arbitrary point in the flow.

TabView seems to be only partially suitable for this dynamic approach. Some online answers suggest that it's needed to change .id(..) of TabView whenever the content changes. I haven't tested it, though.

This is where it becomes interesting with the dynamic approach. Question whether we want to define within the steps of the flow more like a tree, to see possible paths if failures happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, potentially. Let's avoid complicating it too much for now though.

The transitions in my early experiments could work pretty well like this, so we'll see how we get on 😊

Aside: controls for flows like this really feel like something a mobile platform should provide, given how common they are.

Spacer()
}
.scrollVerticallyIfNeeded()

// Bottom buttons
PointOfSaleFlowButtonsView(configuration: flowManager.buttonConfiguration)
}
.padding(POSPadding.xxLarge)
.background(Color.posSurfaceBright)
.containerRelativeFrame([.horizontal, .vertical]) { length, _ in
max(length * 0.75, Constants.modalFrameMaxSmallDimension)
}
.onAppear {
ServiceLocator.analytics.track(.pointOfSaleBarcodeScannerSetupFlowShown)
}
}

// MARK: - Computed Properties
private var currentTitle: String {
switch flowManager.currentState {
case .scannerSelection:
return Localization.setupHeading
case .setupFlow:
return flowManager.getCurrentStep()?.title ?? Localization.setupHeading
}
}

@ViewBuilder
private var currentContent: some View {
switch flowManager.currentState {
case .scannerSelection:
PointOfSaleBarcodeScannerSetupSelectionView(options: scannerOptions) { scannerType in
flowManager.selectScanner(scannerType)
}
case .setupFlow:
if let step = flowManager.getCurrentStep() {
AnyView(step.content)
}
}
}

private var scannerOptions: [PointOfSaleBarcodeScannerSetupFlowOption] {
[
PointOfSaleBarcodeScannerSetupFlowOption(
title: Localization.socketS720Title,
subtitle: Localization.socketS720Subtitle,
scannerType: .socketS720
),
PointOfSaleBarcodeScannerSetupFlowOption(
title: Localization.starBSH20BTitle,
subtitle: Localization.starBSH20BSubtitle,
scannerType: .starBSH20B
),
PointOfSaleBarcodeScannerSetupFlowOption(
title: Localization.tbcScannerTitle,
subtitle: Localization.tbcScannerSubtitle,
scannerType: .tbcScanner
),
PointOfSaleBarcodeScannerSetupFlowOption(
title: Localization.otherTitle,
subtitle: Localization.otherSubtitle,
scannerType: .other
)
]
}
}

// MARK: - Constants
private enum Constants {
static var modalFrameMaxSmallDimension: CGFloat { 752 }
}

// MARK: - Private Localization Extension
@available(iOS 17.0, *)
private extension PointOfSaleBarcodeScannerSetup {
enum Localization {
static let setupHeading = NSLocalizedString(
"pos.barcodeScannerSetup.heading",
value: "Barcode Scanner Setup",
comment: "Heading for the barcode scanner setup flow in POS"
)

static let socketS720Title = NSLocalizedString(
"pos.barcodeScannerSetup.socketS720.title",
value: "Socket S720",
comment: "Title for Socket S720 scanner option in barcode scanner setup"
)
static let socketS720Subtitle = NSLocalizedString(
"pos.barcodeScannerSetup.socketS720.subtitle",
value: "Small handheld scanner with a charging dock or stand",
comment: "Subtitle for Socket S720 scanner option in barcode scanner setup"
)
static let starBSH20BTitle = NSLocalizedString(
"pos.barcodeScannerSetup.starBSH20B.title",
value: "Star BSH-20B",
comment: "Title for Star BSH-20B scanner option in barcode scanner setup"
)
static let starBSH20BSubtitle = NSLocalizedString(
"pos.barcodeScannerSetup.starBSH20B.subtitle",
value: "Ergonomic scanner with a stand",
comment: "Subtitle for Star BSH-20B scanner option in barcode scanner setup"
)
static let tbcScannerTitle = NSLocalizedString(
"pos.barcodeScannerSetup.tbcScanner.title",
value: "Scanner TBC",
comment: "Title for TBC scanner option in barcode scanner setup"
)
static let tbcScannerSubtitle = NSLocalizedString(
"pos.barcodeScannerSetup.tbcScanner.subtitle",
value: "Recommended scanner",
comment: "Subtitle for TBC scanner option in barcode scanner setup"
)
static let otherTitle = NSLocalizedString(
"pos.barcodeScannerSetup.other.title",
value: "Other",
comment: "Title for other scanner option in barcode scanner setup"
)
static let otherSubtitle = NSLocalizedString(
"pos.barcodeScannerSetup.other.subtitle",
value: "General scanner setup instructions",
comment: "Subtitle for other scanner option in barcode scanner setup"
)
}
}

@available(iOS 17.0, *)
#Preview {
PointOfSaleBarcodeScannerSetup(isPresented: .constant(true))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import SwiftUI

// MARK: - Point of Sale Barcode Scanner Setup Flow
@available(iOS 17.0, *)
@Observable
class PointOfSaleBarcodeScannerSetupFlow {
private let scannerType: PointOfSaleBarcodeScannerType
private let onComplete: () -> Void
private let onBackToSelection: () -> Void
private var currentStepIndex: Int = 0

init(scannerType: PointOfSaleBarcodeScannerType,
onComplete: @escaping () -> Void,
onBackToSelection: @escaping () -> Void) {
self.scannerType = scannerType
self.onComplete = onComplete
self.onBackToSelection = onBackToSelection
}

var currentStep: PointOfSaleBarcodeScannerSetupStep? {
steps[safe: currentStepIndex]
}

var isComplete: Bool {
currentStepIndex >= steps.count - 1
}

var nextButtonTitle: String {
isComplete ? Localization.doneButtonTitle : Localization.nextButtonTitle
}

func nextStep() {
if currentStepIndex < steps.count - 1 {
currentStepIndex += 1
} else {
onComplete()
}
}

func previousStep() {
if currentStepIndex > 0 {
currentStepIndex -= 1
} else {
onBackToSelection()
}
}

func restartFlow() {
currentStepIndex = 0
}

func getButtonConfiguration() -> PointOfSaleFlowButtonConfiguration {
guard let step = currentStep else {
return .noButtons()
}

// Use step customization if available
if let customization = step.buttonCustomization {
return customization.customizeButtons(for: self)
}

// Default button configuration
return PointOfSaleFlowButtonConfiguration(
primaryButton: PointOfSaleFlowButtonConfiguration.ButtonConfig(
title: nextButtonTitle,
action: { [weak self] in
self?.nextStep()
}
),
secondaryButton: PointOfSaleFlowButtonConfiguration.ButtonConfig(
title: Localization.backButtonTitle,
action: { [weak self] in
self?.previousStep()
}
)
)
}

private var steps: [PointOfSaleBarcodeScannerSetupStep] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is nice, it will be handy to have all the steps in one place 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. If we need to in future, we can pass them in and define them separately. I'm wary of generalising this too much right now, but also want to keep it open to that; we may have other flows in POS in future, e.g. printer set up, or even configuring a customer's choices for a subscription/booking cart item.

switch scannerType {
case .socketS720:
return [
createWelcomeStep(title: "Socket S720 Setup")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work fully, at least not yet. If I define more steps, the UI is not updated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I'll have a play, thanks.

I would expect it to work, but I pulled multi-step flows out a while ago while focusing on the structure and getting the PR to a managable size. My guess is that there's some failing of observation going on.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@staskus Yes, it was because the Flow wasn't @Observable.

Simple fix in 5e405e7, thanks for the catch and sorry for missing it!

content.changing.mp4

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, a simple fix indeed! Thanks. 👍

// TODO: Add more steps for Socket S720 WOOMOB-698
]
case .starBSH20B:
return [
createWelcomeStep(title: "Star BSH-20B Setup")
// TODO: Add more steps for Star BSH-20B WOOMOB-696
]
case .tbcScanner:
return [
createWelcomeStep(title: "TBC Scanner Setup")
// TODO: Add more steps for TBC Scanner WOOMOB-699
]
case .other:
return [
PointOfSaleBarcodeScannerSetupStep(
title: "General Scanner Setup",
content: { BarcodeScannerInformationContent() }
)
]
}
}

private func createWelcomeStep(title: String) -> PointOfSaleBarcodeScannerSetupStep {
PointOfSaleBarcodeScannerSetupStep(
title: title,
content: { PointOfSaleBarcodeScannerWelcomeView(title: title) },
buttonCustomization: PointOfSaleBarcodeScannerWelcomeButtonCustomization()
)
}
}

// MARK: - Button Customizations
@available(iOS 17.0, *)
struct PointOfSaleBarcodeScannerWelcomeButtonCustomization: PointOfSaleBarcodeScannerButtonCustomization {
func customizeButtons(for flow: PointOfSaleBarcodeScannerSetupFlow) -> PointOfSaleFlowButtonConfiguration {
return PointOfSaleFlowButtonConfiguration(
primaryButton: PointOfSaleFlowButtonConfiguration.ButtonConfig(
title: Localization.doneButtonTitle,
action: { flow.nextStep() }
),
secondaryButton: nil
)
}

private enum Localization {
static let doneButtonTitle = NSLocalizedString(
"pos.barcodeScannerSetup.done.button.title",
value: "Done",
comment: "Title for the done button in barcode scanner setup navigation"
)
}
}

// MARK: - Private Localization Extension
@available(iOS 17.0, *)
private extension PointOfSaleBarcodeScannerSetupFlow {
enum Localization {
static let doneButtonTitle = NSLocalizedString(
"pos.barcodeScannerSetup.done.button.title",
value: "Done",
comment: "Title for the done button in barcode scanner setup navigation"
)
static let nextButtonTitle = NSLocalizedString(
"pos.barcodeScannerSetup.next.button.title",
value: "Next",
comment: "Title for the next button in barcode scanner setup navigation"
)
static let backButtonTitle = NSLocalizedString(
"pos.barcodeScannerSetup.back.button.title",
value: "Back",
comment: "Title for the back button in barcode scanner setup navigation"
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import SwiftUI

// MARK: - Point of Sale Barcode Scanner Setup Flow Manager
@available(iOS 17.0, *)
@Observable
class PointOfSaleBarcodeScannerSetupFlowManager {
var currentState: PointOfSaleBarcodeScannerSetupFlowState = .scannerSelection
@ObservationIgnored @Binding var isPresented: Bool
private var currentFlow: PointOfSaleBarcodeScannerSetupFlow?

init(isPresented: Binding<Bool>) {
self._isPresented = isPresented
}

func selectScanner(_ scannerType: PointOfSaleBarcodeScannerType) {
currentFlow = PointOfSaleBarcodeScannerSetupFlow(scannerType: scannerType, onComplete: { [weak self] in
self?.isPresented = false
}, onBackToSelection: { [weak self] in
self?.goBackToSelection()
})
currentState = .setupFlow(scannerType)
}

func goBackToSelection() {
currentState = .scannerSelection
currentFlow = nil
}

func nextStep() {
currentFlow?.nextStep()
}

func previousStep() {
currentFlow?.previousStep()
}

func getCurrentStep() -> PointOfSaleBarcodeScannerSetupStep? {
currentFlow?.currentStep
}

func isComplete() -> Bool {
currentFlow?.isComplete ?? false
}

var buttonConfiguration: PointOfSaleFlowButtonConfiguration {
switch currentState {
case .scannerSelection:
return .noButtons()
case .setupFlow:
guard let flow = currentFlow else {
return .noButtons()
}

return flow.getButtonConfiguration()
}
}
}
Loading