-
Notifications
You must be signed in to change notification settings - Fork 114
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
[TTPReq] Integrate Education steps after Terms of Service are accepted #14580
Changes from 10 commits
7562b48
97d80fd
b97d807
6547391
29503e2
f9ad911
7615657
3156e83
08699fb
7b1c311
1f840cf
8d1bef8
dab2e9d
875e2b5
37e6088
eca7510
ce08d74
68b9145
f425046
76a6538
39a579f
8c527ad
eac9875
826d1ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/// A type that represents the possible states of the built-in (Tap to Pay) card reader onboarding flow. | ||
public enum BuiltInCardReaderOnboardingState { | ||
/// User has accepted the terms of service | ||
case didAcceptTermsOfService | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -97,4 +97,8 @@ final class CardPresentPaymentsAlertPresenterAdaptor: CardPresentPaymentAlertsPr | |
func reset() { | ||
latestReaderConnectionHandler = nil | ||
} | ||
|
||
func presentMerchantEducation(completion: @escaping () -> Void) { | ||
completion() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like it's not presented, maybe because TTP isn't supported in POS? Maybe it's worth a clarifying comment that this will be updated when TTP is supported. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it wouldn't be presented for POS, unless we start supporting iPhones or collecting payments through TTP. That comes back to your later question about putting |
||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -4,6 +4,7 @@ import UIKit | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import Storage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import SwiftUI | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import Yosemite | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import Experiments | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// Facilitates connecting to a card reader | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -84,6 +85,8 @@ where AlertProvider.AlertDetails == AlertPresenter.AlertDetails { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private let alertsProvider: AlertProvider | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private let featureFlagService: FeatureFlagService | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// The reader we want the user to consider connecting to | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private var candidateReader: CardReader? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -112,6 +115,8 @@ where AlertProvider.AlertDetails == AlertPresenter.AlertDetails { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private var allowTermsOfServiceAcceptance: Bool | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private var isMerchantEducationInProgress: CurrentValueSubject<Bool, Never> = .init(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
init( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
forSiteID: Int64, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
storageManager: StorageManagerType = ServiceLocator.storageManager, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -120,6 +125,7 @@ where AlertProvider.AlertDetails == AlertPresenter.AlertDetails { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
alertsProvider: AlertProvider, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
configuration: CardPresentPaymentsConfiguration, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
analyticsTracker: CardReaderConnectionAnalyticsTracker, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
allowTermsOfServiceAcceptance: Bool = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
siteID = forSiteID | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -130,6 +136,7 @@ where AlertProvider.AlertDetails == AlertPresenter.AlertDetails { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.alertsProvider = alertsProvider | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.configuration = configuration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.analyticsTracker = analyticsTracker | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.featureFlagService = featureFlagService | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.allowTermsOfServiceAcceptance = allowTermsOfServiceAcceptance | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
configureResultsControllers() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -366,6 +373,30 @@ private extension BuiltInCardReaderConnectionController { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.store(in: &self.subscriptions) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
stores.dispatch(softwareUpdateAction) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if featureFlagService.isFeatureFlagEnabled(.tapToPayEducation) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let onboardingAction = CardPresentPaymentAction.observeBuiltInCardReaderOnboardingState { [weak self] onboardingEvents in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
guard let self = self else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
onboardingEvents | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.subscribe(on: DispatchQueue.main) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.sink { [weak self] event in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
guard let self = self else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
switch event { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
case .didAcceptTermsOfService: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.isMerchantEducationInProgress.send(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self.alertsPresenter.presentMerchantEducation { [weak self] in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
self?.isMerchantEducationInProgress.send(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.store(in: &self.subscriptions) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. super nit: can remove a few selves
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
stores.dispatch(onboardingAction) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let options = CardReaderConnectionOptions( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
builtInOptions: BuiltInCardReaderConnectionOptions(termsOfServiceAcceptancePermitted: allowTermsOfServiceAcceptance)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -537,8 +568,15 @@ private extension BuiltInCardReaderConnectionController { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// Calls the completion with a success result | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
private func returnSuccess(result: CardReaderConnectionResult) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
onCompletion?(.success(result)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
state = .idle | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
isMerchantEducationInProgress.eraseToAnyPublisher() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.filter { $0 == false } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.first() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.sink { [weak self] _ in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
guard let self else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
onCompletion?(.success(result)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
state = .idle | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.store(in: &subscriptions) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can see how this works but it makes the function a little misleading. It's changed to "maybe return success sometime", because this is asynchronous, and combine-based. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// Calls the completion with a failure result | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,3 +67,24 @@ struct TapToPayEducationView: View { | |
}) | ||
} | ||
} | ||
|
||
// MARK: - Hosting Controller | ||
|
||
final class TapToPayEducationViewViewHostingController: UIHostingController<TapToPayEducationView> { | ||
staskus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
init(onDismiss: @escaping () -> Void) { | ||
let viewModel = TapToPayEducationViewModel(flow: .onboarding, onDismiss: onDismiss) | ||
super.init(rootView: TapToPayEducationView(viewModel: viewModel)) | ||
|
||
viewModel.onDismiss = { [weak self] in | ||
guard let self else { return } | ||
|
||
self.dismiss(animated: true) { | ||
staskus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
onDismiss() | ||
} | ||
} | ||
} | ||
|
||
@MainActor @preconcurrency required dynamic init?(coder aDecoder: NSCoder) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooh, what's a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Xcode 16 generated this one for me 😀 |
||
fatalError("init(coder:) has not been implemented") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,7 +26,7 @@ final class TapToPayEducationViewModel: ObservableObject { | |
let cardPresentPaymentsOnboardingUseCase: CardPresentPaymentsOnboardingUseCaseProtocol | ||
let siteID: Int64 | ||
|
||
private let onDismiss: () -> Void | ||
var onDismiss: () -> Void | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
init(flow: Flow = .onboarding, | ||
steps: [TapToPayEducationStepViewModel]? = nil, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ protocol CardPresentPaymentAlertsPresenting<AlertDetails> { | |
associatedtype AlertDetails | ||
func present(viewModel: AlertDetails) | ||
func presentWCSettingsWebView(adminURL: URL, completion: @escaping () -> Void) | ||
func presentMerchantEducation(completion: @escaping () -> Void) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❓ Just feeling a bit uncertain about this, as this is just for TTP and I'm not sure if it requires a separate function in fear of this protocol growing very long in the future with new views like this 🤔 @joshheald WDYT about having a separate function here (maybe renaming to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I was going back and forth on how to better approach this. Integrating within We could also create a separate presenter whose sole responsibility would be to present TTP education on top of the existing context. (Example) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I was thinking of refactoring the UIKit version
Nice, I like this approach - it doesn't add complexity to the alerts presenter and the merchant education UI is not an "alert" anyway. When the time comes to support TTP in POS, we can update There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will start manual testing after this thread concludes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I think the second approach of having a separate presenter for education is reasonable. Non-TTP-related controllers don't have to have any knowledge of it. I will include it in the main PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated in dab2e9d There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can see why you went back and forth here!
In some ways it does seem a little strange not to include alerts which (currently) only show during a built in reader connection in It would be perfectly valid to show education outside a connection flow as well, and if we did that, it would seem strange if we needed a The new design is more flexible, because Unfortunately under the existing UIKit design, it's not so clean, as we have the same type of concrete view model class produced for every event, and it does need to be used to present a view. So unless we refactor the existing code to use the new approach, we can't reasonably include the education step in this way. It would probably be ideal to do the refactor. But, it's also not factored in to the project so probably not a reasonable choice here. Since we're not doing it now, let's avoid making it harder to do it in the future. I think that having a separate presenter meets that need. We should try to keep it simple though! |
||
func foundSeveralReaders(readerIDs: [String], | ||
connect: @escaping (String) -> Void, | ||
cancelSearch: @escaping () -> Void) | ||
|
@@ -143,4 +144,14 @@ final class CardPresentPaymentAlertsPresenter: CardPresentPaymentAlertsPresentin | |
dismissCommonAndPresent(animated: true) | ||
dismissSeveralFoundAndPresent(animated: true) | ||
} | ||
|
||
func presentMerchantEducation(completion: @escaping () -> Void) { | ||
let viewController = TapToPayEducationViewViewHostingController(onDismiss: completion) | ||
|
||
if let modalController { | ||
modalController.present(viewController, animated: true) | ||
} else { | ||
rootViewController?.present(viewController, animated: true) | ||
} | ||
staskus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
non-blocking question: do we plan to add more cases to the onboarding state? If not, I wonder if emitting
Void
from the publisher (likevar builtInCardReaderAcceptToSEvents: AnyPublisher<Void, Never> { get }
) on ToS acceptance event is sufficient.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't plan to add any more cases. I created it following the logic of other similar states. However,
builtInCardReaderAcceptToSEvents
is a good idea and would avoid creating this additionalenum
.