diff --git a/StripeCore/StripeCore/Source/Attestation/StripeAttest.swift b/StripeCore/StripeCore/Source/Attestation/StripeAttest.swift index 6cd6af981f3..4dd3c14ea46 100644 --- a/StripeCore/StripeCore/Source/Attestation/StripeAttest.swift +++ b/StripeCore/StripeCore/Source/Attestation/StripeAttest.swift @@ -113,25 +113,25 @@ import UIKit } } - @_spi(STP) public enum AttestationError: Error { + @_spi(STP) public enum AttestationError: String, Error { /// Attestation is not supported on this device. - case attestationNotSupported + case attestationNotSupported = "attestation_not_supported" /// Device ID is unavailable. - case noDeviceID + case noDeviceID = "no_device_id" /// App ID is unavailable. - case noAppID + case noAppID = "no_app_id" /// Retried assertion, but it failed. - case secondAssertionFailureAfterRetryingAttestation + case secondAssertionFailureAfterRetryingAttestation = "second_assertion_failure_after_retrying_attestation" /// Can't attest any more keys today. - case attestationRateLimitExceeded + case attestationRateLimitExceeded = "attestation_rate_limit_exceeded" /// The challenge couldn't be converted to UTF-8 data. - case invalidChallengeData + case invalidChallengeData = "invalid_challenge_data" /// The backend asked us not to attest - case shouldNotAttest + case shouldNotAttest = "should_not_attest" /// The backend asked us to attest, but the key is already attested - case shouldAttestButKeyIsAlreadyAttested + case shouldAttestButKeyIsAlreadyAttested = "should_attest_but_key_is_already_attested" /// A publishable key was not set - case noPublishableKey + case noPublishableKey = "no_publishable_key" } // MARK: - Internal diff --git a/StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj b/StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj index 8f6e6c66f25..0c36c4a2e14 100644 --- a/StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj +++ b/StripeFinancialConnections/StripeFinancialConnections.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ 495539EE2C484DC200543D18 /* FinancialConnectionsTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 495539ED2C484DC200543D18 /* FinancialConnectionsTheme.swift */; }; 496A6AE72C29E0BB00D34F8E /* testmode@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 496A6AE62C29E0BB00D34F8E /* testmode@3x.png */; }; 497142BC2C514B08000DFA64 /* FlowRouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497142BB2C514B08000DFA64 /* FlowRouterTests.swift */; }; + 499EEAFD2D3E948B00E1BE85 /* FinancialConnectionsAPIClientLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 499EEAFC2D3E948B00E1BE85 /* FinancialConnectionsAPIClientLogger.swift */; }; 49A0B5862C5D2F3C00D697D9 /* FinancialConnectionsAPIClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49A0B5852C5D2F3C00D697D9 /* FinancialConnectionsAPIClientTests.swift */; }; 49AC518C2C52DE2C00B712CC /* FinancialConnectionsLinkLoginPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AC518B2C52DE2C00B712CC /* FinancialConnectionsLinkLoginPane.swift */; }; 49C911372C597EAF00589E0D /* LinkLoginDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C911332C597EAF00589E0D /* LinkLoginDataSource.swift */; }; @@ -330,6 +331,7 @@ 495539ED2C484DC200543D18 /* FinancialConnectionsTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsTheme.swift; sourceTree = ""; }; 496A6AE62C29E0BB00D34F8E /* testmode@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "testmode@3x.png"; sourceTree = ""; }; 497142BB2C514B08000DFA64 /* FlowRouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowRouterTests.swift; sourceTree = ""; }; + 499EEAFC2D3E948B00E1BE85 /* FinancialConnectionsAPIClientLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsAPIClientLogger.swift; sourceTree = ""; }; 49A0B5852C5D2F3C00D697D9 /* FinancialConnectionsAPIClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsAPIClientTests.swift; sourceTree = ""; }; 49AC518B2C52DE2C00B712CC /* FinancialConnectionsLinkLoginPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsLinkLoginPane.swift; sourceTree = ""; }; 49C911332C597EAF00589E0D /* LinkLoginDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkLoginDataSource.swift; sourceTree = ""; }; @@ -657,6 +659,7 @@ 4E2EAD7059FF8358E674774A /* FinancialConnectionsAPIClient.swift */, 49F1B8392D2DAE7100136303 /* FinancialConnectionsAsyncAPIClient.swift */, 49F1B83D2D2EC82300136303 /* FinancialConnectionsAsyncAPIClient+Legacy.swift */, + 499EEAFC2D3E948B00E1BE85 /* FinancialConnectionsAPIClientLogger.swift */, ); path = "API Bindings"; sourceTree = ""; @@ -1319,6 +1322,7 @@ CBF7BE2271D309F2B1E794CC /* FinancialConnectionsDataAccessNotice.swift in Sources */, F67624595BD2CD7B6793BFDA /* FinancialConnectionsImage.swift in Sources */, 07712610C7D2F484AAB96982 /* FinancialConnectionsInstitution.swift in Sources */, + 499EEAFD2D3E948B00E1BE85 /* FinancialConnectionsAPIClientLogger.swift in Sources */, 7386E1F9256B23CE29BF996D /* FinancialConnectionsInstitutionSearchResultResource.swift in Sources */, C7D2763ACCE2CC71E788E18F /* FinancialConnectionsLegalDetailsNotice.swift in Sources */, B271AAF41C9FE6AE392B88D3 /* FinancialConnectionsMixedOAuthParams.swift in Sources */, diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift index a9a492c4303..d61c038d382 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift @@ -24,6 +24,8 @@ final class FinancialConnectionsAPIClient { var consumerPublishableKey: String? var consumerSession: ConsumerSessionData? + private lazy var logger = FinancialConnectionsAPIClientLogger() + var requestSurface: String { isLinkWithStripe ? "ios_instant_debits" : "ios_connections" } @@ -46,17 +48,21 @@ final class FinancialConnectionsAPIClient { /// Applies attestation-related parameters to the given base parameters /// In case of an assertion error, returns the unmodified base parameters func assertAndApplyAttestationParameters( - to baseParameters: [String: Any] + to baseParameters: [String: Any], + api: FinancialConnectionsAPIClientLogger.API, + pane: FinancialConnectionsSessionManifest.NextPane ) -> Future<[String: Any]> { let promise = Promise<[String: Any]>() Task { do { let attest = backingAPIClient.stripeAttest let handle = try await attest.assert() + logger.log(.attestationRequestTokenSucceeded(api), pane: pane) let newParameters = baseParameters.merging(handle.assertion.requestFields) { (_, new) in new } promise.resolve(with: newParameters) } catch { // Fail silently if we can't get an assertion, we'll try the request anyway. It may fail. + logger.log(.attestationRequestTokenFailed(api, error), pane: pane) promise.resolve(with: baseParameters) } } @@ -64,10 +70,15 @@ final class FinancialConnectionsAPIClient { } /// Marks the assertion as completed and forwards attestation errors to the `StripeAttest` client for logging. - func completeAssertion(possibleError: Error?) { + func completeAssertion( + possibleError: Error?, + api: FinancialConnectionsAPIClientLogger.API, + pane: FinancialConnectionsSessionManifest.NextPane + ) { let attest = backingAPIClient.stripeAttest Task { if let error = possibleError, StripeAttest.isLinkAssertionError(error: error) { + logger.log(.attestationVerdictFailed(api), pane: pane) await attest.receivedAssertionError(error) } await attest.assertionCompleted() @@ -139,11 +150,16 @@ protocol FinancialConnectionsAPI { var consumerPublishableKey: String? { get set } var consumerSession: ConsumerSessionData? { get set } - func completeAssertion(possibleError: Error?) + func completeAssertion( + possibleError: Error?, + api: FinancialConnectionsAPIClientLogger.API, + pane: FinancialConnectionsSessionManifest.NextPane + ) func synchronize( clientSecret: String, - returnURL: String? + returnURL: String?, + initialSynchronize: Bool ) -> Future func fetchFinancialConnectionsAccounts( @@ -256,7 +272,8 @@ protocol FinancialConnectionsAPI { clientSecret: String, sessionId: String, emailSource: FinancialConnectionsAPIClient.EmailSource, - useMobileEndpoints: Bool + useMobileEndpoints: Bool, + pane: FinancialConnectionsSessionManifest.NextPane ) -> Future // MARK: - Link API's @@ -285,7 +302,8 @@ protocol FinancialConnectionsAPI { amount: Int?, currency: String?, incentiveEligibilitySession: ElementsSessionContext.IntentID?, - useMobileEndpoints: Bool + useMobileEndpoints: Bool, + pane: FinancialConnectionsSessionManifest.NextPane ) -> Future func attachLinkConsumerToLinkAccountSession( @@ -348,7 +366,8 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI { func synchronize( clientSecret: String, - returnURL: String? + returnURL: String?, + initialSynchronize: Bool = false ) -> Future { var parameters: [String: Any] = [ "expand": ["manifest.active_auth_session"], @@ -363,9 +382,14 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI { ] mobileParameters["app_return_url"] = returnURL - let attest = backingAPIClient.stripeAttest - mobileParameters["supports_app_verification"] = attest.isSupported - mobileParameters["verified_app_id"] = Bundle.main.bundleIdentifier + if initialSynchronize { + let attestationIsSupported = backingAPIClient.stripeAttest.isSupported + mobileParameters["supports_app_verification"] = attestationIsSupported + mobileParameters["verified_app_id"] = Bundle.main.bundleIdentifier + if !attestationIsSupported { + logger.log(.attestationInitFailed, pane: .consent) + } + } parameters["mobile"] = mobileParameters return self.post( @@ -915,7 +939,8 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI { clientSecret: String, sessionId: String, emailSource: FinancialConnectionsAPIClient.EmailSource, - useMobileEndpoints: Bool + useMobileEndpoints: Bool, + pane: FinancialConnectionsSessionManifest.NextPane ) -> Future { var parameters: [String: Any] = [ "email_address": @@ -928,17 +953,20 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI { parameters["request_surface"] = requestSurface parameters["session_id"] = sessionId parameters["email_source"] = emailSource.rawValue - return assertAndApplyAttestationParameters(to: parameters) - .chained { [weak self] updatedParameters in - guard let self else { - return Promise(error: FinancialConnectionsSheetError.unknown(debugDescription: "FinancialConnectionsAPIClient was deallocated.")) - } - return self.post( - resource: APIMobileEndpointConsumerSessionLookup, - parameters: updatedParameters, - useConsumerPublishableKeyIfNeeded: false - ) + return assertAndApplyAttestationParameters( + to: parameters, + api: .consumerSessionLookup, + pane: pane + ).chained { [weak self] updatedParameters in + guard let self else { + return Promise(error: FinancialConnectionsSheetError.unknown(debugDescription: "FinancialConnectionsAPIClient was deallocated.")) } + return self.post( + resource: APIMobileEndpointConsumerSessionLookup, + parameters: updatedParameters, + useConsumerPublishableKeyIfNeeded: false + ) + } } else { parameters["client_secret"] = clientSecret return post( @@ -1001,7 +1029,8 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI { amount: Int?, currency: String?, incentiveEligibilitySession: ElementsSessionContext.IntentID?, - useMobileEndpoints: Bool + useMobileEndpoints: Bool, + pane: FinancialConnectionsSessionManifest.NextPane ) -> Future { var parameters: [String: Any] = [ "request_surface": requestSurface, @@ -1038,16 +1067,19 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI { } if useMobileEndpoints { - return assertAndApplyAttestationParameters(to: parameters) - .chained { [weak self] updatedParameters in - guard let self else { - return Promise(error: FinancialConnectionsSheetError.unknown(debugDescription: "FinancialConnectionsAPIClient was deallocated.")) - } - return self.post( - resource: APIMobileEndpointLinkAccountSignUp, - parameters: updatedParameters, - useConsumerPublishableKeyIfNeeded: false - ) + return assertAndApplyAttestationParameters( + to: parameters, + api: .linkSignUp, + pane: pane + ).chained { [weak self] updatedParameters in + guard let self else { + return Promise(error: FinancialConnectionsSheetError.unknown(debugDescription: "FinancialConnectionsAPIClient was deallocated.")) + } + return self.post( + resource: APIMobileEndpointLinkAccountSignUp, + parameters: updatedParameters, + useConsumerPublishableKeyIfNeeded: false + ) } } else { return post( diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClientLogger.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClientLogger.swift new file mode 100644 index 00000000000..79a5c0cdc96 --- /dev/null +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClientLogger.swift @@ -0,0 +1,78 @@ +// +// FinancialConnectionsAPIClientLogger.swift +// StripeFinancialConnections +// +// Created by Mat Schmid on 2025-01-20. +// + +import Foundation +@_spi(STP) import StripeCore + +struct FinancialConnectionsAPIClientLogger { + private var analyticsClient = FinancialConnectionsAnalyticsClient() + + enum API: String { + case consumerSessionLookup = "consumer_session_lookup" + case linkSignUp = "link_sign_up" + } + + enum Event { + /// When checking if generating attestation is supported does not succeed. + case attestationInitFailed + /// When an attestation token gets generated successfully. + case attestationRequestTokenSucceeded(API) + /// When a token generation attempt fails client-side. + case attestationRequestTokenFailed(API, Error) + /// When an attestation verdict fails backend side and we get an attestation related error. + case attestationVerdictFailed(API) + + var name: String { + switch self { + case .attestationInitFailed: + return "attestation.init_failed" + case .attestationRequestTokenSucceeded: + return "attestation.request_token_succeeded" + case .attestationRequestTokenFailed: + return "attestation.request_token_failed" + case .attestationVerdictFailed: + return "attestation.verdict_failed" + } + } + + var parameters: [String: Any] { + switch self { + case .attestationInitFailed: + var reason: String + if #available(iOS 14.0, *) { + // If the iOS version is supported, we assume the device is unsupported (i.e. simulator). + reason = "ios_device_unsupported" + } else { + // Otherwise, attestation is unavailable due to the OS version being unsupported. + reason = "ios_os_version_unsupported" + } + return ["reason": reason] + case .attestationRequestTokenFailed(let api, let error): + var errorReason: String + if let attestationError = error as? StripeAttest.AttestationError { + errorReason = attestationError.rawValue + } else { + errorReason = "unknown" + } + return [ + "api": api.rawValue, + "error_reason": errorReason, + ] + case .attestationRequestTokenSucceeded(let api), .attestationVerdictFailed(let api): + return ["api": api.rawValue] + } + } + } + + func log(_ event: Event, pane: FinancialConnectionsSessionManifest.NextPane) { + analyticsClient.log( + eventName: event.name, + parameters: event.parameters, + pane: pane + ) + } +} diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAsyncAPIClient+Legacy.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAsyncAPIClient+Legacy.swift index 7602da6df4a..443b52a358f 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAsyncAPIClient+Legacy.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAsyncAPIClient+Legacy.swift @@ -44,10 +44,15 @@ extension FinancialConnectionsAsyncAPIClient { extension FinancialConnectionsAsyncAPIClient: FinancialConnectionsAPI { func synchronize( clientSecret: String, - returnURL: String? + returnURL: String?, + initialSynchronize: Bool ) -> Future { wrapAsyncToFuture { - try await self.synchronize(clientSecret: clientSecret, returnURL: returnURL) + try await self.synchronize( + clientSecret: clientSecret, + returnURL: returnURL, + initialSynchronize: initialSynchronize + ) } } @@ -311,7 +316,8 @@ extension FinancialConnectionsAsyncAPIClient: FinancialConnectionsAPI { clientSecret: String, sessionId: String, emailSource: FinancialConnectionsAPIClient.EmailSource, - useMobileEndpoints: Bool + useMobileEndpoints: Bool, + pane: FinancialConnectionsSessionManifest.NextPane ) -> Future { wrapAsyncToFuture { try await self.consumerSessionLookup( @@ -319,7 +325,8 @@ extension FinancialConnectionsAsyncAPIClient: FinancialConnectionsAPI { clientSecret: clientSecret, sessionId: sessionId, emailSource: emailSource, - useMobileEndpoints: useMobileEndpoints + useMobileEndpoints: useMobileEndpoints, + pane: pane ) } } @@ -369,7 +376,8 @@ extension FinancialConnectionsAsyncAPIClient: FinancialConnectionsAPI { amount: Int?, currency: String?, incentiveEligibilitySession: ElementsSessionContext.IntentID?, - useMobileEndpoints: Bool + useMobileEndpoints: Bool, + pane: FinancialConnectionsSessionManifest.NextPane ) -> Future { wrapAsyncToFuture { try await self.linkAccountSignUp( @@ -379,7 +387,8 @@ extension FinancialConnectionsAsyncAPIClient: FinancialConnectionsAPI { amount: amount, currency: currency, incentiveEligibilitySession: incentiveEligibilitySession, - useMobileEndpoints: useMobileEndpoints + useMobileEndpoints: useMobileEndpoints, + pane: pane ) } } diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAsyncAPIClient.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAsyncAPIClient.swift index 475a4338316..acd166db5ad 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAsyncAPIClient.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAsyncAPIClient.swift @@ -23,6 +23,8 @@ final class FinancialConnectionsAsyncAPIClient { var consumerPublishableKey: String? var consumerSession: ConsumerSessionData? + private lazy var logger = FinancialConnectionsAPIClientLogger() + var requestSurface: String { isLinkWithStripe ? "ios_instant_debits" : "ios_connections" } @@ -43,10 +45,15 @@ final class FinancialConnectionsAsyncAPIClient { } /// Marks the assertion as completed and forwards attestation errors to the `StripeAttest` client for logging. - func completeAssertion(possibleError: Error?) { + func completeAssertion( + possibleError: Error?, + api: FinancialConnectionsAPIClientLogger.API, + pane: FinancialConnectionsSessionManifest.NextPane + ) { let attest = backingAPIClient.stripeAttest Task { if let error = possibleError, StripeAttest.isLinkAssertionError(error: error) { + logger.log(.attestationVerdictFailed(api), pane: pane) await attest.receivedAssertionError(error) } await attest.assertionCompleted() @@ -55,14 +62,20 @@ final class FinancialConnectionsAsyncAPIClient { /// Applies attestation-related parameters to the given base parameters /// In case of an assertion error, returns the unmodified base parameters - func assertAndApplyAttestationParameters(to baseParameters: [String: Any]) async -> [String: Any] { + func assertAndApplyAttestationParameters( + to baseParameters: [String: Any], + api: FinancialConnectionsAPIClientLogger.API, + pane: FinancialConnectionsSessionManifest.NextPane + ) async -> [String: Any] { do { let attest = backingAPIClient.stripeAttest let handle = try await attest.assert() + logger.log(.attestationRequestTokenSucceeded(api), pane: pane) let newParameters = baseParameters.merging(handle.assertion.requestFields) { (_, new) in new } return newParameters } catch { // Fail silently if we can't get an assertion, we'll try the request anyway. It may fail. + logger.log(.attestationRequestTokenFailed(api, error), pane: pane) return baseParameters } } @@ -169,7 +182,8 @@ final class FinancialConnectionsAsyncAPIClient { protocol FinancialConnectionsAsyncAPI { func synchronize( clientSecret: String, - returnURL: String? + returnURL: String?, + initialSynchronize: Bool ) async throws -> FinancialConnectionsSynchronize func fetchFinancialConnectionsAccounts( @@ -283,7 +297,8 @@ protocol FinancialConnectionsAsyncAPI { clientSecret: String, sessionId: String, emailSource: FinancialConnectionsAPIClient.EmailSource, - useMobileEndpoints: Bool + useMobileEndpoints: Bool, + pane: FinancialConnectionsSessionManifest.NextPane ) async throws -> LookupConsumerSessionResponse // MARK: - Link API's @@ -312,7 +327,8 @@ protocol FinancialConnectionsAsyncAPI { amount: Int?, currency: String?, incentiveEligibilitySession: ElementsSessionContext.IntentID?, - useMobileEndpoints: Bool + useMobileEndpoints: Bool, + pane: FinancialConnectionsSessionManifest.NextPane ) async throws -> LinkSignUpResponse func attachLinkConsumerToLinkAccountSession( @@ -351,7 +367,8 @@ protocol FinancialConnectionsAsyncAPI { extension FinancialConnectionsAsyncAPIClient: FinancialConnectionsAsyncAPI { func synchronize( clientSecret: String, - returnURL: String? + returnURL: String?, + initialSynchronize: Bool = false ) async throws -> FinancialConnectionsSynchronize { var parameters: [String: Any] = [ "expand": ["manifest.active_auth_session"], @@ -366,9 +383,14 @@ extension FinancialConnectionsAsyncAPIClient: FinancialConnectionsAsyncAPI { ] mobileParameters["app_return_url"] = returnURL - let attest = backingAPIClient.stripeAttest - mobileParameters["supports_app_verification"] = attest.isSupported - mobileParameters["verified_app_id"] = Bundle.main.bundleIdentifier + if initialSynchronize { + let attestationIsSupported = backingAPIClient.stripeAttest.isSupported + mobileParameters["supports_app_verification"] = attestationIsSupported + mobileParameters["verified_app_id"] = Bundle.main.bundleIdentifier + if !attestationIsSupported { + logger.log(.attestationInitFailed, pane: .consent) + } + } parameters["mobile"] = mobileParameters return try await post(endpoint: .synchronize, parameters: parameters) @@ -816,7 +838,8 @@ extension FinancialConnectionsAsyncAPIClient: FinancialConnectionsAsyncAPI { clientSecret: String, sessionId: String, emailSource: FinancialConnectionsAPIClient.EmailSource, - useMobileEndpoints: Bool + useMobileEndpoints: Bool, + pane: FinancialConnectionsSessionManifest.NextPane ) async throws -> LookupConsumerSessionResponse { var parameters: [String: Any] = [ "email_address": @@ -828,7 +851,11 @@ extension FinancialConnectionsAsyncAPIClient: FinancialConnectionsAsyncAPI { parameters["request_surface"] = requestSurface parameters["session_id"] = sessionId parameters["email_source"] = emailSource.rawValue - let updatedParameters = await assertAndApplyAttestationParameters(to: parameters) + let updatedParameters = await assertAndApplyAttestationParameters( + to: parameters, + api: .consumerSessionLookup, + pane: pane + ) return try await post(endpoint: .mobileConsumerSessionLookup, parameters: updatedParameters) } else { parameters["client_secret"] = clientSecret @@ -890,7 +917,8 @@ extension FinancialConnectionsAsyncAPIClient: FinancialConnectionsAsyncAPI { amount: Int?, currency: String?, incentiveEligibilitySession: ElementsSessionContext.IntentID?, - useMobileEndpoints: Bool + useMobileEndpoints: Bool, + pane: FinancialConnectionsSessionManifest.NextPane ) async throws -> LinkSignUpResponse { var parameters: [String: Any] = [ "request_surface": requestSurface, @@ -926,7 +954,11 @@ extension FinancialConnectionsAsyncAPIClient: FinancialConnectionsAsyncAPI { } } if useMobileEndpoints { - let updatedParameters = await assertAndApplyAttestationParameters(to: parameters) + let updatedParameters = await assertAndApplyAttestationParameters( + to: parameters, + api: .linkSignUp, + pane: pane + ) return try await post(endpoint: .mobileLinkAccountSignup, parameters: updatedParameters) } else { return try await post(endpoint: .linkAccountsSignUp, parameters: parameters) diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/FinancialConnectionsAnalyticsClient.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/FinancialConnectionsAnalyticsClient.swift index 7526b07ef97..108d0fca504 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/FinancialConnectionsAnalyticsClient.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Analytics/FinancialConnectionsAnalyticsClient.swift @@ -174,6 +174,7 @@ extension FinancialConnectionsAnalyticsClient { additionalParameters["single_account"] = manifest.singleAccount additionalParameters["allow_manual_entry"] = manifest.allowManualEntry additionalParameters["account_holder_id"] = manifest.accountholderToken + additionalParameters["app_verification_enabled"] = manifest.appVerificationEnabled } static func paneFromViewController( diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Common/HostViewController.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Common/HostViewController.swift index 55b0e941fb5..9aaeaa4f1e0 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/Common/HostViewController.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Common/HostViewController.swift @@ -112,7 +112,8 @@ extension HostViewController { apiClient .synchronize( clientSecret: clientSecret, - returnURL: returnURL + returnURL: returnURL, + initialSynchronize: true ) .observe { [weak self] result in guard let self = self else { return } diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkLogin/LinkLoginDataSource.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkLogin/LinkLoginDataSource.swift index 0e3139b9843..648099ede84 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkLogin/LinkLoginDataSource.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkLogin/LinkLoginDataSource.swift @@ -23,7 +23,10 @@ protocol LinkLoginDataSource: AnyObject { func attachToAccountAndSynchronize( with linkSignUpResponse: LinkSignUpResponse ) -> Future - func completeAssertionIfNeeded(possibleError: Error?) + func completeAssertionIfNeeded( + possibleError: Error?, + api: FinancialConnectionsAPIClientLogger.API + ) } final class LinkLoginDataSourceImplementation: LinkLoginDataSource { @@ -56,7 +59,8 @@ final class LinkLoginDataSourceImplementation: LinkLoginDataSource { func synchronize() -> Future { apiClient.synchronize( clientSecret: clientSecret, - returnURL: returnURL + returnURL: returnURL, + initialSynchronize: false ) .chained { synchronize in if let linkLoginPane = synchronize.text?.linkLoginPane { @@ -73,7 +77,8 @@ final class LinkLoginDataSourceImplementation: LinkLoginDataSource { clientSecret: clientSecret, sessionId: manifest.id, emailSource: manuallyEntered ? .userAction : .customerObject, - useMobileEndpoints: manifest.verified + useMobileEndpoints: manifest.verified, + pane: .linkLogin ) } @@ -89,7 +94,8 @@ final class LinkLoginDataSourceImplementation: LinkLoginDataSource { amount: elementsSessionContext?.amount, currency: elementsSessionContext?.currency, incentiveEligibilitySession: elementsSessionContext?.incentiveEligibilitySession, - useMobileEndpoints: manifest.verified + useMobileEndpoints: manifest.verified, + pane: .linkLogin ) } @@ -107,7 +113,8 @@ final class LinkLoginDataSourceImplementation: LinkLoginDataSource { return apiClient.synchronize( clientSecret: self.clientSecret, - returnURL: self.returnURL + returnURL: self.returnURL, + initialSynchronize: false ) } } @@ -123,8 +130,15 @@ final class LinkLoginDataSourceImplementation: LinkLoginDataSource { } // Marks the assertion as completed and logs possible errors during verified flows. - func completeAssertionIfNeeded(possibleError: Error?) { + func completeAssertionIfNeeded( + possibleError: Error?, + api: FinancialConnectionsAPIClientLogger.API + ) { guard manifest.verified else { return } - apiClient.completeAssertion(possibleError: possibleError) + apiClient.completeAssertion( + possibleError: possibleError, + api: api, + pane: .linkLogin + ) } } diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkLogin/LinkLoginViewController.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkLogin/LinkLoginViewController.swift index 4f7027ac075..249f20dc046 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkLogin/LinkLoginViewController.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkLogin/LinkLoginViewController.swift @@ -171,7 +171,10 @@ final class LinkLoginViewController: UIViewController { footerButton?.isLoading = false guard let self else { return } - self.dataSource.completeAssertionIfNeeded(possibleError: result.error) + self.dataSource.completeAssertionIfNeeded( + possibleError: result.error, + api: .consumerSessionLookup + ) switch result { case .success(let response): @@ -214,7 +217,10 @@ final class LinkLoginViewController: UIViewController { .observe { [weak self] result in guard let self else { return } self.footerButton?.isLoading = false - self.dataSource.completeAssertionIfNeeded(possibleError: result.error) + self.dataSource.completeAssertionIfNeeded( + possibleError: result.error, + api: .linkSignUp + ) switch result { case .success(let response): diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/NetworkingLinkSignupDataSource.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/NetworkingLinkSignupDataSource.swift index d5ddefa7e37..296321dd0e7 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/NetworkingLinkSignupDataSource.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/NetworkingLinkSignupDataSource.swift @@ -20,7 +20,10 @@ protocol NetworkingLinkSignupDataSource: AnyObject { phoneNumber: String, countryCode: String ) -> Future - func completeAssertionIfNeeded(possibleError: Error?) + func completeAssertionIfNeeded( + possibleError: Error?, + api: FinancialConnectionsAPIClientLogger.API + ) } final class NetworkingLinkSignupDataSourceImplementation: NetworkingLinkSignupDataSource { @@ -54,7 +57,8 @@ final class NetworkingLinkSignupDataSourceImplementation: NetworkingLinkSignupDa func synchronize() -> Future { return apiClient.synchronize( clientSecret: clientSecret, - returnURL: returnURL + returnURL: returnURL, + initialSynchronize: false ) .chained { synchronize in if let networkingLinkSignup = synchronize.text?.networkingLinkSignupPane { @@ -71,7 +75,8 @@ final class NetworkingLinkSignupDataSourceImplementation: NetworkingLinkSignupDa clientSecret: clientSecret, sessionId: manifest.id, emailSource: manuallyEntered ? .userAction : .customerObject, - useMobileEndpoints: manifest.verified + useMobileEndpoints: manifest.verified, + pane: .networkingLinkSignupPane ) } @@ -90,7 +95,8 @@ final class NetworkingLinkSignupDataSourceImplementation: NetworkingLinkSignupDa amount: nil, currency: nil, incentiveEligibilitySession: nil, - useMobileEndpoints: manifest.verified + useMobileEndpoints: manifest.verified, + pane: .networkingLinkSignupPane ).chained { [weak self] response -> Future in guard let self else { return Promise(error: FinancialConnectionsSheetError.unknown( @@ -127,8 +133,15 @@ final class NetworkingLinkSignupDataSourceImplementation: NetworkingLinkSignupDa } // Marks the assertion as completed and logs possible errors during verified flows. - func completeAssertionIfNeeded(possibleError: Error?) { + func completeAssertionIfNeeded( + possibleError: Error?, + api: FinancialConnectionsAPIClientLogger.API + ) { guard manifest.verified else { return } - apiClient.completeAssertion(possibleError: possibleError) + apiClient.completeAssertion( + possibleError: possibleError, + api: api, + pane: .networkingLinkSignupPane + ) } } diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/NetworkingLinkSignupViewController.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/NetworkingLinkSignupViewController.swift index 9a01d4402fb..1975e93b226 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/NetworkingLinkSignupViewController.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkSignupPane/NetworkingLinkSignupViewController.swift @@ -213,7 +213,10 @@ final class NetworkingLinkSignupViewController: UIViewController { ) .observe { [weak self] result in guard let self = self else { return } - self.dataSource.completeAssertionIfNeeded(possibleError: result.error) + self.dataSource.completeAssertionIfNeeded( + possibleError: result.error, + api: .linkSignUp + ) switch result { case .success(let customSuccessPaneMessage): @@ -298,7 +301,10 @@ extension NetworkingLinkSignupViewController: LinkSignupFormViewDelegate { ) .observe { [weak self, weak bodyFormView] result in guard let self = self else { return } - self.dataSource.completeAssertionIfNeeded(possibleError: result.error) + self.dataSource.completeAssertionIfNeeded( + possibleError: result.error, + api: .consumerSessionLookup + ) switch result { case .success(let response): diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkVerification/NetworkingLinkVerificationDataSource.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkVerification/NetworkingLinkVerificationDataSource.swift index 1cb9b6502fd..e635cd1f142 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkVerification/NetworkingLinkVerificationDataSource.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/NetworkingLinkVerification/NetworkingLinkVerificationDataSource.swift @@ -106,7 +106,8 @@ final class NetworkingLinkVerificationDataSourceImplementation: NetworkingLinkVe return self.apiClient.synchronize( clientSecret: self.clientSecret, - returnURL: self.returnURL + returnURL: self.returnURL, + initialSynchronize: false ) } } diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/PartnerAuth/PartnerAuthDataSource.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/PartnerAuth/PartnerAuthDataSource.swift index 186a6dcd02a..39d848cad1c 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/PartnerAuth/PartnerAuthDataSource.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/PartnerAuth/PartnerAuthDataSource.swift @@ -77,7 +77,8 @@ final class PartnerAuthDataSourceImplementation: PartnerAuthDataSource { apiClient .synchronize( clientSecret: clientSecret, - returnURL: nil + returnURL: nil, + initialSynchronize: false ) .observe { [weak self] result in guard let self = self else { return } diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/NetworkingOTPView/NetworkingOTPDataSource.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/NetworkingOTPView/NetworkingOTPDataSource.swift index ff31375d743..b1fe3df1269 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/NetworkingOTPView/NetworkingOTPDataSource.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/NetworkingOTPView/NetworkingOTPDataSource.swift @@ -22,7 +22,10 @@ protocol NetworkingOTPDataSource: AnyObject { func lookupConsumerSession() -> Future func startVerificationSession() -> Future func confirmVerificationSession(otpCode: String) -> Future - func completeAssertionIfNeeded(possibleError: Error?) + func completeAssertionIfNeeded( + possibleError: Error?, + api: FinancialConnectionsAPIClientLogger.API + ) } final class NetworkingOTPDataSourceImplementation: NetworkingOTPDataSource { @@ -85,7 +88,8 @@ final class NetworkingOTPDataSourceImplementation: NetworkingOTPDataSource { clientSecret: clientSecret, sessionId: manifest.id, emailSource: .customerObject, - useMobileEndpoints: manifest.verified + useMobileEndpoints: manifest.verified, + pane: pane ) .chained { [weak self] lookupConsumerSessionResponse in self?.consumerSession = lookupConsumerSessionResponse.consumerSession @@ -123,9 +127,16 @@ final class NetworkingOTPDataSourceImplementation: NetworkingOTPDataSource { } // Marks the assertion as completed and logs possible errors during verified flows. - func completeAssertionIfNeeded(possibleError: Error?) { + func completeAssertionIfNeeded( + possibleError: Error?, + api: FinancialConnectionsAPIClientLogger.API + ) { guard manifest.verified else { return } - apiClient.completeAssertion(possibleError: possibleError) + apiClient.completeAssertion( + possibleError: possibleError, + api: api, + pane: pane + ) } } diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/NetworkingOTPView/NetworkingOTPView.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/NetworkingOTPView/NetworkingOTPView.swift index 0d4e78c3383..1b19f71e37c 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/NetworkingOTPView/NetworkingOTPView.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/NetworkingOTPView/NetworkingOTPView.swift @@ -160,7 +160,10 @@ final class NetworkingOTPView: UIView { dataSource.lookupConsumerSession() .observe { [weak self] result in guard let self = self else { return } - self.dataSource.completeAssertionIfNeeded(possibleError: result.error) + self.dataSource.completeAssertionIfNeeded( + possibleError: result.error, + api: .consumerSessionLookup + ) switch result { case .success(let lookupConsumerSessionResponse): diff --git a/StripeFinancialConnections/StripeFinancialConnectionsTests/EmptyFinancialConnectionsAPIClient.swift b/StripeFinancialConnections/StripeFinancialConnectionsTests/EmptyFinancialConnectionsAPIClient.swift index c782686c0ee..fd3add508d0 100644 --- a/StripeFinancialConnections/StripeFinancialConnectionsTests/EmptyFinancialConnectionsAPIClient.swift +++ b/StripeFinancialConnections/StripeFinancialConnectionsTests/EmptyFinancialConnectionsAPIClient.swift @@ -16,7 +16,11 @@ class EmptyFinancialConnectionsAPIClient: FinancialConnectionsAPI { var consumerPublishableKey: String? var consumerSession: StripeFinancialConnections.ConsumerSessionData? - func completeAssertion(possibleError: (any Error)?) {} + func completeAssertion( + possibleError: Error?, + api: FinancialConnectionsAPIClientLogger.API, + pane: FinancialConnectionsSessionManifest.NextPane + ) {} func fetchFinancialConnectionsAccounts(clientSecret: String, startingAfterAccountId: String?) -> Promise< StripeAPI.FinancialConnectionsSession.AccountList @@ -30,7 +34,8 @@ class EmptyFinancialConnectionsAPIClient: FinancialConnectionsAPI { func synchronize( clientSecret: String, - returnURL: String? + returnURL: String?, + initialSynchronize: Bool ) -> Future { return Promise() } @@ -178,7 +183,8 @@ class EmptyFinancialConnectionsAPIClient: FinancialConnectionsAPI { clientSecret: String, sessionId: String, emailSource: FinancialConnectionsAPIClient.EmailSource, - useMobileEndpoints: Bool + useMobileEndpoints: Bool, + pane: FinancialConnectionsSessionManifest.NextPane ) -> Future { return Promise() } @@ -213,7 +219,8 @@ class EmptyFinancialConnectionsAPIClient: FinancialConnectionsAPI { amount: Int?, currency: String?, incentiveEligibilitySession: ElementsSessionContext.IntentID?, - useMobileEndpoints: Bool + useMobileEndpoints: Bool, + pane: FinancialConnectionsSessionManifest.NextPane ) -> Future { return Promise() } diff --git a/StripeFinancialConnections/StripeFinancialConnectionsTests/FinancialConnectionsAPIClientTests.swift b/StripeFinancialConnections/StripeFinancialConnectionsTests/FinancialConnectionsAPIClientTests.swift index a58d117c9d6..6b943fd9f02 100644 --- a/StripeFinancialConnections/StripeFinancialConnectionsTests/FinancialConnectionsAPIClientTests.swift +++ b/StripeFinancialConnections/StripeFinancialConnectionsTests/FinancialConnectionsAPIClientTests.swift @@ -110,8 +110,11 @@ class FinancialConnectionsAPIClientTests: XCTestCase { ] let apiClient = FinancialConnectionsAPIClient(apiClient: mockApiClient) apiClient - .assertAndApplyAttestationParameters(to: baseParameters) - .observe { result in + .assertAndApplyAttestationParameters( + to: baseParameters, + api: .linkSignUp, + pane: .consent + ).observe { result in switch result { case .success(let updatedParameters): XCTAssertNotNil(updatedParameters["base_parameter"])