diff --git a/Stripe.xcworkspace/contents.xcworkspacedata b/Stripe.xcworkspace/contents.xcworkspacedata
index 4bff4165bf6..cc15335c594 100644
--- a/Stripe.xcworkspace/contents.xcworkspacedata
+++ b/Stripe.xcworkspace/contents.xcworkspacedata
@@ -78,7 +78,4 @@
location = "group:IntegrationTester/IntegrationTester.xcodeproj">
-
-
diff --git a/StripeConnect/StripeConnect.xcodeproj/project.pbxproj b/StripeConnect/StripeConnect.xcodeproj/project.pbxproj
index 991b7c50227..c7999acd124 100644
--- a/StripeConnect/StripeConnect.xcodeproj/project.pbxproj
+++ b/StripeConnect/StripeConnect.xcodeproj/project.pbxproj
@@ -39,7 +39,6 @@
41542A692C88B6F2004E728E /* JSONEncoder+extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41542A682C88B6F2004E728E /* JSONEncoder+extension.swift */; };
41542A6B2C88B79E004E728E /* JSONSerialization+extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41542A6A2C88B79E004E728E /* JSONSerialization+extension.swift */; };
4161C2732C9D0A8A005BD67C /* AccountOnboardingViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4161C2722C9D0A8A005BD67C /* AccountOnboardingViewControllerTests.swift */; };
- 4161C2752C9DB1B9005BD67C /* StripeUICore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4161C2742C9DB1B9005BD67C /* StripeUICore.framework */; };
4161C2792C9DB1CE005BD67C /* StripeUICore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4161C2782C9DB1CE005BD67C /* StripeUICore.framework */; };
4161C27E2C9DB566005BD67C /* AccountCollectionOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4161C27D2C9DB566005BD67C /* AccountCollectionOptions.swift */; };
4161C28C2CA1B54E005BD67C /* OnSetterFunctionCalledMessageHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4161C28B2CA1B54E005BD67C /* OnSetterFunctionCalledMessageHandlerTests.swift */; };
@@ -95,6 +94,27 @@
E65691222CA52D5900E0DB00 /* StripeConnect+Exports.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65691212CA52D5900E0DB00 /* StripeConnect+Exports.swift */; };
E65691252CA52F9D00E0DB00 /* NotificationBannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65691232CA52F8600E0DB00 /* NotificationBannerViewController.swift */; };
E65691272CA533CD00E0DB00 /* OnNotificationsChangeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E65691262CA533CD00E0DB00 /* OnNotificationsChangeHandler.swift */; };
+ E6660D8F2CDC418C002A7631 /* ConnectAnalyticEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660D8E2CDC418C002A7631 /* ConnectAnalyticEvent.swift */; };
+ E6660D902CDC418C002A7631 /* ComponentAnalyticsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660D8D2CDC418C002A7631 /* ComponentAnalyticsClient.swift */; };
+ E6660D912CDC418C002A7631 /* AnalyticsClientV2+Connect.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660D8C2CDC418C002A7631 /* AnalyticsClientV2+Connect.swift */; };
+ E6660D9A2CDC4194002A7631 /* PageLoadErrorEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660D972CDC4194002A7631 /* PageLoadErrorEvent.swift */; };
+ E6660D9B2CDC4194002A7631 /* ComponentViewedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660D942CDC4194002A7631 /* ComponentViewedEvent.swift */; };
+ E6660D9C2CDC4194002A7631 /* ComponentCreatedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660D922CDC4194002A7631 /* ComponentCreatedEvent.swift */; };
+ E6660D9D2CDC4194002A7631 /* DeserializeMessageErrorEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660D962CDC4194002A7631 /* DeserializeMessageErrorEvent.swift */; };
+ E6660D9E2CDC4194002A7631 /* UnexpectedLoadErrorTypeEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660D982CDC4194002A7631 /* UnexpectedLoadErrorTypeEvent.swift */; };
+ E6660D9F2CDC4194002A7631 /* UnrecognizedSetterEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660D992CDC4194002A7631 /* UnrecognizedSetterEvent.swift */; };
+ E6660DA02CDC4194002A7631 /* ComponentLoadedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660D932CDC4194002A7631 /* ComponentLoadedEvent.swift */; };
+ E6660DA12CDC4194002A7631 /* ComponentWebPageLoadedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660D952CDC4194002A7631 /* ComponentWebPageLoadedEvent.swift */; };
+ E6660DA42CDC4209002A7631 /* URL+extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660DA32CDC4209002A7631 /* URL+extension.swift */; };
+ E6660DA52CDC4209002A7631 /* Error+extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660DA22CDC4209002A7631 /* Error+extensions.swift */; };
+ E6660DA72CDC42F4002A7631 /* Encodable+Connect.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660DA62CDC42F4002A7631 /* Encodable+Connect.swift */; };
+ E6660DA92CDC563C002A7631 /* AuthenticatedWebViewOpenedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660DA82CDC563C002A7631 /* AuthenticatedWebViewOpenedEvent.swift */; };
+ E6660DAB2CDC5702002A7631 /* AuthenticatedWebViewRedirectedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660DAA2CDC5702002A7631 /* AuthenticatedWebViewRedirectedEvent.swift */; };
+ E6660DAD2CDC5745002A7631 /* AuthenticatedWebViewCanceledEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660DAC2CDC5745002A7631 /* AuthenticatedWebViewCanceledEvent.swift */; };
+ E6660DAF2CDC576F002A7631 /* AuthenticatedWebViewErrorEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660DAE2CDC576F002A7631 /* AuthenticatedWebViewErrorEvent.swift */; };
+ E6660DB12CDD8E25002A7631 /* ComponentAnalyticsClient+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660DB02CDD8E1B002A7631 /* ComponentAnalyticsClient+Mock.swift */; };
+ E6660DB32CDD8F6E002A7631 /* StripeCoreTestUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6660DB22CDD8F6E002A7631 /* StripeCoreTestUtils.framework */; };
+ E6660DB72CDD99EF002A7631 /* MockComponentAnalyticsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6660DB62CDD99E7002A7631 /* MockComponentAnalyticsClient.swift */; };
E688AE002CADD8C400951D97 /* NotificationBannerViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E688ADFF2CADD8C400951D97 /* NotificationBannerViewControllerTests.swift */; };
E688AE032CADE36C00951D97 /* OnNotificationsChangeHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E688AE022CADE36900951D97 /* OnNotificationsChangeHandlerTests.swift */; };
E6C5F5F62C9FEE0200861709 /* AccountManagementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6C5F5F52C9FEE0200861709 /* AccountManagementViewController.swift */; };
@@ -207,6 +227,27 @@
E65691212CA52D5900E0DB00 /* StripeConnect+Exports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StripeConnect+Exports.swift"; sourceTree = ""; };
E65691232CA52F8600E0DB00 /* NotificationBannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationBannerViewController.swift; sourceTree = ""; };
E65691262CA533CD00E0DB00 /* OnNotificationsChangeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnNotificationsChangeHandler.swift; sourceTree = ""; };
+ E6660D8C2CDC418C002A7631 /* AnalyticsClientV2+Connect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnalyticsClientV2+Connect.swift"; sourceTree = ""; };
+ E6660D8D2CDC418C002A7631 /* ComponentAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentAnalyticsClient.swift; sourceTree = ""; };
+ E6660D8E2CDC418C002A7631 /* ConnectAnalyticEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectAnalyticEvent.swift; sourceTree = ""; };
+ E6660D922CDC4194002A7631 /* ComponentCreatedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentCreatedEvent.swift; sourceTree = ""; };
+ E6660D932CDC4194002A7631 /* ComponentLoadedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentLoadedEvent.swift; sourceTree = ""; };
+ E6660D942CDC4194002A7631 /* ComponentViewedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentViewedEvent.swift; sourceTree = ""; };
+ E6660D952CDC4194002A7631 /* ComponentWebPageLoadedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentWebPageLoadedEvent.swift; sourceTree = ""; };
+ E6660D962CDC4194002A7631 /* DeserializeMessageErrorEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeserializeMessageErrorEvent.swift; sourceTree = ""; };
+ E6660D972CDC4194002A7631 /* PageLoadErrorEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageLoadErrorEvent.swift; sourceTree = ""; };
+ E6660D982CDC4194002A7631 /* UnexpectedLoadErrorTypeEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnexpectedLoadErrorTypeEvent.swift; sourceTree = ""; };
+ E6660D992CDC4194002A7631 /* UnrecognizedSetterEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnrecognizedSetterEvent.swift; sourceTree = ""; };
+ E6660DA22CDC4209002A7631 /* Error+extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+extensions.swift"; sourceTree = ""; };
+ E6660DA32CDC4209002A7631 /* URL+extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+extension.swift"; sourceTree = ""; };
+ E6660DA62CDC42F4002A7631 /* Encodable+Connect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Encodable+Connect.swift"; sourceTree = ""; };
+ E6660DA82CDC563C002A7631 /* AuthenticatedWebViewOpenedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatedWebViewOpenedEvent.swift; sourceTree = ""; };
+ E6660DAA2CDC5702002A7631 /* AuthenticatedWebViewRedirectedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatedWebViewRedirectedEvent.swift; sourceTree = ""; };
+ E6660DAC2CDC5745002A7631 /* AuthenticatedWebViewCanceledEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatedWebViewCanceledEvent.swift; sourceTree = ""; };
+ E6660DAE2CDC576F002A7631 /* AuthenticatedWebViewErrorEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatedWebViewErrorEvent.swift; sourceTree = ""; };
+ E6660DB02CDD8E1B002A7631 /* ComponentAnalyticsClient+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComponentAnalyticsClient+Mock.swift"; sourceTree = ""; };
+ E6660DB22CDD8F6E002A7631 /* StripeCoreTestUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = StripeCoreTestUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ E6660DB62CDD99E7002A7631 /* MockComponentAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockComponentAnalyticsClient.swift; sourceTree = ""; };
E688ADFF2CADD8C400951D97 /* NotificationBannerViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationBannerViewControllerTests.swift; sourceTree = ""; };
E688AE022CADE36900951D97 /* OnNotificationsChangeHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnNotificationsChangeHandlerTests.swift; sourceTree = ""; };
E6C5F5F52C9FEE0200861709 /* AccountManagementViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountManagementViewController.swift; sourceTree = ""; };
@@ -233,7 +274,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 4161C2752C9DB1B9005BD67C /* StripeUICore.framework in Frameworks */,
+ E6660DB32CDD8F6E002A7631 /* StripeCoreTestUtils.framework in Frameworks */,
41D17A4B2C5A73A7007C6EE6 /* StripeConnect.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -300,6 +341,7 @@
413987C62C63F34B001D375E /* Internal */ = {
isa = PBXGroup;
children = (
+ E6660D8A2CDC414E002A7631 /* Analytics */,
E640C9CD2CBF26C9009D0C6E /* AuthenticatedWebView */,
416E9ED02C77F6C100A0B917 /* Extensions */,
410D0FE22C6D31C6009B0E26 /* StripeConnectConstants.swift */,
@@ -363,12 +405,15 @@
416E9ED02C77F6C100A0B917 /* Extensions */ = {
isa = PBXGroup;
children = (
- 416E9ED32C77F90600A0B917 /* WKScriptMessage+extension.swift */,
+ E6660DA62CDC42F4002A7631 /* Encodable+Connect.swift */,
+ E6660DA22CDC4209002A7631 /* Error+extensions.swift */,
+ 41054E422C989AAD00383C09 /* Font+Extension.swift */,
E6D3C8EF2CBE1455003CE967 /* HTTPURLResponse+StripeConnect.swift */,
41542A682C88B6F2004E728E /* JSONEncoder+extension.swift */,
41542A6A2C88B79E004E728E /* JSONSerialization+extension.swift */,
- 41054E422C989AAD00383C09 /* Font+Extension.swift */,
E6EF91C62CBA3BE60082DD1B /* UIViewController+StripeConnect.swift */,
+ E6660DA32CDC4209002A7631 /* URL+extension.swift */,
+ 416E9ED32C77F90600A0B917 /* WKScriptMessage+extension.swift */,
);
path = Extensions;
sourceTree = "";
@@ -439,6 +484,7 @@
41A2A5662C5AC5110077FC74 /* Frameworks */ = {
isa = PBXGroup;
children = (
+ E6660DB22CDD8F6E002A7631 /* StripeCoreTestUtils.framework */,
4161C2782C9DB1CE005BD67C /* StripeUICore.framework */,
4161C2742C9DB1B9005BD67C /* StripeUICore.framework */,
41A2A5672C5AC5120077FC74 /* StripeCore.framework */,
@@ -461,6 +507,8 @@
41BCCFEE2C8B3C7900797E01 /* Helpers */ = {
isa = PBXGroup;
children = (
+ E6660DB62CDD99E7002A7631 /* MockComponentAnalyticsClient.swift */,
+ E6660DB02CDD8E1B002A7631 /* ComponentAnalyticsClient+Mock.swift */,
41BCCFEF2C8B3C8900797E01 /* AppearanceWrapper+Default.swift */,
E6F486082C9E8A40000D914F /* ConnectJSURLParamsTests.swift */,
41BCCFF22C8B449800797E01 /* TestHelpers.swift */,
@@ -555,6 +603,36 @@
path = AuthenticatedWebView;
sourceTree = "";
};
+ E6660D8A2CDC414E002A7631 /* Analytics */ = {
+ isa = PBXGroup;
+ children = (
+ E6660D8B2CDC4158002A7631 /* Events */,
+ E6660D8C2CDC418C002A7631 /* AnalyticsClientV2+Connect.swift */,
+ E6660D8D2CDC418C002A7631 /* ComponentAnalyticsClient.swift */,
+ E6660D8E2CDC418C002A7631 /* ConnectAnalyticEvent.swift */,
+ );
+ path = Analytics;
+ sourceTree = "";
+ };
+ E6660D8B2CDC4158002A7631 /* Events */ = {
+ isa = PBXGroup;
+ children = (
+ E6660DAC2CDC5745002A7631 /* AuthenticatedWebViewCanceledEvent.swift */,
+ E6660DAE2CDC576F002A7631 /* AuthenticatedWebViewErrorEvent.swift */,
+ E6660DA82CDC563C002A7631 /* AuthenticatedWebViewOpenedEvent.swift */,
+ E6660DAA2CDC5702002A7631 /* AuthenticatedWebViewRedirectedEvent.swift */,
+ E6660D922CDC4194002A7631 /* ComponentCreatedEvent.swift */,
+ E6660D932CDC4194002A7631 /* ComponentLoadedEvent.swift */,
+ E6660D942CDC4194002A7631 /* ComponentViewedEvent.swift */,
+ E6660D952CDC4194002A7631 /* ComponentWebPageLoadedEvent.swift */,
+ E6660D962CDC4194002A7631 /* DeserializeMessageErrorEvent.swift */,
+ E6660D972CDC4194002A7631 /* PageLoadErrorEvent.swift */,
+ E6660D982CDC4194002A7631 /* UnexpectedLoadErrorTypeEvent.swift */,
+ E6660D992CDC4194002A7631 /* UnrecognizedSetterEvent.swift */,
+ );
+ path = Events;
+ sourceTree = "";
+ };
E688AE012CADE35300951D97 /* NotificationBanner */ = {
isa = PBXGroup;
children = (
@@ -717,6 +795,7 @@
E65691222CA52D5900E0DB00 /* StripeConnect+Exports.swift in Sources */,
E65691252CA52F9D00E0DB00 /* NotificationBannerViewController.swift in Sources */,
410D0FE32C6D31C6009B0E26 /* StripeConnectConstants.swift in Sources */,
+ E6660DAD2CDC5745002A7631 /* AuthenticatedWebViewCanceledEvent.swift in Sources */,
4186664A2C66AC66003DB62E /* OnSetterFunctionCalledMessageHandler.swift in Sources */,
E6D3C8EE2CBE1404003CE967 /* HTTPStatusError.swift in Sources */,
413987CC2C63F34B001D375E /* VoidPayload.swift in Sources */,
@@ -734,17 +813,34 @@
41810D7A2C8A0AAD00F10EB7 /* CustomFontSource.swift in Sources */,
413987CA2C63F34B001D375E /* ScriptMessageHandler.swift in Sources */,
416E9E862C76B35E00A0B917 /* PayoutsViewController.swift in Sources */,
+ E6660DA92CDC563C002A7631 /* AuthenticatedWebViewOpenedEvent.swift in Sources */,
41542A6B2C88B79E004E728E /* JSONSerialization+extension.swift in Sources */,
E6C5F5F62C9FEE0200861709 /* AccountManagementViewController.swift in Sources */,
413987C82C63F34B001D375E /* DebugMessageHandler.swift in Sources */,
410D0FCC2C6CFFDB009B0E26 /* AccountSessionClaimedMessageHandler.swift in Sources */,
+ E6660DAF2CDC576F002A7631 /* AuthenticatedWebViewErrorEvent.swift in Sources */,
41BCCFED2C8B34F600797E01 /* StringCodingKey.swift in Sources */,
4186664E2C66ACB3003DB62E /* OnLoadErrorMessageHandler.swift in Sources */,
410D0FE52C6D32F0009B0E26 /* ApplicationURLOpener.swift in Sources */,
E6F485F82C9E35A5000D914F /* PaymentDetailsViewController.swift in Sources */,
+ E6660D8F2CDC418C002A7631 /* ConnectAnalyticEvent.swift in Sources */,
+ E6660D902CDC418C002A7631 /* ComponentAnalyticsClient.swift in Sources */,
+ E6660D912CDC418C002A7631 /* AnalyticsClientV2+Connect.swift in Sources */,
+ E6660DAB2CDC5702002A7631 /* AuthenticatedWebViewRedirectedEvent.swift in Sources */,
+ E6660D9A2CDC4194002A7631 /* PageLoadErrorEvent.swift in Sources */,
+ E6660D9B2CDC4194002A7631 /* ComponentViewedEvent.swift in Sources */,
+ E6660D9C2CDC4194002A7631 /* ComponentCreatedEvent.swift in Sources */,
+ E6660D9D2CDC4194002A7631 /* DeserializeMessageErrorEvent.swift in Sources */,
+ E6660D9E2CDC4194002A7631 /* UnexpectedLoadErrorTypeEvent.swift in Sources */,
+ E6660D9F2CDC4194002A7631 /* UnrecognizedSetterEvent.swift in Sources */,
+ E6660DA02CDC4194002A7631 /* ComponentLoadedEvent.swift in Sources */,
+ E6660DA12CDC4194002A7631 /* ComponentWebPageLoadedEvent.swift in Sources */,
+ E6660DA72CDC42F4002A7631 /* Encodable+Connect.swift in Sources */,
416E9E762C751B0500A0B917 /* EmbeddedComponentManager.swift in Sources */,
416E9ECF2C77EAA400A0B917 /* EmbeddedComponentError.swift in Sources */,
E640C9CC2CBF0C1E009D0C6E /* AuthenticatedWebViewManager.swift in Sources */,
+ E6660DA42CDC4209002A7631 /* URL+extension.swift in Sources */,
+ E6660DA52CDC4209002A7631 /* Error+extensions.swift in Sources */,
410D0FDF2C6D3176009B0E26 /* ConnectWebViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -757,6 +853,8 @@
410D0FD92C6D1F25009B0E26 /* UpdateConnectInstanceSenderTests.swift in Sources */,
E6F486092C9E8A40000D914F /* ConnectJSURLParamsTests.swift in Sources */,
416E9E892C76B36F00A0B917 /* PayoutsViewControllerTests.swift in Sources */,
+ E6660DB72CDD99EF002A7631 /* MockComponentAnalyticsClient.swift in Sources */,
+ E6660DB12CDD8E25002A7631 /* ComponentAnalyticsClient+Mock.swift in Sources */,
41814EEB2C6BCAB30014EB5E /* DebugMessageHandlerTests.swift in Sources */,
41814EEF2C6BEF2C0014EB5E /* FetchInitParamsMessageHandlerTests.swift in Sources */,
41810D692C88C4B100F10EB7 /* AppearanceTests.swift in Sources */,
diff --git a/StripeConnect/StripeConnect/Source/Components/AccountManagementViewController.swift b/StripeConnect/StripeConnect/Source/Components/AccountManagementViewController.swift
index 010342ec3f0..94bc91c4833 100644
--- a/StripeConnect/StripeConnect/Source/Components/AccountManagementViewController.swift
+++ b/StripeConnect/StripeConnect/Source/Components/AccountManagementViewController.swift
@@ -28,12 +28,14 @@ public class AccountManagementViewController: UIViewController {
init(componentManager: EmbeddedComponentManager,
collectionOptions: AccountCollectionOptions,
- loadContent: Bool) {
+ loadContent: Bool,
+ analyticsClientFactory: ComponentAnalyticsClientFactory) {
super.init(nibName: nil, bundle: nil)
webVC = ConnectComponentWebViewController(
componentManager: componentManager,
componentType: .accountManagement,
- loadContent: loadContent
+ loadContent: loadContent,
+ analyticsClientFactory: analyticsClientFactory
) {
Props(collectionOptions: collectionOptions)
} didFailLoadWithError: { [weak self] error in
diff --git a/StripeConnect/StripeConnect/Source/Components/AccountOnboardingViewController.swift b/StripeConnect/StripeConnect/Source/Components/AccountOnboardingViewController.swift
index 8550d1e49eb..ce33531cb43 100644
--- a/StripeConnect/StripeConnect/Source/Components/AccountOnboardingViewController.swift
+++ b/StripeConnect/StripeConnect/Source/Components/AccountOnboardingViewController.swift
@@ -38,13 +38,15 @@ public class AccountOnboardingViewController: UIViewController {
init(props: Props,
componentManager: EmbeddedComponentManager,
- loadContent: Bool
+ loadContent: Bool,
+ analyticsClientFactory: ComponentAnalyticsClientFactory
) {
super.init(nibName: nil, bundle: nil)
webVC = ConnectComponentWebViewController(
componentManager: componentManager,
componentType: .onboarding,
- loadContent: loadContent
+ loadContent: loadContent,
+ analyticsClientFactory: analyticsClientFactory
) {
props
} didFailLoadWithError: { [weak self] error in
diff --git a/StripeConnect/StripeConnect/Source/Components/NotificationBannerViewController.swift b/StripeConnect/StripeConnect/Source/Components/NotificationBannerViewController.swift
index 022dfe41d38..20d5bfa807e 100644
--- a/StripeConnect/StripeConnect/Source/Components/NotificationBannerViewController.swift
+++ b/StripeConnect/StripeConnect/Source/Components/NotificationBannerViewController.swift
@@ -25,12 +25,14 @@ public class NotificationBannerViewController: UIViewController {
init(componentManager: EmbeddedComponentManager,
collectionOptions: AccountCollectionOptions,
- loadContent: Bool) {
+ loadContent: Bool,
+ analyticsClientFactory: ComponentAnalyticsClientFactory) {
super.init(nibName: nil, bundle: nil)
webVC = ConnectComponentWebViewController(
componentManager: componentManager,
componentType: .notificationBanner,
- loadContent: loadContent
+ loadContent: loadContent,
+ analyticsClientFactory: analyticsClientFactory
) {
Props(collectionOptions: collectionOptions)
} didFailLoadWithError: { [weak self] error in
diff --git a/StripeConnect/StripeConnect/Source/Components/PaymentDetailsViewController.swift b/StripeConnect/StripeConnect/Source/Components/PaymentDetailsViewController.swift
index 2f4271bbc57..0c5953967e2 100644
--- a/StripeConnect/StripeConnect/Source/Components/PaymentDetailsViewController.swift
+++ b/StripeConnect/StripeConnect/Source/Components/PaymentDetailsViewController.swift
@@ -18,12 +18,14 @@ public class PaymentDetailsViewController: UIViewController {
public weak var delegate: PaymentDetailsViewControllerDelegate?
init(componentManager: EmbeddedComponentManager,
- loadContent: Bool) {
+ loadContent: Bool,
+ analyticsClientFactory: ComponentAnalyticsClientFactory) {
super.init(nibName: nil, bundle: nil)
webVC = ConnectComponentWebViewController(
componentManager: componentManager,
componentType: .paymentDetails,
- loadContent: loadContent
+ loadContent: loadContent,
+ analyticsClientFactory: analyticsClientFactory
) { [weak self] error in
guard let self else { return }
delegate?.paymentDetails(self, didFailLoadWithError: error)
diff --git a/StripeConnect/StripeConnect/Source/Components/PayoutsViewController.swift b/StripeConnect/StripeConnect/Source/Components/PayoutsViewController.swift
index e42b57986ae..f52eaa1566e 100644
--- a/StripeConnect/StripeConnect/Source/Components/PayoutsViewController.swift
+++ b/StripeConnect/StripeConnect/Source/Components/PayoutsViewController.swift
@@ -18,12 +18,14 @@ public class PayoutsViewController: UIViewController {
public weak var delegate: PayoutsViewControllerDelegate?
init(componentManager: EmbeddedComponentManager,
- loadContent: Bool) {
+ loadContent: Bool,
+ analyticsClientFactory: ComponentAnalyticsClientFactory) {
super.init(nibName: nil, bundle: nil)
webVC = ConnectComponentWebViewController(
componentManager: componentManager,
componentType: .payouts,
- loadContent: loadContent
+ loadContent: loadContent,
+ analyticsClientFactory: analyticsClientFactory
) { [weak self] error in
guard let self else { return }
delegate?.payouts(self, didFailLoadWithError: error)
diff --git a/StripeConnect/StripeConnect/Source/EmbeddedComponentManager.swift b/StripeConnect/StripeConnect/Source/EmbeddedComponentManager.swift
index 9638427a64d..f58a73b2cf1 100644
--- a/StripeConnect/StripeConnect/Source/EmbeddedComponentManager.swift
+++ b/StripeConnect/StripeConnect/Source/EmbeddedComponentManager.swift
@@ -6,7 +6,7 @@
//
import JavaScriptCore
-import StripeCore
+@_spi(STP) import StripeCore
import UIKit
/// Manages Connect embedded components
@@ -28,6 +28,12 @@ public class EmbeddedComponentManager {
// content should load.
var shouldLoadContent: Bool = true
+ // This should only be used for tests to mock the analytics logger
+ var analyticsClientFactory: ComponentAnalyticsClientFactory = {
+ ComponentAnalyticsClient(client: AnalyticsClientV2.sharedConnect,
+ commonFields: $0)
+ }
+
/**
Initializes a StripeConnect instance.
@@ -66,7 +72,9 @@ public class EmbeddedComponentManager {
/// Creates a payouts component
/// - Seealso: https://docs.stripe.com/connect/supported-embedded-components/payouts
public func createPayoutsViewController() -> PayoutsViewController {
- .init(componentManager: self, loadContent: shouldLoadContent)
+ .init(componentManager: self,
+ loadContent: shouldLoadContent,
+ analyticsClientFactory: analyticsClientFactory)
}
/**
@@ -96,12 +104,15 @@ public class EmbeddedComponentManager {
collectionOptions: collectionOptions
),
componentManager: self,
- loadContent: shouldLoadContent)
+ loadContent: shouldLoadContent,
+ analyticsClientFactory: analyticsClientFactory)
}
@_spi(DashboardOnly)
public func createPaymentDetailsViewController() -> PaymentDetailsViewController {
- .init(componentManager: self, loadContent: shouldLoadContent)
+ .init(componentManager: self,
+ loadContent: shouldLoadContent,
+ analyticsClientFactory: analyticsClientFactory)
}
@_spi(DashboardOnly)
@@ -110,7 +121,8 @@ public class EmbeddedComponentManager {
) -> AccountManagementViewController {
.init(componentManager: self,
collectionOptions: collectionOptions,
- loadContent: shouldLoadContent)
+ loadContent: shouldLoadContent,
+ analyticsClientFactory: analyticsClientFactory)
}
@_spi(DashboardOnly)
@@ -119,7 +131,8 @@ public class EmbeddedComponentManager {
) -> NotificationBannerViewController {
.init(componentManager: self,
collectionOptions: collectionOptions,
- loadContent: shouldLoadContent)
+ loadContent: shouldLoadContent,
+ analyticsClientFactory: analyticsClientFactory)
}
/// Used to keep reference of all web views associated with this component manager.
diff --git a/StripeConnect/StripeConnect/Source/Helpers/ConnectJSURLParams.swift b/StripeConnect/StripeConnect/Source/Helpers/ConnectJSURLParams.swift
index 20e5777e31e..95f2298da4f 100644
--- a/StripeConnect/StripeConnect/Source/Helpers/ConnectJSURLParams.swift
+++ b/StripeConnect/StripeConnect/Source/Helpers/ConnectJSURLParams.swift
@@ -53,12 +53,8 @@ extension ConnectJSURLParams {
}
}
- var url: URL {
- guard let data = try? JSONEncoder().encode(self),
- let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
- // TODO: MXMOBILE-2491 Log error
- return StripeConnectConstants.connectJSBaseURL
- }
+ func url() throws -> URL {
+ let dict = try jsonDictionary(with: .connectEncoder)
// Append as hash params
return URL(string: "#\(URLEncoder.queryString(from: dict))", relativeTo: StripeConnectConstants.connectJSBaseURL)!
diff --git a/StripeConnect/StripeConnect/Source/Internal/Analytics/AnalyticsClientV2+Connect.swift b/StripeConnect/StripeConnect/Source/Internal/Analytics/AnalyticsClientV2+Connect.swift
new file mode 100644
index 00000000000..3323972d25f
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Analytics/AnalyticsClientV2+Connect.swift
@@ -0,0 +1,15 @@
+//
+// AnalyticsClientV2+Connect.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 10/4/24.
+//
+
+@_spi(STP) import StripeCore
+
+extension AnalyticsClientV2 {
+ static let sharedConnect = AnalyticsClientV2(
+ clientId: "mobile_connect_sdk",
+ origin: "stripe-connect-ios"
+ )
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Analytics/ComponentAnalyticsClient.swift b/StripeConnect/StripeConnect/Source/Internal/Analytics/ComponentAnalyticsClient.swift
new file mode 100644
index 00000000000..e5658efcb39
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Analytics/ComponentAnalyticsClient.swift
@@ -0,0 +1,267 @@
+//
+// ComponentAnalyticsClient.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 10/1/24.
+//
+
+@_spi(STP) import StripeCore
+
+typealias ComponentAnalyticsClientFactory = (ComponentAnalyticsClient.CommonFields) -> ComponentAnalyticsClient
+
+/// Wraps `AnalyticsClientV2` with Connect-specific analytic properties.
+/// An analytics client instance should only be used in one component instance
+/// as it tracks component-specific loading metrics.
+@dynamicMemberLookup
+class ComponentAnalyticsClient {
+ /// Fields common to all Connect analytic events
+ struct CommonFields: Encodable {
+ /// The platform's publishable key
+ /// - Note: This will be null when the account is a dashboard account as user keys should never be logged to analytics
+ private(set) var publishableKey: String?
+
+ /// ID of the platform account
+ /// - Note: This is expected to be null unless originating from a dashboard account,
+ /// otherwise the backend derives this value from the `publishableKey`
+ let platformId: String?
+
+ /// ID of the connected account returned by the `AccountSessionClaimedMessageHandler`
+ var merchantId: String?
+
+ /// Represents if the account is in live mode
+ /// - Note: This is expected to be null unless originating from a dashboard account,
+ /// otherwise the backend derives this value from the `publishableKey`
+ let livemode: Bool?
+
+ /// The type of component this analytic is originating from
+ let component: ComponentType
+
+ /// A UUID representing a specific component instance.
+ /// All events related to this instance will have the same UUID
+ let componentInstance: UUID
+ }
+
+ let client: AnalyticsClientV2Protocol
+
+ private(set) var commonFields: CommonFields
+
+ /// The `pageViewID` returned from the `PageDidLoadMessageHandler`
+ var pageViewId: String?
+
+ /// Time the page began to load
+ var loadStart: Date?
+
+ /// Time the component was first viewed
+ var componentFirstViewedTime: Date?
+
+ /// If `ComponentWebPageLoadedEvent` was already logged
+ private(set) var loggedPageLoaded = false
+
+ /// If `ComponentLoadedEvent` was already logged
+ private(set) var loggedComponentLoaded = false
+
+ init(client: AnalyticsClientV2Protocol,
+ commonFields: CommonFields) {
+ self.client = client
+ self.commonFields = commonFields
+ }
+
+ // Makes for easy access to common fields
+ subscript(dynamicMember keyPath: WritableKeyPath) -> T {
+ get { commonFields[keyPath: keyPath] }
+ set { commonFields[keyPath: keyPath] = newValue }
+ }
+
+ func log(event: Event) {
+ do {
+ var dict = try commonFields.jsonDictionary(with: .analyticsEncoder)
+ let metadataDict = try event.metadata.jsonDictionary(with: .analyticsEncoder)
+ dict["event_metadata"] = metadataDict
+
+ // Also log metadata fields as first-level fields to make it easier
+ // to configure alerting
+ dict.mergeAssertingOnOverwrites(metadataDict)
+
+ client.log(eventName: event.name, parameters: dict)
+ } catch {
+ // We were unable to encode the analytic parameters
+ logClientError(error)
+ }
+ }
+
+ /// The component is viewed on screen (`viewDidAppear` lifecycle event)
+ /// - Parameter viewedAt: Time the user viewed the component
+ func logComponentViewed(viewedAt: Date) {
+ componentFirstViewedTime = viewedAt
+ log(event: ComponentViewedEvent())
+ }
+
+ /// The web page finished loading (`didFinish navigation` event)
+ /// - Parameter loadEnd: Time the web page finished loading
+ func logComponentWebPageLoaded(loadEnd: Date) {
+ guard !loggedPageLoaded, let loadStart else {
+ return
+ }
+
+ log(event: ComponentWebPageLoadedEvent(metadata: .init(
+ timeToLoad: loadEnd.timeIntervalSince(loadStart)
+ )))
+
+ // Prevent the analytic from being logged again in the even the page is reloaded.
+ // This can happen if the app is backgrounded for a long period then foregrounded.
+ loggedPageLoaded = true
+ }
+
+ /// The component is successfully loaded within the web view.
+ /// Triggered from `componentDidLoad` message handler from the web view.
+ /// - Parameter loadEnd: Time the component finished loading
+ func logComponentLoaded(loadEnd: Date) {
+ guard !loggedComponentLoaded, let loadStart else {
+ return
+ }
+
+ log(event: ComponentLoadedEvent(metadata: .init(
+ pageViewId: pageViewId,
+ timeToLoad: loadEnd.timeIntervalSince(loadStart),
+ perceivedTimeToLoad: componentFirstViewedTime.map(loadEnd.timeIntervalSince) ?? 0
+ )))
+
+ // Prevent the analytic from being logged again in the even the page is reloaded.
+ // This can happen if the app is backgrounded for a long period then foregrounded.
+ loggedComponentLoaded = true
+ }
+
+ /// The web view sends an onLoadError that can’t be deserialized by the SDK.
+ /// - Parameter type: The error `type` property from web
+ func logUnexpectedLoadErrorType(type: String) {
+ log(event: UnexpectedLoadErrorTypeEvent(metadata: .init(
+ errorType: type,
+ pageViewId: pageViewId
+ )))
+ }
+
+ /// If the web view calls `onSetterFunctionCalled` with a `setter` argument the SDK doesn’t know how to handle.
+ /// - Parameter setter: The `setter` property sent from web
+ func logUnexpectedSetterEvent(setter: String) {
+ log(event: UnrecognizedSetterEvent(metadata: .init(
+ setter: setter,
+ pageViewId: pageViewId
+ )))
+ }
+
+ /// An error occurred deserializing the JSON payload from a web message.
+ /// - Parameters:
+ /// - message: The name of the message
+ /// - error: The error thrown deserializing the message
+ func logDeserializeMessageErrorEvent(message: String, error: Error) {
+ log(event: DeserializeMessageErrorEvent(metadata: .init(
+ message: message,
+ error: error,
+ pageViewId: pageViewId
+ )))
+ }
+
+ /// An authenticated web view was opened
+ /// - Parameter id: ID for the authenticated web view session (sent in `openAuthenticatedWebView` message
+ func logAuthenticatedWebViewOpenedEvent(id: String) {
+ log(event: AuthenticatedWebViewOpenedEvent(metadata: .init(
+ authenticatedWebViewId: id,
+ pageViewId: pageViewId
+ )))
+ }
+
+ /// The authenticated web view either successfully redirected or was canceled by the user
+ /// - Parameters:
+ /// - id: ID for the authenticated web view session (sent in
+ /// `openAuthenticatedWebView` message
+ /// - redirected: True when the authenticated web view successfully redirected back to the app.
+ /// False if the user closed the view before getting directed back to the app.
+ func logAuthenticatedWebViewEventComplete(id: String, redirected: Bool) {
+ if redirected {
+ log(event: AuthenticatedWebViewRedirectedEvent(metadata: .init(
+ authenticatedWebViewId: id,
+ pageViewId: pageViewId
+ )))
+ } else {
+ log(event: AuthenticatedWebViewCanceledEvent(metadata: .init(
+ authenticatedWebViewId: id,
+ pageViewId: pageViewId
+ )))
+ }
+ }
+
+ /// The authenticated web view threw an error and was not successfully redirected back to the app.
+ /// - Parameters:
+ /// - id: ID for the authenticated web view session (sent in
+ /// `openAuthenticatedWebView` message
+ /// - error: The error thrown by the authenticated web view
+ func logAuthenticatedWebViewEventComplete(id: String, error: Error) {
+ log(event: AuthenticatedWebViewErrorEvent(metadata: .init(
+ authenticatedWebViewId: id,
+ error: error,
+ pageViewId: pageViewId
+ )))
+ }
+
+ /**
+ Catch-all for mobile client-side errors
+ - Parameters:
+ - error: Error to log.
+ - file: File name the error was caught on.
+ - line: File line number the error was caught on.
+
+ - Note: If the error type conforms to `AnalyticLoggableErrorV2` then all
+ properties returned by `analyticLoggableSerializeForLogging()` will be encoded
+ into the event payload. Otherwise only domain and code will be encoded.
+
+ File and line number should be explicitly passed if this method is called
+ from a helper function, otherwise it's difficult to determine where the
+ original error was caught.
+ */
+ func logClientError(_ error: Error,
+ file: StaticString = #file,
+ line: UInt = #line) {
+ var params: [String: Any] = [
+ "error": error.analyticsIdentifier,
+ "file": ("\(file)" as NSString).lastPathComponent,
+ "line": line,
+ ]
+ if let loggableError = error as? AnalyticLoggableErrorV2 {
+ params.mergeAssertingOnOverwrites(loggableError.serializeForV2Logging())
+ }
+
+ client.log(
+ eventName: "client_error",
+ parameters: params
+ )
+ }
+}
+
+extension ComponentAnalyticsClient.CommonFields {
+ init(apiClient: STPAPIClient,
+ component: ComponentType,
+ componentInstance: UUID = .init()
+ ) {
+ // Reuse logic in ConnectJSURLParams to determine when to use publicKey
+ // platformId + livemode
+ let params = ConnectJSURLParams(component: component, apiClient: apiClient)
+
+ // Ensures a secret key is never logged to analytics in the event
+ // the platform uses a secret key in their app
+ var publicKey = params.publicKey
+ if publicKey != nil {
+ // Check for nil so we don't log '[REDACTED_LIVE_KEY]' if we don't
+ // intend to log any key (e.g. for user keys)
+ publicKey = apiClient.sanitizedPublishableKey
+ }
+
+ self.init(
+ publishableKey: publicKey,
+ platformId: params.platformIdOverride,
+ merchantId: params.merchantIdOverride,
+ livemode: params.livemodeOverride,
+ component: params.component,
+ componentInstance: componentInstance
+ )
+ }
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Analytics/ConnectAnalyticEvent.swift b/StripeConnect/StripeConnect/Source/Internal/Analytics/ConnectAnalyticEvent.swift
new file mode 100644
index 00000000000..d54a7fac6ef
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Analytics/ConnectAnalyticEvent.swift
@@ -0,0 +1,17 @@
+//
+// ConnectAnalyticEvent.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 10/1/24.
+//
+
+/// Represents an analytics event logged from the Connect SDK
+protocol ConnectAnalyticEvent {
+ associatedtype Metadata: Encodable
+
+ /// The `event_name` field of the event
+ var name: String { get }
+
+ /// Event-specific metadata, encoded as a JSON string in the `metadata` field
+ var metadata: Metadata { get }
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/AuthenticatedWebViewCanceledEvent.swift b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/AuthenticatedWebViewCanceledEvent.swift
new file mode 100644
index 00000000000..e4b3421f5bc
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/AuthenticatedWebViewCanceledEvent.swift
@@ -0,0 +1,21 @@
+//
+// AuthenticatedWebViewCanceledEvent.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 11/6/24.
+//
+
+/// The user closed the authenticated web view before getting redirected back to the app.
+struct AuthenticatedWebViewCanceledEvent: ConnectAnalyticEvent {
+ struct Metadata: Encodable {
+ /// ID for the authenticated web view session (sent in `openAuthenticatedWebView` message
+ let authenticatedWebViewId: String
+
+ /// The `pageViewID` from the web view
+ /// - Note: May be null if not yet sent from web
+ let pageViewId: String?
+ }
+
+ let name = "component.authenticated_web.canceled"
+ let metadata: Metadata
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/AuthenticatedWebViewErrorEvent.swift b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/AuthenticatedWebViewErrorEvent.swift
new file mode 100644
index 00000000000..de132e4f98c
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/AuthenticatedWebViewErrorEvent.swift
@@ -0,0 +1,30 @@
+//
+// AuthenticatedWebViewErrorEvent.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 11/6/24.
+//
+
+/// The authenticated web view threw an error and was not successfully redirected back to the app.
+struct AuthenticatedWebViewErrorEvent: ConnectAnalyticEvent {
+ struct Metadata: Encodable {
+ /// ID for the authenticated web view session (sent in `openAuthenticatedWebView` message
+ let authenticatedWebViewId: String
+
+ /// The error identifier
+ let error: String
+
+ /// The `pageViewID` from the web view
+ /// - Note: May be null if not yet sent from web
+ let pageViewId: String?
+
+ init(authenticatedWebViewId: String, error: Error, pageViewId: String?) {
+ self.authenticatedWebViewId = authenticatedWebViewId
+ self.error = error.analyticsIdentifier
+ self.pageViewId = pageViewId
+ }
+ }
+
+ let name = "component.authenticated_web.error"
+ let metadata: Metadata
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/AuthenticatedWebViewOpenedEvent.swift b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/AuthenticatedWebViewOpenedEvent.swift
new file mode 100644
index 00000000000..af9e0c5ea50
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/AuthenticatedWebViewOpenedEvent.swift
@@ -0,0 +1,21 @@
+//
+// AuthenticatedWebViewOpenedEvent.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 11/6/24.
+//
+
+/// An authenticated web view was opened
+struct AuthenticatedWebViewOpenedEvent: ConnectAnalyticEvent {
+ struct Metadata: Encodable {
+ /// ID for the authenticated web view session (sent in `openAuthenticatedWebView` message
+ let authenticatedWebViewId: String
+
+ /// The `pageViewID` from the web view
+ /// - Note: May be null if not yet sent from web
+ let pageViewId: String?
+ }
+
+ let name = "component.authenticated_web.opened"
+ let metadata: Metadata
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/AuthenticatedWebViewRedirectedEvent.swift b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/AuthenticatedWebViewRedirectedEvent.swift
new file mode 100644
index 00000000000..3f18b6610db
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/AuthenticatedWebViewRedirectedEvent.swift
@@ -0,0 +1,21 @@
+//
+// AuthenticatedWebViewRedirectedEvent.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 11/6/24.
+//
+
+/// The authenticated web view successfully redirected back to the app
+struct AuthenticatedWebViewRedirectedEvent: ConnectAnalyticEvent {
+ struct Metadata: Encodable {
+ /// ID for the authenticated web view session (sent in `openAuthenticatedWebView` message
+ let authenticatedWebViewId: String
+
+ /// The `pageViewID` from the web view
+ /// - Note: May be null if not yet sent from web
+ let pageViewId: String?
+ }
+
+ let name = "component.authenticated_web.redirected"
+ let metadata: Metadata
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/ComponentCreatedEvent.swift b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/ComponentCreatedEvent.swift
new file mode 100644
index 00000000000..5d5401f101a
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/ComponentCreatedEvent.swift
@@ -0,0 +1,14 @@
+//
+// ComponentCreatedEvent.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 10/4/24.
+//
+
+/// A component was instantiated via `create{ComponentType}`.
+struct ComponentCreatedEvent: ConnectAnalyticEvent {
+ struct Metadata: Encodable { }
+
+ let name = "component.created"
+ let metadata = Metadata()
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/ComponentLoadedEvent.swift b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/ComponentLoadedEvent.swift
new file mode 100644
index 00000000000..05f9fb7a0d3
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/ComponentLoadedEvent.swift
@@ -0,0 +1,28 @@
+//
+// ComponentLoadedEvent.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 10/4/24.
+//
+
+/// The component is successfully loaded within the web view.
+/// Triggered from `componentDidLoad` message handler from the web view.
+struct ComponentLoadedEvent: ConnectAnalyticEvent {
+ struct Metadata: Encodable {
+ /// The pageViewID from the web view
+ let pageViewId: String?
+
+ /// Elapsed time in seconds it took the component to load
+ /// (starting when the page first began loading).
+ let timeToLoad: TimeInterval
+
+ /// Elapsed time in seconds in took between when the component was
+ /// initially viewed on screen (`component.viewed`) to when the component
+ /// finished loading. This value will be `0` if the component finished
+ /// loading before being viewed on screen.
+ let perceivedTimeToLoad: TimeInterval
+ }
+
+ let name = "component.web.component_loaded"
+ let metadata: Metadata
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/ComponentViewedEvent.swift b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/ComponentViewedEvent.swift
new file mode 100644
index 00000000000..c4db99b1973
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/ComponentViewedEvent.swift
@@ -0,0 +1,14 @@
+//
+// ComponentViewedEvent.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 10/4/24.
+//
+
+/// The component is viewed on screen (`viewDidAppear` lifecycle event)
+struct ComponentViewedEvent: ConnectAnalyticEvent {
+ struct Metadata: Encodable { }
+
+ let name = "component.viewed"
+ let metadata = Metadata()
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/ComponentWebPageLoadedEvent.swift b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/ComponentWebPageLoadedEvent.swift
new file mode 100644
index 00000000000..aef6f8c956f
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/ComponentWebPageLoadedEvent.swift
@@ -0,0 +1,18 @@
+//
+// ComponentWebPageLoadedEvent.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 10/4/24.
+//
+
+/// The web page finished loading (`didFinish navigation` event).
+struct ComponentWebPageLoadedEvent: ConnectAnalyticEvent {
+ struct Metadata: Encodable {
+ /// Elapsed time in seconds it took the web page to load
+ /// (starting when it first began loading).
+ let timeToLoad: TimeInterval
+ }
+
+ let name = "component.web.page_loaded"
+ let metadata: Metadata
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/DeserializeMessageErrorEvent.swift b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/DeserializeMessageErrorEvent.swift
new file mode 100644
index 00000000000..ef9227dea9a
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/DeserializeMessageErrorEvent.swift
@@ -0,0 +1,34 @@
+//
+// DeserializeMessageErrorEvent.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 10/4/24.
+//
+
+/// An error occurred deserializing the JSON payload from a web message.
+struct DeserializeMessageErrorEvent: ConnectAnalyticEvent {
+ struct Metadata: Encodable {
+ /// The name of the message
+ let message: String
+
+ /// The error identifier
+ let error: String
+
+ /// The error's description, if there is one.
+ let errorDescription: String?
+
+ /// The `pageViewID` from the web view
+ /// - Note: May be null if not yet sent from web
+ let pageViewId: String?
+
+ init(message: String, error: Error, pageViewId: String?) {
+ self.message = message
+ self.error = error.analyticsIdentifier
+ self.errorDescription = (error as NSError).userInfo[NSDebugDescriptionErrorKey] as? String
+ self.pageViewId = pageViewId
+ }
+ }
+
+ let name = "component.web.error.deserialize_message"
+ let metadata: Metadata
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/PageLoadErrorEvent.swift b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/PageLoadErrorEvent.swift
new file mode 100644
index 00000000000..4c47848c134
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/PageLoadErrorEvent.swift
@@ -0,0 +1,34 @@
+//
+// PageLoadErrorEvent.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 10/4/24.
+//
+
+/// The SDK receives a non-200 status code or error loading the web view, other than “Internet connectivity” errors.
+struct PageLoadErrorEvent: ConnectAnalyticEvent {
+ struct Metadata: Encodable {
+ /// http status code if the error was a non-200 response
+ let status: Int?
+
+ /// Error identifier for non http status type errors
+ let error: String?
+
+ /// The URL of the page, excluding hashtag params
+ let url: String?
+
+ init(error: Error, url: URL?) {
+ if let statusError = error as? HTTPStatusError {
+ self.status = statusError.errorCode
+ self.error = nil
+ } else {
+ self.status = nil
+ self.error = error.analyticsIdentifier
+ }
+ self.url = url?.absoluteStringRemovingParams
+ }
+ }
+
+ let name = "component.web.error.page_load"
+ let metadata: Metadata
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/UnexpectedLoadErrorTypeEvent.swift b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/UnexpectedLoadErrorTypeEvent.swift
new file mode 100644
index 00000000000..b574dbc968a
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/UnexpectedLoadErrorTypeEvent.swift
@@ -0,0 +1,21 @@
+//
+// UnexpectedLoadErrorTypeEvent.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 10/4/24.
+//
+
+/// The web view sends an onLoadError that can’t be deserialized by the SDK.
+struct UnexpectedLoadErrorTypeEvent: ConnectAnalyticEvent {
+ struct Metadata: Encodable {
+ /// The error `type` property from web
+ let errorType: String
+
+ /// The pageViewID from the web view
+ /// - Note: May be null if not yet sent from web
+ let pageViewId: String?
+ }
+
+ let name = "component.web.warn.unexpected_load_error_type"
+ let metadata: Metadata
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/UnrecognizedSetterEvent.swift b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/UnrecognizedSetterEvent.swift
new file mode 100644
index 00000000000..79f88702638
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Analytics/Events/UnrecognizedSetterEvent.swift
@@ -0,0 +1,21 @@
+//
+// UnrecognizedSetterEvent.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 10/4/24.
+//
+
+/// If the web view calls `onSetterFunctionCalled` with a `setter` argument the SDK doesn’t know how to handle.
+struct UnrecognizedSetterEvent: ConnectAnalyticEvent {
+ struct Metadata: Encodable {
+ /// The `setter` property sent from web
+ let setter: String
+
+ /// The pageViewID from the web view
+ /// - Note: May be null if not yet sent from web
+ let pageViewId: String?
+ }
+
+ let name = "component.web.warn.unrecognized_setter_function"
+ let metadata: Metadata
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Extensions/Encodable+Connect.swift b/StripeConnect/StripeConnect/Source/Internal/Extensions/Encodable+Connect.swift
new file mode 100644
index 00000000000..2552e6f8570
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Extensions/Encodable+Connect.swift
@@ -0,0 +1,30 @@
+//
+// Encodable+Connect.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 10/4/24.
+//
+
+private enum JSONSerializationError: Int, Error {
+ /// The encoded object was expected to be a dictionary but turned out to be a single value
+ case expectedDictionary = 0
+}
+
+extension Encodable {
+ /// Encodes to a JSON serialized object with the given encoder and options
+ func jsonObject(
+ with encoder: JSONEncoder
+ ) throws -> Any {
+ let data = try encoder.encode(self)
+ return try JSONSerialization.jsonObject(with: data, options: .allowFragments)
+ }
+
+ /// Encodes to a JSON dictionary with the given encoder
+ func jsonDictionary(with encoder: JSONEncoder) throws -> [String: Any] {
+ let json = try jsonObject(with: encoder)
+ guard let dict = json as? [String: Any] else {
+ throw JSONSerializationError.expectedDictionary
+ }
+ return dict
+ }
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Extensions/Error+extensions.swift b/StripeConnect/StripeConnect/Source/Internal/Extensions/Error+extensions.swift
new file mode 100644
index 00000000000..9a30b1b806a
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Extensions/Error+extensions.swift
@@ -0,0 +1,13 @@
+//
+// Error+extensions.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 10/4/24.
+//
+
+extension Error {
+ var analyticsIdentifier: String {
+ let nsError = self as NSError
+ return "\(nsError.domain):\(nsError.code)"
+ }
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Extensions/JSONEncoder+extension.swift b/StripeConnect/StripeConnect/Source/Internal/Extensions/JSONEncoder+extension.swift
index ad319b76301..70a356b71c0 100644
--- a/StripeConnect/StripeConnect/Source/Internal/Extensions/JSONEncoder+extension.swift
+++ b/StripeConnect/StripeConnect/Source/Internal/Extensions/JSONEncoder+extension.swift
@@ -8,10 +8,19 @@
import Foundation
extension JSONEncoder {
- static var connectEncoder: JSONEncoder {
+ /// Encoder used for JS Messaging and URL param encoding
+ static let connectEncoder: JSONEncoder = {
let encoder = JSONEncoder()
// Ensure keys are sorted for test stability.
encoder.outputFormatting = .sortedKeys
return encoder
- }
+ }()
+
+ /// Encoder used for analytics
+ static let analyticsEncoder: JSONEncoder = {
+ let encoder = JSONEncoder()
+ encoder.keyEncodingStrategy = .convertToSnakeCase
+ encoder.dateEncodingStrategy = .secondsSince1970
+ return encoder
+ }()
}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Extensions/URL+extension.swift b/StripeConnect/StripeConnect/Source/Internal/Extensions/URL+extension.swift
new file mode 100644
index 00000000000..8e528315042
--- /dev/null
+++ b/StripeConnect/StripeConnect/Source/Internal/Extensions/URL+extension.swift
@@ -0,0 +1,21 @@
+//
+// URL+extension.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 10/4/24.
+//
+
+extension URL {
+ /// Removes query and hashtag params from the absolute URL.
+ /// - Note: Used for logging sanitized URLs to analytics or to compare URLs without query args
+ var absoluteStringRemovingParams: String {
+ // Remove query params
+ var components = URLComponents(url: self, resolvingAgainstBaseURL: true)
+ components?.queryItems = nil
+
+ let absoluteString = components?.url?.absoluteString ?? self.absoluteString
+
+ // Remove hashtag params
+ return absoluteString.split(separator: "#").first.map(String.init) ?? ""
+ }
+}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Webview/ApplicationURLOpener.swift b/StripeConnect/StripeConnect/Source/Internal/Webview/ApplicationURLOpener.swift
index ada8cc09bb4..f2467099bd1 100644
--- a/StripeConnect/StripeConnect/Source/Internal/Webview/ApplicationURLOpener.swift
+++ b/StripeConnect/StripeConnect/Source/Internal/Webview/ApplicationURLOpener.swift
@@ -5,8 +5,18 @@
// Created by Chris Mays on 8/14/24.
//
+@_spi(STP) import StripeCore
import UIKit
+/// Error thrown when the system can't open a URL.
+/// Used for analytics logging.
+struct URLOpenError: Error, AnalyticLoggableErrorV2 {
+ let url: URL
+
+ func analyticLoggableSerializeForLogging() -> [String: Any] {
+ ["url": url.absoluteStringRemovingParams]
+ }
+}
/// Protocol used to dependency inject `UIApplication.open` for use in tests
protocol ApplicationURLOpener {
func canOpenURL(_ url: URL) -> Bool
@@ -26,10 +36,9 @@ extension ApplicationURLOpener {
typealias OpenCompletionHandler = (Bool) -> Void
#endif
- func openIfPossible(_ url: URL) {
+ func openIfPossible(_ url: URL) throws {
guard canOpenURL(url) else {
- // TODO: MXMOBILE-2491 Log as analytics when url can't be opened.
- return
+ throw URLOpenError(url: url)
}
open(url, options: [:], completionHandler: nil)
}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Webview/ConnectComponentWebViewController.swift b/StripeConnect/StripeConnect/Source/Internal/Webview/ConnectComponentWebViewController.swift
index 7f8f75be32b..a6660846246 100644
--- a/StripeConnect/StripeConnect/Source/Internal/Webview/ConnectComponentWebViewController.swift
+++ b/StripeConnect/StripeConnect/Source/Internal/Webview/ConnectComponentWebViewController.swift
@@ -28,7 +28,7 @@ class ConnectComponentWebViewController: ConnectWebViewController {
/// Manages authenticated web views
private let authenticatedWebViewManager: AuthenticatedWebViewManager
- private let setterMessageHandler: OnSetterFunctionCalledMessageHandler = .init()
+ private lazy var setterMessageHandler: OnSetterFunctionCalledMessageHandler = .init(analyticsClient: analyticsClient)
private var didFailLoadWithError: (Error) -> Void
@@ -43,6 +43,7 @@ class ConnectComponentWebViewController: ConnectWebViewController {
componentManager: EmbeddedComponentManager,
componentType: ComponentType,
loadContent: Bool,
+ analyticsClientFactory: ComponentAnalyticsClientFactory,
fetchInitProps: @escaping () -> InitProps,
didFailLoadWithError: @escaping (Error) -> Void,
// Should only be overridden for tests
@@ -66,7 +67,13 @@ class ConnectComponentWebViewController: ConnectWebViewController {
// embedded YouTube videos.
config.allowsInlineMediaPlayback = true
- super.init(configuration: config)
+ super.init(
+ configuration: config,
+ analyticsClient: analyticsClientFactory(.init(
+ apiClient: componentManager.apiClient,
+ component: componentType
+ ))
+ )
// Setup views
webView.addSubview(activityIndicator)
@@ -83,11 +90,19 @@ class ConnectComponentWebViewController: ConnectWebViewController {
addMessageHandlers(fetchInitProps: fetchInitProps)
addNotificationObservers()
+ // Log created event
+ analyticsClient.log(event: ComponentCreatedEvent())
+
// Load the web page
if loadContent {
activityIndicator.startAnimating()
- let url = ConnectJSURLParams(component: componentType, apiClient: componentManager.apiClient).url
- webView.load(.init(url: url))
+ do {
+ let url = try ConnectJSURLParams(component: componentType, apiClient: componentManager.apiClient).url()
+ analyticsClient.loadStart = .now
+ webView.load(.init(url: url))
+ } catch {
+ showAlertAndLog(error: error)
+ }
}
}
@@ -95,6 +110,7 @@ class ConnectComponentWebViewController: ConnectWebViewController {
convenience init(componentManager: EmbeddedComponentManager,
componentType: ComponentType,
loadContent: Bool,
+ analyticsClientFactory: ComponentAnalyticsClientFactory,
didFailLoadWithError: @escaping (Error) -> Void,
// Should only be overridden for tests
notificationCenter: NotificationCenter = NotificationCenter.default,
@@ -103,6 +119,7 @@ class ConnectComponentWebViewController: ConnectWebViewController {
self.init(componentManager: componentManager,
componentType: componentType,
loadContent: loadContent,
+ analyticsClientFactory: analyticsClientFactory,
fetchInitProps: VoidPayload.init,
didFailLoadWithError: didFailLoadWithError,
notificationCenter: notificationCenter,
@@ -123,19 +140,41 @@ class ConnectComponentWebViewController: ConnectWebViewController {
}
}
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+ analyticsClient.logComponentViewed(viewedAt: .now)
+ }
+
// MARK: - ConnectWebViewController
+ override func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+ super.webView(webView, didFinish: navigation)
+
+ analyticsClient.logComponentWebPageLoaded(loadEnd: .now)
+ }
+
override func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: any Error) {
super.webView(webView, didFail: navigation, withError: error)
+
didFailLoad(error: error)
- // TODO: MXMOBILE-2491 log error
+ analyticsClient.log(event: PageLoadErrorEvent(metadata: .init(
+ error: error,
+ url: webView.url
+ )))
}
override func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy {
+ // If the component web page fails to load with an HTTP error, send a
+ // load failure to event
if let response = navigationResponse.response as? HTTPURLResponse,
+ response.url?.absoluteStringRemovingParams == StripeConnectConstants.connectJSBaseURL.absoluteString,
response.hasErrorStatus {
- didFailLoad(error: HTTPStatusError(errorCode: response.statusCode))
- // TODO: MXMOBILE-2491 log error
+ let error = HTTPStatusError(errorCode: response.statusCode)
+ didFailLoad(error: error)
+ analyticsClient.log(event: PageLoadErrorEvent(metadata: .init(
+ error: error,
+ url: response.url
+ )))
}
return await super.webView(webView, decidePolicyFor: navigationResponse)
@@ -164,8 +203,11 @@ extension ConnectComponentWebViewController {
/// Convenience method to send messages to the webview.
func sendMessage(_ sender: any MessageSender) {
- if let message = sender.javascriptMessage {
+ do {
+ let message = try sender.javascriptMessage()
webView.evaluateJavaScript(message)
+ } catch {
+ analyticsClient.logClientError(error)
}
}
@@ -184,7 +226,8 @@ private extension ConnectComponentWebViewController {
fetchInitProps: @escaping () -> InitProps
) {
addMessageHandler(setterMessageHandler)
- addMessageHandler(OnLoaderStartMessageHandler { [activityIndicator] _ in
+ addMessageHandler(OnLoaderStartMessageHandler { [analyticsClient, activityIndicator] _ in
+ analyticsClient.logComponentLoaded(loadEnd: .now)
activityIndicator.stopAnimating()
})
addMessageHandler(FetchInitParamsMessageHandler.init(didReceiveMessage: {[weak self] _ in
@@ -198,20 +241,20 @@ private extension ConnectComponentWebViewController {
fonts: componentManager.fonts.map({ .init(customFontSource: $0) }))
}))
addMessageHandler(FetchInitComponentPropsMessageHandler(fetchInitProps))
- addMessageHandler(OnLoadErrorMessageHandler { [weak self] value in
- self?.didFailLoad(error: value.error.connectEmbedError)
+ addMessageHandler(OnLoadErrorMessageHandler { [weak self, analyticsClient] value in
+ self?.didFailLoad(error: value.error.connectEmbedError(analyticsClient: analyticsClient))
})
- addMessageHandler(DebugMessageHandler())
+ addMessageHandler(DebugMessageHandler(analyticsClient: analyticsClient))
addMessageHandler(FetchClientSecretMessageHandler { [weak self] _ in
await self?.componentManager.fetchClientSecret()
})
- addMessageHandler(PageDidLoadMessageHandler { _ in
- // TODO: MXMOBILE-2491 Use this for analytics
+ addMessageHandler(PageDidLoadMessageHandler(analyticsClient: analyticsClient) { [analyticsClient] payload in
+ analyticsClient.pageViewId = payload.pageViewId
})
- addMessageHandler(AccountSessionClaimedMessageHandler{ _ in
- // TODO: MXMOBILE-2491 Use this for analytics
+ addMessageHandler(AccountSessionClaimedMessageHandler(analyticsClient: analyticsClient) { [analyticsClient] payload in
+ analyticsClient.merchantId = payload.merchantId
})
- addMessageHandler(OpenAuthenticatedWebViewMessageHandler { [weak self] payload in
+ addMessageHandler(OpenAuthenticatedWebViewMessageHandler(analyticsClient: analyticsClient) { [weak self] payload in
self?.openAuthenticatedWebView(payload)
})
}
@@ -244,13 +287,15 @@ private extension ConnectComponentWebViewController {
func openAuthenticatedWebView(_ payload: OpenAuthenticatedWebViewMessageHandler.Payload) {
Task { @MainActor in
do {
- // TODO: MXMOBILE-2491 log `component.authenticated_web.*` analytic
+ analyticsClient.logAuthenticatedWebViewOpenedEvent(id: payload.id)
+
let returnUrl = try await authenticatedWebViewManager.present(with: payload.url, from: view)
+ analyticsClient.logAuthenticatedWebViewEventComplete(id: payload.id, redirected: returnUrl != nil)
+
sendMessage(ReturnedFromAuthenticatedWebViewSender(payload: .init(url: returnUrl, id: payload.id)))
} catch {
- // TODO: MXMOBILE-2491 log `component.authenticated_web.error` analytic
- debugPrint("Error returning from authenticated web view: \(error)")
+ analyticsClient.logAuthenticatedWebViewEventComplete(id: payload.id, error: error)
}
}
}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Webview/ConnectWebViewController.swift b/StripeConnect/StripeConnect/Source/Internal/Webview/ConnectWebViewController.swift
index 49e715b2bdf..45a5053f2eb 100644
--- a/StripeConnect/StripeConnect/Source/Internal/Webview/ConnectWebViewController.swift
+++ b/StripeConnect/StripeConnect/Source/Internal/Webview/ConnectWebViewController.swift
@@ -9,6 +9,11 @@ import SafariServices
@_spi(STP) import StripeCore
import WebKit
+enum ConnectWebViewControllerError: Int, Error {
+ case downloadFileDoesNotExist
+ case multipleDownloads
+}
+
/**
Custom implementation of a web view that handles:
- Camera access
@@ -30,14 +35,19 @@ class ConnectWebViewController: UIViewController {
/// The file manager responsible for creating temporary file directories to store downloads
let fileManager: FileManager
+ /// The analytics client used to log load errors
+ let analyticsClient: ComponentAnalyticsClient
+
/// The current version for the SDK
let sdkVersion: String?
init(configuration: WKWebViewConfiguration,
+ analyticsClient: ComponentAnalyticsClient,
// Only override for tests
urlOpener: ApplicationURLOpener = UIApplication.shared,
fileManager: FileManager = .default,
sdkVersion: String? = StripeAPIConfiguration.STPSDKVersion) {
+ self.analyticsClient = analyticsClient
self.urlOpener = urlOpener
self.fileManager = fileManager
self.sdkVersion = sdkVersion
@@ -60,6 +70,8 @@ class ConnectWebViewController: UIViewController {
fatalError("init(coder:) has not been implemented")
}
+ // MARK: - UIViewController
+
override func loadView() {
view = webView
}
@@ -70,6 +82,20 @@ class ConnectWebViewController: UIViewController {
// Default disable swipe to dismiss
parent?.isModalInPresentation = true
}
+
+ // MARK: - Internal
+
+ func showAlertAndLog(error: Error,
+ file: StaticString = #file,
+ line: UInt = #line) {
+ analyticsClient.logClientError(error, file: file, line: line)
+
+ let alert = UIAlertController(
+ title: nil,
+ message: NSError.stp_unexpectedErrorMessage(),
+ preferredStyle: .alert)
+ present(alert, animated: true)
+ }
}
// MARK: - Private
@@ -80,6 +106,7 @@ private extension ConnectWebViewController {
func openInPopup(configuration: WKWebViewConfiguration,
navigationAction: WKNavigationAction) -> WKWebView? {
let popupVC = PopupWebViewController(configuration: configuration,
+ analyticsClient: analyticsClient,
navigationAction: navigationAction,
urlOpener: urlOpener,
sdkVersion: sdkVersion)
@@ -102,18 +129,11 @@ private extension ConnectWebViewController {
// Opens with UIApplication.open, if supported
func openOnSystem(url: URL) {
- urlOpener.openIfPossible(url)
- }
-
- func showErrorAlert(for error: Error?) {
- // TODO: MXMOBILE-2491 Log analytic when receiving an error
- debugPrint(String(describing: error))
-
- let alert = UIAlertController(
- title: nil,
- message: NSError.stp_unexpectedErrorMessage(),
- preferredStyle: .alert)
- present(alert, animated: true)
+ do {
+ try urlOpener.openIfPossible(url)
+ } catch {
+ analyticsClient.logClientError(error)
+ }
}
func cleanupDownloadedFile() {
@@ -182,6 +202,10 @@ extension ConnectWebViewController: WKUIDelegate {
@available(iOS 15, *)
extension ConnectWebViewController: WKNavigationDelegate {
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+ // Override from subclass
+ }
+
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: any Error) {
// Override from subclass
}
@@ -248,10 +272,7 @@ extension ConnectWebViewController {
func download(decideDestinationUsing response: URLResponse,
suggestedFilename: String) async -> URL? {
if downloadedFile != nil {
- // If there's already a downloaded file, it means there were multiple
- // simultaneous downloads or we didn't clean up the URL correctly
- // TODO: MXMOBILE-2491 Log error analytic
- debugPrint("Multiple downloads")
+ analyticsClient.logClientError(ConnectWebViewControllerError.multipleDownloads)
}
// The temporary filename must be unique or the download will fail.
@@ -263,7 +284,7 @@ extension ConnectWebViewController {
do {
try fileManager.createDirectory(at: tempDir, withIntermediateDirectories: true)
} catch {
- showErrorAlert(for: error)
+ showAlertAndLog(error: error)
return nil
}
@@ -273,7 +294,7 @@ extension ConnectWebViewController {
func download(didFailWithError error: any Error,
resumeData: Data?) {
- showErrorAlert(for: error)
+ showAlertAndLog(error: error)
}
func downloadDidFinish() {
@@ -282,8 +303,7 @@ extension ConnectWebViewController {
// `downloadedFile` should never be nil here
// If file doesn't exist, it indicates something went wrong creating
// the temp file or the system deleted the temp file too quickly
- // TODO: MXMOBILE-2491 Log error analytic
- showErrorAlert(for: nil)
+ showAlertAndLog(error: ConnectWebViewControllerError.downloadFileDoesNotExist)
cleanupDownloadedFile()
return
}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/AccountSessionClaimedMessageHandler.swift b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/AccountSessionClaimedMessageHandler.swift
index d016195cbee..c063ff8427c 100644
--- a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/AccountSessionClaimedMessageHandler.swift
+++ b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/AccountSessionClaimedMessageHandler.swift
@@ -13,7 +13,10 @@ class AccountSessionClaimedMessageHandler: ScriptMessageHandler Void) {
- super.init(name: "accountSessionClaimed", didReceiveMessage: didReceiveMessage)
+ init(analyticsClient: ComponentAnalyticsClient,
+ didReceiveMessage: @escaping (Payload) -> Void) {
+ super.init(name: "accountSessionClaimed",
+ analyticsClient: analyticsClient,
+ didReceiveMessage: didReceiveMessage)
}
}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/DebugMessageHandler.swift b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/DebugMessageHandler.swift
index 35a492fef11..401cbcb6e70 100644
--- a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/DebugMessageHandler.swift
+++ b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/DebugMessageHandler.swift
@@ -9,7 +9,10 @@ import Foundation
// Emitted when the SDK should print to the console in debug mode.
class DebugMessageHandler: ScriptMessageHandler {
- init(didReceiveMessage: @escaping (String) -> Void = { Swift.debugPrint($0) }) {
- super.init(name: "debug", didReceiveMessage: didReceiveMessage)
+ init(analyticsClient: ComponentAnalyticsClient,
+ didReceiveMessage: @escaping (String) -> Void = { Swift.debugPrint($0) }) {
+ super.init(name: "debug",
+ analyticsClient: analyticsClient,
+ didReceiveMessage: didReceiveMessage)
}
}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/Helpers/OnSetterFunctionCalledMessageHandler.swift b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/Helpers/OnSetterFunctionCalledMessageHandler.swift
index 60f4abe16da..3b5e85ce8c1 100644
--- a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/Helpers/OnSetterFunctionCalledMessageHandler.swift
+++ b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/Helpers/OnSetterFunctionCalledMessageHandler.swift
@@ -64,9 +64,11 @@ class OnSetterFunctionCalledMessageHandler: ScriptMessageHandler: NSObject, WKScriptMessageHandler {
+ struct UnexpectedMessageNameError: Error, AnalyticLoggableErrorV2 {
+ let actual: String
+ let expected: String
+
+ func analyticLoggableSerializeForLogging() -> [String: Any] {
+ [
+ "actual": actual,
+ "expected": expected,
+ ]
+ }
+ }
+
let name: String
let didReceiveMessage: (Payload) -> Void
+ let analyticsClient: ComponentAnalyticsClient
init(name: String,
+ analyticsClient: ComponentAnalyticsClient,
didReceiveMessage: @escaping (Payload) -> Void) {
self.name = name
self.didReceiveMessage = didReceiveMessage
+ self.analyticsClient = analyticsClient
}
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
guard message.name == name else {
- debugPrint("Unexpected message name: \(message.name)")
+ analyticsClient.logClientError(UnexpectedMessageNameError(
+ actual: message.name,
+ expected: name
+ ))
return
}
do {
didReceiveMessage(try message.toDecodable())
} catch {
- // TODO: MXMOBILE-2491 Log as analytics
- debugPrint("Failed to decode body for message with name: \(message.name) \(error.localizedDescription)")
+ analyticsClient.logDeserializeMessageErrorEvent(message: message.name, error: error)
}
}
}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/Helpers/ScriptMessageHandlerWithReply.swift b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/Helpers/ScriptMessageHandlerWithReply.swift
index 3b37b184354..f43fa3e99e4 100644
--- a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/Helpers/ScriptMessageHandlerWithReply.swift
+++ b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/Helpers/ScriptMessageHandlerWithReply.swift
@@ -28,12 +28,8 @@ class ScriptMessageHandlerWithReply: NS
do {
let payload: Payload = try message.toDecodable()
let value = try await didReceiveMessage(payload)
- let responseData = try JSONEncoder.connectEncoder.encode(value)
-
- guard let response = try? JSONSerialization.jsonObject(with: responseData, options: .allowFragments) else {
- return (nil, "Failed to encode response")
- }
+ let response = try value.jsonObject(with: .connectEncoder)
return (response, nil)
} catch {
debugPrint("Error processing message: \((error as NSError).debugDescription)")
diff --git a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/OnLoadErrorMessageHandler.swift b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/OnLoadErrorMessageHandler.swift
index 8481dfba650..efdf21fdb5b 100644
--- a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/OnLoadErrorMessageHandler.swift
+++ b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/OnLoadErrorMessageHandler.swift
@@ -8,10 +8,13 @@
import Foundation
extension OnLoadErrorMessageHandler.Values.ErrorValue {
- var connectEmbedError: EmbeddedComponentError {
+ func connectEmbedError(analyticsClient: ComponentAnalyticsClient) -> EmbeddedComponentError {
// API Error is a catch all so defer to that if we get an unknown type.
- // TODO(MXMOBILE-2491): Log error analytic if `type` is unrecognized
- .init(type: .init(rawValue: type) ?? .apiError, description: message)
+ let errorType = EmbeddedComponentError.ErrorType(rawValue: type)
+ if errorType == nil {
+ analyticsClient.logUnexpectedLoadErrorType(type: type)
+ }
+ return .init(type: .init(rawValue: type) ?? .apiError, description: message)
}
}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/OpenAuthenticatedWebViewMessageHandler.swift b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/OpenAuthenticatedWebViewMessageHandler.swift
index 53a7900a459..893f75b0232 100644
--- a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/OpenAuthenticatedWebViewMessageHandler.swift
+++ b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/OpenAuthenticatedWebViewMessageHandler.swift
@@ -15,7 +15,10 @@ class OpenAuthenticatedWebViewMessageHandler: ScriptMessageHandler Void) {
- super.init(name: "openAuthenticatedWebView", didReceiveMessage: didReceiveMessage)
+ init(analyticsClient: ComponentAnalyticsClient,
+ didReceiveMessage: @escaping (Payload) -> Void) {
+ super.init(name: "openAuthenticatedWebView",
+ analyticsClient: analyticsClient,
+ didReceiveMessage: didReceiveMessage)
}
}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/PageDidLoadMessageHandler.swift b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/PageDidLoadMessageHandler.swift
index 69b5602ed9b..7ec8718c54f 100644
--- a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/PageDidLoadMessageHandler.swift
+++ b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageHandlers/PageDidLoadMessageHandler.swift
@@ -13,7 +13,10 @@ class PageDidLoadMessageHandler: ScriptMessageHandler Void) {
- super.init(name: "pageDidLoad", didReceiveMessage: didReceiveMessage)
+ init(analyticsClient: ComponentAnalyticsClient,
+ didReceiveMessage: @escaping (Payload) -> Void) {
+ super.init(name: "pageDidLoad",
+ analyticsClient: analyticsClient,
+ didReceiveMessage: didReceiveMessage)
}
}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageSenders/MessageSender.swift b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageSenders/MessageSender.swift
index dca24085af7..75628da5c7a 100644
--- a/StripeConnect/StripeConnect/Source/Internal/Webview/MessageSenders/MessageSender.swift
+++ b/StripeConnect/StripeConnect/Source/Internal/Webview/MessageSenders/MessageSender.swift
@@ -7,6 +7,11 @@
import Foundation
+private enum MessageSenderError: Int, Error {
+ /// Error encoding the json to utf-8
+ case stringEncoding
+}
+
/// Sends a message to the webview by calling a function on `window`
protocol MessageSender {
associatedtype Payload: Encodable
@@ -17,11 +22,10 @@ protocol MessageSender {
}
extension MessageSender {
- var javascriptMessage: String? {
- guard let jsonData = try? JSONEncoder.connectEncoder.encode(payload),
- let jsonString = String(data: jsonData, encoding: .utf8) else {
- // TODO: MXMOBILE-2491 Log failure to analytics
- return nil
+ func javascriptMessage() throws -> String {
+ let jsonData = try JSONEncoder.connectEncoder.encode(payload)
+ guard let jsonString = String(data: jsonData, encoding: .utf8) else {
+ throw MessageSenderError.stringEncoding
}
return "window.\(name)(\(jsonString));"
}
diff --git a/StripeConnect/StripeConnect/Source/Internal/Webview/PopupWebViewController.swift b/StripeConnect/StripeConnect/Source/Internal/Webview/PopupWebViewController.swift
index 93fea7c59e3..15ae5d1dbb1 100644
--- a/StripeConnect/StripeConnect/Source/Internal/Webview/PopupWebViewController.swift
+++ b/StripeConnect/StripeConnect/Source/Internal/Webview/PopupWebViewController.swift
@@ -16,12 +16,14 @@ class PopupWebViewController: ConnectWebViewController {
private var titleObserver: NSKeyValueObservation?
init(configuration: WKWebViewConfiguration,
+ analyticsClient: ComponentAnalyticsClient,
navigationAction: WKNavigationAction,
urlOpener: ApplicationURLOpener = UIApplication.shared,
sdkVersion: String? = StripeAPIConfiguration.STPSDKVersion) {
super.init(configuration: configuration,
- urlOpener: urlOpener,
- sdkVersion: sdkVersion)
+ analyticsClient: analyticsClient,
+ urlOpener: urlOpener,
+ sdkVersion: sdkVersion)
webView.load(navigationAction.request)
// Keep navbar title in sync with web view
diff --git a/StripeConnect/StripeConnectTests/Components/AccountManagementViewControllerTests.swift b/StripeConnect/StripeConnectTests/Components/AccountManagementViewControllerTests.swift
index 9027a61be47..65fde0a62dc 100644
--- a/StripeConnect/StripeConnectTests/Components/AccountManagementViewControllerTests.swift
+++ b/StripeConnect/StripeConnectTests/Components/AccountManagementViewControllerTests.swift
@@ -20,6 +20,7 @@ class AccountManagementViewControllerTests: XCTestCase {
super.setUp()
STPAPIClient.shared.publishableKey = "pk_test"
componentManager.shouldLoadContent = false
+ componentManager.analyticsClientFactory = MockComponentAnalyticsClient.init
}
@MainActor
diff --git a/StripeConnect/StripeConnectTests/Components/AccountOnboardingViewControllerTests.swift b/StripeConnect/StripeConnectTests/Components/AccountOnboardingViewControllerTests.swift
index afae58f21c9..32e2aef3226 100644
--- a/StripeConnect/StripeConnectTests/Components/AccountOnboardingViewControllerTests.swift
+++ b/StripeConnect/StripeConnectTests/Components/AccountOnboardingViewControllerTests.swift
@@ -20,6 +20,7 @@ class AccountOnboardingViewControllerTests: XCTestCase {
super.setUp()
STPAPIClient.shared.publishableKey = "pk_test"
componentManager.shouldLoadContent = false
+ componentManager.analyticsClientFactory = MockComponentAnalyticsClient.init
}
@MainActor
diff --git a/StripeConnect/StripeConnectTests/Components/NotificationBannerViewControllerTests.swift b/StripeConnect/StripeConnectTests/Components/NotificationBannerViewControllerTests.swift
index 885428c03fe..df37c21aeaa 100644
--- a/StripeConnect/StripeConnectTests/Components/NotificationBannerViewControllerTests.swift
+++ b/StripeConnect/StripeConnectTests/Components/NotificationBannerViewControllerTests.swift
@@ -20,6 +20,7 @@ class NotificationBannerViewControllerTests: XCTestCase {
super.setUp()
STPAPIClient.shared.publishableKey = "pk_test"
componentManager.shouldLoadContent = false
+ componentManager.analyticsClientFactory = MockComponentAnalyticsClient.init
}
@MainActor
diff --git a/StripeConnect/StripeConnectTests/Components/PayoutsViewControllerTests.swift b/StripeConnect/StripeConnectTests/Components/PayoutsViewControllerTests.swift
index a5199eeba8e..6ceb3df8ef8 100644
--- a/StripeConnect/StripeConnectTests/Components/PayoutsViewControllerTests.swift
+++ b/StripeConnect/StripeConnectTests/Components/PayoutsViewControllerTests.swift
@@ -34,6 +34,7 @@ class PayoutsViewControllerTests: XCTestCase {
super.setUp()
STPAPIClient.shared.publishableKey = "pk_test"
componentManager.shouldLoadContent = false
+ componentManager.analyticsClientFactory = MockComponentAnalyticsClient.init
}
@MainActor
diff --git a/StripeConnect/StripeConnectTests/Helpers/ComponentAnalyticsClient+Mock.swift b/StripeConnect/StripeConnectTests/Helpers/ComponentAnalyticsClient+Mock.swift
new file mode 100644
index 00000000000..9dce9bf5fa7
--- /dev/null
+++ b/StripeConnect/StripeConnectTests/Helpers/ComponentAnalyticsClient+Mock.swift
@@ -0,0 +1,12 @@
+//
+// ComponentAnalyticsClient+Mock.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 11/7/24.
+//
+
+@testable import StripeConnect
+
+extension ComponentAnalyticsClient.CommonFields {
+ static let mock = ComponentAnalyticsClient.CommonFields(platformId: nil, livemode: nil, component: .onboarding, componentInstance: .init())
+}
diff --git a/StripeConnect/StripeConnectTests/Helpers/MockComponentAnalyticsClient.swift b/StripeConnect/StripeConnectTests/Helpers/MockComponentAnalyticsClient.swift
new file mode 100644
index 00000000000..f524f1d6e8b
--- /dev/null
+++ b/StripeConnect/StripeConnectTests/Helpers/MockComponentAnalyticsClient.swift
@@ -0,0 +1,18 @@
+//
+// MockComponentAnalyticsClient.swift
+// StripeConnect
+//
+// Created by Mel Ludowise on 11/7/24.
+//
+
+@testable import StripeConnect
+@_spi(STP) import StripeCoreTestUtils
+import XCTest
+
+class MockComponentAnalyticsClient: ComponentAnalyticsClient {
+ init(commonFields: CommonFields) {
+ super.init(client: MockAnalyticsClientV2(), commonFields: commonFields)
+ }
+
+ // TODO: Add test helpers
+}
diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/ConnectComponentWebViewControllerTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/ConnectComponentWebViewControllerTests.swift
index 204ada941e6..a6bc5ba8718 100644
--- a/StripeConnect/StripeConnectTests/Internal/Webview/ConnectComponentWebViewControllerTests.swift
+++ b/StripeConnect/StripeConnectTests/Internal/Webview/ConnectComponentWebViewControllerTests.swift
@@ -25,6 +25,7 @@ class ConnectComponentWebViewControllerTests: XCTestCase {
let webVC = ConnectComponentWebViewController(componentManager: componentManager,
componentType: .payouts,
loadContent: false,
+ analyticsClientFactory: MockComponentAnalyticsClient.init,
didFailLoadWithError: { _ in })
try await webVC.webView.evaluateMessageWithReply(name: "fetchClientSecret",
@@ -39,6 +40,7 @@ class ConnectComponentWebViewControllerTests: XCTestCase {
let webVC = ConnectComponentWebViewController(componentManager: componentManager,
componentType: .payouts,
loadContent: false,
+ analyticsClientFactory: MockComponentAnalyticsClient.init,
didFailLoadWithError: { _ in },
webLocale: Locale(identifier: "fr_FR"))
@@ -53,6 +55,7 @@ class ConnectComponentWebViewControllerTests: XCTestCase {
let webVC = ConnectComponentWebViewController(componentManager: componentManager,
componentType: .payouts,
loadContent: false,
+ analyticsClientFactory: MockComponentAnalyticsClient.init,
didFailLoadWithError: { _ in },
webLocale: Locale(identifier: "fr_FR"))
var appearance = EmbeddedComponentManager.Appearance()
@@ -77,10 +80,11 @@ class ConnectComponentWebViewControllerTests: XCTestCase {
let componentManager = componentManagerAssertingOnFetch(appearance: appearance)
let webVC = ConnectComponentWebViewController(componentManager: componentManager,
- componentType: .payouts,
- loadContent: false,
- didFailLoadWithError: { _ in },
- webLocale: Locale(identifier: "fr_FR"))
+ componentType: .payouts,
+ loadContent: false,
+ analyticsClientFactory: MockComponentAnalyticsClient.init,
+ didFailLoadWithError: { _ in },
+ webLocale: Locale(identifier: "fr_FR"))
webVC.triggerTraitCollectionChange(style: .dark)
@@ -98,6 +102,7 @@ class ConnectComponentWebViewControllerTests: XCTestCase {
let webVC = ConnectComponentWebViewController(componentManager: componentManager,
componentType: .payouts,
loadContent: false,
+ analyticsClientFactory: MockComponentAnalyticsClient.init,
didFailLoadWithError: { _ in },
webLocale: Locale(identifier: "fr_FR"))
@@ -116,6 +121,7 @@ class ConnectComponentWebViewControllerTests: XCTestCase {
let webVC = ConnectComponentWebViewController(componentManager: componentManager,
componentType: .payouts,
loadContent: false,
+ analyticsClientFactory: MockComponentAnalyticsClient.init,
didFailLoadWithError: { _ in },
webLocale: Locale(identifier: "fr_FR"))
@@ -137,6 +143,7 @@ class ConnectComponentWebViewControllerTests: XCTestCase {
componentManager: componentManager,
componentType: .payouts,
loadContent: false,
+ analyticsClientFactory: MockComponentAnalyticsClient.init,
didFailLoadWithError: { _ in },
notificationCenter: notificationCenter,
webLocale: Locale(identifier: "fr_FR"))
@@ -157,6 +164,7 @@ class ConnectComponentWebViewControllerTests: XCTestCase {
let webVC = ConnectComponentWebViewController(componentManager: componentManager,
componentType: .payouts,
loadContent: false,
+ analyticsClientFactory: MockComponentAnalyticsClient.init,
didFailLoadWithError: { _ in },
webLocale: Locale(identifier: "fr_FR"))
@@ -173,6 +181,7 @@ class ConnectComponentWebViewControllerTests: XCTestCase {
let webVC = ConnectComponentWebViewController(componentManager: componentManager,
componentType: .payouts,
loadContent: false,
+ analyticsClientFactory: MockComponentAnalyticsClient.init,
didFailLoadWithError: { _ in })
// Mock that loading indicator is animating
webVC.activityIndicator.startAnimating()
@@ -190,6 +199,7 @@ class ConnectComponentWebViewControllerTests: XCTestCase {
let webVC = ConnectComponentWebViewController(componentManager: componentManager,
componentType: .payouts,
loadContent: false,
+ analyticsClientFactory: MockComponentAnalyticsClient.init,
didFailLoadWithError: { error = $0 })
// Mock that loading indicator is animating
webVC.activityIndicator.startAnimating()
@@ -206,6 +216,7 @@ class ConnectComponentWebViewControllerTests: XCTestCase {
let webVC = ConnectComponentWebViewController(componentManager: componentManager,
componentType: .payouts,
loadContent: false,
+ analyticsClientFactory: MockComponentAnalyticsClient.init,
didFailLoadWithError: { error = $0 })
// Mock that loading indicator is animating
webVC.activityIndicator.startAnimating()
@@ -223,8 +234,9 @@ class ConnectComponentWebViewControllerTests: XCTestCase {
let webVC = ConnectComponentWebViewController(componentManager: componentManager,
componentType: .payouts,
loadContent: false,
+ analyticsClientFactory: MockComponentAnalyticsClient.init,
didFailLoadWithError: { error = $0 })
- _ = await webVC.webView(webVC.webView, decidePolicyFor: MockNavigationResponse(response: HTTPURLResponse(url: URL(string: "https://stripe.com")!, statusCode: 404, httpVersion: nil, headerFields: nil)!))
+ _ = await webVC.webView(webVC.webView, decidePolicyFor: MockNavigationResponse(response: HTTPURLResponse(url: URL(string: "https://connect-js.stripe.com/v1.0/ios_webview.html")!, statusCode: 404, httpVersion: nil, headerFields: nil)!))
XCTAssertEqual((error as? HTTPStatusError)?.errorCode, 404)
// Loading indicator should stop
XCTAssertFalse(webVC.activityIndicator.isAnimating)
@@ -239,6 +251,7 @@ class ConnectComponentWebViewControllerTests: XCTestCase {
let webVC = ConnectComponentWebViewController(componentManager: componentManager,
componentType: .payouts,
loadContent: false,
+ analyticsClientFactory: MockComponentAnalyticsClient.init,
didFailLoadWithError: { _ in },
authenticatedWebViewManager: authenticatedWebViewManager)
diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/ConnectWebViewControllerTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/ConnectWebViewControllerTests.swift
index 7ef0ca5b5f6..95412fdb1e3 100644
--- a/StripeConnect/StripeConnectTests/Internal/Webview/ConnectWebViewControllerTests.swift
+++ b/StripeConnect/StripeConnectTests/Internal/Webview/ConnectWebViewControllerTests.swift
@@ -17,13 +17,16 @@ class ConnectWebViewControllerTests: XCTestCase {
private var mockURLOpener: MockURLOpener!
private var mockFileManager: MockFileManager!
+ private var mockAnalyticsClient: MockComponentAnalyticsClient!
private var webVC: ConnectWebViewControllerTestWrapper!
override func setUp() {
super.setUp()
mockFileManager = .init()
mockURLOpener = .init()
+ mockAnalyticsClient = .init(commonFields: .mock)
webVC = .init(configuration: .init(),
+ analyticsClient: mockAnalyticsClient,
urlOpener: mockURLOpener,
fileManager: mockFileManager,
sdkVersion: "1.2.3")
@@ -316,7 +319,7 @@ class MockNavigationResponse: WKNavigationResponse {
}
}
-private class MockURLOpener: ApplicationURLOpener {
+class MockURLOpener: ApplicationURLOpener {
var canOpenURLOverride: ((_ url: URL) -> Bool)?
var openURLOverride: ((_ url: URL, _ options: [UIApplication.OpenExternalURLOptionsKey: Any], _ completion: OpenCompletionHandler?) -> Void)?
@@ -361,7 +364,7 @@ private class MockFileManager: FileManager {
}
private class ConnectWebViewControllerTestWrapper: ConnectWebViewController {
- var presentPopup: (UIViewController) -> Void = {_ in }
+ var presentPopup: (UIViewController) -> Void = { _ in }
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
presentPopup(viewControllerToPresent)
diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/AccountSessionClaimedMessageHandlerTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/AccountSessionClaimedMessageHandlerTests.swift
index 9756d76934e..ca5bbbb0c29 100644
--- a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/AccountSessionClaimedMessageHandlerTests.swift
+++ b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/AccountSessionClaimedMessageHandlerTests.swift
@@ -13,10 +13,13 @@ class AccountSessionClaimedMessageHandlerTests: ScriptWebTestBase {
let expectation = self.expectation(description: "Message received")
let merchantId = "acct_1234"
- webView.addMessageHandler(messageHandler: AccountSessionClaimedMessageHandler(didReceiveMessage: { payload in
- expectation.fulfill()
- XCTAssertEqual(payload, .init(merchantId: merchantId))
- }))
+ webView.addMessageHandler(messageHandler: AccountSessionClaimedMessageHandler(
+ analyticsClient: MockComponentAnalyticsClient(commonFields: .mock),
+ didReceiveMessage: { payload in
+ expectation.fulfill()
+ XCTAssertEqual(payload, .init(merchantId: merchantId))
+ }
+ ))
webView.evaluateAccountSessionClaimed(merchantId: merchantId)
waitForExpectations(timeout: TestHelpers.defaultTimeout, handler: nil)
diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/DebugMessageHandlerTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/DebugMessageHandlerTests.swift
index b7445cad1e3..f574db6ab50 100644
--- a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/DebugMessageHandlerTests.swift
+++ b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/DebugMessageHandlerTests.swift
@@ -13,10 +13,13 @@ class DebugMessageHandlerTests: ScriptWebTestBase {
let expectation = self.expectation(description: "Message received")
let debugMessage = "test message"
- webView.addMessageHandler(messageHandler: DebugMessageHandler(didReceiveMessage: { payload in
- expectation.fulfill()
- XCTAssertEqual(payload, debugMessage)
- }))
+ webView.addMessageHandler(messageHandler: DebugMessageHandler(
+ analyticsClient: MockComponentAnalyticsClient(commonFields: .mock),
+ didReceiveMessage: { payload in
+ expectation.fulfill()
+ XCTAssertEqual(payload, debugMessage)
+ }
+ ))
webView.evaluateDebugMessage(message: debugMessage)
diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/NotificationBanner/OnNotificationsChangeHandlerTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/NotificationBanner/OnNotificationsChangeHandlerTests.swift
index f0e09242b83..81df4525bcf 100644
--- a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/NotificationBanner/OnNotificationsChangeHandlerTests.swift
+++ b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/NotificationBanner/OnNotificationsChangeHandlerTests.swift
@@ -12,7 +12,7 @@ class OnNotificationsChangeHandlerTests: ScriptWebTestBase {
@MainActor
func testMessageSend() async throws {
let expectation = self.expectation(description: "Message received")
- let messageHandler = OnSetterFunctionCalledMessageHandler()
+ let messageHandler = OnSetterFunctionCalledMessageHandler(analyticsClient: MockComponentAnalyticsClient(commonFields: .mock))
messageHandler.addHandler(handler: OnNotificationsChangeHandler(didReceiveMessage: { payload in
expectation.fulfill()
diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OnLoadErrorMessageHandlerTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OnLoadErrorMessageHandlerTests.swift
index b92497c4308..79b32b54d0e 100644
--- a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OnLoadErrorMessageHandlerTests.swift
+++ b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OnLoadErrorMessageHandlerTests.swift
@@ -12,7 +12,7 @@ class OnLoadErrorMessageHandlerTests: ScriptWebTestBase {
@MainActor
func testMessageSend() async throws {
let expectation = self.expectation(description: "Message received")
- let messageHandler = OnSetterFunctionCalledMessageHandler()
+ let messageHandler = OnSetterFunctionCalledMessageHandler(analyticsClient: MockComponentAnalyticsClient(commonFields: .mock))
messageHandler.addHandler(handler: OnLoadErrorMessageHandler(didReceiveMessage: { payload in
expectation.fulfill()
diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OnLoaderStartMessageHandlerTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OnLoaderStartMessageHandlerTests.swift
index 60b0fd0db08..88df372c8d6 100644
--- a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OnLoaderStartMessageHandlerTests.swift
+++ b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OnLoaderStartMessageHandlerTests.swift
@@ -13,7 +13,7 @@ class OnLoaderStartMessageHandlerTests: ScriptWebTestBase {
func testMessageSend() async throws {
let expectation = self.expectation(description: "Message received")
- let messageHandler = OnSetterFunctionCalledMessageHandler()
+ let messageHandler = OnSetterFunctionCalledMessageHandler(analyticsClient: MockComponentAnalyticsClient(commonFields: .mock))
messageHandler.addHandler(handler: OnLoaderStartMessageHandler(didReceiveMessage: { payload in
expectation.fulfill()
diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OnSetterFunctionCalledMessageHandlerTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OnSetterFunctionCalledMessageHandlerTests.swift
index 8a1abdf7742..1925bd15b13 100644
--- a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OnSetterFunctionCalledMessageHandlerTests.swift
+++ b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OnSetterFunctionCalledMessageHandlerTests.swift
@@ -12,7 +12,7 @@ class OnSetterFunctionCalledMessageHandlerTests: ScriptWebTestBase {
func testDeallocation() {
weak var weakInstance: OnSetterFunctionCalledMessageHandler?
autoreleasepool {
- let instance = OnSetterFunctionCalledMessageHandler()
+ let instance = OnSetterFunctionCalledMessageHandler(analyticsClient: MockComponentAnalyticsClient(commonFields: .mock))
weakInstance = instance
XCTAssertNotNil(weakInstance)
}
diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/Onboarding/OnExitMessageHandlerTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/Onboarding/OnExitMessageHandlerTests.swift
index 10f103fd830..7a4fb13552c 100644
--- a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/Onboarding/OnExitMessageHandlerTests.swift
+++ b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/Onboarding/OnExitMessageHandlerTests.swift
@@ -12,7 +12,7 @@ class OnExitMessageHandlerTests: ScriptWebTestBase {
@MainActor
func testMessageSend() async throws {
let expectation = self.expectation(description: "Message received")
- let messageHandler = OnSetterFunctionCalledMessageHandler()
+ let messageHandler = OnSetterFunctionCalledMessageHandler(analyticsClient: MockComponentAnalyticsClient(commonFields: .mock))
messageHandler.addHandler(handler: OnExitMessageHandler(didReceiveMessage: {
expectation.fulfill()
diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OpenAuthenticatedWebViewMessageHandlerTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OpenAuthenticatedWebViewMessageHandlerTests.swift
index 3382be6c61e..30932d769b3 100644
--- a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OpenAuthenticatedWebViewMessageHandlerTests.swift
+++ b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/OpenAuthenticatedWebViewMessageHandlerTests.swift
@@ -13,10 +13,13 @@ class OpenAuthenticatedWebViewMessageHandlerTests: ScriptWebTestBase {
let expectation = self.expectation(description: "Message received")
let url = "https://dashboard.stripe.com"
let id = "1234"
- webView.addMessageHandler(messageHandler: OpenAuthenticatedWebViewMessageHandler(didReceiveMessage: { payload in
- expectation.fulfill()
- XCTAssertEqual(payload, .init(url: URL(string: "https://dashboard.stripe.com")!, id: id))
- }))
+ webView.addMessageHandler(messageHandler: OpenAuthenticatedWebViewMessageHandler(
+ analyticsClient: MockComponentAnalyticsClient(commonFields: .mock),
+ didReceiveMessage: { payload in
+ expectation.fulfill()
+ XCTAssertEqual(payload, .init(url: URL(string: "https://dashboard.stripe.com")!, id: id))
+ }
+ ))
webView.evaluateOpenAuthenticatedWebView(url: url, id: id)
diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/PageDidLoadMessageHandlerTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/PageDidLoadMessageHandlerTests.swift
index 29678b9b9e9..fbf2e273f66 100644
--- a/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/PageDidLoadMessageHandlerTests.swift
+++ b/StripeConnect/StripeConnectTests/Internal/Webview/MessageHandlers/PageDidLoadMessageHandlerTests.swift
@@ -14,10 +14,13 @@ class PageDidLoadMessageHandlerTests: ScriptWebTestBase {
let pageViewId = "123"
- webView.addMessageHandler(messageHandler: PageDidLoadMessageHandler(didReceiveMessage: { payload in
- expectation.fulfill()
- XCTAssertEqual(payload, .init(pageViewId: pageViewId))
- }))
+ webView.addMessageHandler(messageHandler: PageDidLoadMessageHandler(
+ analyticsClient: MockComponentAnalyticsClient(commonFields: .mock),
+ didReceiveMessage: { payload in
+ expectation.fulfill()
+ XCTAssertEqual(payload, .init(pageViewId: pageViewId))
+ }
+ ))
webView.evaluatePageDidLoad(pageViewId: pageViewId)
diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/MessageSenders/CallSetterWithSerializableValueSenderTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/MessageSenders/CallSetterWithSerializableValueSenderTests.swift
index 6dd379d3e50..7ebc80a8f52 100644
--- a/StripeConnect/StripeConnectTests/Internal/Webview/MessageSenders/CallSetterWithSerializableValueSenderTests.swift
+++ b/StripeConnect/StripeConnectTests/Internal/Webview/MessageSenders/CallSetterWithSerializableValueSenderTests.swift
@@ -14,9 +14,9 @@ class CallSetterWithSerializableValueSenderTests: ScriptWebTestBase {
try validateMessageSent(sender: CallSetterWithSerializableValueSender(payload: .init(setter: "setPayment", value: "pi_1234")))
}
- func testSenderSignature() {
+ func testSenderSignature() throws {
XCTAssertEqual(
- CallSetterWithSerializableValueSender(payload: .init(setter: "setPayment", value: "pi_1234")).javascriptMessage,
+ try CallSetterWithSerializableValueSender(payload: .init(setter: "setPayment", value: "pi_1234")).javascriptMessage(),
"""
window.callSetterWithSerializableValue({"setter":"setPayment","value":"pi_1234"});
"""
diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/MessageSenders/ReturnedFromAuthenticatedWebViewSenderTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/MessageSenders/ReturnedFromAuthenticatedWebViewSenderTests.swift
index 88216bca75a..66cad0ee0e4 100644
--- a/StripeConnect/StripeConnectTests/Internal/Webview/MessageSenders/ReturnedFromAuthenticatedWebViewSenderTests.swift
+++ b/StripeConnect/StripeConnectTests/Internal/Webview/MessageSenders/ReturnedFromAuthenticatedWebViewSenderTests.swift
@@ -16,7 +16,7 @@ class ReturnedFromAuthenticatedWebViewSenderTests: ScriptWebTestBase {
func testSenderSignature() {
XCTAssertEqual(
- ReturnedFromAuthenticatedWebViewSender(payload: .init(url: URL(string: "https://dashboard.stripe.com")!, id: "123")).javascriptMessage,
+ try ReturnedFromAuthenticatedWebViewSender(payload: .init(url: URL(string: "https://dashboard.stripe.com")!, id: "123")).javascriptMessage(),
"""
window.returnedFromAuthenticatedWebView({"id":"123","url":"https:\\/\\/dashboard.stripe.com"});
"""
diff --git a/StripeConnect/StripeConnectTests/Internal/Webview/MessageSenders/UpdateConnectInstanceSenderTests.swift b/StripeConnect/StripeConnectTests/Internal/Webview/MessageSenders/UpdateConnectInstanceSenderTests.swift
index 949b10cac33..75dedc3d842 100644
--- a/StripeConnect/StripeConnectTests/Internal/Webview/MessageSenders/UpdateConnectInstanceSenderTests.swift
+++ b/StripeConnect/StripeConnectTests/Internal/Webview/MessageSenders/UpdateConnectInstanceSenderTests.swift
@@ -16,7 +16,7 @@ class UpdateConnectInstanceSenderTests: ScriptWebTestBase {
func testSenderSignature() {
XCTAssertEqual(
- UpdateConnectInstanceSender(payload: .init(locale: "en", appearance: .default)).javascriptMessage,
+ try UpdateConnectInstanceSender(payload: .init(locale: "en", appearance: .default)).javascriptMessage(),
"""
window.updateConnectInstance({"appearance":{"variables":{"fontFamily":"-apple-system","fontSizeBase":"16px"}},"locale":"en"});
"""
diff --git a/StripeConnect/StripeConnectTests/WebView+Tests.swift b/StripeConnect/StripeConnectTests/WebView+Tests.swift
index 2ec38d1cc92..f8bcbba9dd2 100644
--- a/StripeConnect/StripeConnectTests/WebView+Tests.swift
+++ b/StripeConnect/StripeConnectTests/WebView+Tests.swift
@@ -115,7 +115,7 @@ extension WKWebView {
}
func sendMessage(sender: Sender) throws {
- evaluateJavaScript(try XCTUnwrap(sender.javascriptMessage)) { (_, error) in
+ evaluateJavaScript(try sender.javascriptMessage()) { (_, error) in
if let error {
XCTFail("JavaScript execution failed: \(error)")
}
diff --git a/StripeCore/StripeCore/Source/API Bindings/STPAPIClient.swift b/StripeCore/StripeCore/Source/API Bindings/STPAPIClient.swift
index 49de3b5026d..dcd7cdd505b 100644
--- a/StripeCore/StripeCore/Source/API Bindings/STPAPIClient.swift
+++ b/StripeCore/StripeCore/Source/API Bindings/STPAPIClient.swift
@@ -43,7 +43,7 @@ import UIKit
/// A publishable key that only contains publishable keys and not secret keys.
///
/// If a secret key is found, returns "[REDACTED_LIVE_KEY]".
- var sanitizedPublishableKey: String? {
+ @_spi(STP) public var sanitizedPublishableKey: String? {
guard let publishableKey = publishableKey else {
return nil
}
diff --git a/StripeCore/StripeCore/Source/Analytics/AnalyticsClientV2.swift b/StripeCore/StripeCore/Source/Analytics/AnalyticsClientV2.swift
index f50b9db9f63..b1f5592b4ff 100644
--- a/StripeCore/StripeCore/Source/Analytics/AnalyticsClientV2.swift
+++ b/StripeCore/StripeCore/Source/Analytics/AnalyticsClientV2.swift
@@ -90,7 +90,14 @@ import UIKit
let payload = payload(withEventName: eventName, parameters: parameters)
#if DEBUG
- NSLog("LOG ANALYTICS: \(payload)")
+ let jsonString = String(
+ data: try! JSONSerialization.data(
+ withJSONObject: payload,
+ options: [.sortedKeys, .prettyPrinted]
+ ),
+ encoding: .utf8
+ )!
+ NSLog("LOG ANALYTICS: \(jsonString)")
#endif
guard AnalyticsClientV2.shouldCollectAnalytics else {