Skip to content

Conversation

@staskus
Copy link
Contributor

@staskus staskus commented Jul 31, 2025

More context: pdfdoF-7It-p2
Last PRs: #15955, #15956
WOOMOB-935

Description

I identified a list of remaining dependencies that POS code depends on the Woo app. I started tracking them more tidily within the Linear task WOOMOB-935. Feel free to pick any of those dependencies to make them reusable for POS module.

I drew an arbitrary line to make a pull request before proceeding to further refactoring.

For each of the dependencies, I need to make a decision:

  • Moved to a shared module (WooFoundation, Yosemite...)
  • Injected to the POS through the adaptor or some other way
  • Duplicated in the POS target
  • ... or just move away outside the POS folder since they are not really used

For now, I decided not to create another module (e.g WooFoundationUI). However, that would be an easy change in the future if we decide to split WooFoundation in more parts. We have many options for the future, including centralizing payments logic in some Payments module and then reusing it between IPP and POS. For now, this is outside the scope.

Moved dependencies

Moved to WooFoundation Module

Shared utilities and UI components used across multiple modules

Core Utilities:

  • VersionHelpersMove to WooFoundation
    • Decision: Shared version comparison utilities used across 13+ files

String & Collection Extensions:

  • String+HelpersMove to WooFoundation

    • Decision: Core string utilities (pluralize, isNotEmpty) used throughout app
  • Array+HelpersMove to WooFoundation

    • Decision: Collection convenience extensions (isNotEmpty, popFirst, etc.) widely used

SwiftUI View Modifiers:

  • View+ConditionalsMove to WooFoundation

    • Decision: SwiftUI conditional view modifiers (renderedIf, if) used across modules
  • View+MeasurementsMove to WooFoundation

    • Decision: SwiftUI measurement helpers (measureHeight, measureWidth) used in multiple modules
  • View+RoundedBorderMove to WooFoundation

    • Decision: Border styling modifiers with dashed support used across UI
  • View+ScrollModifiersMove to WooFoundation

    • Decision: Scroll utilities including scrollVerticallyIfNeeded accessibility helper
  • View+SizeTrackerMove to WooFoundation

    • Decision: View size tracking utility used in multiple components
  • View+AutofocusTextModifierMove to WooFoundation

    • Decision: Text field focus management used across forms

UI Components:

  • ScrollableVStackMove to WooFoundation

    • Decision: Reusable scrollable container component used across modules
  • WooRoundedBorderTextFieldStyleMove to WooFoundation

    • Decision: Shared text field styling used in authentication and forms
  • IndefiniteCircularProgressViewStyleMove to WooFoundation

    • Decision: Animated progress indicator style used across loading states
  • KeyboardObserverMove to WooFoundation

    • Decision: iOS 17+ keyboard state management with environment support

Moved to Yosemite Module

Business logic components that belong in data layer

  • OrderTotalsCalculatorMove to Yosemite
    • Decision: Order calculation logic shared between main app and POS

Created POS-Specific Abstractions

Main app services requiring dependency injection

Navigation Services:

  • DeepLinkNavigator & AppDelegateCreate POSNavigationAdaptor

External View Services:

  • SupportFormViewModelCreate POSExternalViewProvider
    • Decision: Inject support form functionality to remove main app UI dependency. I will use this approach for other Woo app views that have many dependencies as well.

Moved to POS Adaptors away from POS module

Components that bridge main app to POS module

  • CardPresentPaymentsTransactionAlertsProviderMove to POS Adaptors
  • CardPresentPaymentInvalidatablePaymentOrchestratorMove to POS Adaptors
  • CardPresentPaymentsConnectionControllerManagerMove to POS Adaptors
  • POSCollectOrderPaymentAnalyticsAdaptorMove to POS Adaptors

Decision: These components need access to both POS module and main app services

POS-Specific Assets

POS-specific image assets

  • UIImage+Woo extensionsCreate UIImage+POS
    • Decision: Move only POS-used UIImage extensions to POS module

Removed Unused Dependencies

  • POSSessionManagerProvidingRemove

    • Decision: Replaced by externalViewsProvider, no longer needed
  • CardPresentPaymentErrorDelete

    • Decision: Unused error type, clean up

Card Present Payment Dependencies

  • CardPresentPaymentsOnboardingViewModel IPP dependencyRemove and replace with factory that could be accessed by POS
    • Decision: Abstract IPP types to remove coupling

Remaining dependencies to address

  • WooConstants - Shared constants and URLs (external dependencies)
  • FormattableAmountTextField - Used across order/payment flows
  • ProductImageThumbnail - Used throughout product displays
  • AddEditCoupon view - Used in POS and main coupon management
  • POSCouponCreationSheetModifier - Reusable modal pattern
  • PaymentCaptureCelebrationProtocol - Payment success celebration behavior
  • POSEntryPointEligibilityCheckerProtocol - POS access eligibility checking
  • SelectedSiteSettingsProtocol - Site configuration access
  • Parts of WooAnalytics+WooApp - Analytics tracking abstraction
  • WCSettingsWebView - Web view for settings pages
  • ReceiptEligibilityUseCase - Receipt generation eligibility logic
  • CollectOrderPaymentUseCaseError - Payment error handling
  • FormattableAmountTextFieldViewModel - Amount input business logic
  • PriceFieldFormatter - Price display formatting logic
  • WaitingTimeTracker - Time tracking for operations
  • readableCountry extension - CountryCode formatting from SiteAddress
  • CurrencySettings + Sanitized - Currency formatting extensions
  • SwiftUI Shimmer - Loading state animation effect framework
  • POSCollectOrderPaymentPreviewAnalytics - Preview-specific analytics
  • POSPreviewEntryPointEligibilityChecker - Preview eligibility checker
  • POSProductFactory - Delete

Steps to reproduce

  • CI should succeed
  • POS should launch
  • Support Form should work
  • Payments should succed

Testing information

Tested on iPad 18.5 simulator, iPad Air M2 26 device


  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

staskus added 28 commits July 31, 2025 14:42
CardPresentPaymentsOnboardingViewModel is a strong IPP dependency on POS which drags a lot of dependencies.

Solution is to pass CardPresentPaymentOnboardingViewFactory instead of CardPresentPaymentsOnboardingViewModel that hides away ViewModel implementation.

ViewModel is injected at an adaptor level - CardPresentPaymentOnboardingAdaptor.
…ntPaymentsConnectionControllerManager to POS Adaptors
POSCollectOrderPaymentAnalytics depends on concrete IPP implementation that is not mean to be moved to POS.

- Rename POSCollectOrderPaymentAnalytics to POSCollectOrderPaymentAnalyticsAdaptor
- Split POSCollectOrderPaymentAnalyticsTracking POS protocol from IPP CollectOrderPaymentAnalyticsTracking protocol
- Only connect POSCollectOrderPaymentAnalyticsTracking and CollectOrderPaymentAnalyticsTracking at the implementation point
@staskus staskus added this to the 23.0 milestone Jul 31, 2025
@staskus staskus added the type: task An internally driven task. label Jul 31, 2025
@dangermattic
Copy link
Collaborator

2 Warnings
⚠️ View files have been modified, but no screenshot or video is included in the pull request. Consider adding some for clarity.
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.

Generated by 🚫 Danger

@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Jul 31, 2025

App Icon📲 You can test the changes from this Pull Request in WooCommerce iOS Prototype by scanning the QR code below to install the corresponding build.

App NameWooCommerce iOS Prototype
Build Numberpr15968-41e86f1
Version22.9
Bundle IDcom.automattic.alpha.woocommerce
Commit41e86f1
Installation URL5epuqfo6a3838
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@staskus staskus requested review from iamgabrielma and jaclync July 31, 2025 20:05
@jaclync jaclync self-assigned this Aug 1, 2025
Copy link
Contributor

@jaclync jaclync left a comment

Choose a reason for hiding this comment

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

Thanks for the updates! Noticed an issue when testing onboarding for a store with incomplete onboarding state where support and learn more CTAs aren't tappable:

Simulator.Screen.Recording.-.iPad.Air.13-inch.M3.-.2025-08-01.at.12.15.22.mp4

This doesn't happen in trunk. I'll also take a look later.

Comment on lines +418 to +435
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)
}
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.

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 {

}

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?

/// Helpers for working with versions (e.g. comparing two version strings)
///
final class VersionHelpers {
public final class VersionHelpers {
Copy link
Contributor

Choose a reason for hiding this comment

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

nice move, I've been hoping to use this in Yosemite while it was in the app layer.


/// 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?

/// 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.

Comment on lines 205 to 204
SafariView(url: WooConstants.URLs.pointOfSaleDocumentation.asURL())
DocumentationView()
Copy link
Contributor

Choose a reason for hiding this comment

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

just wondering, what are the reasons for this change that require a new DocumentationView from UIKit?

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 thought it's easier to copy rather than to move it into the common place. But probably wiser to move it to WooFoundation instead.

Comment on lines 49 to 59
onboardingScreenViewModelSubject.send(.showOnboarding(viewModel: onboardingViewModel, onCancel: { [weak self] in
self?.readinessSubscription = nil
}))
onboardingScreenViewModelSubject.send(.showOnboarding(
factory: .init(
configuration: onboardingViewModel,
createView: { [weak self] in
guard let self else { return EmptyView() }
return CardPresentPaymentsOnboardingView(viewModel: onboardingViewModel)
}), onCancel: { [weak self] in
self?.readinessSubscription = nil
})
)
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 requirement of the factory wrapper because CardPresentPaymentsOnboardingView isn't accessible in the POS module? Just wanted to confirm because AnyView down the line might come with performance tradeoff. Moving all the payments code to a new module in the future might resolve this.

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 will check if I can work around AnyView change.

Yes, the factory wrapper is required since CardPresentPaymentsOnboardingView and CardPresentPaymentsOnboardingViewModel are IPP views, and then they drag some other dependencies such as CardPresentPaymentsOnboardingUseCase, which in turn drag even more dependencies together with them.

I imagine refactoring them is an option, but it would be a time-consuming option.

@jaclync
Copy link
Contributor

jaclync commented Aug 1, 2025

Noticed an issue when testing onboarding for a store with incomplete onboarding state where support and learn more CTAs aren't tappable:

I'm jumping onto a task to fix a UI test failure in trunk, sharing my progress so far. The only change that I've tried and fixes the issue so far is:

 /// Displays the WooPayments onboarding UI based on the state.
 struct PointOfSaleCardPresentPaymentOnboardingView: View {
     @ObservedObject var viewModel: PointOfSaleCardPresentPaymentOnboardingViewModel
 
     var body: some View {
         VStack(spacing: Constants.verticalSpacing) {
-            AnyView(viewModel.onboardingViewFactory.createView())
+            CardPresentPaymentsOnboardingView(viewModel: viewModel.onboardingViewFactory.configuration as! CardPresentPaymentsOnboardingViewModel)
                 // Hides the navigation bar title `navigationTitle` in `CardPresentPaymentsOnboardingView`.
                 .toolbar(.hidden)
         }
         .posModalCloseButton(action: viewModel.cancelOnboarding,
                              accessibilityLabel: Localization.cancelOnboarding)
         .safariSheet(url: $viewModel.onboardingURL)
         .posModalSizing()
     }
 }

My guess is that there's something about SwiftUI updates. Before this PR, the payment onboarding VM changes probably trigger PointOfSaleCardPresentPaymentOnboardingView updates. Now that the view becomes an AnyView, the view updates might not be triggered anymore.

@jaclync
Copy link
Contributor

jaclync commented Aug 4, 2025

Re Noticed an issue when testing onboarding for a store with incomplete onboarding state where support and learn more CTAs aren't tappable issue above:

With the limited debugging today without a working solution, it seems like the issue is with CardPresentPaymentsOnboardingViewModel life cycle management, potentially due to the SwiftUI hierarchy changes now that CardPresentPaymentsOnboardingView is returned as a generic view. The CardPresentPaymentsOnboardingViewModel instance, when the Learn More CTA is tapped (showURL), differs from the instance when initializing the CardPresentPaymentOnboardingViewFactory and PointOfSaleCardPresentPaymentOnboardingViewModel. Therefore, the showURL setup in PointOfSaleCardPresentPaymentOnboardingViewModel doesn't get carried over to the final view model instance.

It's possible that PointOfSaleCardPresentPaymentOnboardingView reacts to its @ObservedObject var viewModel: PointOfSaleCardPresentPaymentOnboardingViewModel changes but CardPresentPaymentsOnboardingViewConfiguration isn't an ObservableObject at least explicitly. I felt like extracting CardPresentPaymentsOnboardingViewModel and CardPresentPaymentsOnboardingView to a shared module might be worth checking. Let me know how you want to proceed, feel free to merge this PR and resolve the onboarding issue later.

@staskus
Copy link
Contributor Author

staskus commented Aug 4, 2025

@jaclync, thanks for checking it deeply! I was investigating the removal AnyView side of things on Friday, but I haven't reached a good solution.

@wpmobilebot wpmobilebot modified the milestones: 23.0, 23.1 Aug 8, 2025
@wpmobilebot
Copy link
Collaborator

Version 23.0 has now entered code-freeze, so the milestone of this PR has been updated to 23.1.

@staskus
Copy link
Contributor Author

staskus commented Aug 11, 2025

@jaclync, thanks for the review. Only came back to it now!

  • The reason links didn't work seemed to be easier. The self.onboardingViewModel was a computed variable. Since I used it both in the view, and the configuration I would pass different instances. Passing one seemed to fix the issue.
  • I couldn't find a good fix for AnyView for now. If we make it generic, we have to pass the type, which is exactly what I'm trying to hide away from POS. I know it's not optimal performance-wise but might be okay for a couple of views that cannot be easily moved away to WooFoundation due to many dependencies but have to be injected to POS. I understand it wouldn't be an option to any views that are performance sensitive, such as rows. I'll continue looking if we don't have too many of these.

@staskus staskus merged commit c4810b3 into woomob-935-woo-pos-hack-week-pos-modularization Aug 11, 2025
8 of 13 checks passed
@staskus staskus deleted the woomob-935-woo-pos-hack-week-pos-modularization-woo-app-dependencies branch August 11, 2025 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature: POS type: task An internally driven task.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants