Skip to content

Commit

Permalink
[Connect] Fix API inconsistent naming + Add Dashboard-only support fo…
Browse files Browse the repository at this point in the history
…r Account Management component (#4036)

## Summary

- **Adds the account management component with DashboardOnly access**

- **Fixes inconsistent delegate naming in PayoutsViewController**
  The account onboarding component had a delegate signature of:
  ```
accountOnboarding(_ accountOnboarding: AccountOnboardingViewController,
                             didFailLoadWithError error: Error)
  ```
  However, the payouts component was using:
  ```
  payoutsLoadDidFail(_ payouts: PayoutsViewController,
                     withError error: Error)
  ```

During API review, we had intended to use the account onboarding style
signature (see
[papertrail](https://docs.google.com/document/d/195BaU6k2J-2CAs9anNE6e4OlZO34N-e2HTYc6pacpTw/edit?pli=1#bookmark=id.dk457iw2wtca))

## Motivation

https://jira.corp.stripe.com/browse/MXMOBILE-2503

## Testing

Unit tests

---------

Co-authored-by: Chris Mays <[email protected]>
  • Loading branch information
mludowise-stripe and nschris-stripe authored Oct 2, 2024
1 parent 14daabf commit 9e06a39
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 16 deletions.
8 changes: 8 additions & 0 deletions StripeConnect/StripeConnect.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@
E6165CBF2CA7BF2200B76DA5 /* FetchInitComponentPropsMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6165CBE2CA7BF2200B76DA5 /* FetchInitComponentPropsMessageHandler.swift */; };
E65691222CA52D5900E0DB00 /* StripeConnect+Exports.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65691212CA52D5900E0DB00 /* StripeConnect+Exports.swift */; };
E6165CC12CA7D09900B76DA5 /* FetchInitComponentPropsMessageHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6165CC02CA7D09900B76DA5 /* FetchInitComponentPropsMessageHandlerTests.swift */; };
E65691202CA5248300E0DB00 /* AccountManagementViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E656911F2CA5248300E0DB00 /* AccountManagementViewControllerTests.swift */; };
E6C5F5F62C9FEE0200861709 /* AccountManagementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6C5F5F52C9FEE0200861709 /* AccountManagementViewController.swift */; };
E6F485F82C9E35A5000D914F /* PaymentDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F485F72C9E35A5000D914F /* PaymentDetailsViewController.swift */; };
E6F485FC2C9E360A000D914F /* ConnectJSURLParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F485FB2C9E360A000D914F /* ConnectJSURLParams.swift */; };
E6F485FE2C9E36B2000D914F /* PaymentDetailsViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6F485FD2C9E36B2000D914F /* PaymentDetailsViewControllerTests.swift */; };
Expand Down Expand Up @@ -190,6 +192,8 @@
41D17A632C5A7429007C6EE6 /* Version.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = "<group>"; };
E65691212CA52D5900E0DB00 /* StripeConnect+Exports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StripeConnect+Exports.swift"; sourceTree = "<group>"; };
E6165CBE2CA7BF2200B76DA5 /* FetchInitComponentPropsMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchInitComponentPropsMessageHandler.swift; sourceTree = "<group>"; };
E656911F2CA5248300E0DB00 /* AccountManagementViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountManagementViewControllerTests.swift; sourceTree = "<group>"; };
E6C5F5F52C9FEE0200861709 /* AccountManagementViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountManagementViewController.swift; sourceTree = "<group>"; };
E6165CC02CA7D09900B76DA5 /* FetchInitComponentPropsMessageHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchInitComponentPropsMessageHandlerTests.swift; sourceTree = "<group>"; };
E6F485F72C9E35A5000D914F /* PaymentDetailsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentDetailsViewController.swift; sourceTree = "<group>"; };
E6F485FB2C9E360A000D914F /* ConnectJSURLParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectJSURLParams.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -313,6 +317,7 @@
416E9E792C762C6E00A0B917 /* Components */ = {
isa = PBXGroup;
children = (
E6C5F5F52C9FEE0200861709 /* AccountManagementViewController.swift */,
4171B1582C9A5EEC00547F7D /* AccountOnboardingViewController.swift */,
416E9E832C76AE0900A0B917 /* ComponentType.swift */,
E6F485F72C9E35A5000D914F /* PaymentDetailsViewController.swift */,
Expand All @@ -324,6 +329,7 @@
416E9E882C76B36F00A0B917 /* Components */ = {
isa = PBXGroup;
children = (
E656911F2CA5248300E0DB00 /* AccountManagementViewControllerTests.swift */,
4161C2722C9D0A8A005BD67C /* AccountOnboardingViewControllerTests.swift */,
E6F485FD2C9E36B2000D914F /* PaymentDetailsViewControllerTests.swift */,
416E9E872C76B36F00A0B917 /* PayoutsViewControllerTests.swift */,
Expand Down Expand Up @@ -664,6 +670,7 @@
413987CA2C63F34B001D375E /* ScriptMessageHandler.swift in Sources */,
416E9E862C76B35E00A0B917 /* PayoutsViewController.swift in Sources */,
41542A6B2C88B79E004E728E /* JSONSerialization+extension.swift in Sources */,
E6C5F5F62C9FEE0200861709 /* AccountManagementViewController.swift in Sources */,
413987C82C63F34B001D375E /* DebugMessageHandler.swift in Sources */,
410D0FCC2C6CFFDB009B0E26 /* AccountSessionClaimedMessageHandler.swift in Sources */,
416E9ED22C77F6E000A0B917 /* Locale+extension.swift in Sources */,
Expand Down Expand Up @@ -694,6 +701,7 @@
E6F485FE2C9E36B2000D914F /* PaymentDetailsViewControllerTests.swift in Sources */,
416E9E782C753B7900A0B917 /* ConnectComponentWebViewTests.swift in Sources */,
410D0FD42C6D051B009B0E26 /* OpenAuthenticatedWebViewMessageHandlerTests.swift in Sources */,
E65691202CA5248300E0DB00 /* AccountManagementViewControllerTests.swift in Sources */,
410D0FCA2C6CFE27009B0E26 /* OnLoadErrorMessageHandlerTests.swift in Sources */,
41BCCFF32C8B449800797E01 /* TestHelpers.swift in Sources */,
410D0FDB2C6D21B0009B0E26 /* CallSetterWithSerializableValueSenderTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// AccountManagementViewController.swift
// StripeConnect
//
// Created by Mel Ludowise on 9/21/24.
//

import UIKit

/**
Show details of a given payment and allow users to manage disputes and perform refunds.
*/
@_spi(DashboardOnly)
@available(iOS 15, *)
public class AccountManagementViewController: UIViewController {

struct Props: Encodable {
let collectionOptions: AccountCollectionOptions

enum CodingKeys: String, CodingKey {
case collectionOptions = "setCollectionOptions"
}
}

let webView: ConnectComponentWebView

public weak var delegate: AccountManagementViewControllerDelegate?

init(componentManager: EmbeddedComponentManager,
collectionOptions: AccountCollectionOptions) {
webView = ConnectComponentWebView(
componentManager: componentManager,
componentType: .accountManagement
) {
Props(collectionOptions: collectionOptions)
}
super.init(nibName: nil, bundle: nil)

webView.addMessageHandler(OnLoadErrorMessageHandler { [weak self] value in
guard let self else { return }
self.delegate?.accountManagement(self, didFailLoadWithError: value.error.connectEmbedError)
})

// TODO(MXMOBILE-2796): Send collection options to web view

webView.presentPopup = { [weak self] vc in
self?.present(vc, animated: true)
}
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

public override func loadView() {
view = webView
}
}

@_spi(DashboardOnly)
@available(iOS 15, *)
public protocol AccountManagementViewControllerDelegate: AnyObject {
/**
Triggered when an error occurs loading the account management component
- Parameters:
- accountManagement: The account management component that errored when loading
- error: The error that occurred when loading the component
*/
func accountManagement(_ accountManagement: AccountManagementViewController,
didFailLoadWithError error: Error)
}

@available(iOS 15, *)
public extension AccountManagementViewControllerDelegate {
// Default implementation to make optional
func accountManagement(_ accountManagement: AccountManagementViewController,
didFailLoadWithError error: Error) { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Foundation

/// The name of the embedded component tag in JS ([docs](https://docs.stripe.com/connect/supported-embedded-components))
enum ComponentType: String, Encodable {
case accountManagement = "account-management"
/// Displays the balance summary, the payout schedule, and a list of payouts for the connected account
case payouts
/// The onboarding flow for the account.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class PaymentDetailsViewController: UIViewController {
super.init(nibName: nil, bundle: nil)
webView.addMessageHandler(OnLoadErrorMessageHandler { [weak self] value in
guard let self else { return }
self.delegate?.paymentDetailsLoadDidFail(self, withError: value.error.connectEmbedError)
self.delegate?.paymentDetails(self, didFailLoadWithError: value.error.connectEmbedError)
})
webView.presentPopup = { [weak self] vc in
self?.present(vc, animated: true)
Expand Down Expand Up @@ -59,14 +59,14 @@ public protocol PaymentDetailsViewControllerDelegate: AnyObject {
- paymentDetails: The payment details component that errored when loading
- error: The error that occurred when loading the component
*/
func paymentDetailsLoadDidFail(_ paymentDetails: PaymentDetailsViewController,
withError error: Error)
func paymentDetails(_ paymentDetails: PaymentDetailsViewController,
didFailLoadWithError error: Error)

}

@available(iOS 15, *)
public extension PaymentDetailsViewControllerDelegate {
// Default implementation to make optional
func paymentDetailsLoadDidFail(_ paymentDetails: PaymentDetailsViewController,
withError error: Error) { }
func paymentDetails(_ paymentDetails: PaymentDetailsViewController,
didFailLoadWithError error: Error) { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class PayoutsViewController: UIViewController {
super.init(nibName: nil, bundle: nil)
webView.addMessageHandler(OnLoadErrorMessageHandler { [weak self] value in
guard let self else { return }
self.delegate?.payoutsLoadDidFail(self, withError: value.error.connectEmbedError)
self.delegate?.payouts(self, didFailLoadWithError: value.error.connectEmbedError)
})
webView.presentPopup = { [weak self] vc in
self?.present(vc, animated: true)
Expand All @@ -54,14 +54,14 @@ public protocol PayoutsViewControllerDelegate: AnyObject {
- payouts: The payouts component that errored when loading
- error: The error that occurred when loading the component
*/
func payoutsLoadDidFail(_ payouts: PayoutsViewController,
withError error: Error)
func payouts(_ payouts: PayoutsViewController,
didFailLoadWithError error: Error)

}

@available(iOS 15, *)
public extension PayoutsViewControllerDelegate {
// Default implementation to make optional
func payoutsLoadDidFail(_ payouts: PayoutsViewController,
withError error: Error) { }
func payouts(_ payouts: PayoutsViewController,
didFailLoadWithError error: Error) { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ public class EmbeddedComponentManager {
.init(componentManager: self)
}

@_spi(DashboardOnly)
public func createAccountManagementViewController(
collectionOptions: AccountCollectionOptions = .init()) -> AccountManagementViewController {
.init(componentManager: self,
collectionOptions: collectionOptions)
}

/// Used to keep reference of all web views associated with this component manager.
/// - Parameters:
/// - webView: The web view associated with this component manager
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// AccountManagementViewControllerTests.swift
// StripeConnectTests
//
// Created by Mel Ludowise on 9/25/24.
//

import SafariServices
@_spi(PrivateBetaConnect) @_spi(DashboardOnly) @testable import StripeConnect
@_spi(STP) import StripeCore
import WebKit
import XCTest

class AccountManagementViewControllerTests: XCTestCase {
let componentManager = EmbeddedComponentManager(fetchClientSecret: {
return nil
})

override func setUp() {
super.setUp()
STPAPIClient.shared.publishableKey = "pk_test"
componentManager.shouldLoadContent = false
}

@MainActor
func testDelegate() async throws {
let vc = componentManager.createAccountManagementViewController()

let expectationDidFail = XCTestExpectation(description: "didFail called")
let delegate = AccountManagementViewControllerDelegatePassThrough { onboardingVC, error in
XCTAssertEqual(vc, onboardingVC)
XCTAssertEqual((error as? EmbeddedComponentError)?.type, .rateLimitError)
XCTAssertEqual((error as? EmbeddedComponentError)?.description, "Error message")
expectationDidFail.fulfill()
}

vc.delegate = delegate
try await vc.webView.evaluateOnLoadError(type: "rate_limit_error", message: "Error message")
await fulfillment(of: [expectationDidFail], timeout: TestHelpers.defaultTimeout)
}

@MainActor
func testFetchInitComponentProps() async throws {
let vc = componentManager.createAccountManagementViewController(
collectionOptions: {
var collectionOptions = AccountCollectionOptions()
collectionOptions.fields = .eventuallyDue
collectionOptions.futureRequirements = .include
return collectionOptions
}()
)

try await vc.webView.evaluateMessageWithReply(name: "fetchInitComponentProps",
json: "{}",
expectedResponse: """
{"setCollectionOptions":{"fields":"eventually_due","futureRequirements":"include"}}
""")
}

}

private class AccountManagementViewControllerDelegatePassThrough: AccountManagementViewControllerDelegate {

var didFailLoad: ((_ accountManagement: AccountManagementViewController, _ error: Error) -> Void)?

init(didFailLoad: ((AccountManagementViewController, Error) -> Void)? = nil) {
self.didFailLoad = didFailLoad
}

func accountManagement(_ accountManagement: AccountManagementViewController,
didFailLoadWithError error: Error)
{
didFailLoad?(accountManagement, error)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ class PaymentDetailsViewControllerTests: XCTestCase {

private class PaymentDetailsViewControllerDelegatePassThrough: PaymentDetailsViewControllerDelegate {

var loadDidFail: ((_ paymentDetails: PaymentDetailsViewController, _ error: any Error) -> Void)?
var didFailLoad: ((_ paymentDetails: PaymentDetailsViewController, _ error: any Error) -> Void)?

init(loadDidFail: ((PaymentDetailsViewController, any Error) -> Void)? = nil) {
self.loadDidFail = loadDidFail
init(didFailLoad: ((PaymentDetailsViewController, any Error) -> Void)? = nil) {
self.didFailLoad = didFailLoad
}

func paymentDetailsLoadDidFail(_ paymentDetails: PaymentDetailsViewController, withError error: any Error) {
loadDidFail?(paymentDetails, error)
func paymentDetails(_ paymentDetails: PaymentDetailsViewController, didFailLoadWithError error: any Error) {
didFailLoad?(paymentDetails, error)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class PayoutsViewControllerTests: XCTestCase {

var payoutDidFail: ((_ payouts: PayoutsViewController, _ error: any Error) -> Void)?

func payoutsLoadDidFail(_ payouts: PayoutsViewController, withError error: any Error) {
func payouts(_ payouts: PayoutsViewController, didFailLoadWithError error: any Error) {
payoutDidFail?(payouts, error)
}
}
Expand Down

0 comments on commit 9e06a39

Please sign in to comment.