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

[Experiment] Card payments adaptor #12735

Closed
wants to merge 4 commits into from

Conversation

joshheald
Copy link
Contributor

Part of: #12734

Description

This is WIP.

I've wrapped all the existing payments code in an adaptor, and provided an interface to start a payment (only.)

Note that this is in the App target, and intended to be used from there. Using it from the POS target would take extra work, which may, or may not, be worthwhile.

The output alerts/onboarding can be handled by providing a class that conforms to CardPresentPaymentsAlertHandling. Not all this UI would need to be new, we could reuse the existing SwiftUI screens but present them differently. The idea is that is the most that would be new.

The adaptor is intended to perhaps be used as a singleton, or at least be as long lived as a particular store is selected. That way, the same connection controllers are used every time we take a payment (through the adaptor), and we should have just one source of truth for that.

I'm going to continue working on this and actually try to use it from a view tomorrow. This isn't the only possible approach, and nothing's definite, but I'd love your thoughts on it so far.

@bozidarsevo @jaclync @iamgabrielma

At this point, I think the adaptor could be used as a singleton to give us one source of truth for card reader connections.

I’ve cut some corners in the POC and this still doesn’t totally prove that we can avoid UIKit use, but I _think_ the principle holds.
@joshheald joshheald added the status: do not merge Dependent on another PR, ready for review but not ready for merge. label May 14, 2024
@wpmobilebot
Copy link
Collaborator

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

App NameWooCommerce iOS WooCommerce iOS
Build Numberpr12735-610ac3c
Version18.6
Bundle IDcom.automattic.alpha.woocommerce
Commit610ac3c
App Center BuildWooCommerce - Prototype Builds #9055
Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.

@dangermattic
Copy link
Collaborator

1 Message
📖 This PR is still a Draft: some checks will be skipped.

Generated by 🚫 Danger

}

func collectPayment(for order: Order,
using discoveryMethod: CardReaderDiscoveryMethod) async -> CardPresentPaymentResult {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to switch over a result type here?

Suggested change
using discoveryMethod: CardReaderDiscoveryMethod) async -> CardPresentPaymentResult {
using discoveryMethod: CardReaderDiscoveryMethod) async -> Result<Order, CardPresentPaymentError> {

The enum can be simplified and moved away as well:

- enum CardPresentPaymentResult {
-    case success(Order)
+ enum CardPresentPaymentError: Error {
    case failure(CardPaymentErrorProtocol)
    case cancellation
}

And the continuation would also be clearer:

- continuation.resume(returning: .cancellation)
+ continuation.resume(returning: .failure(.cancellation))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could, and I probably wouldn't even pick it up at review if someone did that... but personally I don't really like to use Result for tri-states like this.

Cancellation isn't an error or failure, it's a user action and we're doing what they want us to. I think that making it a failure tends to confuse the call site, as you have to unwrap the errors and check whether they were actually cancellations before you handle them.

}
}

extension CardPresentPaymentsAdaptor: CardPresentPaymentsOnboardingPresenting, CardPresentPaymentAlertsPresenting {
Copy link
Contributor

Choose a reason for hiding this comment

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

Will the adaptor also split responsibilities here along *_OnboardingPresenting and *_AlertsPresenting? Or we keep all in one?

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 have a strong feeling on splitting them. We should do it if we found that it made it easier to use at the call site, e.g. because the SwiftUI view modifiers we use to present each were different.

For the purposes of the experiment, it doesn't make a lot of difference though.

Comment on lines +134 to +135
// In here we still need to handle the button actions wrt the UIViewControllers the closures are passed.
// That said, very few of the alerts actually use the UIVCs, so it might be just as easy to remove the dependency from both sides
Copy link
Contributor

Choose a reason for hiding this comment

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

We should also update (or remove) var image: UIImage { get } property since uses a specific UIKit image type. The auxiliaryAttributedButtonTitle property is not a problem for SwiftUI, but it's used only once through all modals, so maybe is worth take a look and see if we should remove it as well for simplicity.

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, I'm not totally sure how much we want to carry across from the CardPresentPaymentsModalViewModel here, but the nice thing is that we don't need to include the image if we don't want to. We can handle that however we choose within the consuming code – perhaps the image is the same for all of them, perhaps we determine it based on the title, or we add an enum somewhere for translating (back?) to a more declarative type of output.

Comment on lines +99 to +101
func showOnboardingIfRequired(from: UIViewController, readyToCollectPayment: @escaping () -> Void) {
paymentsAlertHandler?.showOnboarding()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

actually try to use it from a view tomorrow.

Not sure if you've already thought/solved this, but this function would still require to pass a UIViewController in, keeping UIKit as dependency. How should we go about it? Some sort of extension to the UIViewController?

Suggested change
func showOnboardingIfRequired(from: UIViewController, readyToCollectPayment: @escaping () -> Void) {
paymentsAlertHandler?.showOnboarding()
}
extension UIViewController: Presenter { }
protocol Presenter {
func present(_ viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)?)
var navigationController: UINavigationController? { get }
func show(_ vc: UIViewController, sender: Any?)
}
func showOnboardingIfRequired(from: Presenter, readyToCollectPayment: @escaping () -> Void) {
paymentsAlertHandler?.showOnboarding()
}

Problem is that we'll have troubles in SwiftUI when handling this method, perhaps by using generics for the view adapter and a hosting controller? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this function would still require to pass a UIViewController in, keeping UIKit as dependency. How should we go about it? Some sort of extension to the UIViewController?

The UIViewController gets passed in from the existing Card Payments (UIKit-heavy) code that we're adapting... and then we ignore it, so there's no need to pass one in from SwiftUI.

This is the adaptor impleementing the OnboardingPresenter protocol, for the benefit of the existing payments code, and then translating (adapting) the showOnboardingIfRequired call to a more SwiftUI friendly handler. That paymentsAlertHandler is what gets passed in from SwiftUI, and it doesn't know about the UIViewController.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

still require to pass a UIViewController in, keeping UIKit as dependency. How should we go about it? Some sort of extension to the UIViewController?

PS – this is exactly what I intend to do for rootViewController, so that I can pass in an adaptor implementation instead of UIViewController() as I'm doing at the moment. It may be that we can just ignore whatever calls we get on rootViewController when using it via the adaptor, but if we can't, this is the kind of approach I'd take.

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.

Just some questions for my learning!

let preflightController = await CardPresentPaymentPreflightController(
siteID: siteID,
configuration: CardPresentConfigurationLoader().configuration,
rootViewController: UIViewController(),
Copy link
Contributor

Choose a reason for hiding this comment

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

just for my learning, it looks like we can just pass any view controller here and in CollectOrderPaymentUseCase, does that mean this rootViewController parameter isn't really necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the moment it is used, by the existing code. I'm only passing this as a temporary measure so that I could work on onboarding, etc. Here's the list where it's used at the moment:

  • CardPresentPaymentAlertsPresenter
    • present(present, animated: false) in dismissSeveralFoundAndPresent and dismissCommonAndPresent
    • these are used to show the UIKit payment alerts, so in our adapted case we won't need to use them because we'll publish the view models for the caller to display those alerts.
  • CollectOrderPaymentUseCase
    • self.rootViewController.presentedViewController?.dismiss() in failure handler for attemptPayment in collectPayment.
      • I think that by managing our own display of alerts in the caller, we can avoid this need too
    • present used in receipts
      • Receipts – I'm not tackling these head on at the moment, because they're really something that comes after the payment flow.
    • set as noticePresenter.presentingViewController when receipt printing fails
  • CardPresentPreflightController
    • onboardingPresenter.showOnboardingIfRequired(from: rootViewController)
      • safe to ignore since we don't use it in our implementation of the adaptor for onboarding

So all in all, from the adaptor we may (end up) being able to get away without a VC here.

I'd do that by replacing the requirement for a UIViewController, with a requirement for a protocol with the above methods, and then pass in a null object from the adaptor so we don't end up with warnings or UIKit getting confused about a VC outside the view hierarchy being used for presentation.

}
}

extension CardPresentPaymentsAdaptor: CardPresentPaymentsOnboardingPresenting, CardPresentPaymentAlertsPresenting {
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be a naive question so feel free to correct: I think it'd be nice if the consumer only knows about the payments/reader-related interface like collectPayment(for:using:), and not the functions/variables that come with these protocol conformance. We can also create a protocol for the adaptor to address this if possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, agreed. I didn't do it initially in the experiment but then did it to answer a P2 question, and it makes it clearer what the consumer should know about.

Having the adaptor implement a bunch of other protocols to do its work makes it harder to have clarity there, but the protocol helps.

@malinajirka
Copy link
Contributor

Closing as this PR is not active anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature: POS status: do not merge Dependent on another PR, ready for review but not ready for merge.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants