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

[WIP] [Connect] Add logout() #4055

Closed
wants to merge 4 commits into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,30 @@ class MainViewController: UITableViewController {
super.viewDidLoad()
title = merchant.displayName ?? merchant.merchantId
navigationItem.titleView = navbarTitleButton
addChangeAppearanceButtonNavigationItem(to: self)
addGlobalButtonNavigationItems(to: self)
}

func addChangeAppearanceButtonNavigationItem(to viewController: UIViewController) {
// Add a button to change the appearance
let button = UIBarButtonItem(
image: UIImage(systemName: "paintpalette"),
style: .plain,
target: self,
action: #selector(selectAppearance)
)
button.accessibilityLabel = "Change appearance"
var buttonItems = viewController.navigationItem.rightBarButtonItems ?? []
buttonItems = [button] + buttonItems
viewController.navigationItem.rightBarButtonItems = buttonItems
}
func addGlobalButtonNavigationItems(to viewController: UIViewController) {
// Add a button to change the appearance
let appearanceButton = UIBarButtonItem(
image: UIImage(systemName: "paintpalette"),
primaryAction: UIAction(handler: { [weak self] _ in
guard let self else { return }
self.navigationController?.present(AppearanceSettings(componentManager: embeddedComponentManager).containerViewController, animated: true)
})
)
appearanceButton.accessibilityLabel = "Change appearance"

let logoutButton = UIBarButtonItem(
image: UIImage(systemName: "rectangle.portrait.and.arrow.right"),
primaryAction: UIAction(handler: { [weak embeddedComponentManager] _ in
embeddedComponentManager?.logout()
})
)
logoutButton.accessibilityLabel = "Log out"

viewController.navigationItem.rightBarButtonItems = [appearanceButton, logoutButton]
}

@objc
func selectAppearance() {
Expand All @@ -112,7 +120,7 @@ class MainViewController: UITableViewController {
}

viewControllerToPush.navigationItem.backButtonDisplayMode = .minimal
addChangeAppearanceButtonNavigationItem(to: viewControllerToPush)
addGlobalButtonNavigationItems(to: viewControllerToPush)
navigationController?.pushViewController(viewControllerToPush, animated: true)
}

Expand Down
22 changes: 17 additions & 5 deletions StripeConnect/StripeConnect.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +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 */; };
E65691002CA37FAC00E0DB00 /* LogoutSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65690FF2CA37FAC00E0DB00 /* LogoutSender.swift */; };
E65691022CA3B50B00E0DB00 /* LogoutSenderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65691012CA3B50B00E0DB00 /* LogoutSenderTests.swift */; };
E65691082CA3C3F000E0DB00 /* EmbeddedComponentManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65691072CA3C3F000E0DB00 /* EmbeddedComponentManagerTests.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 @@ -185,6 +188,9 @@
41D17A612C5A7429007C6EE6 /* StripeiOS-Release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "StripeiOS-Release.xcconfig"; sourceTree = "<group>"; };
41D17A622C5A7429007C6EE6 /* StripeiOS-Shared.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "StripeiOS-Shared.xcconfig"; sourceTree = "<group>"; };
41D17A632C5A7429007C6EE6 /* Version.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = "<group>"; };
E65690FF2CA37FAC00E0DB00 /* LogoutSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutSender.swift; sourceTree = "<group>"; };
E65691012CA3B50B00E0DB00 /* LogoutSenderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutSenderTests.swift; sourceTree = "<group>"; };
E65691072CA3C3F000E0DB00 /* EmbeddedComponentManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedComponentManagerTests.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>"; };
E6F485FD2C9E36B2000D914F /* PaymentDetailsViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentDetailsViewControllerTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -217,6 +223,7 @@
isa = PBXGroup;
children = (
410D0FDA2C6D21B0009B0E26 /* CallSetterWithSerializableValueSenderTests.swift */,
E65691012CA3B50B00E0DB00 /* LogoutSenderTests.swift */,
410D0FDC2C6D23AD009B0E26 /* ReturnedFromAuthenticatedWebViewSenderTests.swift */,
410D0FD82C6D1F25009B0E26 /* UpdateConnectInstanceSenderTests.swift */,
);
Expand Down Expand Up @@ -244,6 +251,7 @@
isa = PBXGroup;
children = (
413987D32C640848001D375E /* CallSetterWithSerializableValueSender.swift */,
E65690FF2CA37FAC00E0DB00 /* LogoutSender.swift */,
413987BE2C63F34B001D375E /* MessageSender.swift */,
413987D52C64088E001D375E /* ReturnedFromAuthenticatedWebViewSender.swift */,
413987BF2C63F34B001D375E /* UpdateConnectInstanceSender.swift */,
Expand Down Expand Up @@ -280,9 +288,9 @@
isa = PBXGroup;
children = (
413987BC2C63F34B001D375E /* VoidPayload.swift */,
418666492C66AC66003DB62E /* OnSetterFunctionCalledMessageHandler.swift */,
413987BA2C63F34B001D375E /* ScriptMessageHandler.swift */,
413987BB2C63F34B001D375E /* ScriptMessageHandlerWithReply.swift */,
418666492C66AC66003DB62E /* OnSetterFunctionCalledMessageHandler.swift */,
);
path = Helpers;
sourceTree = "<group>";
Expand Down Expand Up @@ -468,13 +476,14 @@
41D17A4E2C5A73A7007C6EE6 /* StripeConnectTests */ = {
isa = PBXGroup;
children = (
41BCCFFC2C8BA44700797E01 /* Resources */,
41BCCFEE2C8B3C7900797E01 /* Helpers */,
41810D682C88C4B100F10EB7 /* AppearanceTests.swift */,
416E9E882C76B36F00A0B917 /* Components */,
41BCCFFA2C8B95BB00797E01 /* CustomFontSourceTests.swift */,
E65691072CA3C3F000E0DB00 /* EmbeddedComponentManagerTests.swift */,
41BCCFEE2C8B3C7900797E01 /* Helpers */,
41814EE62C6BC8690014EB5E /* Internal */,
41BCCFFC2C8BA44700797E01 /* Resources */,
416E9E812C76994C00A0B917 /* WebView+Tests.swift */,
41810D682C88C4B100F10EB7 /* AppearanceTests.swift */,
41BCCFFA2C8B95BB00797E01 /* CustomFontSourceTests.swift */,
);
path = StripeConnectTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -641,6 +650,7 @@
410D0FE32C6D31C6009B0E26 /* StripeConnectConstants.swift in Sources */,
4186664A2C66AC66003DB62E /* OnSetterFunctionCalledMessageHandler.swift in Sources */,
413987CC2C63F34B001D375E /* VoidPayload.swift in Sources */,
E65691002CA37FAC00E0DB00 /* LogoutSender.swift in Sources */,
413987DD2C640A29001D375E /* FetchInitParamsMessageHandler.swift in Sources */,
413987D42C640848001D375E /* CallSetterWithSerializableValueSender.swift in Sources */,
416E9E742C751A1A00A0B917 /* ConnectComponentWebView.swift in Sources */,
Expand Down Expand Up @@ -688,13 +698,15 @@
410D0FDB2C6D21B0009B0E26 /* CallSetterWithSerializableValueSenderTests.swift in Sources */,
410D0FE72C6D3BBC009B0E26 /* ConnectWebViewTests.swift in Sources */,
410D0FD02C6D0319009B0E26 /* PageDidLoadMessageHandlerTests.swift in Sources */,
E65691022CA3B50B00E0DB00 /* LogoutSenderTests.swift in Sources */,
41814EED2C6BED8C0014EB5E /* FetchClientSecretMessageHandlerTests.swift in Sources */,
41814EE82C6BC8800014EB5E /* ScriptWebTestBase.swift in Sources */,
41BCCFF02C8B3C8900797E01 /* AppearanceWrapper+Default.swift in Sources */,
416E9E822C76994C00A0B917 /* WebView+Tests.swift in Sources */,
41BCCFFB2C8B95BB00797E01 /* CustomFontSourceTests.swift in Sources */,
4161C28C2CA1B54E005BD67C /* OnSetterFunctionCalledMessageHandlerTests.swift in Sources */,
41814EF32C6BFA4B0014EB5E /* OnLoaderStartMessageHandlerTests.swift in Sources */,
E65691082CA3C3F000E0DB00 /* EmbeddedComponentManagerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ public class EmbeddedComponentManager {
// Weakly held web views who get notified when appearance updates.
private(set) var childWebViews: NSHashTable<ConnectComponentWebView> = .weakObjects()

/// Strong reference to a logout proxy.
/// This is needed so that we can logout of the web view and clear cookies, even if there are no components in memory
private lazy var logoutProxyWebView = ConnectComponentWebView(componentManager: self, componentType: nil)

let fetchClientSecret: () async -> String?
let fonts: [EmbeddedComponentManager.CustomFontSource]
private(set) var appearance: EmbeddedComponentManager.Appearance
Expand Down Expand Up @@ -49,6 +53,7 @@ public class EmbeddedComponentManager {
self.fetchClientSecret = fetchClientSecret
self.fonts = fonts
self.appearance = appearance
childWebViews.add(logoutProxyWebView)
}

/// Updates the appearance of components created from this EmbeddedComponentManager
Expand All @@ -60,6 +65,20 @@ public class EmbeddedComponentManager {
}
}

/// Invalidates the account session and removes any persisted authentication state
/// associated with account for all components created from this EmbeddedComponentManager.
///
/// You must call this method when the user logs out from your app.
/// This will ensure that any persisted authentication state for embedded components,
/// such as authentication cookies, is also cleared during logout.
///
/// - Seealso: https://docs.stripe.com/connect/get-started-connect-embedded-components#log-out
public func logout() {
childWebViews.allObjects.forEach {
$0.logout()
}
}

/// Creates a payouts component
/// - Seealso: https://docs.stripe.com/connect/supported-embedded-components/payouts
public func createPayoutsViewController() -> PayoutsViewController {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: ComponentType?

/// The platform publishable key. Required for non-dashboard accounts
private(set) var publicKey: String?
Expand All @@ -36,7 +36,7 @@ struct ConnectJSURLParams: Encodable {
}

extension ConnectJSURLParams {
init(component: ComponentType, apiClient: STPAPIClient) {
init(component: ComponentType?, apiClient: STPAPIClient) {
self.component = component

// Validate that publishable key has been set
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class ConnectComponentWebView: ConnectWebView {
var componentManager: EmbeddedComponentManager

/// The component type that should be loaded.
private var componentType: ComponentType
let componentType: ComponentType?

/// The content controller that registers JS -> Swift message handlers
private let contentController: WKUserContentController
Expand All @@ -38,7 +38,7 @@ class ConnectComponentWebView: ConnectWebView {
}()

init(componentManager: EmbeddedComponentManager,
componentType: ComponentType,
componentType: ComponentType?,
// Should only be overridden for tests
notificationCenter: NotificationCenter = NotificationCenter.default,
webLocale: Locale = Locale.autoupdatingCurrent,
Expand Down Expand Up @@ -78,6 +78,10 @@ class ConnectComponentWebView: ConnectWebView {
sendMessage(UpdateConnectInstanceSender.init(payload: .init(locale: webLocale.webIdentifier, appearance: .init(appearance: appearance, traitCollection: traitCollection))))
}

func logout() {
sendMessage(LogoutSender())
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// LogoutSender.swift
// StripeConnect
//
// Created by Mel Ludowise on 9/24/24.
//

import Foundation

struct LogoutSender: MessageSender {
let name = "logout"
let payload = VoidPayload()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// EmbeddedComponentManagerTests.swift
// StripeConnectTests
//
// Created by Mel Ludowise on 9/24/24.
//

@_spi(PrivateBetaConnect) @testable import StripeConnect
import XCTest

class EmbeddedComponentManagerTests: XCTestCase {
var componentManager = EmbeddedComponentManager {
return nil
}

@MainActor
func testLogout() async throws {
// Expect a logout proxy
let logoutProxy = try XCTUnwrap(componentManager.childWebViews.allObjects.first)
XCTAssertEqual(componentManager.childWebViews.count, 1)
XCTAssertNil(logoutProxy.componentType)

let expectation = try logoutProxy.expectationForMessageReceived(sender: LogoutSender())

componentManager.logout()

await fulfillment(of: [expectation], timeout: TestHelpers.defaultTimeout)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,18 @@ class ConnectComponentWebViewTests: XCTestCase {
{"appearance":{"variables":{"fontFamily":"-apple-system","fontSizeBase":"16px"}},"fonts":[{"family":".AppleSystemUIFont","src":"url(data:font\\/txt;charset=utf-8;base64,dGVzdAo=)","weight":"400"}],"locale":"fr-FR"}
""")
}

@MainActor
func testLogout() async throws {
let componentManager = componentManagerAssertingOnFetch()

let webView = ConnectComponentWebView(componentManager: componentManager,
componentType: .payouts,
webLocale: Locale(identifier: "fr_FR"),
loadContent: false)

let expectation = try webView.expectationForMessageReceived(sender: LogoutSender())
componentManager.logout()
await fulfillment(of: [expectation], timeout: TestHelpers.defaultTimeout)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// LogoutSenderTests.swift
// StripeConnectTests
//
// Created by Mel Ludowise on 9/24/24.
//

@testable import StripeConnect
import XCTest

class LogoutSenderTests: ScriptWebTestBase {
func testSendMessage() throws {
try validateMessageSent(sender: LogoutSender())
}

func testSenderSignature() {
XCTAssertEqual(
LogoutSender().javascriptMessage,
"""
window.logout({});
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ideally this would be empty args, but there's no easy way to do that with the MessageSender. I tested that sending extra arguments to this function doesn't seem to have any negative effect in JS.

"""
)
}
}
Loading