Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b29dd16
Create POSNavigationAdaptor to remove DeepLinkNavigator and AppDelega…
staskus Jul 30, 2025
cf92419
Remove SafariView dependency by moving SafariView to PointOfSaleDashb…
staskus Jul 30, 2025
5bd198c
Create POSExternalViewProvider to extract SupportForm away from POS
staskus Jul 30, 2025
ba66da9
Remove CardPresentPaymentsOnboardingViewModel IPP dependency from POS
staskus Jul 30, 2025
8d9111d
Remove CardPresentPaymentsOnboardingViewModel dependency from POS tests
staskus Jul 30, 2025
b785662
Delete unused CardPresentPaymentError
staskus Jul 30, 2025
efb555e
Move CardPresentPaymentInvalidatablePaymentOrchestrator and CardPrese…
staskus Jul 30, 2025
f7d89b1
Move alerts provider to POS Adaptors
staskus Jul 30, 2025
ba8dd96
Split POSCollectOrderPaymentAnalytics implementation and protocol
staskus Jul 30, 2025
dc4987f
Move Orders-related POS events to POSAnalyticsEvent to be reachable o…
staskus Jul 30, 2025
85c6e66
Move OrderTotalsCalculator to Yosemite to share between Woo app and POS
staskus Jul 30, 2025
776fe4c
Remove POSSessionManagerProviding since it was replaced by externalVi…
staskus Jul 31, 2025
b1781c1
Move VersionHelpers to WooFoundation
staskus Jul 31, 2025
f8d0cec
Move Array+Helpers to WooFoundation
staskus Jul 31, 2025
2e7d591
Move some String+Helpers to WooFoundation
staskus Jul 31, 2025
f45e920
Move IndefiniteCircularProgressViewStyle to WooFoundation
staskus Jul 31, 2025
6b4ab2c
Move KeyboardObserver to WooFoundation
staskus Jul 31, 2025
ad46f34
Move View+Measurements to WooFoundation
staskus Jul 31, 2025
01280eb
Move View+ScrollModifiers to WooFoundation
staskus Jul 31, 2025
b874f2c
Move ScrollableVStack to WooFoundation
staskus Jul 31, 2025
a13955e
Move View+Conditionals to WooFoundation
staskus Jul 31, 2025
495c14c
Move View+RoundedBorder to WooFoundation
staskus Jul 31, 2025
4509f48
Make additional WooFoundation imports
staskus Jul 31, 2025
d46f71b
Move WooRoundedBorderTextFieldStyle to WooFoundation
staskus Jul 31, 2025
ba85d6c
Move View+ AutofocusTextModifier to WooFoundation
staskus Jul 31, 2025
f5e18e6
Move View+SizeTracker to WooFoundation
staskus Jul 31, 2025
5edfbfc
Swiftlint updates
staskus Jul 31, 2025
264a0c5
Move UIImages used in POS to UIImage+POS
staskus Jul 31, 2025
40eb857
Update IndefiniteCircularProgressViewStyle.swift
staskus Aug 11, 2025
e614f93
Mark UIImage+POS specifically from the main bundle
staskus Aug 11, 2025
931b614
Move SafariView to WooFoundation
staskus Aug 11, 2025
41e86f1
Use the same instance of onboardingViewModel for configuration and vi…
staskus Aug 11, 2025
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
4 changes: 2 additions & 2 deletions BuildTools/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions Modules/Sources/PointOfSale/Analytics/POSAnalyticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,34 @@ public extension PaymentMethod {
}
}
}

extension WooAnalyticsEvent {
struct Orders {
// MARK: - Order Creation Events

/// Matches errors on Android for consistency
/// Only coupon tracking is relevant for now
enum OrderCreationErrorType: String {
case invalidCoupon = "INVALID_COUPON"
}

static func orderCreationFailed(
usesGiftCard: Bool,
errorContext: String,
errorDescription: String,
errorType: OrderCreationErrorType? = nil
) -> WooAnalyticsEvent {
var properties: [String: WooAnalyticsEventPropertyType] = [
"use_gift_card": usesGiftCard,
"error_context": errorContext,
"error_description": errorDescription
]

if let errorType {
properties["error_type"] = errorType.rawValue
}

return WooAnalyticsEvent(statName: .orderCreationFailed, properties: properties)
}
Comment on lines +418 to +435
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this seems to be tracked in both POS and IPP, WDYT having this extension in WooFoundation for example for use? this way, any modifications to the tracking apply to both use cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know if we should strive to reuse 100%. Since it's the only event so far that had the same logic, I felt it was fine to keep it separately next to other POS events. In fact, I think we track OrderCreationErrorType only on POS for now.

}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import Foundation
import SwiftUI
import WooFoundation
import Experiments
import protocol Yosemite.Action
import struct Yosemite.Site

/// Main dependency provider protocol for POS module
/// This abstracts away direct ServiceLocator access
/// This abstracts away dependencies from the main Woo app
public protocol POSDependencyProviding {
var analytics: POSAnalyticsProviding { get }
var currency: POSCurrencySettingsProviding { get }
var featureFlags: POSFeatureFlagProviding { get }
var session: POSSessionManagerProviding { get }
var connectivity: POSConnectivityProviding { get }
var externalNavigation: POSExternalNavigationProviding { get }
var externalViews: POSExternalViewProviding { get }
}

public protocol POSAnalyticsProviding {
Expand All @@ -21,10 +23,6 @@ public protocol POSAnalyticsProviding {
func track(_ stat: WooAnalyticsStat)
}

public protocol POSSessionManagerProviding {
var defaultSite: Site? { get }
}

public protocol POSFeatureFlagProviding {
func isFeatureFlagEnabled(_ flag: FeatureFlag) -> Bool
}
Expand All @@ -36,3 +34,13 @@ public protocol POSCurrencySettingsProviding {
public protocol POSConnectivityProviding {
var connectivityObserver: ConnectivityObserver { get }
}

/// Navigation to the Woo app service abstraction
public protocol POSExternalNavigationProviding {
func navigateToCreateOrder()
}

/// External view service abstraction
public protocol POSExternalViewProviding {
func createSupportFormView(isPresented: Binding<Bool>) -> AnyView
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: a bit scary seeing AnyView usage due to the rumored performance impact, any chance this could return a view builder to avoid AnyView?

}
39 changes: 27 additions & 12 deletions Modules/Sources/PointOfSale/Environment/POSEnvironmentKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,21 @@ public struct POSFeatureFlagsKey: EnvironmentKey {
public static let defaultValue: POSFeatureFlagProviding = EmptyPOSFeatureFlags()
}

/// Environment key for POS session manager
public struct POSSessionManagerKey: EnvironmentKey {
public static let defaultValue: POSSessionManagerProviding = EmptyPOSSessionManager()
}

/// Environment key for POS connectivity
public struct POSConnectivityKey: EnvironmentKey {
public static let defaultValue: POSConnectivityProviding = EmptyPOSConnectivityProvider()
}

/// Environment key for POS navigation service
public struct POSExternalNavigationKey: EnvironmentKey {
public static let defaultValue: POSExternalNavigationProviding = EmptyPOSExternalNavigation()
}

/// Environment key for POS external view service
public struct POSExternalViewKey: EnvironmentKey {
public static let defaultValue: POSExternalViewProviding = EmptyPOSExternalView()
}

public extension EnvironmentValues {
var posAnalytics: POSAnalyticsProviding {
get { self[POSAnalyticsKey.self] }
Expand All @@ -46,21 +51,26 @@ public extension EnvironmentValues {
set { self[POSFeatureFlagsKey.self] = newValue }
}

var posSession: POSSessionManagerProviding {
get { self[POSSessionManagerKey.self] }
set { self[POSSessionManagerKey.self] = newValue }
}

var posConnectivityProvider: POSConnectivityProviding {
get { self[POSConnectivityKey.self] }
set { self[POSConnectivityKey.self] = newValue }
}

var posExternalNavigation: POSExternalNavigationProviding {
get { self[POSExternalNavigationKey.self] }
set { self[POSExternalNavigationKey.self] = newValue }
}

var posExternalViews: POSExternalViewProviding {
get { self[POSExternalViewKey.self] }
set { self[POSExternalViewKey.self] = newValue }
}
}

// MARK: - Empty Default Values

public struct EmptyPOSSessionManager: POSSessionManagerProviding {
public var defaultSite: Site? = nil
public struct EmptyPOSExternalNavigation: POSExternalNavigationProviding {
public func navigateToCreateOrder() {}
public init() {}
}

Expand Down Expand Up @@ -94,3 +104,8 @@ public struct EmptyPOSAnalytics: POSAnalyticsProviding {
public func track(_ stat: WooFoundationCore.WooAnalyticsStat, parameters: [String: WooAnalyticsEventPropertyType], error: any Error) {}
public init() {}
}

public struct EmptyPOSExternalView: POSExternalViewProviding {
public func createSupportFormView(isPresented: Binding<Bool>) -> AnyView { AnyView(EmptyView()) }
public init() {}
}
24 changes: 24 additions & 0 deletions Modules/Sources/PointOfSale/Extensions/UIImage+POS.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import UIKit

/// Point of Sale specific UIImage extensions
/// Contains only the UIImage extensions needed by the POS module to minimize external dependencies
extension UIImage {

/// App icon (iPhone size) - used in receipt eligibility banner
///
static var appIconDefault: UIImage {
return UIImage(named: "AppIcon60x60")!
Copy link
Contributor

Choose a reason for hiding this comment

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

is the image catalog shared automatically across modules? these images aren't used in POS yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So far as I have tested, it works. Both on the device and the simulator, although I wouldn't have expected that. I specified main .bundle now. We can tell once we move the POS code to the module if everything works all right.

}

/// Card Reader Update arrow - used in reader update progress
///
static var cardReaderUpdateProgressArrow: UIImage {
return UIImage(named: "card-reader-update-progress-arrow")!
}

/// Card Reader Update checkmark - used in reader update progress completion
///
static var cardReaderUpdateProgressCheckmark: UIImage {
return UIImage(named: "card-reader-update-progress-checkmark")!
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import Foundation


// MARK: - Array Helpers
//
extension Array {
public extension Array {
/// Removes and returns the first element in the array. If any!
///
mutating func popFirst() -> Element? {
Expand All @@ -27,7 +26,7 @@ extension Array {

// MARK: - Sequence Helpers
//
extension Sequence {
public extension Sequence {
/// Get the keypaths for a elemtents in a sequence.
///
func map<T>(_ keyPath: KeyPath<Element, T>) -> [T] {
Expand All @@ -40,12 +39,12 @@ extension Sequence {
}
}

extension Sequence where Element: Numeric {
public extension Sequence where Element: Numeric {
/// Returns the sum of all elements in the collection.
func sum() -> Element { return reduce(0, +) }
}

extension Sequence where Element: Equatable {
public extension Sequence where Element: Equatable {
/// Returns the sequence with any duplicate elements after the first one removed.
func removingDuplicates() -> [Element] {
var result = [Element]()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,28 @@ public struct IndefiniteCircularProgressViewStyle: ProgressViewStyle {
@State private var viewRotation: Angle = .radians(0)
@State private var arcTimer: Timer?

public init(
size: CGFloat,
lineWidth: CGFloat = Constants.lineWidth,
lineCap: CGLineCap = .round,
circleColor: Color = Color(.primaryButtonBackground).opacity(Constants.backgroundOpacity),
fillColor: Color = Color(.primaryButtonBackground),
arcEnd: Double = Constants.initialArcEnd,
rotation: Angle = Constants.threeQuarterRotation,
viewRotation: Angle = .radians(0),
arcTimer: Timer? = nil
) {
self.size = size
self.lineWidth = lineWidth
self.lineCap = lineCap
self.circleColor = circleColor
self.fillColor = fillColor
self.arcEnd = arcEnd
self.rotation = rotation
self.viewRotation = viewRotation
self.arcTimer = arcTimer
}

public func makeBody(configuration: ProgressViewStyleConfiguration) -> some View {
VStack {
ZStack {
Expand Down Expand Up @@ -78,20 +100,20 @@ public struct IndefiniteCircularProgressViewStyle: ProgressViewStyle {
}
}

private extension IndefiniteCircularProgressViewStyle {
enum Constants {
static let lineWidth: CGFloat = 10.0
static let backgroundOpacity: CGFloat = 0.2
public extension IndefiniteCircularProgressViewStyle {
public enum Constants {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: getting 'public' modifier is redundant for enum declared in a public extension warning

Suggested change
public enum Constants {
enum Constants {

public static let lineWidth: CGFloat = 10.0
public static let backgroundOpacity: CGFloat = 0.2

static let initialArcStart: Double = 0
static let initialArcEnd: Double = 0.05
static let fullCircle: Double = 1
public static let initialArcStart: Double = 0
public static let initialArcEnd: Double = 0.05
public static let fullCircle: Double = 1

static let threeQuarterRotation: Angle = .radians((9 * Double.pi)/6)
static let fullRotation: Angle = .radians(Double.pi * 2)
public static let threeQuarterRotation: Angle = .radians((9 * Double.pi)/6)
public static let fullRotation: Angle = .radians(Double.pi * 2)
}

enum Localization {
public enum Localization {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: does this string need to be public?

static let inProgressAccessibilityLabel = NSLocalizedString(
"In progress",
comment: "Accessibility label for an indeterminate loading indicator")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@ import Combine

@available(iOS 17.0, *)
@Observable
final class KeyboardObserver {
private(set) var isKeyboardVisible: Bool = false
private(set) var keyboardHeight: CGFloat = 0
public final class KeyboardObserver {
private(set) public var isKeyboardVisible: Bool = false
private(set) public var keyboardHeight: CGFloat = 0

/// When an external keyboard is in use, iPadOS shows a quicktype bar at the bottom of the screen.
/// This is reported as a keyboard with height, so `isKeyboardVisible` will be true and
/// keyboard height will be > 0.
/// However, it's much less of an impingement on the view, so there may be no modification to the view required.
/// `isFullSizeKeyboardVisible` is true when the full software keyboard is shown.
var isFullSizeKeyboardVisible: Bool {
public var isFullSizeKeyboardVisible: Bool {
return keyboardHeight > Constants.hardwareKeyboardHelperBarHeightThreshold
}

private var cancellables = Set<AnyCancellable>()

init() {
public init() {
NotificationCenter.Publisher(center: .default, name: UIResponder.keyboardWillShowNotification)
.merge(with: NotificationCenter.Publisher(center: .default, name: UIResponder.keyboardDidShowNotification))
.receive(on: DispatchQueue.main)
Expand Down Expand Up @@ -64,24 +64,24 @@ private struct KeyboardObserverKey: EnvironmentKey {
}

@available(iOS 17.0, *)
extension EnvironmentValues {
public extension EnvironmentValues {
var keyboardObserver: KeyboardObserver {
get { self[KeyboardObserverKey.self] }
set { self[KeyboardObserverKey.self] = newValue }
}
}

@available(iOS 17.0, *)
struct KeyboardObserverProvider: ViewModifier {
public struct KeyboardObserverProvider: ViewModifier {
@State private var observer = KeyboardObserver()

func body(content: Content) -> some View {
public func body(content: Content) -> some View {
content.environment(\.keyboardObserver, observer)
}
}

@available(iOS 17.0, *)
extension View {
public extension View {
func injectKeyboardObserver() -> some View {
modifier(KeyboardObserverProvider())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import SwiftUI
import struct WooFoundation.ScrollableVStack

/// Wraps a VStack inside a ScrollView, ensuring the content expands to fill the available space
///
struct ScrollableVStack<Content: View>: View {
public struct ScrollableVStack<Content: View>: View {
let alignment: HorizontalAlignment
let padding: CGFloat
let spacing: CGFloat?
let content: Content

init(
public init(
alignment: HorizontalAlignment = .center,
padding: CGFloat = 24,
spacing: CGFloat? = nil,
Expand All @@ -20,7 +21,7 @@ struct ScrollableVStack<Content: View>: View {
self.content = content()
}

var body: some View {
public var body: some View {
GeometryReader { geometry in
ScrollView {
VStack(alignment: alignment, spacing: spacing) {
Expand Down
Loading
Loading