From a14055383758eb0f8c0b1c5d821eecdc94a42edd Mon Sep 17 00:00:00 2001 From: Mel Ludowise Date: Fri, 27 Sep 2024 23:06:59 -0700 Subject: [PATCH] Onboarding init props --- .../StripeConnect.xcodeproj/project.pbxproj | 8 +++++ .../AccountOnboardingViewController.swift | 28 +++++++++++---- .../Source/Components/ComponentType.swift | 27 ++++++++++++-- .../Source/EmbeddedComponentManager.swift | 17 +++++---- .../Source/Helpers/ConnectJSURLParams.swift | 4 +-- .../Webview/ConnectComponentWebView.swift | 9 +++-- ...etchInitComponentPropsMessageHandler.swift | 18 ++++++++++ ...allSetterWithSerializableValueSender.swift | 6 ++++ .../Models/AccountCollectionOptions.swift | 2 +- .../Helpers/ConnectJSURLParamsTests.swift | 6 ++-- .../ConnectComponentWebViewTests.swift | 14 ++++++++ ...nitComponentPropsMessageHandlerTests.swift | 35 +++++++++++++++++++ 12 files changed, 150 insertions(+), 24 deletions(-) create mode 100644 StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/FetchInitComponentPropsMessageHandler.swift create mode 100644 StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/FetchInitComponentPropsMessageHandlerTests.swift diff --git a/StripeConnect/StripeConnect.xcodeproj/project.pbxproj b/StripeConnect/StripeConnect.xcodeproj/project.pbxproj index 092a2f0a28d..534282da85f 100644 --- a/StripeConnect/StripeConnect.xcodeproj/project.pbxproj +++ b/StripeConnect/StripeConnect.xcodeproj/project.pbxproj @@ -87,7 +87,9 @@ 41D17A6C2C5A7429007C6EE6 /* StripeiOS-Release.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 41D17A612C5A7429007C6EE6 /* StripeiOS-Release.xcconfig */; }; 41D17A6D2C5A7429007C6EE6 /* StripeiOS-Shared.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 41D17A622C5A7429007C6EE6 /* StripeiOS-Shared.xcconfig */; }; 41D17A6E2C5A7429007C6EE6 /* Version.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 41D17A632C5A7429007C6EE6 /* Version.xcconfig */; }; + 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 */; }; 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 */; }; @@ -187,6 +189,8 @@ 41D17A622C5A7429007C6EE6 /* StripeiOS-Shared.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "StripeiOS-Shared.xcconfig"; sourceTree = ""; }; 41D17A632C5A7429007C6EE6 /* Version.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = ""; }; E65691212CA52D5900E0DB00 /* StripeConnect+Exports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StripeConnect+Exports.swift"; sourceTree = ""; }; + E6165CBE2CA7BF2200B76DA5 /* FetchInitComponentPropsMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchInitComponentPropsMessageHandler.swift; sourceTree = ""; }; + E6165CC02CA7D09900B76DA5 /* FetchInitComponentPropsMessageHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchInitComponentPropsMessageHandlerTests.swift; sourceTree = ""; }; E6F485F72C9E35A5000D914F /* PaymentDetailsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentDetailsViewController.swift; sourceTree = ""; }; E6F485FB2C9E360A000D914F /* ConnectJSURLParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectJSURLParams.swift; sourceTree = ""; }; E6F485FD2C9E36B2000D914F /* PaymentDetailsViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentDetailsViewControllerTests.swift; sourceTree = ""; }; @@ -233,6 +237,7 @@ 413987B82C63F34B001D375E /* DebugMessageHandler.swift */, 413987B92C63F34B001D375E /* FetchClientSecretMessageHandler.swift */, 413987DC2C640A29001D375E /* FetchInitParamsMessageHandler.swift */, + E6165CBE2CA7BF2200B76DA5 /* FetchInitComponentPropsMessageHandler.swift */, 4186664B2C66AC8C003DB62E /* OnLoaderStartMessageHandler.swift */, 4186664D2C66ACB3003DB62E /* OnLoadErrorMessageHandler.swift */, 413987E02C641688001D375E /* PageDidLoadMessageHandler.swift */, @@ -373,6 +378,7 @@ 41814EEA2C6BCAB30014EB5E /* DebugMessageHandlerTests.swift */, 41814EEC2C6BED8C0014EB5E /* FetchClientSecretMessageHandlerTests.swift */, 41814EEE2C6BEF2C0014EB5E /* FetchInitParamsMessageHandlerTests.swift */, + E6165CC02CA7D09900B76DA5 /* FetchInitComponentPropsMessageHandlerTests.swift */, 41814EF22C6BFA4B0014EB5E /* OnLoaderStartMessageHandlerTests.swift */, 410D0FC92C6CFE27009B0E26 /* OnLoadErrorMessageHandlerTests.swift */, 410D0FCF2C6D0319009B0E26 /* PageDidLoadMessageHandlerTests.swift */, @@ -635,6 +641,7 @@ 41542A692C88B6F2004E728E /* JSONEncoder+extension.swift in Sources */, 416E9E842C76AE0A00A0B917 /* ComponentType.swift in Sources */, 410D0FCE2C6D000B009B0E26 /* OpenAuthenticatedWebViewMessageHandler.swift in Sources */, + E6165CBF2CA7BF2200B76DA5 /* FetchInitComponentPropsMessageHandler.swift in Sources */, 413D18482C7FBAA70051AA42 /* CSSHelpers.swift in Sources */, 413987CD2C63F34B001D375E /* MessageSender.swift in Sources */, 41BCCFF52C8B5A0A00797E01 /* CustomFontSourceWrapper.swift in Sources */, @@ -693,6 +700,7 @@ 410D0FE72C6D3BBC009B0E26 /* ConnectWebViewTests.swift in Sources */, 410D0FD02C6D0319009B0E26 /* PageDidLoadMessageHandlerTests.swift in Sources */, 41814EED2C6BED8C0014EB5E /* FetchClientSecretMessageHandlerTests.swift in Sources */, + E6165CC12CA7D09900B76DA5 /* FetchInitComponentPropsMessageHandlerTests.swift in Sources */, 41814EE82C6BC8800014EB5E /* ScriptWebTestBase.swift in Sources */, 41BCCFF02C8B3C8900797E01 /* AppearanceWrapper+Default.swift in Sources */, 416E9E822C76994C00A0B917 /* WebView+Tests.swift in Sources */, diff --git a/StripeConnect/StripeConnect/Source/Components/AccountOnboardingViewController.swift b/StripeConnect/StripeConnect/Source/Components/AccountOnboardingViewController.swift index 5c12b9a7c87..7f56ccba94a 100644 --- a/StripeConnect/StripeConnect/Source/Components/AccountOnboardingViewController.swift +++ b/StripeConnect/StripeConnect/Source/Components/AccountOnboardingViewController.swift @@ -12,23 +12,37 @@ import UIKit @_spi(PrivateBetaConnect) public class AccountOnboardingViewController: UIViewController { - /// Delegate that recieves callbacks for this component + struct Props: Encodable { + let fullTermsOfServiceUrl: URL? + let recipientTermsOfServiceUrl: URL? + let privacyPolicyUrl: URL? + let skipTermsOfServiceCollection: Bool? + let collectionOptions: AccountCollectionOptions + + // Used in FetchInitComponentPropsMessageHandler + // Each property key should match its JS setter name + enum CodingKeys: String, CodingKey { + case fullTermsOfServiceUrl = "setFullTermsOfServiceUrl" + case recipientTermsOfServiceUrl = "setRecipientTermsOfServiceUrl" + case privacyPolicyUrl = "setPrivacyPolicyUrl" + case skipTermsOfServiceCollection = "setSkipTermsOfServiceCollection" + case collectionOptions = "setCollectionOptions" + } + } + + /// Delegate that receives callbacks for this component public weak var delegate: AccountOnboardingViewControllerDelegate? let webView: ConnectComponentWebView - init(fullTermsOfServiceUrl: URL? = nil, - recipientTermsOfServiceUrl: URL? = nil, - privacyPolicyUrl: URL? = nil, - skipTermsOfServiceCollection: Bool? = nil, - collectionOptions: AccountCollectionOptions = .init(), + init(props: Props, componentManager: EmbeddedComponentManager, // Test Only loadContent: Bool = true ) { webView = ConnectComponentWebView( componentManager: componentManager, - componentType: .onboarding, + componentType: .onboarding(props), loadContent: loadContent ) super.init(nibName: nil, bundle: nil) diff --git a/StripeConnect/StripeConnect/Source/Components/ComponentType.swift b/StripeConnect/StripeConnect/Source/Components/ComponentType.swift index b5206490568..81ae9d96178 100644 --- a/StripeConnect/StripeConnect/Source/Components/ComponentType.swift +++ b/StripeConnect/StripeConnect/Source/Components/ComponentType.swift @@ -8,11 +8,32 @@ import Foundation /// The name of the embedded component tag in JS ([docs](https://docs.stripe.com/connect/supported-embedded-components)) -enum ComponentType: String, Encodable { +enum ComponentType: Encodable { /// Displays the balance summary, the payout schedule, and a list of payouts for the connected account case payouts /// The onboarding flow for the account. - case onboarding = "account-onboarding" + case onboarding(AccountOnboardingViewController.Props) /// Show details of a given payment and allow users to manage disputes and perform refunds. - case paymentDetails = "payment-details" + case paymentDetails + + var name: String { + switch self { + case .payouts: + return "payouts" + case .onboarding: + return "account-onboarding" + case .paymentDetails: + return "payment-details" + } + } + + func encode(to encoder: any Encoder) throws { + switch self { + case .onboarding(let initParams): + try initParams.encode(to: encoder) + case .payouts, + .paymentDetails: + try VoidPayload().encode(to: encoder) + } + } } diff --git a/StripeConnect/StripeConnect/Source/EmbeddedComponentManager.swift b/StripeConnect/StripeConnect/Source/EmbeddedComponentManager.swift index 3ccf01e7ac4..a9c3a7a1e67 100644 --- a/StripeConnect/StripeConnect/Source/EmbeddedComponentManager.swift +++ b/StripeConnect/StripeConnect/Source/EmbeddedComponentManager.swift @@ -84,13 +84,16 @@ public class EmbeddedComponentManager { skipTermsOfServiceCollection: Bool? = nil, collectionOptions: AccountCollectionOptions = .init() ) -> AccountOnboardingViewController { - return .init(fullTermsOfServiceUrl: fullTermsOfServiceUrl, - recipientTermsOfServiceUrl: recipientTermsOfServiceUrl, - privacyPolicyUrl: privacyPolicyUrl, - skipTermsOfServiceCollection: skipTermsOfServiceCollection, - collectionOptions: collectionOptions, - componentManager: self, - loadContent: shouldLoadContent) + return .init( + props: .init( + fullTermsOfServiceUrl: fullTermsOfServiceUrl, + recipientTermsOfServiceUrl: recipientTermsOfServiceUrl, + privacyPolicyUrl: privacyPolicyUrl, + skipTermsOfServiceCollection: skipTermsOfServiceCollection, + collectionOptions: collectionOptions + ), + componentManager: self, + loadContent: shouldLoadContent) } @_spi(DashboardOnly) diff --git a/StripeConnect/StripeConnect/Source/Helpers/ConnectJSURLParams.swift b/StripeConnect/StripeConnect/Source/Helpers/ConnectJSURLParams.swift index 20e5777e31e..1bd92c5fe0b 100644 --- a/StripeConnect/StripeConnect/Source/Helpers/ConnectJSURLParams.swift +++ b/StripeConnect/StripeConnect/Source/Helpers/ConnectJSURLParams.swift @@ -11,7 +11,7 @@ import Foundation /// Structured parameters for URL params accepted by the iOS ConnectJS wrapper struct ConnectJSURLParams: Encodable { /// The component type - let component: ComponentType + let component: String /// The platform publishable key. Required for non-dashboard accounts private(set) var publicKey: String? @@ -37,7 +37,7 @@ struct ConnectJSURLParams: Encodable { extension ConnectJSURLParams { init(component: ComponentType, apiClient: STPAPIClient) { - self.component = component + self.component = component.name // Validate that publishable key has been set STPAPIClient.validateKey(apiClient.publishableKey) diff --git a/StripeConnect/StripeConnect/Source/Internal/Webview/ConnectComponentWebView.swift b/StripeConnect/StripeConnect/Source/Internal/Webview/ConnectComponentWebView.swift index e0978d676c1..2ffc7f036b6 100644 --- a/StripeConnect/StripeConnect/Source/Internal/Webview/ConnectComponentWebView.swift +++ b/StripeConnect/StripeConnect/Source/Internal/Webview/ConnectComponentWebView.swift @@ -147,13 +147,18 @@ private extension ConnectComponentWebView { appearance: .init(appearance: componentManager.appearance, traitCollection: self.traitCollection), fonts: componentManager.fonts.map({ .init(customFontSource: $0) })) })) + addMessageHandler(FetchInitComponentPropsMessageHandler(componentType: componentType)) addMessageHandler(DebugMessageHandler()) addMessageHandler(FetchClientSecretMessageHandler { [weak self] _ in await self?.componentManager.fetchClientSecret() }) - addMessageHandler(PageDidLoadMessageHandler { _ in }) - addMessageHandler(AccountSessionClaimedMessageHandler{ _ in + addMessageHandler(PageDidLoadMessageHandler { payload in // TODO: MXMOBILE-2491 Use this for analytics + debugPrint("pageDidLoad: \(payload.pageViewId)") + }) + addMessageHandler(AccountSessionClaimedMessageHandler { payload in + // TODO: MXMOBILE-2491 Use this for analytics + debugPrint("accountSessionClaimed: \(payload.merchantId)") }) } diff --git a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/FetchInitComponentPropsMessageHandler.swift b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/FetchInitComponentPropsMessageHandler.swift new file mode 100644 index 00000000000..6c657bd9aa5 --- /dev/null +++ b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/FetchInitComponentPropsMessageHandler.swift @@ -0,0 +1,18 @@ +// +// FetchInitComponentPropsMessageHandler.swift +// StripeConnect +// +// Created by Mel Ludowise on 9/27/24. +// + +import Foundation + +// Fetches initial property values specific to this component. +@available(iOS 15, *) +class FetchInitComponentPropsMessageHandler: ScriptMessageHandlerWithReply { + init(componentType: ComponentType) { + super.init(name: "fetchInitComponentProps") { _ in + componentType + } + } +} diff --git a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageSenders/CallSetterWithSerializableValueSender.swift b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageSenders/CallSetterWithSerializableValueSender.swift index a87b50a86bf..87d0e8974c5 100644 --- a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageSenders/CallSetterWithSerializableValueSender.swift +++ b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageSenders/CallSetterWithSerializableValueSender.swift @@ -17,3 +17,9 @@ struct CallSetterWithSerializableValueSender: Messag let name: String = "callSetterWithSerializableValue" let payload: Payload } + +extension CallSetterWithSerializableValueSender { + init(_ setter: String, with value: Value) { + self.init(payload: .init(setter: setter, value: value)) + } +} diff --git a/StripeConnect/StripeConnect/Source/Models/AccountCollectionOptions.swift b/StripeConnect/StripeConnect/Source/Models/AccountCollectionOptions.swift index 8e4c06eca3e..153ed4ac6b0 100644 --- a/StripeConnect/StripeConnect/Source/Models/AccountCollectionOptions.swift +++ b/StripeConnect/StripeConnect/Source/Models/AccountCollectionOptions.swift @@ -9,7 +9,7 @@ import Foundation @_spi(PrivateBetaConnect) /// Collection options for account onboarding -public struct AccountCollectionOptions: Codable { +public struct AccountCollectionOptions: Codable, Equatable { public enum FieldOption: String, Codable { case currentlyDue = "currently_due" diff --git a/StripeConnect/StripeConnectTests/Helpers/ConnectJSURLParamsTests.swift b/StripeConnect/StripeConnectTests/Helpers/ConnectJSURLParamsTests.swift index 1d50cb45d38..073f0e6610e 100644 --- a/StripeConnect/StripeConnectTests/Helpers/ConnectJSURLParamsTests.swift +++ b/StripeConnect/StripeConnectTests/Helpers/ConnectJSURLParamsTests.swift @@ -21,7 +21,8 @@ class ConnectJSURLParamsTests: XCTestCase { func testInitFromApiClient_publicKey() { apiClient.publishableKey = "pk_1234" - let urlParams = ConnectJSURLParams(component: .onboarding, apiClient: apiClient) + let urlParams = ConnectJSURLParams(component: .payouts, apiClient: apiClient) + XCTAssertEqual(urlParams.component, "payouts") XCTAssertEqual(urlParams.publicKey, "pk_1234") XCTAssertNil(urlParams.apiKeyOverride) XCTAssertNil(urlParams.merchantIdOverride) @@ -32,7 +33,8 @@ class ConnectJSURLParamsTests: XCTestCase { func testInitFromApiClient_userKeyIncludesOverrideParams() { apiClient.publishableKey = "uk_1234" - let urlParams = ConnectJSURLParams(component: .onboarding, apiClient: apiClient) + let urlParams = ConnectJSURLParams(component: .payouts, apiClient: apiClient) + XCTAssertEqual(urlParams.component, "payouts") XCTAssertNil(urlParams.publicKey) XCTAssertEqual(urlParams.apiKeyOverride, "uk_1234") XCTAssertEqual(urlParams.merchantIdOverride, "acct_123") diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/ConnectComponentWebViewTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/ConnectComponentWebViewTests.swift index eb55cd85065..4adbc60960a 100644 --- a/StripeConnect/StripeConnectTests/Internal/Webview/ConnectComponentWebViewTests.swift +++ b/StripeConnect/StripeConnectTests/Internal/Webview/ConnectComponentWebViewTests.swift @@ -81,6 +81,20 @@ class ConnectComponentWebViewTests: XCTestCase { """) } + @MainActor + func testFetchInitComponentPropsMessageHandler() async throws { + let componentManager = componentManagerAssertingOnFetch() + + let webView = ConnectComponentWebView(componentManager: componentManager, + componentType: .payouts, + webLocale: Locale(identifier: "fr_FR"), + loadContent: false) + + try await webView.evaluateMessageWithReply(name: "fetchInitComponentProps", + json: "{}", + expectedResponse: "{}") + } + @MainActor func testUpdateTraitCollection() async throws { var appearance = EmbeddedComponentManager.Appearance() diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/FetchInitComponentPropsMessageHandlerTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/FetchInitComponentPropsMessageHandlerTests.swift new file mode 100644 index 00000000000..733ab5d0b51 --- /dev/null +++ b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/FetchInitComponentPropsMessageHandlerTests.swift @@ -0,0 +1,35 @@ +// +// FetchInitComponentPropsMessageHandlerTests.swift +// StripeConnectTests +// +// Created by Mel Ludowise on 9/27/24. +// + +@_spi(PrivateBetaConnect) @testable import StripeConnect +import XCTest + +class FetchInitComponentPropsMessageHandlerTests: ScriptWebTestBase { + + @MainActor + func testMessageSend() async throws { + let componentType = ComponentType.onboarding(.init( + fullTermsOfServiceUrl: URL(string: "https://fullTermsOfServiceUrl.com")!, + recipientTermsOfServiceUrl: URL(string: "https://recipientTermsOfServiceUrl.com")!, + privacyPolicyUrl: URL(string: "https://privacyPolicyUrl.com")!, + skipTermsOfServiceCollection: true, + collectionOptions: { + var collectionOptions = AccountCollectionOptions() + collectionOptions.fields = .eventuallyDue + collectionOptions.futureRequirements = .include + return collectionOptions + }() + )) + webView.addMessageReplyHandler(messageHandler: FetchInitComponentPropsMessageHandler(componentType: componentType)) + + try await webView.evaluateMessageWithReply(name: "fetchInitComponentProps", + json: "{}", + expectedResponse: """ + {"setCollectionOptions":{"fields":"eventually_due","futureRequirements":"include"},"setFullTermsOfServiceUrl":"https:\\/\\/fullTermsOfServiceUrl.com","setPrivacyPolicyUrl":"https:\\/\\/privacyPolicyUrl.com","setRecipientTermsOfServiceUrl":"https:\\/\\/recipientTermsOfServiceUrl.com","setSkipTermsOfServiceCollection":true} + """) + } +}