Skip to content

Commit

Permalink
Merge pull request #1457 from stripe-ios/kg-accountpicker-final
Browse files Browse the repository at this point in the history
Financial Connections: Added more native Account Picker polish + created a CloseConfirmationAlertHandler
  • Loading branch information
kgaidis-stripe authored Sep 26, 2022
2 parents 12d5f91 + b31d2ca commit 39cd6d9
Show file tree
Hide file tree
Showing 13 changed files with 124 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
6A2318D428B3C36000F2A7D8 /* AccountPickerSelectionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A2318D328B3C36000F2A7D8 /* AccountPickerSelectionListView.swift */; };
6A2318D728B571E000F2A7D8 /* ManualEntryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A2318D628B571E000F2A7D8 /* ManualEntryViewController.swift */; };
6A2318D928B57E5100F2A7D8 /* ManualEntryTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A2318D828B57E5100F2A7D8 /* ManualEntryTextField.swift */; };
6A4FA13928E11B3D00F07D42 /* CloseConfirmationAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4FA13828E11B3D00F07D42 /* CloseConfirmationAlertHandler.swift */; };
6A542DB12887259600958ED1 /* FeaturedInstitutionGridCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A542DB02887259600958ED1 /* FeaturedInstitutionGridCell.swift */; };
6A542DB328889AC400958ED1 /* InstitutionSearchTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A542DB228889AC400958ED1 /* InstitutionSearchTableView.swift */; };
6A542DB52889A1CB00958ED1 /* InstitutionSearchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A542DB42889A1CB00958ED1 /* InstitutionSearchTableViewCell.swift */; };
Expand Down Expand Up @@ -262,6 +263,7 @@
6A2318D328B3C36000F2A7D8 /* AccountPickerSelectionListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountPickerSelectionListView.swift; sourceTree = "<group>"; };
6A2318D628B571E000F2A7D8 /* ManualEntryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualEntryViewController.swift; sourceTree = "<group>"; };
6A2318D828B57E5100F2A7D8 /* ManualEntryTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualEntryTextField.swift; sourceTree = "<group>"; };
6A4FA13828E11B3D00F07D42 /* CloseConfirmationAlertHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseConfirmationAlertHandler.swift; sourceTree = "<group>"; };
6A542DB02887259600958ED1 /* FeaturedInstitutionGridCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedInstitutionGridCell.swift; sourceTree = "<group>"; };
6A542DB228889AC400958ED1 /* InstitutionSearchTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstitutionSearchTableView.swift; sourceTree = "<group>"; };
6A542DB42889A1CB00958ED1 /* InstitutionSearchTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstitutionSearchTableViewCell.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -699,6 +701,7 @@
6A8B4B0428CFD31800128356 /* PaneWithHeaderLayoutView.swift */,
6A7C861628D273940025B8DF /* SuccessIconView.swift */,
6AE2E5AC28DB916E00623523 /* MerchantDataAccessView.swift */,
6A4FA13828E11B3D00F07D42 /* CloseConfirmationAlertHandler.swift */,
);
path = Shared;
sourceTree = "<group>";
Expand Down Expand Up @@ -904,6 +907,7 @@
6A2318D928B57E5100F2A7D8 /* ManualEntryTextField.swift in Sources */,
6A59FCA628B7BD7100F0F33E /* ManualEntryFooterView.swift in Sources */,
3CAD99BB284E381100B163EB /* FinancialConnectionsInstitution.swift in Sources */,
6A4FA13928E11B3D00F07D42 /* CloseConfirmationAlertHandler.swift in Sources */,
6AE2E5B328DCFBEF00623523 /* String+Localized.swift in Sources */,
6AE2E5AB28DB366000623523 /* InstitutionSearchErrorView.swift in Sources */,
6A78179B28BCF2960017CB1E /* ManualEntrySuccessTransactionTableView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import UIKit

extension UIColor {

// TODO(kgaidis): the background color we use
// TODO: the background color we use
// across many screens. added for future support around dark mode
static var customBackgroundColor: UIColor {
return .white
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import UIKit
protocol AccountPickerViewControllerDelegate: AnyObject {
func accountPickerViewController(
_ viewController: AccountPickerViewController,
didLinkAccounts linkedAccounts: [FinancialConnectionsPartnerAccount],
didSelectAccounts selectedAccounts: [FinancialConnectionsPartnerAccount],
skipToSuccess: Bool
)
func accountPickerViewControllerDidSelectAnotherBank(_ viewController: AccountPickerViewController)
Expand Down Expand Up @@ -77,7 +77,7 @@ final class AccountPickerViewController: UIViewController {
init(dataSource: AccountPickerDataSource) {
self.dataSource = dataSource
self.accountPickerType = {
if dataSource.authorizationSession.skipAccountSelection == true && dataSource.manifest.singleAccount && dataSource.authorizationSession.flow?.isOAuth() == true {
if dataSource.authorizationSession.institutionSkipAccountSelection == true && dataSource.manifest.singleAccount && dataSource.authorizationSession.flow?.isOAuth() == true {
return .dropdown
} else {
return dataSource.manifest.singleAccount ? .radioButton : .checkbox
Expand All @@ -93,6 +93,8 @@ final class AccountPickerViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
// account picker ALWAYS hides the back button
navigationItem.hidesBackButton = true
view.backgroundColor = .customBackgroundColor
pollAuthSessionAccounts()
}
Expand All @@ -118,7 +120,7 @@ final class AccountPickerViewController: UIViewController {
// ...handle it here since API did not throw error
self.showAccountLoadErrorView() // "API returned an empty list of accounts"
} else if self.dataSource.authorizationSession.skipAccountSelection ?? false {
self.delegate?.accountPickerViewController(self, didLinkAccounts: accounts, skipToSuccess: true)
self.delegate?.accountPickerViewController(self, didSelectAccounts: accounts, skipToSuccess: true)
} else if
self.dataSource.manifest.singleAccount,
self.dataSource.authorizationSession.institutionSkipAccountSelection ?? false,
Expand Down Expand Up @@ -216,11 +218,15 @@ final class AccountPickerViewController: UIViewController {
}
}(),
subtitle: {
if dataSource.manifest.singleAccount {
if let businessName = businessName {
return String(format: STPLocalizedString("%@ only needs one account at this time.", "A subtitle/description of a screen that allows users to select which bank accounts they want to use to pay for something. This text tries to portray that they only need to select one bank account. %@ will be filled with the business name, ex. Coca-Cola Company."), businessName)
if accountPickerType == .dropdown {
if dataSource.manifest.isStripeDirect == true {
return STPLocalizedString("Stripe only needs one account at this time.", "A subtitle/description of a screen that allows users to select which bank accounts they want to use to pay for something. This text tries to portray that they only need to select one bank account.")
} else {
return STPLocalizedString("This merchant only needs one account at this time.", "A subtitle/description of a screen that allows users to select which bank accounts they want to use to pay for something. This text tries to portray that they only need to select one bank account.")
if let businessName = businessName {
return String(format: STPLocalizedString("%@ only needs one account at this time.", "A subtitle/description of a screen that allows users to select which bank accounts they want to use to pay for something. This text tries to portray that they only need to select one bank account. %@ will be filled with the business name, ex. Coca-Cola Company."), businessName)
} else {
return STPLocalizedString("This merchant only needs one account at this time.", "A subtitle/description of a screen that allows users to select which bank accounts they want to use to pay for something. This text tries to portray that they only need to select one bank account.")
}
}
} else {
return nil // no subtitle
Expand All @@ -239,8 +245,6 @@ final class AccountPickerViewController: UIViewController {
paneLayoutView.scrollView.addGestureRecognizer(tapOutsideOfDropdownGestureRecognizer)
}

// TODO(kgaidis): does this account for disabled accounts?
// select an initial set of accounts for the user by default
switch accountPickerType {
case .checkbox:
// select all accounts
Expand Down Expand Up @@ -282,7 +286,6 @@ final class AccountPickerViewController: UIViewController {
self.errorView?.removeFromSuperview()
}
self.errorView = errorView
navigationItem.hidesBackButton = (errorView != nil)
}

private func didSelectLinkAccounts() {
Expand Down Expand Up @@ -313,20 +316,18 @@ final class AccountPickerViewController: UIViewController {
}()
)
view.addAndPinSubviewToSafeArea(linkingAccountsLoadingView)
navigationItem.hidesBackButton = true

dataSource
.selectAuthSessionAccounts()
.observe(on: .main) { [weak self] result in
guard let self = self else { return }
self.navigationItem.hidesBackButton = false // reset
linkingAccountsLoadingView.removeFromSuperview()

switch result {
case .success(let linkedAccounts):
self.delegate?.accountPickerViewController(
self,
didLinkAccounts: linkedAccounts.data,
didSelectAccounts: linkedAccounts.data,
skipToSuccess: false
)
case .failure(let error):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,51 +287,65 @@ private extension AuthFlowController {

@objc
func didTapClose() {
// TODO(kgaidis): `showConfirmationAlert` shouldn't always be `false`, it depends what Pane are dismissing from
closeAuthFlow(showConfirmationAlert: false, error: nil)
// TODO(kgaidis): implement `showConfirmationAlert` for more panes
let showConfirmationAlert = (dataManager.nextPane() == .accountPicker)
closeAuthFlow(showConfirmationAlert: showConfirmationAlert, error: nil)
}

// There's at least three types of close cases:
// 1. User closes when getting an error. In that case `error != nil`. That's an error.
// 2. User closes, there is no error, and fetching accounts returns accounts (or `paymentAccount`). That's a success.
// 3. User closes, there is no error, and fetching accounts returns NO accounts. That's a cancel.
@available(iOSApplicationExtension, unavailable)
private func closeAuthFlow(
showConfirmationAlert: Bool,
error closeAuthFlowError: Error? = nil // user can also close AuthFlow while looking at an error screen
) {
// TODO(kgaidis): implement `showConfirmationAlert`

dataManager
.completeFinancialConnectionsSession()
.observe(on: .main) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let session):
if let closeAuthFlowError = closeAuthFlowError {
self.finishAuthSession(result: .failed(error: closeAuthFlowError))
} else {
// TODO(kgaidis): Stripe.js does some more additional handling for Link.
// TODO(kgaidis): Stripe.js also seems to collect ALL accounts (because this API call returns only a part of the accounts [its paginated?])

if session.accounts.data.count > 0 || session.paymentAccount != nil || session.bankAccountToken != nil {
self.finishAuthSession(result: .completed(session: session))
let completeFinancialConnectionsSession = { [weak self] in
guard let self = self else { return }
self.dataManager
.completeFinancialConnectionsSession()
.observe(on: .main) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let session):
if let closeAuthFlowError = closeAuthFlowError {
self.finishAuthSession(result: .failed(error: closeAuthFlowError))
} else {
if let terminalError = self.dataManager.terminalError {
self.finishAuthSession(result: .failed(error: terminalError))
// TODO(kgaidis): Stripe.js does some more additional handling for Link.
// TODO(kgaidis): Stripe.js also seems to collect ALL accounts (because this API call returns only a part of the accounts [its paginated?])

if session.accounts.data.count > 0 || session.paymentAccount != nil || session.bankAccountToken != nil {
self.finishAuthSession(result: .completed(session: session))
} else {
self.finishAuthSession(result: .canceled)
if let terminalError = self.dataManager.terminalError {
self.finishAuthSession(result: .failed(error: terminalError))
} else {
self.finishAuthSession(result: .canceled)
}
// TODO(kgaidis): user can press "X" any time they have an error, should we route all errors up to `AuthFlowController` so we can return "failed" if user sees?
}
// TODO(kgaidis): user can press "X" any time they have an error, should we route all errors up to `AuthFlowController` so we can return "failed" if user sees?
}
}
case .failure(let completeFinancialConnectionsSessionError):
if let closeAuthFlowError = closeAuthFlowError {
self.finishAuthSession(result: .failed(error: closeAuthFlowError))
} else {
self.finishAuthSession(result: .failed(error: completeFinancialConnectionsSessionError))
case .failure(let completeFinancialConnectionsSessionError):
if let closeAuthFlowError = closeAuthFlowError {
self.finishAuthSession(result: .failed(error: closeAuthFlowError))
} else {
self.finishAuthSession(result: .failed(error: completeFinancialConnectionsSessionError))
}
}
}
}
}

if showConfirmationAlert {
CloseConfirmationAlertHandler.present(
businessName: dataManager.manifest.businessName,
didSelectOK: {
completeFinancialConnectionsSession()
}
)
} else {
completeFinancialConnectionsSession()
}
}

private func finishAuthSession(result: FinancialConnectionsSheet.Result) {
Expand Down Expand Up @@ -416,10 +430,10 @@ extension AuthFlowController: AccountPickerViewControllerDelegate {

func accountPickerViewController(
_ viewController: AccountPickerViewController,
didLinkAccounts linkedAccounts: [FinancialConnectionsPartnerAccount],
didSelectAccounts selectedAccounts: [FinancialConnectionsPartnerAccount],
skipToSuccess: Bool
) {
dataManager.didLinkAccounts(linkedAccounts, skipToSuccess: skipToSuccess)
dataManager.didSelectAccounts(selectedAccounts, skipToSuccess: skipToSuccess)
}

func accountPickerViewControllerDidSelectAnotherBank(_ viewController: AccountPickerViewController) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ protocol AuthFlowDataManager: AnyObject {
func startManualEntry()
func picked(institution: FinancialConnectionsInstitution)
func didCompletePartnerAuth(authSession: FinancialConnectionsAuthorizationSession)
func didLinkAccounts(_ linkedAccounts: [FinancialConnectionsPartnerAccount], skipToSuccess: Bool)
func didSelectAccounts(_ linkedAccounts: [FinancialConnectionsPartnerAccount], skipToSuccess: Bool)
func didCompleteManualEntry(
withPaymentAccountResource paymentAccountResource: FinancialConnectionsPaymentAccountResource,
accountNumberLast4: String
Expand Down Expand Up @@ -130,7 +130,7 @@ class AuthFlowAPIDataManager: AuthFlowDataManager {
print("^ didCompletePartnerAuth called \(Date())") // TODO(kgaidis): this is temporarily here to debug an issue where account picker appears twice?
}

func didLinkAccounts(_ linkedAccounts: [FinancialConnectionsPartnerAccount], skipToSuccess: Bool) {
func didSelectAccounts(_ linkedAccounts: [FinancialConnectionsPartnerAccount], skipToSuccess: Bool) {
self.linkedAccounts = linkedAccounts

if skipToSuccess {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ final class ManualEntryFooterView: UIView {
return continueButtonConfiguration
}()
)
continueButton.title = "Continue" // TODO(kgaidis): replace with String.Localized.continue when we localize
continueButton.title = "Continue" // TODO: replace with String.Localized.continue when we localize
continueButton.addTarget(self, action: #selector(didSelectContinueButton), for: .touchUpInside)
continueButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ final class ManualEntryFormView: UIView {
private lazy var accountNumberTextField: ManualEntryTextField = {
let accountNumberTextField = ManualEntryTextField(
// STPLocalizedString_("Account number", "The title of a user-input-field that appears when a user is manually entering their bank account information. It instructs user to type the account number."),
title: "Account number", // TODO(kgaidis): replace with String.Localized.accountNumber (or fix SDK localized strings)
title: "Account number", // TODO: replace with String.Localized.accountNumber (or fix SDK localized strings)
placeholder: "000123456789",
footerText: STPLocalizedString("Your account can be checkings or savings.", "A description under a user-input-field that appears when a user is manually entering their bank account information. It the user that the bank account number can be either checkings or savings.")
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private func CreateFooterView(_ buttonTarget: ManualEntrySuccessViewController)
return doneButtonConfiguration
}()
)
doneButton.title = "Done" // TODO(kgaidis): replace with UIButton.doneButtonTitle once the SDK is localized
doneButton.title = "Done" // TODO: replace with UIButton.doneButtonTitle once the SDK is localized
doneButton.addTarget(
buttonTarget,
action: #selector(ManualEntrySuccessViewController.didSelectDone),
Expand Down
Loading

0 comments on commit 39cd6d9

Please sign in to comment.