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

[Connect] Fix API inconsistent naming + Add Dashboard-only support for Account Management component #4036

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Comment on lines +57 to +58
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We had intended to make this consistent with the AccountOnboarding delegate style, but it slipped past API review. Paper trail of API approval:
https://docs.google.com/document/d/195BaU6k2J-2CAs9anNE6e4OlZO34N-e2HTYc6pacpTw/edit?pli=1#bookmark=id.dk457iw2wtca


}

@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 {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not sure yet whether we plan to set account collection options or whether this does anything for Dashboard, but putting it in for now so we can test it

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