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/Source/API Bindings/FinancialConnectionsAPIClient.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift index c4d8f538f77..8c1c0561345 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClient.swift @@ -49,6 +49,7 @@ final class FinancialConnectionsAPIClient { /// In case of an assertion error, returns the unmodified base parameters func assertAndApplyAttestationParameters( to baseParameters: [String: Any], + api: FinancialConnectionsAPIClientLogger.API, pane: FinancialConnectionsSessionManifest.NextPane ) -> Future<[String: Any]> { let promise = Promise<[String: Any]>() @@ -56,12 +57,12 @@ final class FinancialConnectionsAPIClient { do { let attest = backingAPIClient.stripeAttest let handle = try await attest.assert() - logger.log(.attestationRequestTokenSucceeded, pane: pane) + 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, pane: pane) + logger.log(.attestationRequestTokenFailed(api, error), pane: pane) promise.resolve(with: baseParameters) } } @@ -69,11 +70,15 @@ final class FinancialConnectionsAPIClient { } /// Marks the assertion as completed and forwards attestation errors to the `StripeAttest` client for logging. - func completeAssertion(possibleError: Error?, pane: FinancialConnectionsSessionManifest.NextPane) { + 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, pane: pane) + logger.log(.attestationVerdictFailed(api), pane: pane) await attest.receivedAssertionError(error) } await attest.assertionCompleted() @@ -147,6 +152,7 @@ protocol FinancialConnectionsAPI { func completeAssertion( possibleError: Error?, + api: FinancialConnectionsAPIClientLogger.API, pane: FinancialConnectionsSessionManifest.NextPane ) @@ -381,7 +387,6 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI { if attest.isSupported { mobileParameters["supports_app_verification"] = true mobileParameters["verified_app_id"] = Bundle.main.bundleIdentifier - logger.log(.attestationInitSucceeded, pane: .consent) } else { logger.log(.attestationInitFailed, pane: .consent) } @@ -949,17 +954,20 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI { parameters["request_surface"] = requestSurface parameters["session_id"] = sessionId parameters["email_source"] = emailSource.rawValue - return assertAndApplyAttestationParameters(to: parameters, 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 - ) + 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( @@ -1060,16 +1068,19 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI { } if useMobileEndpoints { - return assertAndApplyAttestationParameters(to: parameters, 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 - ) + 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 index b7a70236d1e..79a5c0cdc96 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClientLogger.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAPIClientLogger.swift @@ -6,26 +6,28 @@ // 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 succeeds. - case attestationInitSucceeded /// When checking if generating attestation is supported does not succeed. case attestationInitFailed /// When an attestation token gets generated successfully. - case attestationRequestTokenSucceeded + case attestationRequestTokenSucceeded(API) /// When a token generation attempt fails client-side. - case attestationRequestTokenFailed + case attestationRequestTokenFailed(API, Error) /// When an attestation verdict fails backend side and we get an attestation related error. - case attestationVerdictFailed + case attestationVerdictFailed(API) var name: String { switch self { - case .attestationInitSucceeded: - return "attestation.init_succeeded" case .attestationInitFailed: return "attestation.init_failed" case .attestationRequestTokenSucceeded: @@ -49,8 +51,19 @@ struct FinancialConnectionsAPIClientLogger { reason = "ios_os_version_unsupported" } return ["reason": reason] - default: - return [:] + 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] } } } diff --git a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAsyncAPIClient.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAsyncAPIClient.swift index 4479507355d..0893554c428 100644 --- a/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAsyncAPIClient.swift +++ b/StripeFinancialConnections/StripeFinancialConnections/Source/API Bindings/FinancialConnectionsAsyncAPIClient.swift @@ -45,11 +45,15 @@ final class FinancialConnectionsAsyncAPIClient { } /// Marks the assertion as completed and forwards attestation errors to the `StripeAttest` client for logging. - func completeAssertion(possibleError: Error?, pane: FinancialConnectionsSessionManifest.NextPane) { + 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, pane: pane) + logger.log(.attestationVerdictFailed(api), pane: pane) await attest.receivedAssertionError(error) } await attest.assertionCompleted() @@ -60,17 +64,18 @@ final class FinancialConnectionsAsyncAPIClient { /// In case of an assertion error, returns the unmodified base parameters 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, pane: pane) + 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, pane: pane) + logger.log(.attestationRequestTokenFailed(api, error), pane: pane) return baseParameters } } @@ -383,7 +388,6 @@ extension FinancialConnectionsAsyncAPIClient: FinancialConnectionsAsyncAPI { if attest.isSupported { mobileParameters["supports_app_verification"] = true mobileParameters["verified_app_id"] = Bundle.main.bundleIdentifier - logger.log(.attestationInitSucceeded, pane: .consent) } else { logger.log(.attestationInitFailed, pane: .consent) } @@ -847,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, pane: pane) + let updatedParameters = await assertAndApplyAttestationParameters( + to: parameters, + api: .consumerSessionLookup, + pane: pane + ) return try await post(endpoint: .mobileConsumerSessionLookup, parameters: updatedParameters) } else { parameters["client_secret"] = clientSecret @@ -946,7 +954,11 @@ extension FinancialConnectionsAsyncAPIClient: FinancialConnectionsAsyncAPI { } } if useMobileEndpoints { - let updatedParameters = await assertAndApplyAttestationParameters(to: parameters, pane: pane) + 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/Native/LinkLogin/LinkLoginDataSource.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/LinkLogin/LinkLoginDataSource.swift index be73a578e7e..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 { @@ -127,10 +130,14 @@ 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, + 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 8831d650f6f..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 { @@ -130,10 +133,14 @@ 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, + 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/Shared/NetworkingOTPView/NetworkingOTPDataSource.swift b/StripeFinancialConnections/StripeFinancialConnections/Source/Native/Shared/NetworkingOTPView/NetworkingOTPDataSource.swift index 6b274aa434e..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 { @@ -124,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, pane: pane) + 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):