From 6154bfb248fc6f898933c34bb8d3559d3353e28b Mon Sep 17 00:00:00 2001 From: Hitesh Maurya Date: Thu, 23 Apr 2026 00:44:58 +0530 Subject: [PATCH 01/71] WIP: Creates new targets AppCheckCoreProvider and RecaptchaEnterpriseProvider (#85) Signed-off-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Co-authored-by: Paul Beusterien Co-authored-by: Nick Cooke Co-authored-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> --- AppCheckCore.podspec | 1 + .../AppCheckCore/GACAppCheckAPIService.h | 65 +++++++++++++++ .../AppCheckCore/GACAppCheckErrorUtil.h | 68 ++++++++++++++++ .../AppCheckCore/GACURLSessionDataResponse.h | 31 +++++++ Package.swift | 11 +++ .../RecaptchaEnterpriseCoreAPIService.swift | 81 +++++++++++++++++++ ...CheckCoreRecaptchaEnterpriseProvider.swift | 73 +++++++++++++++++ ...ecaptchaEnterpriseCoreTokenGenerator.swift | 57 +++++++++++++ 8 files changed, 387 insertions(+) create mode 100644 AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h create mode 100644 AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h create mode 100644 AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h create mode 100644 RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift create mode 100644 RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift create mode 100644 RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift diff --git a/AppCheckCore.podspec b/AppCheckCore.podspec index 3a5784ae..27429b98 100644 --- a/AppCheckCore.podspec +++ b/AppCheckCore.podspec @@ -46,6 +46,7 @@ Pod::Spec.new do |s| s.dependency 'PromisesObjC', '~> 2.4' s.dependency 'GoogleUtilities/Environment', '~> 8.0' s.dependency 'GoogleUtilities/UserDefaults', '~> 8.0' + # TODO(ncooke3): Wire up recaptcha sources in mixed-lang pod. s.pod_target_xcconfig = { 'GCC_C_LANGUAGE_STANDARD' => 'c99', diff --git a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h new file mode 100644 index 00000000..ba9bcf00 --- /dev/null +++ b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h @@ -0,0 +1,65 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import "GACAppCheckProvider.h" + +@class FBLPromise; +@class GACURLSessionDataResponse; +@class GACAppCheckToken; + +NS_ASSUME_NONNULL_BEGIN +NS_SWIFT_NAME(AppCheckCoreAPIServiceProtocol) +@protocol GACAppCheckAPIServiceProtocol + +@property(nonatomic, readonly) NSString *baseURL; + +- (FBLPromise *) + sendRequestWithURL:(NSURL *)requestURL + HTTPMethod:(NSString *)HTTPMethod + body:(nullable NSData *)body + additionalHeaders:(nullable NSDictionary *)additionalHeaders; + +- (FBLPromise *)appCheckTokenWithAPIResponse: + (GACURLSessionDataResponse *)response; + +@end +NS_SWIFT_NAME(AppCheckCoreAPIService) +@interface GACAppCheckAPIService : NSObject + +/** + * The default initializer. + * @param session The URL session used to make network requests. + * @param baseURL The base URL for the App Check service, e.g., + * `https://firebaseappcheck.googleapis.com/v1`. + * @param APIKey The Google Cloud Platform API key, if needed, or nil. + * @param requestHooks Hooks that will be invoked on requests through this service. + */ +- (instancetype)initWithURLSession:(NSURLSession *)session + baseURL:(nullable NSString *)baseURL + APIKey:(nullable NSString *)APIKey + requestHooks:(nullable NSArray *)requestHooks + NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +- (FBLPromise *)appCheckTokenWithAPIResponse: + (GACURLSessionDataResponse *)response; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h new file mode 100644 index 00000000..5df000f7 --- /dev/null +++ b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +@class GACAppCheckHTTPError; + +NS_ASSUME_NONNULL_BEGIN + +void GACAppCheckSetErrorToPointer(NSError *error, NSError **pointer); + +@interface GACAppCheckErrorUtil : NSObject + ++ (NSError *)publicDomainErrorWithError:(NSError *)error; + +// MARK: - Internal errors + ++ (NSError *)cachedTokenNotFound; + ++ (NSError *)cachedTokenExpired; + ++ (NSError *)keychainErrorWithError:(NSError *)error; + ++ (GACAppCheckHTTPError *)APIErrorWithHTTPResponse:(NSHTTPURLResponse *)HTTPResponse + data:(nullable NSData *)data; + ++ (NSError *)APIErrorWithNetworkError:(NSError *)networkError; + ++ (NSError *)appCheckTokenResponseErrorWithMissingField:(NSString *)fieldName; + ++ (NSError *)appAttestAttestationResponseErrorWithMissingField:(NSString *)fieldName; + ++ (NSError *)JSONSerializationError:(NSError *)error; + ++ (NSError *)errorWithFailureReason:(NSString *)failureReason; + ++ (NSError *)unsupportedAttestationProvider:(NSString *)providerName; + +// MARK: - App Attest Errors + ++ (NSError *)appAttestKeyIDNotFound; + ++ (NSError *)appAttestGenerateKeyFailedWithError:(NSError *)error; + ++ (NSError *)appAttestAttestKeyFailedWithError:(NSError *)error + keyId:(NSString *)keyId + clientDataHash:(NSData *)clientDataHash; + ++ (NSError *)appAttestGenerateAssertionFailedWithError:(NSError *)error + keyId:(NSString *)keyId + clientDataHash:(NSData *)clientDataHash; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h b/AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h new file mode 100644 index 00000000..e5482d45 --- /dev/null +++ b/AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h @@ -0,0 +1,31 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** The class represents HTTP response received from `NSURLSession`. */ +@interface GACURLSessionDataResponse : NSObject + +@property(nonatomic, readonly) NSHTTPURLResponse *HTTPResponse; +@property(nonatomic, nullable, readonly) NSData *HTTPBody; + +- (instancetype)initWithResponse:(NSHTTPURLResponse *)response HTTPBody:(nullable NSData *)body; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Package.swift b/Package.swift index 0135c03e..1d70c693 100644 --- a/Package.swift +++ b/Package.swift @@ -39,6 +39,10 @@ let package = Package( url: "https://github.com/erikdoe/ocmock.git", revision: "2c0bfd373289f4a7716db5d6db471640f91a6507" ), + .package( + url: "https://github.com/google/interop-ios-for-google-sdks.git", + "101.0.0" ..< "102.0.0" + ), ], targets: [ .target(name: "AppCheckCore", @@ -58,6 +62,13 @@ let package = Package( .when(platforms: [.iOS, .macCatalyst, .macOS, .tvOS, .appCheckVisionOS]) ), ]), + .target(name: "RecaptchaEnterpriseProvider", + dependencies: [ + "AppCheckCore", + .product(name: "RecaptchaInterop", package: "interop-ios-for-google-sdks"), + .product(name: "Promises", package: "Promises"), + ], + path: "RecaptchaEnterpriseProvider/Sources"), .testTarget( name: "AppCheckCoreUnit", dependencies: [ diff --git a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift new file mode 100644 index 00000000..91fc9579 --- /dev/null +++ b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift @@ -0,0 +1,81 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import AppCheckCoreProvider +import Foundation +import Promises + +private enum Constants { + static let contentTypeKey = "Content-Type" + static let jsonContentType = "application/json" + static let recaptchaTokenField = "recaptcha_enterprise_token" + static let limitedUseField = "limited_use" +} + +@objc(GARecaptchaEnterpriseAPIService) +final class RecaptchaEnterpriseAPIService: NSObject { + private var APIService: AppCheckCoreAPIServiceProtocol? = nil + private let resourceName: String + + init(APIService: AppCheckCoreAPIServiceProtocol, resourceName: String) { + self.APIService = APIService + self.resourceName = resourceName + } + + func appCheckToken(withRecaptchaToken recaptchaToken: String, + limitedUse: Bool) -> Promise { + let urlString = "\(APIService!.baseURL)/\(resourceName):exchangeRecaptchaEnterpriseToken" + guard let url = URL(string: urlString) else { + return Promise(GACAppCheckErrorUtil.error(withFailureReason: "Invalid URL string")) + } + + return httpBody(withRecaptchaToken: recaptchaToken, limitedUse: limitedUse) + .then { httpBody in + Promise(self.APIService!.sendRequest(with: url, + httpMethod: "POST", + body: httpBody, + additionalHeaders: [Constants + .contentTypeKey: Constants + .jsonContentType])) + }.then { response in + Promise(self.APIService!.appCheckToken(withAPIResponse: response)) + } + } + + private func httpBody(withRecaptchaToken recaptchaToken: String, + limitedUse: Bool) -> Promise { + guard !recaptchaToken.isEmpty else { + return Promise(GACAppCheckErrorUtil + .error(withFailureReason: "Recaptcha token cannot be empty")) + } + + return Promise(on: backgroundQueue()) { + let payload: [String: Any] = [ + Constants.recaptchaTokenField: recaptchaToken, + Constants.limitedUseField: limitedUse, + ] + + do { + let jsonData = try JSONSerialization.data(withJSONObject: payload, options: []) + return jsonData + } catch { + throw GACAppCheckErrorUtil.jsonSerializationError(error) + } + } + } + + private func backgroundQueue() -> DispatchQueue { + return DispatchQueue.global(qos: .utility) + } +} diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift new file mode 100644 index 00000000..55bd19d5 --- /dev/null +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -0,0 +1,73 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import AppCheckCoreProvider +import Foundation +import Promises +import RecaptchaInterop + +@objc(GACRecaptchaEnterpriseProvider) +public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCoreProvider { + private let tokenGenerator: RecaptchaEnterpriseTokenGenerator! + private let apiService: RecaptchaEnterpriseAPIService + + public init(siteKey: String, resourceName: String, APIKey: String, + requestHooks: [@convention(block) (NSMutableURLRequest) -> Void]? = nil) { + let recaptchaAction = + NSClassFromString("RecaptchaEnterprise.RCAAction") as? RCAActionProtocol.Type + let action = recaptchaAction?.init(customAction: "fire_app_check") + tokenGenerator = RecaptchaEnterpriseTokenGenerator(siteKey: siteKey, action: action!) + + let urlSession = URLSession(configuration: .ephemeral) + let appCheckAPIService = AppCheckCoreAPIService(urlSession: urlSession, + baseURL: nil, + apiKey: APIKey, + requestHooks: requestHooks) + apiService = RecaptchaEnterpriseAPIService( + APIService: appCheckAPIService, + resourceName: resourceName + ) + } + + @objc(getTokenWithCompletion:) + public func getToken(completion handler: @escaping (AppCheckCoreToken?, (any Error)?) -> Void) { + getToken(limitedUse: false) + .then { token in + handler(token, nil) + }.catch { error in + handler(nil, error) + } + } + + @objc(getLimitedUseTokenWithCompletion:) + public func getLimitedUseToken(completion handler: @escaping (AppCheckCoreToken?, (any Error)?) + -> Void) { + getToken(limitedUse: true) + .then { token in + handler(token, nil) + }.catch { error in + handler(nil, error) + } + } + + private func getToken(limitedUse: Bool) -> Promise { + return tokenGenerator.getRecaptchaToken() + .then { recaptchaToken in + self.apiService.appCheckToken( + withRecaptchaToken: recaptchaToken, + limitedUse: limitedUse + ) + } + } +} diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift new file mode 100644 index 00000000..d62cd0a1 --- /dev/null +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -0,0 +1,57 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation +import Promises +import RecaptchaInterop + +final class RecaptchaEnterpriseTokenGenerator { + private let siteKey: String + private let action: RCAActionProtocol + + private var recaptchaPromise: Promise? + + init(siteKey: String, action: RCAActionProtocol) { + self.siteKey = siteKey + self.action = action + recaptchaPromise = Promise { fulfill, reject in + guard let recaptcha = + NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol + .Type else { + throw NSError(domain: "RecaptchaEnterprise", code: 1, userInfo: nil) + } + recaptcha.fetchClient(withSiteKey: siteKey) { client, error in + if let client = client { + fulfill(client) + } else { + reject(error!) + } + } + } + } + + func getRecaptchaToken() -> Promise { + recaptchaPromise!.then { client in + Promise { fulfill, reject in + client.execute(withAction: self.action) { token, error in + if let token = token { + fulfill(token) + } else { + reject(error!) + } + } + } + } + } +} From 01170d02406ea39a566e80e0a9c6597f5d75b887 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 22 Apr 2026 15:52:35 -0400 Subject: [PATCH 02/71] moves --- .../Core/APIService/GACAppCheckAPIService.h | 62 ----------------- .../APIService/GACURLSessionDataResponse.h | 31 --------- .../Core/Errors/GACAppCheckErrorUtil.h | 68 ------------------- 3 files changed, 161 deletions(-) delete mode 100644 AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h delete mode 100644 AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h delete mode 100644 AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h diff --git a/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h b/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h deleted file mode 100644 index fdca400a..00000000 --- a/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import - -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckProvider.h" - -@class FBLPromise; -@class GACURLSessionDataResponse; -@class GACAppCheckToken; - -NS_ASSUME_NONNULL_BEGIN - -@protocol GACAppCheckAPIServiceProtocol - -@property(nonatomic, readonly) NSString *baseURL; - -- (FBLPromise *) - sendRequestWithURL:(NSURL *)requestURL - HTTPMethod:(NSString *)HTTPMethod - body:(nullable NSData *)body - additionalHeaders:(nullable NSDictionary *)additionalHeaders; - -- (FBLPromise *)appCheckTokenWithAPIResponse: - (GACURLSessionDataResponse *)response; - -@end - -@interface GACAppCheckAPIService : NSObject - -/** - * The default initializer. - * @param session The URL session used to make network requests. - * @param baseURL The base URL for the App Check service, e.g., - * `https://firebaseappcheck.googleapis.com/v1`. - * @param APIKey The Google Cloud Platform API key, if needed, or nil. - * @param requestHooks Hooks that will be invoked on requests through this service. - */ -- (instancetype)initWithURLSession:(NSURLSession *)session - baseURL:(nullable NSString *)baseURL - APIKey:(nullable NSString *)APIKey - requestHooks:(nullable NSArray *)requestHooks - NS_DESIGNATED_INITIALIZER; - -- (instancetype)init NS_UNAVAILABLE; - -@end - -NS_ASSUME_NONNULL_END diff --git a/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h b/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h deleted file mode 100644 index e5482d45..00000000 --- a/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import - -NS_ASSUME_NONNULL_BEGIN - -/** The class represents HTTP response received from `NSURLSession`. */ -@interface GACURLSessionDataResponse : NSObject - -@property(nonatomic, readonly) NSHTTPURLResponse *HTTPResponse; -@property(nonatomic, nullable, readonly) NSData *HTTPBody; - -- (instancetype)initWithResponse:(NSHTTPURLResponse *)response HTTPBody:(nullable NSData *)body; - -@end - -NS_ASSUME_NONNULL_END diff --git a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h deleted file mode 100644 index 5df000f7..00000000 --- a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import - -@class GACAppCheckHTTPError; - -NS_ASSUME_NONNULL_BEGIN - -void GACAppCheckSetErrorToPointer(NSError *error, NSError **pointer); - -@interface GACAppCheckErrorUtil : NSObject - -+ (NSError *)publicDomainErrorWithError:(NSError *)error; - -// MARK: - Internal errors - -+ (NSError *)cachedTokenNotFound; - -+ (NSError *)cachedTokenExpired; - -+ (NSError *)keychainErrorWithError:(NSError *)error; - -+ (GACAppCheckHTTPError *)APIErrorWithHTTPResponse:(NSHTTPURLResponse *)HTTPResponse - data:(nullable NSData *)data; - -+ (NSError *)APIErrorWithNetworkError:(NSError *)networkError; - -+ (NSError *)appCheckTokenResponseErrorWithMissingField:(NSString *)fieldName; - -+ (NSError *)appAttestAttestationResponseErrorWithMissingField:(NSString *)fieldName; - -+ (NSError *)JSONSerializationError:(NSError *)error; - -+ (NSError *)errorWithFailureReason:(NSString *)failureReason; - -+ (NSError *)unsupportedAttestationProvider:(NSString *)providerName; - -// MARK: - App Attest Errors - -+ (NSError *)appAttestKeyIDNotFound; - -+ (NSError *)appAttestGenerateKeyFailedWithError:(NSError *)error; - -+ (NSError *)appAttestAttestKeyFailedWithError:(NSError *)error - keyId:(NSString *)keyId - clientDataHash:(NSData *)clientDataHash; - -+ (NSError *)appAttestGenerateAssertionFailedWithError:(NSError *)error - keyId:(NSString *)keyId - clientDataHash:(NSData *)clientDataHash; - -@end - -NS_ASSUME_NONNULL_END From 0f548c24439b2c3fbac97c05f675c172c9e9c46a Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 27 Apr 2026 17:39:11 -0400 Subject: [PATCH 03/71] todos --- AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h | 2 ++ .../Sources/Core/APIService/GACURLSessionDataResponse.h | 2 ++ AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h | 2 ++ 3 files changed, 6 insertions(+) create mode 100644 AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h create mode 100644 AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h create mode 100644 AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h diff --git a/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h b/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h new file mode 100644 index 00000000..2a35bd7a --- /dev/null +++ b/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h @@ -0,0 +1,2 @@ +// TODO(ncooke3): Clean this up near the end of the PR's progress. +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" diff --git a/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h b/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h new file mode 100644 index 00000000..e59b91e3 --- /dev/null +++ b/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h @@ -0,0 +1,2 @@ +// TODO(ncooke3): Clean this up near the end of the PR's progress. +#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" diff --git a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h new file mode 100644 index 00000000..238d023d --- /dev/null +++ b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h @@ -0,0 +1,2 @@ +// TODO(ncooke3): Clean this up near the end of the PR's progress. +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" From eb4effd954c2b12955ea09b667643adfec28e4ff Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:45:06 -0400 Subject: [PATCH 04/71] fixes --- .../Core/APIService/GACAppCheckAPIService.h | 16 ++++++++++++++++ .../Core/APIService/GACURLSessionDataResponse.h | 16 ++++++++++++++++ .../Sources/Core/Errors/GACAppCheckErrorUtil.h | 16 ++++++++++++++++ Package.swift | 1 + .../API/RecaptchaEnterpriseCoreAPIService.swift | 2 +- ...AppCheckCoreRecaptchaEnterpriseProvider.swift | 2 +- .../RecaptchaEnterpriseCoreTokenGenerator.swift | 2 +- 7 files changed, 52 insertions(+), 3 deletions(-) diff --git a/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h b/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h index 2a35bd7a..12e9e14b 100644 --- a/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h +++ b/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h @@ -1,2 +1,18 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + // TODO(ncooke3): Clean this up near the end of the PR's progress. #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" diff --git a/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h b/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h index e59b91e3..5b36f867 100644 --- a/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h +++ b/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h @@ -1,2 +1,18 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + // TODO(ncooke3): Clean this up near the end of the PR's progress. #import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" diff --git a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h index 238d023d..ad0efd61 100644 --- a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h +++ b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h @@ -1,2 +1,18 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + // TODO(ncooke3): Clean this up near the end of the PR's progress. #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" diff --git a/Package.swift b/Package.swift index 1d70c693..0dd5b9fa 100644 --- a/Package.swift +++ b/Package.swift @@ -41,6 +41,7 @@ let package = Package( ), .package( url: "https://github.com/google/interop-ios-for-google-sdks.git", + // TODO(ncooke3): Is this the correct supported version window? "101.0.0" ..< "102.0.0" ), ], diff --git a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift index 91fc9579..4487fc7e 100644 --- a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift +++ b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index 55bd19d5..3e61094b 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index d62cd0a1..97ab9d6f 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC +// Copyright 2026 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. From bfb35e39f12f008a904843130b067133486bbfe5 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:54:10 -0400 Subject: [PATCH 05/71] fix custom action name --- .../Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index 3e61094b..fbb672a4 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -26,7 +26,7 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo requestHooks: [@convention(block) (NSMutableURLRequest) -> Void]? = nil) { let recaptchaAction = NSClassFromString("RecaptchaEnterprise.RCAAction") as? RCAActionProtocol.Type - let action = recaptchaAction?.init(customAction: "fire_app_check") + let action = recaptchaAction?.init(customAction: "app_check_ios") tokenGenerator = RecaptchaEnterpriseTokenGenerator(siteKey: siteKey, action: action!) let urlSession = URLSession(configuration: .ephemeral) From 157bd6d4489b4addcb7601ce18abab124d18edb0 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 27 Apr 2026 18:30:23 -0400 Subject: [PATCH 06/71] umbrella header fixes --- AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h b/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h index a0752153..d3dceb7c 100644 --- a/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h +++ b/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h @@ -31,3 +31,8 @@ // App Attest provider. #import "GACAppAttestProvider.h" + +// Internal headers exposed for interop with the Swift implementation. +#import "GACAppCheckErrorUtil.h" +#import "GACAppCheckAPIService.h" +#import "GACURLSessionDataResponse.h" From a3ee169c7ff5136052a704af4c77bbfba45f9ba4 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 28 Apr 2026 15:31:39 -0400 Subject: [PATCH 07/71] Add agents workflow guide --- agents.md | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 agents.md diff --git a/agents.md b/agents.md new file mode 100644 index 00000000..d08df5f9 --- /dev/null +++ b/agents.md @@ -0,0 +1,122 @@ +# App Check - Agent Workflow Instructions + +The goal of this document is to ensure high-quality, reproducible, and +verifiable contributions in a fully autonomous loop for the App Check +repository. + +--- + +## 📥 Input Requirements + +Before starting any work, the agent must require or acquire: +1. **Feature Specification**: A detailed description of the feature, bug, or + task. +2. **Project Configuration**: Access to necessary credentials or configurations + if applicable. +3. **External Scripts**: Access to the `firebase-ios-sdk` scripts. If not using + the cloned scripts via `./setup-scripts.sh`, ensure they are available in a + local clone of `firebase-ios-sdk` (commonly at + `/scripts` but path may vary). If the path is not + found, ask the human for it. + +## 📤 Output Requirements + +A successful task completion MUST produce: +1. **Code Changes**: The implemented feature or fix and corresponding tests. +2. **Unit & Integration Tests**: Demonstrating success and handling edge cases. +3. **Implementation Plan** (For complex tasks only): A scannable proposal + before starting work. +4. **Walkthrough Artifact**: A summary containing verification results and + reproduction snippets. + +--- + +## 🔄 The Agentic Loop: Step-by-Step + +### Step 0: Workflow Selection & Planning (Hybrid Approach) +- **Prerequisite**: Verify that external scripts are accessible or that + `./setup-scripts.sh` has been run to link them. If you cannot find them, ask + the human for the path to the `firebase-ios-sdk` repository. +- **Action**: Assess the complexity of the task. + - **Simple Task**: Proceed directly to **Step 1: TDD**. + - **Complex Task**: Create a highly scannable **Implementation Plan** and + get human approval. +- **Plan Requirements (Highly Scannable)**: + - Keep it brief and hit key points. + - Use bullet points for readability. + - Focus on *what* changes and *why*, avoiding detailed *how*. + - Highlight any open questions or design decisions requiring human input. + +### Step 1: Test-Driven Development (TDD) +- **Constraint**: You MUST write tests before writing implementation code. +- **Action**: + 1. Write a failing unit or integration test asserting the new behavior. + 2. Verify it fails by running the appropriate test command. + +### Step 2: Implementation +- Implement the feature or fix. +- Follow project conventions and guidelines if available. + +### Step 3: Verification +- **Action**: Run tests using the cloned scripts or by referencing the external + ones (e.g., in ``). +- **Iteration Workflow**: To get into a faster iterative loop, use the external + scripts directly if possible. Set an environment variable like + `FIREBASE_IOS_SDK_PATH` if your path differs from the default + ``. +- **Commands**: + - **Primary (Fast Iteration)**: For SPM testing (which uses `xcodebuild` + under the hood): + `${FIREBASE_IOS_SDK_PATH:-}/scripts/build.sh AppCheck spm` + (where `` is `iOS`, `tvOS`, `macOS`, or `catalyst`). + - For CocoaPods linting: + `${FIREBASE_IOS_SDK_PATH:-}/scripts/pod_lib_lint.rb AppCheckCore.podspec --platforms=ios` + (or other platforms: `tvos`, `macos --skip-tests`, `watchos`). + - Alternatively, run `./setup-scripts.sh` to clone scripts locally and use + `scripts/pod_lib_lint.rb`. + - For Catalyst testing: + `${FIREBASE_IOS_SDK_PATH:-}/scripts/test_catalyst.sh AppCheckCore test`. +- **xcodebuild Iteration**: For direct `xcodebuild` invocations, follow the + order: `build`, `build-for-testing`, then `test`. This allows for faster + iteration. + +### Step 4: Public API Visibility +- **Requirement**: Identify and report any new public APIs created. +- **Method**: Check for changes in public headers or symbols. + +--- + +## 🏆 Quality Gates & Best Practices + +- **Error Handling**: Test edge cases and error paths. +- **Code Style**: Use `/scripts/style.sh` if + applicable to maintain consistency. +- **No Hardcoded Secrets**: Ensure no secrets are committed. +- **Code Reuse & Refactoring**: Prioritize understanding existing structures + to reuse or extend them with minor refactors rather than adding redundant + code. + +--- + +## ✅ Pre-Commit Checklist +- [ ] **Unit Tests**: Passed all unit tests. +- [ ] **Integration Tests**: Passed all integration tests. +- [ ] **Style Applied**: Verified code style if applicable. +- [ ] **Concurrency**: Verified that the changes do not introduce potential + race conditions or deadlocks. +- [ ] **Memory Management**: Ensured no retain cycles or memory leaks are + introduced. + +--- + +## 📝 Final Walkthrough Structure +The task is not done until a `walkthrough.md` artifact is created containing: +1. **Summary of Changes**: High-level overview. +2. **Public API Diff**: Any new public APIs. +3. **Verification Results**: Snippets showing successful test runs. + +--- + +## 🧠 Post Change: Continuous Improvement +Perform self-reflection after completing the task and update this file or +create/update a Knowledge Item to help future agents. From 5ac8a38fd78d5bc3987af06af40fa87dd5c2bb8e Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 29 Apr 2026 09:58:10 -0400 Subject: [PATCH 08/71] fix(recaptcha): improve safety and fix compilation issues in Recaptcha provider - Change implicitly unwrapped optionals to standard optionals or constants to prevent runtime crashes. - Handle Recaptcha SDK dynamic lookups safely without force-unwrapping. - Standardize error handling to use GACAppCheckErrorUtil and AppCheckCoreErrorDomain. - Add missing AppCheckCore imports to fix scope build errors. - Remove outdated TODO from AppCheckCore.podspec. --- AppCheckCore.podspec | 6 +++++- .../Public/AppCheckCore/AppCheckCore.h | 2 +- .../RecaptchaEnterpriseCoreAPIService.swift | 20 +++++++++---------- ...CheckCoreRecaptchaEnterpriseProvider.swift | 14 ++++++++++--- ...ecaptchaEnterpriseCoreTokenGenerator.swift | 14 ++++++++----- agents.md | 2 ++ 6 files changed, 38 insertions(+), 20 deletions(-) diff --git a/AppCheckCore.podspec b/AppCheckCore.podspec index 27429b98..567d2012 100644 --- a/AppCheckCore.podspec +++ b/AppCheckCore.podspec @@ -37,6 +37,9 @@ Pod::Spec.new do |s| s.source_files = [ base_dir + 'Sources/**/*.[mh]', ] + s.ios.source_files = [ + 'RecaptchaEnterpriseProvider/Sources/**/*.swift', + ] s.public_header_files = base_dir + 'Sources/Public/AppCheckCore/*.h' s.ios.weak_framework = 'DeviceCheck' @@ -44,9 +47,10 @@ Pod::Spec.new do |s| s.tvos.weak_framework = 'DeviceCheck' s.dependency 'PromisesObjC', '~> 2.4' + s.dependency 'PromisesSwift', '~> 2.4' s.dependency 'GoogleUtilities/Environment', '~> 8.0' s.dependency 'GoogleUtilities/UserDefaults', '~> 8.0' - # TODO(ncooke3): Wire up recaptcha sources in mixed-lang pod. + s.ios.dependency 'RecaptchaInterop', '~> 101.0' s.pod_target_xcconfig = { 'GCC_C_LANGUAGE_STANDARD' => 'c99', diff --git a/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h b/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h index d3dceb7c..f229fa45 100644 --- a/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h +++ b/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h @@ -33,6 +33,6 @@ #import "GACAppAttestProvider.h" // Internal headers exposed for interop with the Swift implementation. -#import "GACAppCheckErrorUtil.h" #import "GACAppCheckAPIService.h" +#import "GACAppCheckErrorUtil.h" #import "GACURLSessionDataResponse.h" diff --git a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift index 4487fc7e..203f877d 100644 --- a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift +++ b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import AppCheckCoreProvider +import AppCheckCore import Foundation import Promises @@ -25,7 +25,7 @@ private enum Constants { @objc(GARecaptchaEnterpriseAPIService) final class RecaptchaEnterpriseAPIService: NSObject { - private var APIService: AppCheckCoreAPIServiceProtocol? = nil + private let APIService: AppCheckCoreAPIServiceProtocol private let resourceName: String init(APIService: AppCheckCoreAPIServiceProtocol, resourceName: String) { @@ -35,21 +35,21 @@ final class RecaptchaEnterpriseAPIService: NSObject { func appCheckToken(withRecaptchaToken recaptchaToken: String, limitedUse: Bool) -> Promise { - let urlString = "\(APIService!.baseURL)/\(resourceName):exchangeRecaptchaEnterpriseToken" + let urlString = "\(APIService.baseURL)/\(resourceName):exchangeRecaptchaEnterpriseToken" guard let url = URL(string: urlString) else { return Promise(GACAppCheckErrorUtil.error(withFailureReason: "Invalid URL string")) } return httpBody(withRecaptchaToken: recaptchaToken, limitedUse: limitedUse) .then { httpBody in - Promise(self.APIService!.sendRequest(with: url, - httpMethod: "POST", - body: httpBody, - additionalHeaders: [Constants - .contentTypeKey: Constants - .jsonContentType])) + Promise(self.APIService.sendRequest(with: url, + httpMethod: "POST", + body: httpBody, + additionalHeaders: [Constants + .contentTypeKey: Constants + .jsonContentType])) }.then { response in - Promise(self.APIService!.appCheckToken(withAPIResponse: response)) + Promise(self.APIService.appCheckToken(withAPIResponse: response)) } } diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index fbb672a4..34be067e 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import AppCheckCoreProvider +import AppCheckCore import Foundation import Promises import RecaptchaInterop @objc(GACRecaptchaEnterpriseProvider) public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCoreProvider { - private let tokenGenerator: RecaptchaEnterpriseTokenGenerator! + private var tokenGenerator: RecaptchaEnterpriseTokenGenerator? private let apiService: RecaptchaEnterpriseAPIService public init(siteKey: String, resourceName: String, APIKey: String, @@ -27,7 +27,12 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo let recaptchaAction = NSClassFromString("RecaptchaEnterprise.RCAAction") as? RCAActionProtocol.Type let action = recaptchaAction?.init(customAction: "app_check_ios") - tokenGenerator = RecaptchaEnterpriseTokenGenerator(siteKey: siteKey, action: action!) + + if let action = action { + tokenGenerator = RecaptchaEnterpriseTokenGenerator(siteKey: siteKey, action: action) + } else { + tokenGenerator = nil + } let urlSession = URLSession(configuration: .ephemeral) let appCheckAPIService = AppCheckCoreAPIService(urlSession: urlSession, @@ -62,6 +67,9 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo } private func getToken(limitedUse: Bool) -> Promise { + guard let tokenGenerator = tokenGenerator else { + return Promise(GACAppCheckErrorUtil.unsupportedAttestationProvider("RecaptchaEnterprise")) + } return tokenGenerator.getRecaptchaToken() .then { recaptchaToken in self.apiService.appCheckToken( diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index 97ab9d6f..891282ff 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import AppCheckCore import Foundation import Promises import RecaptchaInterop @@ -20,7 +21,7 @@ final class RecaptchaEnterpriseTokenGenerator { private let siteKey: String private let action: RCAActionProtocol - private var recaptchaPromise: Promise? + private let recaptchaPromise: Promise init(siteKey: String, action: RCAActionProtocol) { self.siteKey = siteKey @@ -29,26 +30,29 @@ final class RecaptchaEnterpriseTokenGenerator { guard let recaptcha = NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol .Type else { - throw NSError(domain: "RecaptchaEnterprise", code: 1, userInfo: nil) + throw GACAppCheckErrorUtil.unsupportedAttestationProvider("RecaptchaEnterprise") } recaptcha.fetchClient(withSiteKey: siteKey) { client, error in if let client = client { fulfill(client) } else { - reject(error!) + reject(error ?? GACAppCheckErrorUtil + .error(withFailureReason: "Failed to fetch Recaptcha client")) } } } } + // TODO(ncooke3): Investigate whether we need a backoff mechanism. func getRecaptchaToken() -> Promise { - recaptchaPromise!.then { client in + recaptchaPromise.then { client in Promise { fulfill, reject in client.execute(withAction: self.action) { token, error in if let token = token { fulfill(token) } else { - reject(error!) + reject(error ?? GACAppCheckErrorUtil + .error(withFailureReason: "Failed to execute Recaptcha action")) } } } diff --git a/agents.md b/agents.md index d08df5f9..49002344 100644 --- a/agents.md +++ b/agents.md @@ -64,6 +64,8 @@ A successful task completion MUST produce: scripts directly if possible. Set an environment variable like `FIREBASE_IOS_SDK_PATH` if your path differs from the default ``. + - To bypass the CI secret check in `check_secrets.sh` when running external + scripts in a trusted environment, export `FIREBASECI_IS_TRUSTED_ENV="true"`. - **Commands**: - **Primary (Fast Iteration)**: For SPM testing (which uses `xcodebuild` under the hood): From 4c6523e55af07ac4e33f10ddf536dde27c4e85f1 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 29 Apr 2026 10:27:05 -0400 Subject: [PATCH 09/71] fix(recaptcha): improve safety and fix compilation issues in Recaptcha provider --- .../RecaptchaEnterpriseCoreAPIService.swift | 4 ++- ...CheckCoreRecaptchaEnterpriseProvider.swift | 4 ++- ...ecaptchaEnterpriseCoreTokenGenerator.swift | 4 ++- agents.md | 34 +++++++++++++++++-- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift index 203f877d..2dcd68a4 100644 --- a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift +++ b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import AppCheckCore +#if SWIFT_PACKAGE + import AppCheckCore +#endif import Foundation import Promises diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index 34be067e..ad911976 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import AppCheckCore +#if SWIFT_PACKAGE + import AppCheckCore +#endif import Foundation import Promises import RecaptchaInterop diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index 891282ff..93dbfbff 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import AppCheckCore +#if SWIFT_PACKAGE + import AppCheckCore +#endif import Foundation import Promises import RecaptchaInterop diff --git a/agents.md b/agents.md index 49002344..cd9c1b5c 100644 --- a/agents.md +++ b/agents.md @@ -91,8 +91,9 @@ A successful task completion MUST produce: ## 🏆 Quality Gates & Best Practices - **Error Handling**: Test edge cases and error paths. -- **Code Style**: Use `/scripts/style.sh` if - applicable to maintain consistency. +- **Code Style**: You MUST run `/scripts/style.sh` to + maintain consistency. Since style changes are non-functional, you do NOT need + to re-run tests after applying style fixes. - **No Hardcoded Secrets**: Ensure no secrets are committed. - **Code Reuse & Refactoring**: Prioritize understanding existing structures to reuse or extend them with minor refactors rather than adding redundant @@ -111,6 +112,35 @@ A successful task completion MUST produce: --- +## 📦 Git & Commits + +- **Commit Often**: Pause and commit work frequently. +- **Scope**: Optimize for smaller commits that represent a complete piece of work + or a specific milestone within a larger task. +- **Convention**: Follow conventional commit practices (e.g. `feat:`, `fix:`, `refactor:`). + +--- + +## 🛠️ Environment & Troubleshooting + +When operating in a restricted or sandboxed environment (like the Jetski IDE), +you may encounter the following blockers. Use these workarounds: + +- **Terminal Sandbox (SPM `sandbox-exec` errors)**: `swift build` may fail if + run inside a sandbox. Disable the terminal sandbox in the IDE settings + (`enableTerminalSandbox: false`) or use `swift build --disable-sandbox`. +- **Missing `python` Command**: Modern macOS lacks `python` (Python 2). If + external scripts fail, create a local wrapper script that forwards to + `python3` and add it to the `PATH`: + `mkdir -p tmp/bin && echo '#!/bin/sh\nexec python3 "$@"' > tmp/bin/python && chmod +x tmp/bin/python && export PATH="$PWD/tmp/bin:$PATH"` +- **Ruby Version Conflicts**: External scripts (like `pod_lib_lint.rb`) may + fail if `rbenv` tries to use the external repo's `.ruby-version`. Force the + local Ruby version by prefixing the command with `RBENV_VERSION=2.7.5`. +- **Quality Gates**: Do not skip `style.sh` and `pod_lib_lint.rb`. They are + critical for verification. + +--- + ## 📝 Final Walkthrough Structure The task is not done until a `walkthrough.md` artifact is created containing: 1. **Summary of Changes**: High-level overview. From 95bc4c55a5acf213a761c4fe065e86c6f32dab92 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 29 Apr 2026 10:29:44 -0400 Subject: [PATCH 10/71] docs(agent-workflow): enforce highly scannable communication style and environment guidelines - Add new Communication Guidelines section prioritizing categorized bullet points and visual indicators. - Add Environment & Troubleshooting section for dealing with sandboxes, Python paths, and Ruby version conflicts. - Standardize Git & Commits guidance to encourage frequent, scoped commits. --- agents.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/agents.md b/agents.md index cd9c1b5c..9d8e2c10 100644 --- a/agents.md +++ b/agents.md @@ -31,6 +31,15 @@ A successful task completion MUST produce: --- +## 💬 Communication Guidelines + +When reporting back to the user, prioritize scannability and clarity: +1. **Use Categorized Bullet Points**: Group findings and results into clear categories (e.g., "Build & Test Results", "Code Changes"). +2. **Use Indicators**: Prefix status updates with checkmarks (✅) or caution symbols (⚠️) for immediate visual parsing. +3. **Be Concise**: Avoid conversational filler. Get straight to the results and next steps. + +--- + ## 🔄 The Agentic Loop: Step-by-Step ### Step 0: Workflow Selection & Planning (Hybrid Approach) From 83e6937eee748f78d46683a56fe36f9ec9eeb934 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 29 Apr 2026 11:21:39 -0400 Subject: [PATCH 11/71] test(recaptcha): add comprehensive unit tests for Recaptcha Enterprise provider - Add `RecaptchaEnterpriseProviderUnit` test target to SPM package. - Verify provider initialization, dynamic SDK loading, and graceful error states. - Validate API token exchange payload formatting and responses, including limited-use configurations. - Ensure strict compatibility with Objective-C `FBLPromise` and `AppCheckCoreErrorCode` APIs. - Apply standard formatting via style.sh. --- Package.swift | 11 ++ ...CoreRecaptchaEnterpriseProviderTests.swift | 78 ++++++++ ...captchaEnterpriseCoreAPIServiceTests.swift | 180 ++++++++++++++++++ ...chaEnterpriseCoreTokenGeneratorTests.swift | 64 +++++++ 4 files changed, 333 insertions(+) create mode 100644 RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift create mode 100644 RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift create mode 100644 RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift diff --git a/Package.swift b/Package.swift index 0dd5b9fa..a39c58bf 100644 --- a/Package.swift +++ b/Package.swift @@ -97,6 +97,17 @@ let package = Package( .headerSearchPath("../.."), ] ), + .testTarget( + name: "RecaptchaEnterpriseProviderUnit", + dependencies: [ + "RecaptchaEnterpriseProvider", + .product(name: "OCMock", package: "ocmock"), + ], + path: "RecaptchaEnterpriseProvider/Tests", + cSettings: [ + .headerSearchPath("../.."), + ] + ), ] ) diff --git a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift new file mode 100644 index 00000000..dfcd2598 --- /dev/null +++ b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift @@ -0,0 +1,78 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import XCTest + +@testable import AppCheckCore +import Promises +@testable import RecaptchaEnterpriseProvider + +final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { + private var provider: AppCheckCoreRecaptchaEnterpriseProvider! + private let testSiteKey = "test-site-key" + private let testResourceName = "projects/test-project/apps/test-app" + private let testAPIKey = "test-api-key" + + override func setUp() { + super.setUp() + provider = AppCheckCoreRecaptchaEnterpriseProvider( + siteKey: testSiteKey, + resourceName: testResourceName, + APIKey: testAPIKey, + requestHooks: nil + ) + } + + override func tearDown() { + provider = nil + super.tearDown() + } + + func testGetTokenWithoutRecaptchaSDK() { + // When the Recaptcha SDK is not linked, the tokenGenerator will be nil. + // We should expect an unsupported attestation provider error. + + let expectation = self.expectation(description: "Get token fails without SDK") + + provider.getToken { token, error in + XCTAssertNil(token) + XCTAssertNotNil(error) + + let nsError = error as NSError? + XCTAssertEqual(nsError?.domain, AppCheckCoreErrorDomain) + XCTAssertEqual(nsError?.code, AppCheckCoreErrorCode.unsupported.rawValue) + + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } + + func testGetLimitedUseTokenWithoutRecaptchaSDK() { + let expectation = self.expectation(description: "Get limited use token fails without SDK") + + provider.getLimitedUseToken { token, error in + XCTAssertNil(token) + XCTAssertNotNil(error) + + let nsError = error as NSError? + XCTAssertEqual(nsError?.domain, AppCheckCoreErrorDomain) + XCTAssertEqual(nsError?.code, AppCheckCoreErrorCode.unsupported.rawValue) + + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } +} diff --git a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift new file mode 100644 index 00000000..6846ad87 --- /dev/null +++ b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift @@ -0,0 +1,180 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import XCTest + +@testable import AppCheckCore +import FBLPromises +@testable import RecaptchaEnterpriseProvider + +final class RecaptchaEnterpriseCoreAPIServiceTests: XCTestCase { + private var apiService: RecaptchaEnterpriseAPIService! + private var mockCoreAPIService: MockAppCheckCoreAPIService! + private let testResourceName = "projects/test-project/apps/test-app" + private let testRecaptchaToken = "recaptcha-token-123" + + override func setUp() { + super.setUp() + mockCoreAPIService = MockAppCheckCoreAPIService() + apiService = RecaptchaEnterpriseAPIService( + APIService: mockCoreAPIService, + resourceName: testResourceName + ) + } + + override func tearDown() { + apiService = nil + mockCoreAPIService = nil + super.tearDown() + } + + func testAppCheckTokenSuccess() throws { + // Arrange + let expectedAppCheckToken = AppCheckCoreToken( + token: "app-check-token-456", + expirationDate: Date(timeIntervalSinceNow: 3600) + ) + mockCoreAPIService.expectedToken = expectedAppCheckToken + + let expectation = self.expectation(description: "Token exchange completes successfully") + + // Act + apiService.appCheckToken(withRecaptchaToken: testRecaptchaToken, limitedUse: false) + .then { token in + // Assert + XCTAssertEqual(token.token, expectedAppCheckToken.token) + XCTAssertEqual(token.expirationDate, expectedAppCheckToken.expirationDate) + + // Verify request + guard let request = self.mockCoreAPIService.lastRequest else { + XCTFail("No request was sent") + return + } + + XCTAssertEqual( + request.url?.absoluteString, + "https://test.com/\(self.testResourceName):exchangeRecaptchaEnterpriseToken" + ) + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertEqual(request.additionalHeaders?["Content-Type"], "application/json") + + if let body = request.body { + let json = try? JSONSerialization.jsonObject(with: body, options: []) as? [String: Any] + XCTAssertEqual(json?["recaptcha_enterprise_token"] as? String, self.testRecaptchaToken) + XCTAssertEqual(json?["limited_use"] as? Bool, false) + } else { + XCTFail("Request body was empty") + } + + expectation.fulfill() + }.catch { error in + XCTFail("Unexpected error: \(error)") + } + + waitForExpectations(timeout: 1.0) + } + + func testAppCheckTokenLimitedUseSuccess() throws { + // Arrange + let expectedAppCheckToken = AppCheckCoreToken( + token: "app-check-token-456", + expirationDate: Date(timeIntervalSinceNow: 3600) + ) + mockCoreAPIService.expectedToken = expectedAppCheckToken + + let expectation = self + .expectation(description: "Limited use token exchange completes successfully") + + // Act + apiService.appCheckToken(withRecaptchaToken: testRecaptchaToken, limitedUse: true) + .then { token in + // Assert + guard let request = self.mockCoreAPIService.lastRequest, let body = request.body else { + XCTFail("No request or body") + return + } + + let json = try? JSONSerialization.jsonObject(with: body, options: []) as? [String: Any] + XCTAssertEqual(json?["limited_use"] as? Bool, true) + + expectation.fulfill() + }.catch { error in + XCTFail("Unexpected error: \(error)") + } + + waitForExpectations(timeout: 1.0) + } + + func testAppCheckTokenEmptyRecaptchaToken() { + let expectation = self.expectation(description: "Token exchange fails with empty token") + + apiService.appCheckToken(withRecaptchaToken: "", limitedUse: false).then { token in + XCTFail("Should not succeed with empty token") + }.catch { error in + XCTAssertNotNil(error) + XCTAssertEqual((error as NSError).domain, AppCheckCoreErrorDomain) + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } +} + +// MARK: - Mocks + +private class MockAppCheckCoreAPIService: NSObject, AppCheckCoreAPIServiceProtocol { + var baseURL: String = "https://test.com" + + struct RequestData { + let url: URL? + let httpMethod: String? + let body: Data? + let additionalHeaders: [String: String]? + } + + var lastRequest: RequestData? + var expectedResponse: GACURLSessionDataResponse? + var expectedToken: AppCheckCoreToken? + var expectedError: Error? + + func sendRequest(with url: URL, httpMethod: String, body: Data?, + additionalHeaders: [String: String]?) -> FBLPromise { + lastRequest = RequestData( + url: url, + httpMethod: httpMethod, + body: body, + additionalHeaders: additionalHeaders + ) + + let resolution: Any = (expectedError as NSError?) ?? + (expectedResponse ?? GACURLSessionDataResponse( + response: HTTPURLResponse(), + httpBody: Data() + )) + let promiseClass = NSClassFromString("FBLPromise") as! NSObject.Type + let unmanaged = promiseClass.perform(NSSelectorFromString("resolvedWith:"), with: resolution)! + return unmanaged.takeUnretainedValue() as! FBLPromise + } + + func appCheckToken(withAPIResponse response: GACURLSessionDataResponse) + -> FBLPromise { + let resolution: Any = (expectedError as NSError?) ?? (expectedToken ?? AppCheckCoreToken( + token: "dummy", + expirationDate: Date() + )) + let promiseClass = NSClassFromString("FBLPromise") as! NSObject.Type + let unmanaged = promiseClass.perform(NSSelectorFromString("resolvedWith:"), with: resolution)! + return unmanaged.takeUnretainedValue() as! FBLPromise + } +} diff --git a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift new file mode 100644 index 00000000..8224754c --- /dev/null +++ b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift @@ -0,0 +1,64 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import XCTest + +@testable import AppCheckCore +import Promises +@testable import RecaptchaEnterpriseProvider +import RecaptchaInterop + +final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { + private let testSiteKey = "test-site-key" + private var mockAction: MockRCAAction! + + override func setUp() { + super.setUp() + mockAction = MockRCAAction(customAction: "test_action") + } + + func testGetRecaptchaTokenWithoutSDK() { + let generator = RecaptchaEnterpriseTokenGenerator(siteKey: testSiteKey, action: mockAction) + + let expectation = self.expectation(description: "Fails to generate token without Recaptcha SDK") + + generator.getRecaptchaToken().then { token in + XCTFail("Should not succeed without SDK") + }.catch { error in + XCTAssertNotNil(error) + let nsError = error as NSError + XCTAssertEqual(nsError.domain, AppCheckCoreErrorDomain) + XCTAssertEqual(nsError.code, AppCheckCoreErrorCode.unsupported.rawValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } +} + +// MARK: - Mocks + +private class MockRCAAction: NSObject, RCAActionProtocol { + var action: String { return customAction } + + static var login: RCAActionProtocol { return MockRCAAction(customAction: "login") } + static var signup: RCAActionProtocol { return MockRCAAction(customAction: "signup") } + + let customAction: String + + required init(customAction: String) { + self.customAction = customAction + super.init() + } +} From 71a5698e4ead7e21279b25372f2e566937e46527 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 29 Apr 2026 11:28:30 -0400 Subject: [PATCH 12/71] docs(agent-workflow): add Swift/Obj-C bridging best practices and TDD guidelines - Document known compiler issues with `FBLPromise` generics in Swift bridging. - Add exact syntax workarounds for unlabeled Obj-C static methods and dynamic dispatch fallbacks. - Specify SPM target scoping requirements and `swift test --filter` syntax in TDD step. --- agents.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/agents.md b/agents.md index 9d8e2c10..5eb27a32 100644 --- a/agents.md +++ b/agents.md @@ -59,8 +59,9 @@ When reporting back to the user, prioritize scannability and clarity: ### Step 1: Test-Driven Development (TDD) - **Constraint**: You MUST write tests before writing implementation code. - **Action**: - 1. Write a failing unit or integration test asserting the new behavior. - 2. Verify it fails by running the appropriate test command. + 1. Create or identify the correct test target in `Package.swift`. Keep Swift and Obj-C test targets separate. + 2. Write a failing unit or integration test asserting the new behavior. + 3. Verify it fails by running the appropriate test command (e.g., `swift test --filter `). ### Step 2: Implementation - Implement the feature or fix. @@ -148,6 +149,21 @@ you may encounter the following blockers. Use these workarounds: - **Quality Gates**: Do not skip `style.sh` and `pod_lib_lint.rb`. They are critical for verification. +### Swift / Objective-C Interoperability Pitfalls + +When working on mixed-language targets (e.g., Swift tests for Objective-C core code), you will encounter strict compiler bridging issues, particularly with generic classes like `FBLPromise`. To avoid build loop failures: + +- **FBLPromise Instantiation**: `FBLPromise.init()` is `NS_UNAVAILABLE`. The standard Objective-C factory methods (`resolvedWith:` and `promiseWithError:`) bridge to Swift as **unlabeled** static methods that lose type inference. +- **The Fix**: Do NOT attempt to specify generics on the receiver (e.g., `FBLPromise.resolved(...)` will fail). Instead, call the base method and force-cast the result: + ```swift + // Success + return FBLPromise.resolved(response) as! FBLPromise + + // Failure (Must explicitly cast to NSError) + return FBLPromise.resolved(error as NSError) as! FBLPromise + ``` +- **Dynamic Dispatch fallback**: If Swift inference completely fails in test mocks, use Objective-C dynamic dispatch to bypass the compiler: `promiseClass.perform(NSSelectorFromString("resolvedWith:"), with: resolution)`. + --- ## 📝 Final Walkthrough Structure From 0d638f2cf2587814b88d1df08a9df03f795e7271 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 30 Apr 2026 12:28:00 -0400 Subject: [PATCH 13/71] refactor: remove forwarding headers and update imports to public headers - Deleted forwarding headers in `AppCheckCore/Sources/Core/...` - Updated imports in all Objective-C sources and tests to use `AppCheckCore/Sources/Public/AppCheckCore/...` - Verified build is successful via `swift test --list-tests` --- .../API/GACAppAttestAPIService.m | 6 +++--- .../API/GACAppAttestAttestationResponse.m | 2 +- .../Errors/GACAppAttestRejectionError.m | 2 +- .../AppAttestProvider/GACAppAttestProvider.m | 4 ++-- .../Storage/GACAppAttestArtifactStorage.m | 2 +- .../Storage/GACAppAttestKeyIDStorage.m | 2 +- .../Core/APIService/GACAppCheckAPIService.h | 18 ------------------ .../Core/APIService/GACAppCheckAPIService.m | 6 +++--- .../APIService/GACAppCheckToken+APIResponse.m | 2 +- .../APIService/GACURLSessionDataResponse.h | 18 ------------------ .../APIService/GACURLSessionDataResponse.m | 2 +- .../Core/APIService/NSURLSession+GACPromises.m | 2 +- .../Core/Backoff/GACAppCheckBackoffWrapper.m | 2 +- .../Sources/Core/Errors/GACAppCheckErrorUtil.h | 18 ------------------ .../Sources/Core/Errors/GACAppCheckErrorUtil.m | 2 +- .../Sources/Core/Errors/GACAppCheckHTTPError.m | 2 +- AppCheckCore/Sources/Core/GACAppCheck.m | 2 +- .../Sources/Core/Storage/GACAppCheckStorage.m | 2 +- .../API/GACAppCheckDebugProviderAPIService.m | 4 ++-- .../DebugProvider/GACAppCheckDebugProvider.m | 2 +- .../API/GACDeviceCheckAPIService.m | 4 ++-- .../GACDeviceCheckProvider.m | 4 ++-- .../GACDeviceCheckAPIServiceE2ETests.m | 2 +- .../GACAppAttestAPIServiceTests.m | 6 +++--- .../GACAppAttestProviderTests.m | 2 +- .../Storage/GACAppAttestArtifactStorageTests.m | 2 +- .../Storage/GACAppAttestKeyIDStorageTests.m | 2 +- .../Unit/Core/GACAppCheckAPIServiceTests.m | 6 +++--- .../Tests/Unit/Core/GACAppCheckStorageTests.m | 2 +- .../Tests/Unit/Core/GACAppCheckTests.m | 2 +- .../GACAppCheckDebugProviderAPIServiceTests.m | 6 +++--- .../GACDeviceCheckAPIServiceTests.m | 6 +++--- .../GACDeviceCheckProviderTests.m | 2 +- 33 files changed, 46 insertions(+), 100 deletions(-) delete mode 100644 AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h delete mode 100644 AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h delete mode 100644 AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h diff --git a/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.m b/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.m index fe6dfd4f..68bb7350 100644 --- a/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.m +++ b/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.m @@ -23,9 +23,9 @@ #endif #import "AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.h" -#import "AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h" -#import "AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.m b/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.m index 175e5939..eeb59635 100644 --- a/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.m +++ b/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.m @@ -17,7 +17,7 @@ #import "AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.h" #import "AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" static NSString *const kResponseFieldAppCheckTokenDict = @"appCheckToken"; static NSString *const kResponseFieldArtifact = @"artifact"; diff --git a/AppCheckCore/Sources/AppAttestProvider/Errors/GACAppAttestRejectionError.m b/AppCheckCore/Sources/AppAttestProvider/Errors/GACAppAttestRejectionError.m index 1d0abf82..7429af77 100644 --- a/AppCheckCore/Sources/AppAttestProvider/Errors/GACAppAttestRejectionError.m +++ b/AppCheckCore/Sources/AppAttestProvider/Errors/GACAppAttestRejectionError.m @@ -18,7 +18,7 @@ #import "AppCheckCore/Sources/AppAttestProvider/Errors/GACAppAttestRejectionError.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" @implementation GACAppAttestRejectionError diff --git a/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m b/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m index 4db96470..86476861 100644 --- a/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m +++ b/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m @@ -30,16 +30,16 @@ #import "AppCheckCore/Sources/AppAttestProvider/GACAppAttestService.h" #import "AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestArtifactStorage.h" #import "AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestKeyIDStorage.h" -#import "AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.h" #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" #import "AppCheckCore/Sources/Core/Utils/GACAppCheckCryptoUtils.h" #import "AppCheckCore/Sources/AppAttestProvider/Errors/GACAppAttestRejectionError.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestArtifactStorage.m b/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestArtifactStorage.m index e8effbc6..b4318524 100644 --- a/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestArtifactStorage.m +++ b/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestArtifactStorage.m @@ -25,7 +25,7 @@ #import #import "AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestStoredArtifact.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestKeyIDStorage.m b/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestKeyIDStorage.m index 4f3a97cd..87e04831 100644 --- a/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestKeyIDStorage.m +++ b/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestKeyIDStorage.m @@ -24,7 +24,7 @@ #import -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" /// The `GULUserDefaults` suite name for the storage location of the app attest key ID. static NSString *const kKeyIDStorageDefaultsSuiteName = @"com.firebase.GACAppAttestKeyIDStorage"; diff --git a/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h b/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h deleted file mode 100644 index 12e9e14b..00000000 --- a/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// TODO(ncooke3): Clean this up near the end of the PR's progress. -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" diff --git a/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m b/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m index 2fc40307..814c1543 100644 --- a/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m +++ b/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#import "AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" #if __has_include() #import @@ -22,12 +22,12 @@ #endif #import "AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.h" -#import "AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h" #import "AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckLogger.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.m b/AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.m index 6daf96b0..be907380 100644 --- a/AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.m +++ b/AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.m @@ -22,7 +22,7 @@ #import "FBLPromises.h" #endif -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" static NSString *const kResponseFieldToken = @"token"; static NSString *const kResponseFieldTTL = @"ttl"; diff --git a/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h b/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h deleted file mode 100644 index 5b36f867..00000000 --- a/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// TODO(ncooke3): Clean this up near the end of the PR's progress. -#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" diff --git a/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.m b/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.m index 6f050634..51b61eae 100644 --- a/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.m +++ b/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.m @@ -14,7 +14,7 @@ * limitations under the License. */ -#import "AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" @implementation GACURLSessionDataResponse diff --git a/AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.m b/AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.m index 831f3737..377bf319 100644 --- a/AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.m +++ b/AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.m @@ -22,7 +22,7 @@ #import "FBLPromises.h" #endif -#import "AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" @implementation NSURLSession (GACPromises) diff --git a/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m b/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m index 6a25eb8d..d8c2b8b4 100644 --- a/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m +++ b/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m @@ -22,8 +22,8 @@ #import "FBLPromises.h" #endif -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h deleted file mode 100644 index ad0efd61..00000000 --- a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// TODO(ncooke3): Clean this up near the end of the PR's progress. -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" diff --git a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m index 6983232f..5c83d1d4 100644 --- a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m +++ b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m @@ -14,7 +14,7 @@ * limitations under the License. */ -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import diff --git a/AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.m b/AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.m index 4a2b20ea..d42d7f6a 100644 --- a/AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.m +++ b/AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.m @@ -16,7 +16,7 @@ #import "AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" @implementation GACAppCheckHTTPError diff --git a/AppCheckCore/Sources/Core/GACAppCheck.m b/AppCheckCore/Sources/Core/GACAppCheck.m index 6e1781d9..e990efe9 100644 --- a/AppCheckCore/Sources/Core/GACAppCheck.m +++ b/AppCheckCore/Sources/Core/GACAppCheck.m @@ -29,11 +29,11 @@ #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckTokenDelegate.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckTokenResult.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" #import "AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.h" #import "AppCheckCore/Sources/Core/TokenRefresh/GACAppCheckTokenRefreshResult.h" #import "AppCheckCore/Sources/Core/TokenRefresh/GACAppCheckTokenRefresher.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.m b/AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.m index 588a8ea2..c5ec970d 100644 --- a/AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.m +++ b/AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.m @@ -24,8 +24,8 @@ #import -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Core/Storage/GACAppCheckStoredToken+GACAppCheckToken.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.m b/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.m index e968b29e..d4921ece 100644 --- a/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.m +++ b/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.m @@ -22,11 +22,11 @@ #import "FBLPromises.h" #endif -#import "AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m b/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m index b27e0868..38908969 100644 --- a/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m +++ b/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m @@ -24,9 +24,9 @@ #import -#import "AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" #import "AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" diff --git a/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.m b/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.m index 1fd7d22e..1dee9c92 100644 --- a/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.m +++ b/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.m @@ -22,11 +22,11 @@ #import "FBLPromises.h" #endif -#import "AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m b/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m index 4888bc75..0ce2315e 100644 --- a/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m +++ b/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m @@ -26,12 +26,12 @@ #import "AppCheckCore/Sources/Public/AppCheckCore/GACDeviceCheckProvider.h" -#import "AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" #import "AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.h" #import "AppCheckCore/Sources/DeviceCheckProvider/DCDevice+GACDeviceCheckTokenGenerator.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Tests/Integration/GACDeviceCheckAPIServiceE2ETests.m b/AppCheckCore/Tests/Integration/GACDeviceCheckAPIServiceE2ETests.m index 0724ffec..23bae1d3 100644 --- a/AppCheckCore/Tests/Integration/GACDeviceCheckAPIServiceE2ETests.m +++ b/AppCheckCore/Tests/Integration/GACDeviceCheckAPIServiceE2ETests.m @@ -29,8 +29,8 @@ #import "FBLPromise+Testing.h" -#import "AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h" #import "AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" // TODO: Replace with real resource name to run on CI diff --git a/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestAPIServiceTests.m b/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestAPIServiceTests.m index ad5ea665..04538d52 100644 --- a/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestAPIServiceTests.m +++ b/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestAPIServiceTests.m @@ -21,12 +21,12 @@ #import "AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.h" #import "AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.h" -#import "AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h" -#import "AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" #import "AppCheckCore/Tests/Unit/Utils/GACFixtureLoader.h" #import "AppCheckCore/Tests/Utils/Date/GACDateTestUtils.h" diff --git a/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestProviderTests.m b/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestProviderTests.m index 946367ac..5684e7de 100644 --- a/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestProviderTests.m +++ b/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestProviderTests.m @@ -31,8 +31,8 @@ #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" #import "AppCheckCore/Sources/AppAttestProvider/Errors/GACAppAttestRejectionError.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Tests/Utils/AppCheckBackoffWrapperFake/GACAppCheckBackoffWrapperFake.h" diff --git a/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestArtifactStorageTests.m b/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestArtifactStorageTests.m index f2308e28..fa79f154 100644 --- a/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestArtifactStorageTests.m +++ b/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestArtifactStorageTests.m @@ -34,7 +34,7 @@ #import "FBLPromise+Testing.h" #import "AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestArtifactStorage.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" static NSString *const kAppName = @"GACAppAttestArtifactStorageTests"; static NSString *const kAppID = @"1:100000000000:ios:aaaaaaaaaaaaaaaaaaaaaaaa"; diff --git a/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestKeyIDStorageTests.m b/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestKeyIDStorageTests.m index c404c4b2..a0a74082 100644 --- a/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestKeyIDStorageTests.m +++ b/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestKeyIDStorageTests.m @@ -20,7 +20,7 @@ #import "AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestKeyIDStorage.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" static NSString *const kAppName = @"GACAppAttestKeyIDStorageTestsApp"; static NSString *const kAppID = @"app_id"; diff --git a/AppCheckCore/Tests/Unit/Core/GACAppCheckAPIServiceTests.m b/AppCheckCore/Tests/Unit/Core/GACAppCheckAPIServiceTests.m index b8de7a7a..45f10a36 100644 --- a/AppCheckCore/Tests/Unit/Core/GACAppCheckAPIServiceTests.m +++ b/AppCheckCore/Tests/Unit/Core/GACAppCheckAPIServiceTests.m @@ -19,12 +19,12 @@ #import #import "FBLPromise+Testing.h" -#import "AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h" -#import "AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h" #import "AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" #import "AppCheckCore/Tests/Unit/Utils/GACFixtureLoader.h" #import "AppCheckCore/Tests/Utils/Date/GACDateTestUtils.h" diff --git a/AppCheckCore/Tests/Unit/Core/GACAppCheckStorageTests.m b/AppCheckCore/Tests/Unit/Core/GACAppCheckStorageTests.m index e77130cc..51f05129 100644 --- a/AppCheckCore/Tests/Unit/Core/GACAppCheckStorageTests.m +++ b/AppCheckCore/Tests/Unit/Core/GACAppCheckStorageTests.m @@ -35,7 +35,7 @@ #import "AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" static NSString *const kAppName = @"GACAppCheckStorageTestsApp"; diff --git a/AppCheckCore/Tests/Unit/Core/GACAppCheckTests.m b/AppCheckCore/Tests/Unit/Core/GACAppCheckTests.m index b17071d7..450f27b5 100644 --- a/AppCheckCore/Tests/Unit/Core/GACAppCheckTests.m +++ b/AppCheckCore/Tests/Unit/Core/GACAppCheckTests.m @@ -25,10 +25,10 @@ #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckProvider.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckSettings.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.h" #import "AppCheckCore/Sources/Core/TokenRefresh/GACAppCheckTokenRefreshResult.h" #import "AppCheckCore/Sources/Core/TokenRefresh/GACAppCheckTokenRefresher.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckTokenDelegate.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckTokenResult.h" diff --git a/AppCheckCore/Tests/Unit/DebugProvider/GACAppCheckDebugProviderAPIServiceTests.m b/AppCheckCore/Tests/Unit/DebugProvider/GACAppCheckDebugProviderAPIServiceTests.m index 1c9f619f..5bdc8cd6 100644 --- a/AppCheckCore/Tests/Unit/DebugProvider/GACAppCheckDebugProviderAPIServiceTests.m +++ b/AppCheckCore/Tests/Unit/DebugProvider/GACAppCheckDebugProviderAPIServiceTests.m @@ -22,10 +22,10 @@ #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheck.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" -#import "AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h" -#import "AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" #import "AppCheckCore/Tests/Utils/URLSession/GACURLSessionOCMockStub.h" diff --git a/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckAPIServiceTests.m b/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckAPIServiceTests.m index 887676a8..3acd2c37 100644 --- a/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckAPIServiceTests.m +++ b/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckAPIServiceTests.m @@ -19,12 +19,12 @@ #import #import "FBLPromise+Testing.h" -#import "AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.h" -#import "AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" #import "AppCheckCore/Tests/Unit/Utils/GACFixtureLoader.h" #import "AppCheckCore/Tests/Utils/URLSession/GACURLSessionOCMockStub.h" diff --git a/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckProviderTests.m b/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckProviderTests.m index 12eca4ef..d014c559 100644 --- a/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckProviderTests.m +++ b/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckProviderTests.m @@ -19,9 +19,9 @@ #import #import "FBLPromise+Testing.h" -#import "AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.h" #import "AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckTokenGenerator.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACDeviceCheckProvider.h" From 99b009e0526594e8b4d3cb8bdea9fc58f74c5017 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 30 Apr 2026 12:28:37 -0400 Subject: [PATCH 14/71] docs: update agents.md with workflow refinements and troubleshooting - Added requirement for final report (summary + commit message) - Added mandate for updating agents.md with learnings - Documented fixture loading issue on macOS with SPM --- agents.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/agents.md b/agents.md index 5eb27a32..aeacbb5c 100644 --- a/agents.md +++ b/agents.md @@ -37,6 +37,7 @@ When reporting back to the user, prioritize scannability and clarity: 1. **Use Categorized Bullet Points**: Group findings and results into clear categories (e.g., "Build & Test Results", "Code Changes"). 2. **Use Indicators**: Prefix status updates with checkmarks (✅) or caution symbols (⚠️) for immediate visual parsing. 3. **Be Concise**: Avoid conversational filler. Get straight to the results and next steps. +4. **Final Report**: Conclude the task with a concise summary of work and a recommended conventional commit message. --- @@ -148,6 +149,7 @@ you may encounter the following blockers. Use these workarounds: local Ruby version by prefixing the command with `RBENV_VERSION=2.7.5`. - **Quality Gates**: Do not skip `style.sh` and `pod_lib_lint.rb`. They are critical for verification. +- **Fixture Loading in Tests**: When running tests via `swift test` on macOS, `GACFixtureLoader` may fail to find JSON fixtures due to bundle resolution issues, causing tests to fail with `nil URL argument` exceptions. This is often an environment-specific issue with SPM resource bundles on macOS. ### Swift / Objective-C Interoperability Pitfalls @@ -175,5 +177,4 @@ The task is not done until a `walkthrough.md` artifact is created containing: --- ## 🧠 Post Change: Continuous Improvement -Perform self-reflection after completing the task and update this file or -create/update a Knowledge Item to help future agents. +Perform self-reflection after completing the task. You MUST update this file (`agents.md`) with any new learnings, context, or troubleshooting steps that were needed and will be needed again to refine the process for future agents. Alternatively, create or update a Knowledge Item. From 1eecc52961c018e3e4b63ffa4743dae4e0ca86b5 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 30 Apr 2026 15:25:31 -0400 Subject: [PATCH 15/71] docs: refine agents.md with explicit steps and line wrapping - Added Step 5 for style application and Step 6 for doc formatting - Removed redundant style instructions from Quality Gates - Wrapped long lines in `agents.md` to 80 characters --- agents.md | 66 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/agents.md b/agents.md index aeacbb5c..4822f427 100644 --- a/agents.md +++ b/agents.md @@ -34,10 +34,14 @@ A successful task completion MUST produce: ## 💬 Communication Guidelines When reporting back to the user, prioritize scannability and clarity: -1. **Use Categorized Bullet Points**: Group findings and results into clear categories (e.g., "Build & Test Results", "Code Changes"). -2. **Use Indicators**: Prefix status updates with checkmarks (✅) or caution symbols (⚠️) for immediate visual parsing. -3. **Be Concise**: Avoid conversational filler. Get straight to the results and next steps. -4. **Final Report**: Conclude the task with a concise summary of work and a recommended conventional commit message. +1. **Use Categorized Bullet Points**: Group findings and results into clear + categories (e.g., "Build & Test Results", "Code Changes"). +2. **Use Indicators**: Prefix status updates with checkmarks (✅) or caution + symbols (⚠️) for immediate visual parsing. +3. **Be Concise**: Avoid conversational filler. Get straight to the results + and next steps. +4. **Final Report**: Conclude the task with a concise summary of work and a + recommended conventional commit message. --- @@ -60,9 +64,11 @@ When reporting back to the user, prioritize scannability and clarity: ### Step 1: Test-Driven Development (TDD) - **Constraint**: You MUST write tests before writing implementation code. - **Action**: - 1. Create or identify the correct test target in `Package.swift`. Keep Swift and Obj-C test targets separate. + 1. Create or identify the correct test target in `Package.swift`. Keep + Swift and Obj-C test targets separate. 2. Write a failing unit or integration test asserting the new behavior. - 3. Verify it fails by running the appropriate test command (e.g., `swift test --filter `). + 3. Verify it fails by running the appropriate test command (e.g., `swift + test --filter `). ### Step 2: Implementation - Implement the feature or fix. @@ -97,18 +103,24 @@ When reporting back to the user, prioritize scannability and clarity: - **Requirement**: Identify and report any new public APIs created. - **Method**: Check for changes in public headers or symbols. +### Step 5: Style Application +- **Action**: You MUST run `/scripts/style.sh` to + maintain consistency. +- **Constraint**: Since style changes are non-functional, you do NOT need to + re-run tests after applying style fixes. + +### Step 6: Documentation Formatting +- **Requirement**: Wrap all documentation files (like `agents.md`) to be 80 + characters or less (excluding code blocks). Remove all trailing whitespace. + --- ## 🏆 Quality Gates & Best Practices - **Error Handling**: Test edge cases and error paths. -- **Code Style**: You MUST run `/scripts/style.sh` to - maintain consistency. Since style changes are non-functional, you do NOT need - to re-run tests after applying style fixes. - **No Hardcoded Secrets**: Ensure no secrets are committed. -- **Code Reuse & Refactoring**: Prioritize understanding existing structures - to reuse or extend them with minor refactors rather than adding redundant - code. +- **Code Reuse & Refactoring**: Prioritize understanding existing structures to + reuse or extend them with minor refactors rather than adding redundant code. --- @@ -128,7 +140,8 @@ When reporting back to the user, prioritize scannability and clarity: - **Commit Often**: Pause and commit work frequently. - **Scope**: Optimize for smaller commits that represent a complete piece of work or a specific milestone within a larger task. -- **Convention**: Follow conventional commit practices (e.g. `feat:`, `fix:`, `refactor:`). +- **Convention**: Follow conventional commit practices (e.g. `feat:`, `fix:`, + `refactor:`). --- @@ -149,14 +162,23 @@ you may encounter the following blockers. Use these workarounds: local Ruby version by prefixing the command with `RBENV_VERSION=2.7.5`. - **Quality Gates**: Do not skip `style.sh` and `pod_lib_lint.rb`. They are critical for verification. -- **Fixture Loading in Tests**: When running tests via `swift test` on macOS, `GACFixtureLoader` may fail to find JSON fixtures due to bundle resolution issues, causing tests to fail with `nil URL argument` exceptions. This is often an environment-specific issue with SPM resource bundles on macOS. +- **Fixture Loading in Tests**: When running tests via `swift test` on macOS, + `GACFixtureLoader` may fail to find JSON fixtures due to bundle resolution + issues, causing tests to fail with `nil URL argument` exceptions. This is + often an environment-specific issue with SPM resource bundles on macOS. ### Swift / Objective-C Interoperability Pitfalls -When working on mixed-language targets (e.g., Swift tests for Objective-C core code), you will encounter strict compiler bridging issues, particularly with generic classes like `FBLPromise`. To avoid build loop failures: +When working on mixed-language targets (e.g., Swift tests for Objective-C core +code), you will encounter strict compiler bridging issues, particularly with +generic classes like `FBLPromise`. To avoid build loop failures: -- **FBLPromise Instantiation**: `FBLPromise.init()` is `NS_UNAVAILABLE`. The standard Objective-C factory methods (`resolvedWith:` and `promiseWithError:`) bridge to Swift as **unlabeled** static methods that lose type inference. -- **The Fix**: Do NOT attempt to specify generics on the receiver (e.g., `FBLPromise.resolved(...)` will fail). Instead, call the base method and force-cast the result: +- **FBLPromise Instantiation**: `FBLPromise.init()` is `NS_UNAVAILABLE`. The + standard Objective-C factory methods (`resolvedWith:` and `promiseWithError:`) + bridge to Swift as **unlabeled** static methods that lose type inference. +- **The Fix**: Do NOT attempt to specify generics on the receiver (e.g., + `FBLPromise.resolved(...)` will fail). Instead, call the base method and + force-cast the result: ```swift // Success return FBLPromise.resolved(response) as! FBLPromise @@ -164,7 +186,10 @@ When working on mixed-language targets (e.g., Swift tests for Objective-C core c // Failure (Must explicitly cast to NSError) return FBLPromise.resolved(error as NSError) as! FBLPromise ``` -- **Dynamic Dispatch fallback**: If Swift inference completely fails in test mocks, use Objective-C dynamic dispatch to bypass the compiler: `promiseClass.perform(NSSelectorFromString("resolvedWith:"), with: resolution)`. +- **Dynamic Dispatch fallback**: If Swift inference completely fails in test + mocks, use Objective-C dynamic dispatch to bypass the compiler: + `promiseClass.perform(NSSelectorFromString("resolvedWith:"), with: + resolution)`. --- @@ -177,4 +202,7 @@ The task is not done until a `walkthrough.md` artifact is created containing: --- ## 🧠 Post Change: Continuous Improvement -Perform self-reflection after completing the task. You MUST update this file (`agents.md`) with any new learnings, context, or troubleshooting steps that were needed and will be needed again to refine the process for future agents. Alternatively, create or update a Knowledge Item. +Perform self-reflection after completing the task. You MUST update this file +(`agents.md`) with any new learnings, context, or troubleshooting steps that +were needed and will be needed again to refine the process for future agents. +Alternatively, create or update a Knowledge Item. From e242784e9c9f699d3de60c5097fc8c10f2846b83 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 1 May 2026 16:32:32 -0400 Subject: [PATCH 16/71] add tests and expose target in existing lib --- Package.swift | 2 +- ...CheckCoreRecaptchaEnterpriseProvider.swift | 11 +- ...ecaptchaEnterpriseCoreTokenGenerator.swift | 9 +- ...CoreRecaptchaEnterpriseProviderTests.swift | 43 +++++++ .../Tests/MockRecaptchaSupport.swift | 121 ++++++++++++++++++ ...captchaEnterpriseCoreAPIServiceTests.swift | 48 ------- ...chaEnterpriseCoreTokenGeneratorTests.swift | 86 +++++++++++-- 7 files changed, 255 insertions(+), 65 deletions(-) create mode 100644 RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift diff --git a/Package.swift b/Package.swift index a39c58bf..401222c1 100644 --- a/Package.swift +++ b/Package.swift @@ -23,7 +23,7 @@ let package = Package( products: [ .library( name: "AppCheckCore", - targets: ["AppCheckCore"] + targets: ["AppCheckCore", "RecaptchaEnterpriseProvider"] ), ], dependencies: [ diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index ad911976..abb95872 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -24,8 +24,8 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo private var tokenGenerator: RecaptchaEnterpriseTokenGenerator? private let apiService: RecaptchaEnterpriseAPIService - public init(siteKey: String, resourceName: String, APIKey: String, - requestHooks: [@convention(block) (NSMutableURLRequest) -> Void]? = nil) { + @objc public init(siteKey: String, resourceName: String, APIKey: String, + requestHooks: [@convention(block) (NSMutableURLRequest) -> Void]? = nil) { let recaptchaAction = NSClassFromString("RecaptchaEnterprise.RCAAction") as? RCAActionProtocol.Type let action = recaptchaAction?.init(customAction: "app_check_ios") @@ -47,6 +47,13 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo ) } + init(tokenGenerator: RecaptchaEnterpriseTokenGenerator?, + apiService: RecaptchaEnterpriseAPIService) { + self.tokenGenerator = tokenGenerator + self.apiService = apiService + super.init() + } + @objc(getTokenWithCompletion:) public func getToken(completion handler: @escaping (AppCheckCoreToken?, (any Error)?) -> Void) { getToken(limitedUse: false) diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index 93dbfbff..858313a6 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -25,13 +25,14 @@ final class RecaptchaEnterpriseTokenGenerator { private let recaptchaPromise: Promise - init(siteKey: String, action: RCAActionProtocol) { + init(siteKey: String, action: RCAActionProtocol, + recaptchaClass: RCARecaptchaProtocol.Type? = nil) { self.siteKey = siteKey self.action = action recaptchaPromise = Promise { fulfill, reject in - guard let recaptcha = - NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol - .Type else { + let recaptcha = recaptchaClass ?? + NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol.Type + guard let recaptcha = recaptcha else { throw GACAppCheckErrorUtil.unsupportedAttestationProvider("RecaptchaEnterprise") } recaptcha.fetchClient(withSiteKey: siteKey) { client, error in diff --git a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift index dfcd2598..10a159b0 100644 --- a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift @@ -75,4 +75,47 @@ final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { waitForExpectations(timeout: 1.0) } + + func testGetTokenSuccess() { + // Arrange + let mockClient = MockRecaptchaClient(dummy: ()) + mockClient.mockToken = "valid-recaptcha-token" + MockRecaptcha.mockClient = mockClient + + let tokenGenerator = RecaptchaEnterpriseTokenGenerator( + siteKey: testSiteKey, + action: MockRCAAction(customAction: "app_check_ios"), + recaptchaClass: MockRecaptcha.self + ) + + let mockCoreAPIService = MockAppCheckCoreAPIService() + let expectedAppCheckToken = AppCheckCoreToken( + token: "app-check-token-456", + expirationDate: Date(timeIntervalSinceNow: 3600) + ) + mockCoreAPIService.expectedToken = expectedAppCheckToken + + let apiService = RecaptchaEnterpriseAPIService( + APIService: mockCoreAPIService, + resourceName: testResourceName + ) + + let providerWithMocks = AppCheckCoreRecaptchaEnterpriseProvider( + tokenGenerator: tokenGenerator, + apiService: apiService + ) + + let expectation = self.expectation(description: "Get token succeeds") + + // Act + providerWithMocks.getToken { token, error in + // Assert + XCTAssertNotNil(token) + XCTAssertNil(error) + XCTAssertEqual(token?.token, expectedAppCheckToken.token) + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } } diff --git a/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift b/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift new file mode 100644 index 00000000..69d4bb81 --- /dev/null +++ b/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift @@ -0,0 +1,121 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@testable import AppCheckCore +import FBLPromises +import Foundation +import Promises +@testable import RecaptchaEnterpriseProvider +import RecaptchaInterop + +class MockRCAAction: NSObject, RCAActionProtocol { + var action: String { return customAction } + + static var login: RCAActionProtocol { return MockRCAAction(customAction: "login") } + static var signup: RCAActionProtocol { return MockRCAAction(customAction: "signup") } + + let customAction: String + + required init(customAction: String) { + self.customAction = customAction + super.init() + } +} + +final class MockRecaptcha: NSObject, RCARecaptchaProtocol { + static var mockClient: MockRecaptchaClient? + static var mockError: Error? + + init(dummy: Void = ()) { + super.init() + } + + static func fetchClient(withSiteKey siteKey: String, + completion: @escaping (RCARecaptchaClientProtocol?, Error?) -> Void) { + if let error = mockError { + completion(nil, error) + } else { + completion(mockClient, nil) + } + } +} + +final class MockRecaptchaClient: NSObject, RCARecaptchaClientProtocol { + var mockToken: String? + var mockError: Error? + + init(dummy: Void = ()) { + super.init() + } + + func execute(withAction action: RCAActionProtocol, + completion: @escaping (String?, Error?) -> Void) { + if let error = mockError { + completion(nil, error) + } else { + completion(mockToken, nil) + } + } + + func execute(withAction action: RCAActionProtocol, withTimeout timeout: Double, + completion: @escaping (String?, Error?) -> Void) { + execute(withAction: action, completion: completion) + } +} + +class MockAppCheckCoreAPIService: NSObject, AppCheckCoreAPIServiceProtocol { + var baseURL: String = "https://test.com" + + struct RequestData { + let url: URL? + let httpMethod: String? + let body: Data? + let additionalHeaders: [String: String]? + } + + var lastRequest: RequestData? + var expectedResponse: GACURLSessionDataResponse? + var expectedToken: AppCheckCoreToken? + var expectedError: Error? + + func sendRequest(with url: URL, httpMethod: String, body: Data?, + additionalHeaders: [String: String]?) -> FBLPromise { + lastRequest = RequestData( + url: url, + httpMethod: httpMethod, + body: body, + additionalHeaders: additionalHeaders + ) + + let resolution: Any = (expectedError as NSError?) ?? + (expectedResponse ?? GACURLSessionDataResponse( + response: HTTPURLResponse(), + httpBody: Data() + )) + let promiseClass = NSClassFromString("FBLPromise") as! NSObject.Type + let unmanaged = promiseClass.perform(NSSelectorFromString("resolvedWith:"), with: resolution)! + return unmanaged.takeUnretainedValue() as! FBLPromise + } + + func appCheckToken(withAPIResponse response: GACURLSessionDataResponse) + -> FBLPromise { + let resolution: Any = (expectedError as NSError?) ?? (expectedToken ?? AppCheckCoreToken( + token: "dummy", + expirationDate: Date() + )) + let promiseClass = NSClassFromString("FBLPromise") as! NSObject.Type + let unmanaged = promiseClass.perform(NSSelectorFromString("resolvedWith:"), with: resolution)! + return unmanaged.takeUnretainedValue() as! FBLPromise + } +} diff --git a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift index 6846ad87..3b7f5799 100644 --- a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift @@ -130,51 +130,3 @@ final class RecaptchaEnterpriseCoreAPIServiceTests: XCTestCase { waitForExpectations(timeout: 1.0) } } - -// MARK: - Mocks - -private class MockAppCheckCoreAPIService: NSObject, AppCheckCoreAPIServiceProtocol { - var baseURL: String = "https://test.com" - - struct RequestData { - let url: URL? - let httpMethod: String? - let body: Data? - let additionalHeaders: [String: String]? - } - - var lastRequest: RequestData? - var expectedResponse: GACURLSessionDataResponse? - var expectedToken: AppCheckCoreToken? - var expectedError: Error? - - func sendRequest(with url: URL, httpMethod: String, body: Data?, - additionalHeaders: [String: String]?) -> FBLPromise { - lastRequest = RequestData( - url: url, - httpMethod: httpMethod, - body: body, - additionalHeaders: additionalHeaders - ) - - let resolution: Any = (expectedError as NSError?) ?? - (expectedResponse ?? GACURLSessionDataResponse( - response: HTTPURLResponse(), - httpBody: Data() - )) - let promiseClass = NSClassFromString("FBLPromise") as! NSObject.Type - let unmanaged = promiseClass.perform(NSSelectorFromString("resolvedWith:"), with: resolution)! - return unmanaged.takeUnretainedValue() as! FBLPromise - } - - func appCheckToken(withAPIResponse response: GACURLSessionDataResponse) - -> FBLPromise { - let resolution: Any = (expectedError as NSError?) ?? (expectedToken ?? AppCheckCoreToken( - token: "dummy", - expirationDate: Date() - )) - let promiseClass = NSClassFromString("FBLPromise") as! NSObject.Type - let unmanaged = promiseClass.perform(NSSelectorFromString("resolvedWith:"), with: resolution)! - return unmanaged.takeUnretainedValue() as! FBLPromise - } -} diff --git a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift index 8224754c..3c67c0c0 100644 --- a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift @@ -26,6 +26,8 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { override func setUp() { super.setUp() mockAction = MockRCAAction(customAction: "test_action") + MockRecaptcha.mockClient = nil + MockRecaptcha.mockError = nil } func testGetRecaptchaTokenWithoutSDK() { @@ -45,20 +47,84 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { waitForExpectations(timeout: 1.0) } -} -// MARK: - Mocks + func testGetRecaptchaTokenSuccess() { + // Arrange + let mockClient = MockRecaptchaClient(dummy: ()) + mockClient.mockToken = "valid-recaptcha-token" + MockRecaptcha.mockClient = mockClient + + let generator = RecaptchaEnterpriseTokenGenerator( + siteKey: testSiteKey, + action: mockAction, + recaptchaClass: MockRecaptcha.self + ) + + let expectation = self.expectation(description: "Generates token successfully") + + // Act + generator.getRecaptchaToken().then { token in + // Assert + XCTAssertEqual(token, "valid-recaptcha-token") + expectation.fulfill() + }.catch { error in + XCTFail("Unexpected error: \(error)") + } + + waitForExpectations(timeout: 1.0) + } + + func testGetRecaptchaTokenFetchClientFailure() { + // Arrange + let expectedError = NSError(domain: "test", code: -1, userInfo: nil) + MockRecaptcha.mockError = expectedError + + let generator = RecaptchaEnterpriseTokenGenerator( + siteKey: testSiteKey, + action: mockAction, + recaptchaClass: MockRecaptcha.self + ) + + let expectation = self.expectation(description: "Fails when fetchClient fails") -private class MockRCAAction: NSObject, RCAActionProtocol { - var action: String { return customAction } + // Act + generator.getRecaptchaToken().then { token in + XCTFail("Should not succeed when fetchClient fails") + }.catch { error in + // Assert + XCTAssertEqual((error as NSError).domain, expectedError.domain) + XCTAssertEqual((error as NSError).code, expectedError.code) + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } - static var login: RCAActionProtocol { return MockRCAAction(customAction: "login") } - static var signup: RCAActionProtocol { return MockRCAAction(customAction: "signup") } + func testGetRecaptchaTokenExecutionFailure() { + // Arrange + let mockClient = MockRecaptchaClient(dummy: ()) + let expectedError = NSError(domain: "test", code: -2, userInfo: nil) + mockClient.mockError = expectedError + MockRecaptcha.mockClient = mockClient - let customAction: String + let generator = RecaptchaEnterpriseTokenGenerator( + siteKey: testSiteKey, + action: mockAction, + recaptchaClass: MockRecaptcha.self + ) - required init(customAction: String) { - self.customAction = customAction - super.init() + let expectation = self.expectation(description: "Fails when execute fails") + + // Act + generator.getRecaptchaToken().then { token in + XCTFail("Should not succeed when execute fails") + }.catch { error in + // Assert + XCTAssertEqual((error as NSError).domain, expectedError.domain) + XCTAssertEqual((error as NSError).code, expectedError.code) + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) } } From a5c0fd9601627353d8befcb5322df0a80afdbfcd Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 1 May 2026 16:33:01 -0400 Subject: [PATCH 17/71] add temp files for discussion --- provider_integration_paths.md | 91 +++++++++++++++++++++++++++++++++++ todos.md | 6 +++ 2 files changed, 97 insertions(+) create mode 100644 provider_integration_paths.md create mode 100644 todos.md diff --git a/provider_integration_paths.md b/provider_integration_paths.md new file mode 100644 index 00000000..bfe280ce --- /dev/null +++ b/provider_integration_paths.md @@ -0,0 +1,91 @@ +# Recaptcha Provider Integration Paths + +This document outlines two possible paths for integrating the new +`RecaptchaEnterpriseProvider` into the `FirebaseAppCheck` SDK within the +`firebase-ios-sdk` repository. + +## Background + +`AppCheckCore` is an internal dependency of `FirebaseAppCheck`. Customers do +not depend on `AppCheckCore` directly; they consume `FirebaseAppCheck`. + +Currently, all other providers (`DeviceCheck`, `AppAttest`, `Debug`) are +bundled within the monolithic `AppCheckCore` product in the `app-check` repo +and the `FirebaseAppCheck` target in the `firebase-ios-sdk` repo. + +--- + +## Path A: Consistent Path (Current Choice) + +This path follows the existing pattern used by other providers. + +### In `app-check` Repository + +* **Approach**: Bundle `RecaptchaEnterpriseProvider` target into the existing + `AppCheckCore` library product. +* **`Package.swift` Changes**: + ```swift + products: [ + .library( + name: "AppCheckCore", + targets: ["AppCheckCore", "RecaptchaEnterpriseProvider"] + ), + ] + ``` + +### In `firebase-ios-sdk` Repository + +* **Integration**: `FirebaseAppCheck` target will automatically get the core + reCAPTCHA glue code because it depends on `AppCheckCore`. +* **Wrapper**: Add `FIRRecaptchaEnterpriseProvider` wrapper files directly + into `FirebaseAppCheck/Sources`. + +### Developer Experience + +* **Opt-in**: The customer just imports `FirebaseAppCheck`. The glue code is + present but inert. To make it work, they must manually add the + `RecaptchaEnterprise` SDK to their project. +* **Pros**: Consistent with existing architecture; lowest friction for users + who want reCAPTCHA (no new product dependency). +* **Cons**: All users get the glue code, even if not used (though size is + negligible). + +--- + +## Path B: Modular Path + +This path follows the design document's goal of allowing exclusion to save +binary size, treating the reCAPTCHA provider as a separate component. + +### In `app-check` Repository + +* **Approach**: Expose `RecaptchaEnterpriseProvider` as a separate library + product. +* **`Package.swift` Changes**: + ```swift + products: [ + .library( + name: "AppCheckCore", + targets: ["AppCheckCore"] + ), + .library( + name: "RecaptchaEnterpriseProvider", + targets: ["RecaptchaEnterpriseProvider"] + ), + ] + ``` + +### In `firebase-ios-sdk` Repository + +* **Integration**: Create a new, separate target (e.g., + `FirebaseRecaptchaEnterpriseProvider`) that depends on `FirebaseAppCheck` + and the core `RecaptchaEnterpriseProvider` product. + +### Developer Experience + +* **Opt-in**: The customer must explicitly add both `FirebaseAppCheck` and + `FirebaseRecaptchaEnterpriseProvider` to their target, plus manually add the + `RecaptchaEnterprise` SDK. +* **Pros**: Strict modularity; users can fully exclude the provider and its + interop dependency. +* **Cons**: Higher friction for users (multiple dependencies to add). diff --git a/todos.md b/todos.md new file mode 100644 index 00000000..299c7257 --- /dev/null +++ b/todos.md @@ -0,0 +1,6 @@ +# TODOs + +- [ ] Add `serviceName` to `AppCheckCoreRecaptchaEnterpriseProvider.init` for + consistency with other providers. +- [ ] Implement exponential backoff in `RecaptchaEnterpriseProvider` for + transient errors (need to reach out to people first). From 18477215970cc3045462304a98f72423a41957fb Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 1 May 2026 17:14:59 -0400 Subject: [PATCH 18/71] feat(AppCheck): depend on RecaptchaEnterpriseProvider product from app-check --- Package.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 401222c1..d376ade2 100644 --- a/Package.swift +++ b/Package.swift @@ -23,8 +23,14 @@ let package = Package( products: [ .library( name: "AppCheckCore", - targets: ["AppCheckCore", "RecaptchaEnterpriseProvider"] + targets: ["AppCheckCore"] ), + + .library( + name: "RecaptchaEnterpriseProvider", + targets: ["RecaptchaEnterpriseProvider"] + ), + ], dependencies: [ .package( From 02dc75c70825b06fe0bf289648650425cb7e6ab6 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 5 May 2026 14:22:48 -0400 Subject: [PATCH 19/71] rework initializers, make var a let, remove dup name --- .../AppCheckCoreRecaptchaEnterpriseProvider.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index abb95872..9eadecfd 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -21,16 +21,18 @@ import RecaptchaInterop @objc(GACRecaptchaEnterpriseProvider) public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCoreProvider { - private var tokenGenerator: RecaptchaEnterpriseTokenGenerator? + private let tokenGenerator: RecaptchaEnterpriseTokenGenerator? private let apiService: RecaptchaEnterpriseAPIService - @objc public init(siteKey: String, resourceName: String, APIKey: String, - requestHooks: [@convention(block) (NSMutableURLRequest) -> Void]? = nil) { + @objc public convenience init(siteKey: String, resourceName: String, APIKey: String, + requestHooks: [@convention(block) (NSMutableURLRequest) -> Void]? = + nil) { let recaptchaAction = NSClassFromString("RecaptchaEnterprise.RCAAction") as? RCAActionProtocol.Type let action = recaptchaAction?.init(customAction: "app_check_ios") - if let action = action { + let tokenGenerator: RecaptchaEnterpriseTokenGenerator? + if let action { tokenGenerator = RecaptchaEnterpriseTokenGenerator(siteKey: siteKey, action: action) } else { tokenGenerator = nil @@ -41,10 +43,12 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo baseURL: nil, apiKey: APIKey, requestHooks: requestHooks) - apiService = RecaptchaEnterpriseAPIService( + let apiService = RecaptchaEnterpriseAPIService( APIService: appCheckAPIService, resourceName: resourceName ) + + self.init(tokenGenerator: tokenGenerator, apiService: apiService) } init(tokenGenerator: RecaptchaEnterpriseTokenGenerator?, From df9a664802dad8d5e84a0fc13e0c9efb1bbee941 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 5 May 2026 15:49:07 -0400 Subject: [PATCH 20/71] feat: add assertion for missing Recaptcha SDK and update TODOs --- .../AppCheckCoreRecaptchaEnterpriseProvider.swift | 3 +++ ...AppCheckCoreRecaptchaEnterpriseProviderTests.swift | 11 +++++++---- todos.md | 2 ++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index 9eadecfd..c4895968 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -29,6 +29,9 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo nil) { let recaptchaAction = NSClassFromString("RecaptchaEnterprise.RCAAction") as? RCAActionProtocol.Type + if recaptchaAction == nil { + assertionFailure("The reCAPTCHA SDK is not linked. Please see the documentation.") + } let action = recaptchaAction?.init(customAction: "app_check_ios") let tokenGenerator: RecaptchaEnterpriseTokenGenerator? diff --git a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift index 10a159b0..784db833 100644 --- a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift @@ -26,11 +26,14 @@ final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { override func setUp() { super.setUp() + let mockCoreAPIService = MockAppCheckCoreAPIService() + let apiService = RecaptchaEnterpriseAPIService( + APIService: mockCoreAPIService, + resourceName: testResourceName + ) provider = AppCheckCoreRecaptchaEnterpriseProvider( - siteKey: testSiteKey, - resourceName: testResourceName, - APIKey: testAPIKey, - requestHooks: nil + tokenGenerator: nil, + apiService: apiService ) } diff --git a/todos.md b/todos.md index 299c7257..6b10fd09 100644 --- a/todos.md +++ b/todos.md @@ -4,3 +4,5 @@ consistency with other providers. - [ ] Implement exponential backoff in `RecaptchaEnterpriseProvider` for transient errors (need to reach out to people first). +- [ ] Bring up API proposal for coordinating assertion behavior when optional + SDKs (like reCAPTCHA) are missing across platforms. From 9d503b91559b267ac40b7649c1816244d1ad1dff Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 5 May 2026 16:21:13 -0400 Subject: [PATCH 21/71] feat(recaptcha): improve assertion for missing SDK and refactor constants --- .../AppCheckCoreRecaptchaEnterpriseProvider.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index c4895968..f70a097c 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -21,6 +21,10 @@ import RecaptchaInterop @objc(GACRecaptchaEnterpriseProvider) public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCoreProvider { + private static let recaptchaActionClassName = "RecaptchaEnterprise.RCAAction" + private static let appCheckActionName = "app_check_ios" + private static let providerName = "RecaptchaEnterprise" + private let tokenGenerator: RecaptchaEnterpriseTokenGenerator? private let apiService: RecaptchaEnterpriseAPIService @@ -28,11 +32,13 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo requestHooks: [@convention(block) (NSMutableURLRequest) -> Void]? = nil) { let recaptchaAction = - NSClassFromString("RecaptchaEnterprise.RCAAction") as? RCAActionProtocol.Type + NSClassFromString(Self.recaptchaActionClassName) as? RCAActionProtocol.Type if recaptchaAction == nil { - assertionFailure("The reCAPTCHA SDK is not linked. Please see the documentation.") + assertionFailure( + "The reCAPTCHA Enterprise SDK is not linked. See https://cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment" + ) } - let action = recaptchaAction?.init(customAction: "app_check_ios") + let action = recaptchaAction?.init(customAction: Self.appCheckActionName) let tokenGenerator: RecaptchaEnterpriseTokenGenerator? if let action { @@ -84,7 +90,7 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo private func getToken(limitedUse: Bool) -> Promise { guard let tokenGenerator = tokenGenerator else { - return Promise(GACAppCheckErrorUtil.unsupportedAttestationProvider("RecaptchaEnterprise")) + return Promise(GACAppCheckErrorUtil.unsupportedAttestationProvider(Self.providerName)) } return tokenGenerator.getRecaptchaToken() .then { recaptchaToken in From eb4cb4a88d18c90125ec4dfe3b8b88b676e199e1 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 5 May 2026 17:15:23 -0400 Subject: [PATCH 22/71] add clarifying comment, future todo --- ...CheckCoreRecaptchaEnterpriseProvider.swift | 2 ++ todos.md | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index f70a097c..8e71922e 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -34,6 +34,8 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo let recaptchaAction = NSClassFromString(Self.recaptchaActionClassName) as? RCAActionProtocol.Type if recaptchaAction == nil { + // Fail fast in Debug (-Onone) builds to alert the developer. + // In Release (-O) builds, this falls back to returning an error in getToken. assertionFailure( "The reCAPTCHA Enterprise SDK is not linked. See https://cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment" ) diff --git a/todos.md b/todos.md index 6b10fd09..fc6345c9 100644 --- a/todos.md +++ b/todos.md @@ -6,3 +6,31 @@ transient errors (need to reach out to people first). - [ ] Bring up API proposal for coordinating assertion behavior when optional SDKs (like reCAPTCHA) are missing across platforms. + +## Error Handling for Missing reCAPTCHA SDK in Production + +When the reCAPTCHA SDK is not linked in a release build, we currently return a +generic "unsupported" error. We discussed three options for improving this: + +### Option 1: Follow Precedent (Keep as is) +- **Code**: `GACAppCheckErrorCodeUnsupported` (4) +- **Message**: "The attestation provider RecaptchaEnterprise is not supported on + current platform and OS version." +- **Pros**: Consistent with other providers (DeviceCheck, AppAttest) when they + are not supported. +- **Cons**: The message implies a platform/OS limitation, not a missing + dependency. + +### Option 2: Prioritize Message Accuracy +- **Code**: `GACAppCheckErrorCodeUnknown` (0) +- **Message**: "The reCAPTCHA Enterprise SDK is not linked. See + https://cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment" +- **Pros**: Provides the exact, clear message in logs. +- **Cons**: Uses a generic error code (`Unknown`) instead of `Unsupported`. + +### Option 3: Enhance the Utility (Recommended for long term) +- **Code**: `GACAppCheckErrorCodeUnsupported` (4) +- **Message**: Specific message as in Option 2. +- **Pros**: Best error code + best message. +- **Cons**: Requires adding a new method to `GACAppCheckErrorUtil` (internal + change). From 5cbadaf12fe93e21269867254ff2fb4c7ef21e23 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 6 May 2026 12:27:45 -0400 Subject: [PATCH 23/71] refactor: prefer swift shorthand binding --- .../Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift | 2 +- .../Sources/RecaptchaEnterpriseCoreTokenGenerator.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index 8e71922e..82985d70 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -91,7 +91,7 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo } private func getToken(limitedUse: Bool) -> Promise { - guard let tokenGenerator = tokenGenerator else { + guard let tokenGenerator else { return Promise(GACAppCheckErrorUtil.unsupportedAttestationProvider(Self.providerName)) } return tokenGenerator.getRecaptchaToken() diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index 858313a6..861e75e1 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -32,11 +32,11 @@ final class RecaptchaEnterpriseTokenGenerator { recaptchaPromise = Promise { fulfill, reject in let recaptcha = recaptchaClass ?? NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol.Type - guard let recaptcha = recaptcha else { + guard let recaptcha else { throw GACAppCheckErrorUtil.unsupportedAttestationProvider("RecaptchaEnterprise") } recaptcha.fetchClient(withSiteKey: siteKey) { client, error in - if let client = client { + if let client { fulfill(client) } else { reject(error ?? GACAppCheckErrorUtil From f457a4108bc19cc4a5b9f8af46bd28b7d0cb1cbf Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 6 May 2026 12:54:10 -0400 Subject: [PATCH 24/71] refactor(recaptcha): apply idiomatic Swift renames to properties and parameters --- .../API/RecaptchaEnterpriseCoreAPIService.swift | 12 ++++++------ .../AppCheckCoreRecaptchaEnterpriseProvider.swift | 4 ++-- .../RecaptchaEnterpriseCoreTokenGenerator.swift | 14 +++++++------- ...CheckCoreRecaptchaEnterpriseProviderTests.swift | 6 +++--- .../RecaptchaEnterpriseCoreAPIServiceTests.swift | 2 +- ...ecaptchaEnterpriseCoreTokenGeneratorTests.swift | 11 +++++++---- 6 files changed, 26 insertions(+), 23 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift index 2dcd68a4..0496e5df 100644 --- a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift +++ b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift @@ -27,31 +27,31 @@ private enum Constants { @objc(GARecaptchaEnterpriseAPIService) final class RecaptchaEnterpriseAPIService: NSObject { - private let APIService: AppCheckCoreAPIServiceProtocol + private let apiService: AppCheckCoreAPIServiceProtocol private let resourceName: String - init(APIService: AppCheckCoreAPIServiceProtocol, resourceName: String) { - self.APIService = APIService + init(apiService: AppCheckCoreAPIServiceProtocol, resourceName: String) { + self.apiService = apiService self.resourceName = resourceName } func appCheckToken(withRecaptchaToken recaptchaToken: String, limitedUse: Bool) -> Promise { - let urlString = "\(APIService.baseURL)/\(resourceName):exchangeRecaptchaEnterpriseToken" + let urlString = "\(apiService.baseURL)/\(resourceName):exchangeRecaptchaEnterpriseToken" guard let url = URL(string: urlString) else { return Promise(GACAppCheckErrorUtil.error(withFailureReason: "Invalid URL string")) } return httpBody(withRecaptchaToken: recaptchaToken, limitedUse: limitedUse) .then { httpBody in - Promise(self.APIService.sendRequest(with: url, + Promise(self.apiService.sendRequest(with: url, httpMethod: "POST", body: httpBody, additionalHeaders: [Constants .contentTypeKey: Constants .jsonContentType])) }.then { response in - Promise(self.APIService.appCheckToken(withAPIResponse: response)) + Promise(self.apiService.appCheckToken(withAPIResponse: response)) } } diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index 82985d70..900082c0 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -44,7 +44,7 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo let tokenGenerator: RecaptchaEnterpriseTokenGenerator? if let action { - tokenGenerator = RecaptchaEnterpriseTokenGenerator(siteKey: siteKey, action: action) + tokenGenerator = RecaptchaEnterpriseTokenGenerator(siteKey: siteKey, recaptchaAction: action) } else { tokenGenerator = nil } @@ -55,7 +55,7 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo apiKey: APIKey, requestHooks: requestHooks) let apiService = RecaptchaEnterpriseAPIService( - APIService: appCheckAPIService, + apiService: appCheckAPIService, resourceName: resourceName ) diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index 861e75e1..cf556c38 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -21,15 +21,15 @@ import RecaptchaInterop final class RecaptchaEnterpriseTokenGenerator { private let siteKey: String - private let action: RCAActionProtocol + private let recaptchaAction: RCAActionProtocol - private let recaptchaPromise: Promise + private let recaptchaClient: Promise - init(siteKey: String, action: RCAActionProtocol, + init(siteKey: String, recaptchaAction: RCAActionProtocol, recaptchaClass: RCARecaptchaProtocol.Type? = nil) { self.siteKey = siteKey - self.action = action - recaptchaPromise = Promise { fulfill, reject in + self.recaptchaAction = recaptchaAction + recaptchaClient = Promise { fulfill, reject in let recaptcha = recaptchaClass ?? NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol.Type guard let recaptcha else { @@ -48,9 +48,9 @@ final class RecaptchaEnterpriseTokenGenerator { // TODO(ncooke3): Investigate whether we need a backoff mechanism. func getRecaptchaToken() -> Promise { - recaptchaPromise.then { client in + recaptchaClient.then { client in Promise { fulfill, reject in - client.execute(withAction: self.action) { token, error in + client.execute(withAction: self.recaptchaAction) { token, error in if let token = token { fulfill(token) } else { diff --git a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift index 784db833..4af68279 100644 --- a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift @@ -28,7 +28,7 @@ final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { super.setUp() let mockCoreAPIService = MockAppCheckCoreAPIService() let apiService = RecaptchaEnterpriseAPIService( - APIService: mockCoreAPIService, + apiService: mockCoreAPIService, resourceName: testResourceName ) provider = AppCheckCoreRecaptchaEnterpriseProvider( @@ -87,7 +87,7 @@ final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { let tokenGenerator = RecaptchaEnterpriseTokenGenerator( siteKey: testSiteKey, - action: MockRCAAction(customAction: "app_check_ios"), + recaptchaAction: MockRCAAction(customAction: "app_check_ios"), recaptchaClass: MockRecaptcha.self ) @@ -99,7 +99,7 @@ final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { mockCoreAPIService.expectedToken = expectedAppCheckToken let apiService = RecaptchaEnterpriseAPIService( - APIService: mockCoreAPIService, + apiService: mockCoreAPIService, resourceName: testResourceName ) diff --git a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift index 3b7f5799..e4f60945 100644 --- a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift @@ -28,7 +28,7 @@ final class RecaptchaEnterpriseCoreAPIServiceTests: XCTestCase { super.setUp() mockCoreAPIService = MockAppCheckCoreAPIService() apiService = RecaptchaEnterpriseAPIService( - APIService: mockCoreAPIService, + apiService: mockCoreAPIService, resourceName: testResourceName ) } diff --git a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift index 3c67c0c0..2b69d06e 100644 --- a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift @@ -31,7 +31,10 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { } func testGetRecaptchaTokenWithoutSDK() { - let generator = RecaptchaEnterpriseTokenGenerator(siteKey: testSiteKey, action: mockAction) + let generator = RecaptchaEnterpriseTokenGenerator( + siteKey: testSiteKey, + recaptchaAction: mockAction + ) let expectation = self.expectation(description: "Fails to generate token without Recaptcha SDK") @@ -56,7 +59,7 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { let generator = RecaptchaEnterpriseTokenGenerator( siteKey: testSiteKey, - action: mockAction, + recaptchaAction: mockAction, recaptchaClass: MockRecaptcha.self ) @@ -81,7 +84,7 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { let generator = RecaptchaEnterpriseTokenGenerator( siteKey: testSiteKey, - action: mockAction, + recaptchaAction: mockAction, recaptchaClass: MockRecaptcha.self ) @@ -109,7 +112,7 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { let generator = RecaptchaEnterpriseTokenGenerator( siteKey: testSiteKey, - action: mockAction, + recaptchaAction: mockAction, recaptchaClass: MockRecaptcha.self ) From 652e9f2a80ed0c413b585f7cb89d6dc5a28e1940 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 13:07:47 -0400 Subject: [PATCH 25/71] fixes? --- .github/workflows/spm.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spm.yml b/.github/workflows/spm.yml index 947ce905..6818b0ea 100644 --- a/.github/workflows/spm.yml +++ b/.github/workflows/spm.yml @@ -32,4 +32,4 @@ jobs: - name: Initialize xcodebuild run: xcodebuild -list - name: iOS Unit Tests - run: scripts/third_party/travis/retry.sh scripts/build.sh AppCheck ${{ matrix.platform }} spm + run: scripts/third_party/travis/retry.sh scripts/build.sh AppCheck-Package ${{ matrix.platform }} spm From 3770f8b389395874b721347af3494397ba0ef25b Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 16:03:56 -0400 Subject: [PATCH 26/71] feat: Implement exponential backoff for reCAPTCHA Enterprise provider - Move GACAppCheckBackoffWrapper.h to public headers in AppCheckCore. - Wrap reCAPTCHA execute calls with backoff wrapper. - Map transient reCAPTCHA errors (1 and 100) to ServerUnreachable to trigger backoff. - Preserve underlying errors during mapping. - Remove reflection from Swift bridging by using Promises library methods. --- .../AppAttestProvider/GACAppAttestProvider.m | 2 +- .../Core/Backoff/GACAppCheckBackoffWrapper.m | 2 +- .../GACDeviceCheckProvider.m | 2 +- .../Public/AppCheckCore/AppCheckCore.h | 1 + .../AppCheckCore}/GACAppCheckBackoffWrapper.h | 0 .../Core/GACAppCheckBackoffWrapperTests.m | 2 +- .../GACAppCheckBackoffWrapperFake.h | 8 +- ...CheckCoreRecaptchaEnterpriseProvider.swift | 7 +- ...ecaptchaEnterpriseCoreTokenGenerator.swift | 72 ++++- ...chaEnterpriseCoreTokenGeneratorTests.swift | 266 ++++++++++++++++++ 10 files changed, 352 insertions(+), 10 deletions(-) rename AppCheckCore/Sources/{Core/Backoff => Public/AppCheckCore}/GACAppCheckBackoffWrapper.h (100%) diff --git a/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m b/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m index 86476861..78a0cf73 100644 --- a/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m +++ b/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m @@ -30,9 +30,9 @@ #import "AppCheckCore/Sources/AppAttestProvider/GACAppAttestService.h" #import "AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestArtifactStorage.h" #import "AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestKeyIDStorage.h" -#import "AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.h" #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" #import "AppCheckCore/Sources/Core/Utils/GACAppCheckCryptoUtils.h" diff --git a/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m b/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m index d8c2b8b4..626df8ec 100644 --- a/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m +++ b/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m @@ -14,7 +14,7 @@ * limitations under the License. */ -#import "AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h" #if __has_include() #import diff --git a/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m b/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m index 0ce2315e..ba61674a 100644 --- a/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m +++ b/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m @@ -26,11 +26,11 @@ #import "AppCheckCore/Sources/Public/AppCheckCore/GACDeviceCheckProvider.h" -#import "AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.h" #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" #import "AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.h" #import "AppCheckCore/Sources/DeviceCheckProvider/DCDevice+GACDeviceCheckTokenGenerator.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" diff --git a/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h b/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h index f229fa45..d97d0650 100644 --- a/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h +++ b/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h @@ -34,5 +34,6 @@ // Internal headers exposed for interop with the Swift implementation. #import "GACAppCheckAPIService.h" +#import "GACAppCheckBackoffWrapper.h" #import "GACAppCheckErrorUtil.h" #import "GACURLSessionDataResponse.h" diff --git a/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.h b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h similarity index 100% rename from AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.h rename to AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h diff --git a/AppCheckCore/Tests/Unit/Core/GACAppCheckBackoffWrapperTests.m b/AppCheckCore/Tests/Unit/Core/GACAppCheckBackoffWrapperTests.m index 7194285e..e96dc6a8 100644 --- a/AppCheckCore/Tests/Unit/Core/GACAppCheckBackoffWrapperTests.m +++ b/AppCheckCore/Tests/Unit/Core/GACAppCheckBackoffWrapperTests.m @@ -23,7 +23,7 @@ #import "FBLPromises.h" #endif -#import "AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.h" +#import #import "AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.h" diff --git a/AppCheckCore/Tests/Utils/AppCheckBackoffWrapperFake/GACAppCheckBackoffWrapperFake.h b/AppCheckCore/Tests/Utils/AppCheckBackoffWrapperFake/GACAppCheckBackoffWrapperFake.h index 30c89360..1c38bfe4 100644 --- a/AppCheckCore/Tests/Utils/AppCheckBackoffWrapperFake/GACAppCheckBackoffWrapperFake.h +++ b/AppCheckCore/Tests/Utils/AppCheckBackoffWrapperFake/GACAppCheckBackoffWrapperFake.h @@ -18,7 +18,13 @@ #import -#import "AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.h" +#if __has_include() +#import +#else +#import "FBLPromises.h" +#endif + +#import NS_ASSUME_NONNULL_BEGIN diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index 900082c0..5c3bd859 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -44,7 +44,12 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo let tokenGenerator: RecaptchaEnterpriseTokenGenerator? if let action { - tokenGenerator = RecaptchaEnterpriseTokenGenerator(siteKey: siteKey, recaptchaAction: action) + let backoffWrapper = GACAppCheckBackoffWrapper() + tokenGenerator = RecaptchaEnterpriseTokenGenerator( + siteKey: siteKey, + recaptchaAction: action, + backoffWrapper: backoffWrapper + ) } else { tokenGenerator = nil } diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index cf556c38..7eb1d082 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -15,6 +15,7 @@ #if SWIFT_PACKAGE import AppCheckCore #endif +import FBLPromises import Foundation import Promises import RecaptchaInterop @@ -25,10 +26,14 @@ final class RecaptchaEnterpriseTokenGenerator { private let recaptchaClient: Promise + private let backoffWrapper: GACAppCheckBackoffWrapperProtocol? + init(siteKey: String, recaptchaAction: RCAActionProtocol, - recaptchaClass: RCARecaptchaProtocol.Type? = nil) { + recaptchaClass: RCARecaptchaProtocol.Type? = nil, + backoffWrapper: GACAppCheckBackoffWrapperProtocol? = nil) { self.siteKey = siteKey self.recaptchaAction = recaptchaAction + self.backoffWrapper = backoffWrapper recaptchaClient = Promise { fulfill, reject in let recaptcha = recaptchaClass ?? NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol.Type @@ -46,19 +51,78 @@ final class RecaptchaEnterpriseTokenGenerator { } } - // TODO(ncooke3): Investigate whether we need a backoff mechanism. func getRecaptchaToken() -> Promise { + guard let backoffWrapper = backoffWrapper else { + return getRecaptchaTokenNoBackoff() + } + + return recaptchaClient.then { client in + let operationProvider: GACAppCheckBackoffOperationProvider = { + let swiftPromise = Promise { fulfill, reject in + client.execute(withAction: self.recaptchaAction) { token, error in + if let token = token { + fulfill(token as AnyObject) + } else { + reject(self.mapRecaptchaError(error)) + } + } + } + return swiftPromise.asObjCPromise() + } + + let errorHandler: GACAppCheckBackoffErrorHandler = { error in + let nsError = error as NSError + if nsError.domain == AppCheckCoreErrorDomain && nsError.code == AppCheckCoreErrorCode + .serverUnreachable.rawValue { + return .typeExponential + } + return .typeNone + } + + let fblPromise = backoffWrapper.applyBackoff( + toOperation: operationProvider, + errorHandler: errorHandler + ) + + return Promise(fblPromise).then { result in + result as! String + } + } + } + + private func getRecaptchaTokenNoBackoff() -> Promise { recaptchaClient.then { client in Promise { fulfill, reject in client.execute(withAction: self.recaptchaAction) { token, error in if let token = token { fulfill(token) } else { - reject(error ?? GACAppCheckErrorUtil - .error(withFailureReason: "Failed to execute Recaptcha action")) + reject(self.mapRecaptchaError(error)) } } } } } + + private func mapRecaptchaError(_ error: Error?) -> Error { + guard let error = error as NSError? else { + return GACAppCheckErrorUtil.error(withFailureReason: "Failed to execute Recaptcha action") + } + + // Map RecaptchaErrorNetworkError (1) and RecaptchaErrorCodeInternalError (100) + if error.code == 1 || error.code == 100 { + return GACAppCheckErrorUtil.apiError(withNetworkError: error) + } + + // Preserve underlying error for others + let userInfo: [String: Any] = [ + NSUnderlyingErrorKey: error, + NSLocalizedFailureReasonErrorKey: error.userInfo[NSLocalizedFailureReasonErrorKey] as Any, + ] + return NSError( + domain: AppCheckCoreErrorDomain, + code: AppCheckCoreErrorCode.unknown.rawValue, + userInfo: userInfo + ) + } } diff --git a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift index 2b69d06e..2ba0b45b 100644 --- a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift @@ -15,10 +15,40 @@ import XCTest @testable import AppCheckCore +import FBLPromises import Promises @testable import RecaptchaEnterpriseProvider import RecaptchaInterop +class MockBackoffWrapper: NSObject, GACAppCheckBackoffWrapperProtocol { + var applyBackoffCalled = false + var shouldReturnError = false + var mockError: NSError? + var mockResult: Any? + var capturedErrorHandler: GACAppCheckBackoffErrorHandler? + + func applyBackoff(toOperation operationProvider: @escaping GACAppCheckBackoffOperationProvider, + errorHandler: @escaping GACAppCheckBackoffErrorHandler) + -> FBLPromise { + applyBackoffCalled = true + capturedErrorHandler = errorHandler + if shouldReturnError { + let error = mockError ?? NSError(domain: "MockBackoffWrapper", code: -1, userInfo: nil) + let swiftPromise = Promise(error as Error) + return swiftPromise.asObjCPromise() + } + if let result = mockResult { + let swiftPromise = Promise(result as AnyObject) + return swiftPromise.asObjCPromise() + } + return operationProvider() + } + + func defaultAppCheckProviderErrorHandler() -> GACAppCheckBackoffErrorHandler { + return { error in .typeExponential } + } +} + final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { private let testSiteKey = "test-site-key" private var mockAction: MockRCAAction! @@ -121,6 +151,73 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { // Act generator.getRecaptchaToken().then { token in XCTFail("Should not succeed when execute fails") + }.catch { error in + // Assert + let nsError = error as NSError + XCTAssertEqual(nsError.domain, AppCheckCoreErrorDomain) + XCTAssertEqual(nsError.code, AppCheckCoreErrorCode.unknown.rawValue) + + let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError + XCTAssertNotNil(underlyingError) + XCTAssertEqual(underlyingError?.domain, expectedError.domain) + XCTAssertEqual(underlyingError?.code, expectedError.code) + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } + + func testGetRecaptchaTokenCallsBackoffWrapper() { + // Arrange + let mockClient = MockRecaptchaClient(dummy: ()) + mockClient.mockToken = "valid-recaptcha-token" + MockRecaptcha.mockClient = mockClient + + let mockBackoffWrapper = MockBackoffWrapper() + + let generator = RecaptchaEnterpriseTokenGenerator( + siteKey: testSiteKey, + recaptchaAction: mockAction, + recaptchaClass: MockRecaptcha.self, + backoffWrapper: mockBackoffWrapper + ) + + let expectation = self.expectation(description: "Calls backoff wrapper") + + // Act + generator.getRecaptchaToken().then { token in + // Assert + XCTAssertTrue(mockBackoffWrapper.applyBackoffCalled) + expectation.fulfill() + }.catch { error in + XCTFail("Unexpected error: \(error)") + } + + waitForExpectations(timeout: 1.0) + } + + func testGetRecaptchaTokenBackoffWrapperError() { + // Arrange + let mockClient = MockRecaptchaClient(dummy: ()) + MockRecaptcha.mockClient = mockClient + + let mockBackoffWrapper = MockBackoffWrapper() + mockBackoffWrapper.shouldReturnError = true + let expectedError = NSError(domain: "test", code: -3, userInfo: nil) + mockBackoffWrapper.mockError = expectedError + + let generator = RecaptchaEnterpriseTokenGenerator( + siteKey: testSiteKey, + recaptchaAction: mockAction, + recaptchaClass: MockRecaptcha.self, + backoffWrapper: mockBackoffWrapper + ) + + let expectation = self.expectation(description: "Fails when backoff wrapper fails") + + // Act + generator.getRecaptchaToken().then { token in + XCTFail("Should not succeed when backoff wrapper fails") }.catch { error in // Assert XCTAssertEqual((error as NSError).domain, expectedError.domain) @@ -130,4 +227,173 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { waitForExpectations(timeout: 1.0) } + + func testGetRecaptchaTokenMapsNetworkErrorToServerUnreachable() { + // Arrange + let mockClient = MockRecaptchaClient(dummy: ()) + let recaptchaError = NSError(domain: "RecaptchaErrorDomain", code: 1, userInfo: nil) + mockClient.mockError = recaptchaError + MockRecaptcha.mockClient = mockClient + + let mockBackoffWrapper = MockBackoffWrapper() + + let generator = RecaptchaEnterpriseTokenGenerator( + siteKey: testSiteKey, + recaptchaAction: mockAction, + recaptchaClass: MockRecaptcha.self, + backoffWrapper: mockBackoffWrapper + ) + + let expectation = self.expectation(description: "Maps NetworkError to ServerUnreachable") + + // Act + generator.getRecaptchaToken().then { token in + XCTFail("Should not succeed when execute fails") + }.catch { error in + // Assert + let nsError = error as NSError + XCTAssertEqual(nsError.domain, AppCheckCoreErrorDomain) + XCTAssertEqual(nsError.code, AppCheckCoreErrorCode.serverUnreachable.rawValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } + + func testGetRecaptchaTokenMapsInternalErrorToServerUnreachable() { + // Arrange + let mockClient = MockRecaptchaClient(dummy: ()) + let recaptchaError = NSError(domain: "RecaptchaErrorDomain", code: 100, userInfo: nil) + mockClient.mockError = recaptchaError + MockRecaptcha.mockClient = mockClient + + let mockBackoffWrapper = MockBackoffWrapper() + + let generator = RecaptchaEnterpriseTokenGenerator( + siteKey: testSiteKey, + recaptchaAction: mockAction, + recaptchaClass: MockRecaptcha.self, + backoffWrapper: mockBackoffWrapper + ) + + let expectation = self.expectation(description: "Maps InternalError to ServerUnreachable") + + // Act + generator.getRecaptchaToken().then { token in + XCTFail("Should not succeed when execute fails") + }.catch { error in + // Assert + let nsError = error as NSError + XCTAssertEqual(nsError.domain, AppCheckCoreErrorDomain) + XCTAssertEqual(nsError.code, AppCheckCoreErrorCode.serverUnreachable.rawValue) + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } + + func testErrorHandlerTriggersBackoffForServerUnreachable() { + // Arrange + let mockClient = MockRecaptchaClient(dummy: ()) + mockClient.mockToken = "valid-recaptcha-token" + MockRecaptcha.mockClient = mockClient + + let mockBackoffWrapper = MockBackoffWrapper() + + let generator = RecaptchaEnterpriseTokenGenerator( + siteKey: testSiteKey, + recaptchaAction: mockAction, + recaptchaClass: MockRecaptcha.self, + backoffWrapper: mockBackoffWrapper + ) + + let expectation = self.expectation(description: "Calls backoff wrapper") + + // Act + generator.getRecaptchaToken().then { _ in + // Assert + XCTAssertNotNil(mockBackoffWrapper.capturedErrorHandler) + if let errorHandler = mockBackoffWrapper.capturedErrorHandler { + let serverUnreachableError = NSError( + domain: AppCheckCoreErrorDomain, + code: AppCheckCoreErrorCode.serverUnreachable.rawValue, + userInfo: nil + ) + let backoffType = errorHandler(serverUnreachableError) + XCTAssertEqual(backoffType, .typeExponential) + } + expectation.fulfill() + }.catch { error in + XCTFail("Unexpected error: \(error)") + } + + waitForExpectations(timeout: 1.0) + } + + func testErrorHandlerDoesNotTriggerBackoffForOtherErrors() { + // Arrange + let mockClient = MockRecaptchaClient(dummy: ()) + mockClient.mockToken = "valid-recaptcha-token" + MockRecaptcha.mockClient = mockClient + + let mockBackoffWrapper = MockBackoffWrapper() + + let generator = RecaptchaEnterpriseTokenGenerator( + siteKey: testSiteKey, + recaptchaAction: mockAction, + recaptchaClass: MockRecaptcha.self, + backoffWrapper: mockBackoffWrapper + ) + + let expectation = self.expectation(description: "Calls backoff wrapper") + + // Act + generator.getRecaptchaToken().then { _ in + // Assert + XCTAssertNotNil(mockBackoffWrapper.capturedErrorHandler) + if let errorHandler = mockBackoffWrapper.capturedErrorHandler { + let otherError = NSError( + domain: AppCheckCoreErrorDomain, + code: AppCheckCoreErrorCode.unknown.rawValue, + userInfo: nil + ) + let backoffType = errorHandler(otherError) + XCTAssertEqual(backoffType, .typeNone) + } + expectation.fulfill() + }.catch { error in + XCTFail("Unexpected error: \(error)") + } + + waitForExpectations(timeout: 1.0) + } + + func testGetRecaptchaTokenExecutionNilNilFallback() { + // Arrange + let mockClient = MockRecaptchaClient(dummy: ()) + MockRecaptcha.mockClient = mockClient + + let generator = RecaptchaEnterpriseTokenGenerator( + siteKey: testSiteKey, + recaptchaAction: mockAction, + recaptchaClass: MockRecaptcha.self + ) + + let expectation = self + .expectation(description: "Fails with fallback error when execute returns nil, nil") + + // Act + generator.getRecaptchaToken().then { token in + XCTFail("Should not succeed when execute returns nil, nil") + }.catch { error in + // Assert + let nsError = error as NSError + XCTAssertEqual(nsError.domain, AppCheckCoreErrorDomain) + XCTAssertEqual(nsError.code, AppCheckCoreErrorCode.unknown.rawValue) + XCTAssertEqual(nsError.localizedFailureReason, "Failed to execute Recaptcha action") + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } } From 0f1f47b860fdbd7409354acd03aa347f241a363c Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 16:10:49 -0400 Subject: [PATCH 27/71] bump podspec and changelog --- AppCheckCore.podspec | 2 +- CHANGELOG.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/AppCheckCore.podspec b/AppCheckCore.podspec index 567d2012..76e67bb1 100644 --- a/AppCheckCore.podspec +++ b/AppCheckCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'AppCheckCore' - s.version = '11.2.0' + s.version = '11.3.0' s.summary = 'App Check Core SDK.' s.description = <<-DESC diff --git a/CHANGELOG.md b/CHANGELOG.md index d97d8a21..0fd105fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 11.3.0 +- [changed] Added Recaptcha Enterprise attestation provider. + # 11.2.0 - [changed] To prevent reusing expired artifacts, skip local cache when making network requests. From 7719b2d942e16217fa778c5a6be058420837d435 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 17:10:29 -0400 Subject: [PATCH 28/71] fixes, new error message --- .../Core/Errors/GACAppCheckErrorUtil.m | 9 +++++ .../AppCheckCore/GACAppCheckErrorUtil.h | 2 ++ Package.swift | 1 - ...CheckCoreRecaptchaEnterpriseProvider.swift | 2 +- ...ecaptchaEnterpriseCoreTokenGenerator.swift | 8 +++-- ...CoreRecaptchaEnterpriseProviderTests.swift | 8 +++++ ...chaEnterpriseCoreTokenGeneratorTests.swift | 12 +++++-- todos.md | 36 ------------------- 8 files changed, 36 insertions(+), 42 deletions(-) delete mode 100644 todos.md diff --git a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m index 5c83d1d4..e5916f27 100644 --- a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m +++ b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m @@ -108,6 +108,15 @@ + (NSError *)unsupportedAttestationProvider:(NSString *)providerName { underlyingError:nil]; } ++ (NSError *)missingRecaptchaSDKError { + NSString *failureReason = + @"The reCAPTCHA Enterprise SDK is not linked. See " + @"https://cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment"; + return [self appCheckErrorWithCode:GACAppCheckErrorCodeUnsupported + failureReason:failureReason + underlyingError:nil]; +} + + (NSError *)errorWithFailureReason:(NSString *)failureReason { return [self appCheckErrorWithCode:GACAppCheckErrorCodeUnknown failureReason:failureReason diff --git a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h index 5df000f7..0c7480a3 100644 --- a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h +++ b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h @@ -49,6 +49,8 @@ void GACAppCheckSetErrorToPointer(NSError *error, NSError **pointer); + (NSError *)unsupportedAttestationProvider:(NSString *)providerName; ++ (NSError *)missingRecaptchaSDKError; + // MARK: - App Attest Errors + (NSError *)appAttestKeyIDNotFound; diff --git a/Package.swift b/Package.swift index d376ade2..e2761de7 100644 --- a/Package.swift +++ b/Package.swift @@ -47,7 +47,6 @@ let package = Package( ), .package( url: "https://github.com/google/interop-ios-for-google-sdks.git", - // TODO(ncooke3): Is this the correct supported version window? "101.0.0" ..< "102.0.0" ), ], diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index 5c3bd859..46b5931a 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -97,7 +97,7 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo private func getToken(limitedUse: Bool) -> Promise { guard let tokenGenerator else { - return Promise(GACAppCheckErrorUtil.unsupportedAttestationProvider(Self.providerName)) + return Promise(GACAppCheckErrorUtil.missingRecaptchaSDKError()) } return tokenGenerator.getRecaptchaToken() .then { recaptchaToken in diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index 7eb1d082..bee81ed5 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -21,6 +21,9 @@ import Promises import RecaptchaInterop final class RecaptchaEnterpriseTokenGenerator { + static let networkErrorCode = 1 + static let internalErrorCode = 100 + private let siteKey: String private let recaptchaAction: RCAActionProtocol @@ -109,8 +112,9 @@ final class RecaptchaEnterpriseTokenGenerator { return GACAppCheckErrorUtil.error(withFailureReason: "Failed to execute Recaptcha action") } - // Map RecaptchaErrorNetworkError (1) and RecaptchaErrorCodeInternalError (100) - if error.code == 1 || error.code == 100 { + // Map RecaptchaErrorNetworkError and RecaptchaErrorCodeInternalError. + // See https://docs.cloud.google.com/recaptcha/docs/reference/ios/client/api/Enums/RecaptchaErrorCode.html + if error.code == Self.networkErrorCode || error.code == Self.internalErrorCode { return GACAppCheckErrorUtil.apiError(withNetworkError: error) } diff --git a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift index 4af68279..4152fb8b 100644 --- a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift @@ -55,6 +55,10 @@ final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { let nsError = error as NSError? XCTAssertEqual(nsError?.domain, AppCheckCoreErrorDomain) XCTAssertEqual(nsError?.code, AppCheckCoreErrorCode.unsupported.rawValue) + XCTAssertEqual( + nsError?.localizedFailureReason, + "The reCAPTCHA Enterprise SDK is not linked. See https://cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment" + ) expectation.fulfill() } @@ -72,6 +76,10 @@ final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { let nsError = error as NSError? XCTAssertEqual(nsError?.domain, AppCheckCoreErrorDomain) XCTAssertEqual(nsError?.code, AppCheckCoreErrorCode.unsupported.rawValue) + XCTAssertEqual( + nsError?.localizedFailureReason, + "The reCAPTCHA Enterprise SDK is not linked. See https://cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment" + ) expectation.fulfill() } diff --git a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift index 2ba0b45b..324b59dd 100644 --- a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift @@ -231,7 +231,11 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { func testGetRecaptchaTokenMapsNetworkErrorToServerUnreachable() { // Arrange let mockClient = MockRecaptchaClient(dummy: ()) - let recaptchaError = NSError(domain: "RecaptchaErrorDomain", code: 1, userInfo: nil) + let recaptchaError = NSError( + domain: "RecaptchaErrorDomain", + code: RecaptchaEnterpriseTokenGenerator.networkErrorCode, + userInfo: nil + ) mockClient.mockError = recaptchaError MockRecaptcha.mockClient = mockClient @@ -263,7 +267,11 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { func testGetRecaptchaTokenMapsInternalErrorToServerUnreachable() { // Arrange let mockClient = MockRecaptchaClient(dummy: ()) - let recaptchaError = NSError(domain: "RecaptchaErrorDomain", code: 100, userInfo: nil) + let recaptchaError = NSError( + domain: "RecaptchaErrorDomain", + code: RecaptchaEnterpriseTokenGenerator.internalErrorCode, + userInfo: nil + ) mockClient.mockError = recaptchaError MockRecaptcha.mockClient = mockClient diff --git a/todos.md b/todos.md deleted file mode 100644 index fc6345c9..00000000 --- a/todos.md +++ /dev/null @@ -1,36 +0,0 @@ -# TODOs - -- [ ] Add `serviceName` to `AppCheckCoreRecaptchaEnterpriseProvider.init` for - consistency with other providers. -- [ ] Implement exponential backoff in `RecaptchaEnterpriseProvider` for - transient errors (need to reach out to people first). -- [ ] Bring up API proposal for coordinating assertion behavior when optional - SDKs (like reCAPTCHA) are missing across platforms. - -## Error Handling for Missing reCAPTCHA SDK in Production - -When the reCAPTCHA SDK is not linked in a release build, we currently return a -generic "unsupported" error. We discussed three options for improving this: - -### Option 1: Follow Precedent (Keep as is) -- **Code**: `GACAppCheckErrorCodeUnsupported` (4) -- **Message**: "The attestation provider RecaptchaEnterprise is not supported on - current platform and OS version." -- **Pros**: Consistent with other providers (DeviceCheck, AppAttest) when they - are not supported. -- **Cons**: The message implies a platform/OS limitation, not a missing - dependency. - -### Option 2: Prioritize Message Accuracy -- **Code**: `GACAppCheckErrorCodeUnknown` (0) -- **Message**: "The reCAPTCHA Enterprise SDK is not linked. See - https://cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment" -- **Pros**: Provides the exact, clear message in logs. -- **Cons**: Uses a generic error code (`Unknown`) instead of `Unsupported`. - -### Option 3: Enhance the Utility (Recommended for long term) -- **Code**: `GACAppCheckErrorCodeUnsupported` (4) -- **Message**: Specific message as in Option 2. -- **Pros**: Best error code + best message. -- **Cons**: Requires adding a new method to `GACAppCheckErrorUtil` (internal - change). From 8f511c4f76f09f3f21b3886d02082410cf720fea Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 7 May 2026 17:16:05 -0400 Subject: [PATCH 29/71] remove unneeded file --- provider_integration_paths.md | 91 ----------------------------------- 1 file changed, 91 deletions(-) delete mode 100644 provider_integration_paths.md diff --git a/provider_integration_paths.md b/provider_integration_paths.md deleted file mode 100644 index bfe280ce..00000000 --- a/provider_integration_paths.md +++ /dev/null @@ -1,91 +0,0 @@ -# Recaptcha Provider Integration Paths - -This document outlines two possible paths for integrating the new -`RecaptchaEnterpriseProvider` into the `FirebaseAppCheck` SDK within the -`firebase-ios-sdk` repository. - -## Background - -`AppCheckCore` is an internal dependency of `FirebaseAppCheck`. Customers do -not depend on `AppCheckCore` directly; they consume `FirebaseAppCheck`. - -Currently, all other providers (`DeviceCheck`, `AppAttest`, `Debug`) are -bundled within the monolithic `AppCheckCore` product in the `app-check` repo -and the `FirebaseAppCheck` target in the `firebase-ios-sdk` repo. - ---- - -## Path A: Consistent Path (Current Choice) - -This path follows the existing pattern used by other providers. - -### In `app-check` Repository - -* **Approach**: Bundle `RecaptchaEnterpriseProvider` target into the existing - `AppCheckCore` library product. -* **`Package.swift` Changes**: - ```swift - products: [ - .library( - name: "AppCheckCore", - targets: ["AppCheckCore", "RecaptchaEnterpriseProvider"] - ), - ] - ``` - -### In `firebase-ios-sdk` Repository - -* **Integration**: `FirebaseAppCheck` target will automatically get the core - reCAPTCHA glue code because it depends on `AppCheckCore`. -* **Wrapper**: Add `FIRRecaptchaEnterpriseProvider` wrapper files directly - into `FirebaseAppCheck/Sources`. - -### Developer Experience - -* **Opt-in**: The customer just imports `FirebaseAppCheck`. The glue code is - present but inert. To make it work, they must manually add the - `RecaptchaEnterprise` SDK to their project. -* **Pros**: Consistent with existing architecture; lowest friction for users - who want reCAPTCHA (no new product dependency). -* **Cons**: All users get the glue code, even if not used (though size is - negligible). - ---- - -## Path B: Modular Path - -This path follows the design document's goal of allowing exclusion to save -binary size, treating the reCAPTCHA provider as a separate component. - -### In `app-check` Repository - -* **Approach**: Expose `RecaptchaEnterpriseProvider` as a separate library - product. -* **`Package.swift` Changes**: - ```swift - products: [ - .library( - name: "AppCheckCore", - targets: ["AppCheckCore"] - ), - .library( - name: "RecaptchaEnterpriseProvider", - targets: ["RecaptchaEnterpriseProvider"] - ), - ] - ``` - -### In `firebase-ios-sdk` Repository - -* **Integration**: Create a new, separate target (e.g., - `FirebaseRecaptchaEnterpriseProvider`) that depends on `FirebaseAppCheck` - and the core `RecaptchaEnterpriseProvider` product. - -### Developer Experience - -* **Opt-in**: The customer must explicitly add both `FirebaseAppCheck` and - `FirebaseRecaptchaEnterpriseProvider` to their target, plus manually add the - `RecaptchaEnterprise` SDK. -* **Pros**: Strict modularity; users can fully exclude the provider and its - interop dependency. -* **Cons**: Higher friction for users (multiple dependencies to add). From eb8379c9952db1688fc0776c0125c82b4f89cb84 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 11 May 2026 13:45:31 -0400 Subject: [PATCH 30/71] refactor: address code review feedback for RecaptchaEnterpriseProvider --- .../API/RecaptchaEnterpriseCoreAPIService.swift | 12 ++++++++---- .../AppCheckCoreRecaptchaEnterpriseProvider.swift | 10 +++++++--- .../RecaptchaEnterpriseCoreTokenGenerator.swift | 8 +++++++- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift index 0496e5df..13da6a25 100644 --- a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift +++ b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift @@ -23,9 +23,11 @@ private enum Constants { static let jsonContentType = "application/json" static let recaptchaTokenField = "recaptcha_enterprise_token" static let limitedUseField = "limited_use" + static let exchangeEndpoint = "exchangeRecaptchaEnterpriseToken" + static let httpMethodPost = "POST" } -@objc(GARecaptchaEnterpriseAPIService) +@objc(GACRecaptchaEnterpriseAPIService) final class RecaptchaEnterpriseAPIService: NSObject { private let apiService: AppCheckCoreAPIServiceProtocol private let resourceName: String @@ -37,15 +39,17 @@ final class RecaptchaEnterpriseAPIService: NSObject { func appCheckToken(withRecaptchaToken recaptchaToken: String, limitedUse: Bool) -> Promise { - let urlString = "\(apiService.baseURL)/\(resourceName):exchangeRecaptchaEnterpriseToken" + let urlString = "\(apiService.baseURL)/\(resourceName):\(Constants.exchangeEndpoint)" guard let url = URL(string: urlString) else { - return Promise(GACAppCheckErrorUtil.error(withFailureReason: "Invalid URL string")) + return Promise(GACAppCheckErrorUtil + .error(withFailureReason: "Invalid URL string: \(urlString)")) } return httpBody(withRecaptchaToken: recaptchaToken, limitedUse: limitedUse) .then { httpBody in Promise(self.apiService.sendRequest(with: url, - httpMethod: "POST", + httpMethod: Constants + .httpMethodPost, body: httpBody, additionalHeaders: [Constants .contentTypeKey: Constants diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index 46b5931a..59ca2d89 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -22,12 +22,18 @@ import RecaptchaInterop @objc(GACRecaptchaEnterpriseProvider) public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCoreProvider { private static let recaptchaActionClassName = "RecaptchaEnterprise.RCAAction" + // This action name should never change without coordination with the backend. private static let appCheckActionName = "app_check_ios" private static let providerName = "RecaptchaEnterprise" + private static let missingSDKMessage = + "The reCAPTCHA Enterprise SDK is not linked. See https://cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment" private let tokenGenerator: RecaptchaEnterpriseTokenGenerator? private let apiService: RecaptchaEnterpriseAPIService + // `@convention(block)` is required because the Swift compiler cannot automatically + // bridge collections of closures (like an Array) to Objective-C blocks. This attribute + // changes the closure's representation to match the Objective-C block heap layout. @objc public convenience init(siteKey: String, resourceName: String, APIKey: String, requestHooks: [@convention(block) (NSMutableURLRequest) -> Void]? = nil) { @@ -36,9 +42,7 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo if recaptchaAction == nil { // Fail fast in Debug (-Onone) builds to alert the developer. // In Release (-O) builds, this falls back to returning an error in getToken. - assertionFailure( - "The reCAPTCHA Enterprise SDK is not linked. See https://cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment" - ) + assertionFailure(Self.missingSDKMessage) } let action = recaptchaAction?.init(customAction: Self.appCheckActionName) diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index bee81ed5..a5f09aaa 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -21,9 +21,15 @@ import Promises import RecaptchaInterop final class RecaptchaEnterpriseTokenGenerator { + // Corresponds to RecaptchaErrorNetworkError. These codes are not in the interop. + // See https://docs.cloud.google.com/recaptcha/docs/reference/ios/client/api/Enums/RecaptchaErrorCode.html#recaptchaerrornetworkerror static let networkErrorCode = 1 + // Corresponds to RecaptchaErrorCodeInternalError. These codes are not in the interop. + // See https://docs.cloud.google.com/recaptcha/docs/reference/ios/client/api/Enums/RecaptchaErrorCode.html#recaptchaerrorcodeinternalerror static let internalErrorCode = 100 + private static let recaptchaClassName = "RecaptchaEnterprise.RCARecaptcha" + private let siteKey: String private let recaptchaAction: RCAActionProtocol @@ -39,7 +45,7 @@ final class RecaptchaEnterpriseTokenGenerator { self.backoffWrapper = backoffWrapper recaptchaClient = Promise { fulfill, reject in let recaptcha = recaptchaClass ?? - NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol.Type + NSClassFromString(Self.recaptchaClassName) as? RCARecaptchaProtocol.Type guard let recaptcha else { throw GACAppCheckErrorUtil.unsupportedAttestationProvider("RecaptchaEnterprise") } From 8886e93e8e6a3c9402a2991da65afe71f0ea439a Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 11 May 2026 13:47:39 -0400 Subject: [PATCH 31/71] endpoint comment --- .../Sources/API/RecaptchaEnterpriseCoreAPIService.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift index 13da6a25..93f2bd2a 100644 --- a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift +++ b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift @@ -23,6 +23,7 @@ private enum Constants { static let jsonContentType = "application/json" static let recaptchaTokenField = "recaptcha_enterprise_token" static let limitedUseField = "limited_use" + // This endpoint should never change without coordination with the backend. static let exchangeEndpoint = "exchangeRecaptchaEnterpriseToken" static let httpMethodPost = "POST" } From dd72cd418434e01a4432401d28e97ea7d32ae690 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 11 May 2026 13:57:39 -0400 Subject: [PATCH 32/71] refactor: address RecaptchaEnterprise code review feedback --- .../Sources/API/RecaptchaEnterpriseCoreAPIService.swift | 6 +++--- .../Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift | 2 +- .../Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift index 93f2bd2a..1b5030be 100644 --- a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift +++ b/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift @@ -38,7 +38,7 @@ final class RecaptchaEnterpriseAPIService: NSObject { self.resourceName = resourceName } - func appCheckToken(withRecaptchaToken recaptchaToken: String, + func appCheckToken(with recaptchaToken: String, limitedUse: Bool) -> Promise { let urlString = "\(apiService.baseURL)/\(resourceName):\(Constants.exchangeEndpoint)" guard let url = URL(string: urlString) else { @@ -46,7 +46,7 @@ final class RecaptchaEnterpriseAPIService: NSObject { .error(withFailureReason: "Invalid URL string: \(urlString)")) } - return httpBody(withRecaptchaToken: recaptchaToken, limitedUse: limitedUse) + return httpBody(with: recaptchaToken, limitedUse: limitedUse) .then { httpBody in Promise(self.apiService.sendRequest(with: url, httpMethod: Constants @@ -60,7 +60,7 @@ final class RecaptchaEnterpriseAPIService: NSObject { } } - private func httpBody(withRecaptchaToken recaptchaToken: String, + private func httpBody(with recaptchaToken: String, limitedUse: Bool) -> Promise { guard !recaptchaToken.isEmpty else { return Promise(GACAppCheckErrorUtil diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index 59ca2d89..69c50d8b 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -106,7 +106,7 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo return tokenGenerator.getRecaptchaToken() .then { recaptchaToken in self.apiService.appCheckToken( - withRecaptchaToken: recaptchaToken, + with: recaptchaToken, limitedUse: limitedUse ) } diff --git a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift index e4f60945..8f2e116e 100644 --- a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift @@ -50,7 +50,7 @@ final class RecaptchaEnterpriseCoreAPIServiceTests: XCTestCase { let expectation = self.expectation(description: "Token exchange completes successfully") // Act - apiService.appCheckToken(withRecaptchaToken: testRecaptchaToken, limitedUse: false) + apiService.appCheckToken(with: testRecaptchaToken, limitedUse: false) .then { token in // Assert XCTAssertEqual(token.token, expectedAppCheckToken.token) @@ -97,7 +97,7 @@ final class RecaptchaEnterpriseCoreAPIServiceTests: XCTestCase { .expectation(description: "Limited use token exchange completes successfully") // Act - apiService.appCheckToken(withRecaptchaToken: testRecaptchaToken, limitedUse: true) + apiService.appCheckToken(with: testRecaptchaToken, limitedUse: true) .then { token in // Assert guard let request = self.mockCoreAPIService.lastRequest, let body = request.body else { @@ -119,7 +119,7 @@ final class RecaptchaEnterpriseCoreAPIServiceTests: XCTestCase { func testAppCheckTokenEmptyRecaptchaToken() { let expectation = self.expectation(description: "Token exchange fails with empty token") - apiService.appCheckToken(withRecaptchaToken: "", limitedUse: false).then { token in + apiService.appCheckToken(with: "", limitedUse: false).then { token in XCTFail("Should not succeed with empty token") }.catch { error in XCTAssertNotNil(error) From 82989b98385c7c275ad75262832ee1e66a6cc661 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 11 May 2026 15:08:50 -0400 Subject: [PATCH 33/71] fixes --- ...CheckCoreRecaptchaEnterpriseProvider.swift | 38 +++++++++++++------ ...ecaptchaEnterpriseCoreTokenGenerator.swift | 11 +----- ...chaEnterpriseCoreTokenGeneratorTests.swift | 21 ---------- 3 files changed, 29 insertions(+), 41 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index 69c50d8b..18b8c78e 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -37,24 +37,19 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo @objc public convenience init(siteKey: String, resourceName: String, APIKey: String, requestHooks: [@convention(block) (NSMutableURLRequest) -> Void]? = nil) { - let recaptchaAction = - NSClassFromString(Self.recaptchaActionClassName) as? RCAActionProtocol.Type - if recaptchaAction == nil { - // Fail fast in Debug (-Onone) builds to alert the developer. - // In Release (-O) builds, this falls back to returning an error in getToken. - assertionFailure(Self.missingSDKMessage) - } - let action = recaptchaAction?.init(customAction: Self.appCheckActionName) - let tokenGenerator: RecaptchaEnterpriseTokenGenerator? - if let action { + + if let sdk = RecaptchaEnterpriseSDK(customAction: Self.appCheckActionName) { let backoffWrapper = GACAppCheckBackoffWrapper() + tokenGenerator = RecaptchaEnterpriseTokenGenerator( siteKey: siteKey, - recaptchaAction: action, + recaptchaAction: sdk.action, + recaptchaClass: sdk.recaptchaClass, backoffWrapper: backoffWrapper ) } else { + assertionFailure(Self.missingSDKMessage) tokenGenerator = nil } @@ -112,3 +107,24 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo } } } + +private struct RecaptchaEnterpriseSDK { + private static let recaptchaActionClassName = "RecaptchaEnterprise.RCAAction" + private static let recaptchaClassName = "RecaptchaEnterprise.RCARecaptcha" + + let action: RCAActionProtocol + let recaptchaClass: RCARecaptchaProtocol.Type + + init?(customAction: String) { + let actionObj: AnyClass? = NSClassFromString(Self.recaptchaActionClassName) + let recaptchaObj: AnyClass? = NSClassFromString(Self.recaptchaClassName) + + guard let actionClass = actionObj as? RCAActionProtocol.Type, + let recaptchaClass = recaptchaObj as? RCARecaptchaProtocol.Type else { + return nil + } + + action = actionClass.init(customAction: customAction) + self.recaptchaClass = recaptchaClass + } +} diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index a5f09aaa..7e2a891d 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -28,8 +28,6 @@ final class RecaptchaEnterpriseTokenGenerator { // See https://docs.cloud.google.com/recaptcha/docs/reference/ios/client/api/Enums/RecaptchaErrorCode.html#recaptchaerrorcodeinternalerror static let internalErrorCode = 100 - private static let recaptchaClassName = "RecaptchaEnterprise.RCARecaptcha" - private let siteKey: String private let recaptchaAction: RCAActionProtocol @@ -38,18 +36,13 @@ final class RecaptchaEnterpriseTokenGenerator { private let backoffWrapper: GACAppCheckBackoffWrapperProtocol? init(siteKey: String, recaptchaAction: RCAActionProtocol, - recaptchaClass: RCARecaptchaProtocol.Type? = nil, + recaptchaClass: RCARecaptchaProtocol.Type, backoffWrapper: GACAppCheckBackoffWrapperProtocol? = nil) { self.siteKey = siteKey self.recaptchaAction = recaptchaAction self.backoffWrapper = backoffWrapper recaptchaClient = Promise { fulfill, reject in - let recaptcha = recaptchaClass ?? - NSClassFromString(Self.recaptchaClassName) as? RCARecaptchaProtocol.Type - guard let recaptcha else { - throw GACAppCheckErrorUtil.unsupportedAttestationProvider("RecaptchaEnterprise") - } - recaptcha.fetchClient(withSiteKey: siteKey) { client, error in + recaptchaClass.fetchClient(withSiteKey: siteKey) { client, error in if let client { fulfill(client) } else { diff --git a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift index 324b59dd..d808af90 100644 --- a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift @@ -60,27 +60,6 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { MockRecaptcha.mockError = nil } - func testGetRecaptchaTokenWithoutSDK() { - let generator = RecaptchaEnterpriseTokenGenerator( - siteKey: testSiteKey, - recaptchaAction: mockAction - ) - - let expectation = self.expectation(description: "Fails to generate token without Recaptcha SDK") - - generator.getRecaptchaToken().then { token in - XCTFail("Should not succeed without SDK") - }.catch { error in - XCTAssertNotNil(error) - let nsError = error as NSError - XCTAssertEqual(nsError.domain, AppCheckCoreErrorDomain) - XCTAssertEqual(nsError.code, AppCheckCoreErrorCode.unsupported.rawValue) - expectation.fulfill() - } - - waitForExpectations(timeout: 1.0) - } - func testGetRecaptchaTokenSuccess() { // Arrange let mockClient = MockRecaptchaClient(dummy: ()) From 21b66e1276c8d4c8a79329f607333136db17f766 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 11 May 2026 15:14:12 -0400 Subject: [PATCH 34/71] remove --- .../Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index 18b8c78e..060fd883 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -21,7 +21,6 @@ import RecaptchaInterop @objc(GACRecaptchaEnterpriseProvider) public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCoreProvider { - private static let recaptchaActionClassName = "RecaptchaEnterprise.RCAAction" // This action name should never change without coordination with the backend. private static let appCheckActionName = "app_check_ios" private static let providerName = "RecaptchaEnterprise" From 3242ef9b20a33eef67ee1c2bd010f77a49097e60 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 11 May 2026 15:22:59 -0400 Subject: [PATCH 35/71] promise fixes --- .../Tests/MockRecaptchaSupport.swift | 37 ++++++++++++------- agents.md | 12 ++++-- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift b/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift index 69d4bb81..0f91d192 100644 --- a/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift +++ b/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift @@ -98,24 +98,35 @@ class MockAppCheckCoreAPIService: NSObject, AppCheckCoreAPIServiceProtocol { additionalHeaders: additionalHeaders ) - let resolution: Any = (expectedError as NSError?) ?? - (expectedResponse ?? GACURLSessionDataResponse( + let promise = Promise.pending() + + if let error = expectedError { + promise.reject(error) + } else { + let response = expectedResponse ?? GACURLSessionDataResponse( response: HTTPURLResponse(), httpBody: Data() - )) - let promiseClass = NSClassFromString("FBLPromise") as! NSObject.Type - let unmanaged = promiseClass.perform(NSSelectorFromString("resolvedWith:"), with: resolution)! - return unmanaged.takeUnretainedValue() as! FBLPromise + ) + promise.fulfill(response) + } + + return promise.asObjCPromise() } func appCheckToken(withAPIResponse response: GACURLSessionDataResponse) -> FBLPromise { - let resolution: Any = (expectedError as NSError?) ?? (expectedToken ?? AppCheckCoreToken( - token: "dummy", - expirationDate: Date() - )) - let promiseClass = NSClassFromString("FBLPromise") as! NSObject.Type - let unmanaged = promiseClass.perform(NSSelectorFromString("resolvedWith:"), with: resolution)! - return unmanaged.takeUnretainedValue() as! FBLPromise + let promise = Promise.pending() + + if let error = expectedError { + promise.reject(error) + } else { + let token = expectedToken ?? AppCheckCoreToken( + token: "dummy", + expirationDate: Date() + ) + promise.fulfill(token) + } + + return promise.asObjCPromise() } } diff --git a/agents.md b/agents.md index 4822f427..9cce38e8 100644 --- a/agents.md +++ b/agents.md @@ -186,10 +186,14 @@ generic classes like `FBLPromise`. To avoid build loop failures: // Failure (Must explicitly cast to NSError) return FBLPromise.resolved(error as NSError) as! FBLPromise ``` -- **Dynamic Dispatch fallback**: If Swift inference completely fails in test - mocks, use Objective-C dynamic dispatch to bypass the compiler: - `promiseClass.perform(NSSelectorFromString("resolvedWith:"), with: - resolution)`. +- **PromisesSwift Interoperability**: If you need to return an `FBLPromise` from + Swift (e.g., in test mocks), prefer creating a Swift `Promise` and converting + it using `asObjCPromise()` rather than using reflection or dynamic dispatch: + ```swift + let promise = Promise.pending() + // ... fulfill or reject ... + return promise.asObjCPromise() + ``` --- From 032ceef333975fe6bbc912af1b12357b68373e61 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 11 May 2026 17:30:27 -0400 Subject: [PATCH 36/71] docs --- .../AppCheckCoreRecaptchaEnterpriseProvider.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift index 060fd883..904bdc16 100644 --- a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -19,6 +19,10 @@ import Foundation import Promises import RecaptchaInterop +/// Firebase App Check provider that verifies app integrity using the +/// [reCAPTCHA Enterprise](https://cloud.google.com/recaptcha/docs/instrument-ios-apps) +/// API. This class is available on all platforms for select OS versions. See +/// https://firebase.google.com/docs/ios/learn-more for more details. @objc(GACRecaptchaEnterpriseProvider) public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCoreProvider { // This action name should never change without coordination with the backend. @@ -30,6 +34,13 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo private let tokenGenerator: RecaptchaEnterpriseTokenGenerator? private let apiService: RecaptchaEnterpriseAPIService + /// The default initializer. + /// - Parameters: + /// - siteKey: The reCAPTCHA site key. + /// - resourceName: The name of the resource protected by App Check; for a Firebase App this is + /// "projects/{project_id}/apps/{app_id}". + /// - APIKey: The Google Cloud Platform API key. + /// - requestHooks: Hooks that will be invoked on requests through this service. // `@convention(block)` is required because the Swift compiler cannot automatically // bridge collections of closures (like an Array) to Objective-C blocks. This attribute // changes the closure's representation to match the Objective-C block heap layout. From f89584d98b5243dfc87af6539374a763a55417f0 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 11 May 2026 17:31:12 -0400 Subject: [PATCH 37/71] moves --- .../{ => Public}/AppCheckCoreRecaptchaEnterpriseProvider.swift | 0 .../Sources/{API => }/RecaptchaEnterpriseCoreAPIService.swift | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename RecaptchaEnterpriseProvider/Sources/{ => Public}/AppCheckCoreRecaptchaEnterpriseProvider.swift (100%) rename RecaptchaEnterpriseProvider/Sources/{API => }/RecaptchaEnterpriseCoreAPIService.swift (100%) diff --git a/RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift similarity index 100% rename from RecaptchaEnterpriseProvider/Sources/AppCheckCoreRecaptchaEnterpriseProvider.swift rename to RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift diff --git a/RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreAPIService.swift similarity index 100% rename from RecaptchaEnterpriseProvider/Sources/API/RecaptchaEnterpriseCoreAPIService.swift rename to RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreAPIService.swift From f54e7631f3b76c0af2eeab0f3de2b7aa55d7b3a4 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 12 May 2026 13:41:35 -0400 Subject: [PATCH 38/71] Apply suggestions from code review Co-authored-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Signed-off-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> --- .../Public/AppCheckCoreRecaptchaEnterpriseProvider.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift index 904bdc16..773c1fd0 100644 --- a/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -119,7 +119,11 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo } private struct RecaptchaEnterpriseSDK { + // This symbol is specified in the RecaptchaEnterprise SDK. + // See https://github.com/GoogleCloudPlatform/recaptcha-enterprise-mobile-sdk/blob/18.9.0/Sources/RecaptchaEnterprise/RecaptchaInteropBidings.swift private static let recaptchaActionClassName = "RecaptchaEnterprise.RCAAction" + // This symbol is specified in the RecaptchaEnterprise SDK. + // See https://github.com/GoogleCloudPlatform/recaptcha-enterprise-mobile-sdk/blob/18.9.0/Sources/RecaptchaEnterprise/RecaptchaInteropBidings.swift private static let recaptchaClassName = "RecaptchaEnterprise.RCARecaptcha" let action: RCAActionProtocol From 356fdc7c3f5e74d5106751506687a50aa669f778 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 12 May 2026 13:59:08 -0400 Subject: [PATCH 39/71] prefer non-shorthand --- .../Public/AppCheckCoreRecaptchaEnterpriseProvider.swift | 2 +- .../Sources/RecaptchaEnterpriseCoreTokenGenerator.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift index 773c1fd0..536c9ce5 100644 --- a/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -105,7 +105,7 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo } private func getToken(limitedUse: Bool) -> Promise { - guard let tokenGenerator else { + guard let tokenGenerator = tokenGenerator else { return Promise(GACAppCheckErrorUtil.missingRecaptchaSDKError()) } return tokenGenerator.getRecaptchaToken() diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index 7e2a891d..acc8724d 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -43,7 +43,7 @@ final class RecaptchaEnterpriseTokenGenerator { self.backoffWrapper = backoffWrapper recaptchaClient = Promise { fulfill, reject in recaptchaClass.fetchClient(withSiteKey: siteKey) { client, error in - if let client { + if let client = client { fulfill(client) } else { reject(error ?? GACAppCheckErrorUtil From b5ff78d62022038ac0c2ea1523fc466a80e4dfa5 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 12 May 2026 14:10:04 -0400 Subject: [PATCH 40/71] back to shorthand --- .../Public/AppCheckCoreRecaptchaEnterpriseProvider.swift | 2 +- .../Sources/RecaptchaEnterpriseCoreTokenGenerator.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift index 536c9ce5..773c1fd0 100644 --- a/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -105,7 +105,7 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo } private func getToken(limitedUse: Bool) -> Promise { - guard let tokenGenerator = tokenGenerator else { + guard let tokenGenerator else { return Promise(GACAppCheckErrorUtil.missingRecaptchaSDKError()) } return tokenGenerator.getRecaptchaToken() diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index acc8724d..7e2a891d 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -43,7 +43,7 @@ final class RecaptchaEnterpriseTokenGenerator { self.backoffWrapper = backoffWrapper recaptchaClient = Promise { fulfill, reject in recaptchaClass.fetchClient(withSiteKey: siteKey) { client, error in - if let client = client { + if let client { fulfill(client) } else { reject(error ?? GACAppCheckErrorUtil From a280438e0479f019682fd27832ceee42ff2c9a12 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 12 May 2026 15:44:29 -0400 Subject: [PATCH 41/71] shorthand --- .../Sources/RecaptchaEnterpriseCoreTokenGenerator.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index 7e2a891d..4e681a93 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -54,7 +54,7 @@ final class RecaptchaEnterpriseTokenGenerator { } func getRecaptchaToken() -> Promise { - guard let backoffWrapper = backoffWrapper else { + guard let backoffWrapper else { return getRecaptchaTokenNoBackoff() } @@ -62,7 +62,7 @@ final class RecaptchaEnterpriseTokenGenerator { let operationProvider: GACAppCheckBackoffOperationProvider = { let swiftPromise = Promise { fulfill, reject in client.execute(withAction: self.recaptchaAction) { token, error in - if let token = token { + if let token { fulfill(token as AnyObject) } else { reject(self.mapRecaptchaError(error)) @@ -96,7 +96,7 @@ final class RecaptchaEnterpriseTokenGenerator { recaptchaClient.then { client in Promise { fulfill, reject in client.execute(withAction: self.recaptchaAction) { token, error in - if let token = token { + if let token { fulfill(token) } else { reject(self.mapRecaptchaError(error)) From 62c864edf9ee76b87ebe92fcfceb3f7e2d3755f2 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 12 May 2026 15:45:27 -0400 Subject: [PATCH 42/71] fixes --- .../Tests/MockRecaptchaSupport.swift | 16 ++++++++-------- ...aptchaEnterpriseCoreTokenGeneratorTests.swift | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift b/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift index 0f91d192..95af2e1c 100644 --- a/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift +++ b/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift @@ -43,8 +43,8 @@ final class MockRecaptcha: NSObject, RCARecaptchaProtocol { static func fetchClient(withSiteKey siteKey: String, completion: @escaping (RCARecaptchaClientProtocol?, Error?) -> Void) { - if let error = mockError { - completion(nil, error) + if let mockError { + completion(nil, mockError) } else { completion(mockClient, nil) } @@ -61,8 +61,8 @@ final class MockRecaptchaClient: NSObject, RCARecaptchaClientProtocol { func execute(withAction action: RCAActionProtocol, completion: @escaping (String?, Error?) -> Void) { - if let error = mockError { - completion(nil, error) + if let mockError { + completion(nil, mockError) } else { completion(mockToken, nil) } @@ -100,8 +100,8 @@ class MockAppCheckCoreAPIService: NSObject, AppCheckCoreAPIServiceProtocol { let promise = Promise.pending() - if let error = expectedError { - promise.reject(error) + if let expectedError { + promise.reject(expectedError) } else { let response = expectedResponse ?? GACURLSessionDataResponse( response: HTTPURLResponse(), @@ -117,8 +117,8 @@ class MockAppCheckCoreAPIService: NSObject, AppCheckCoreAPIServiceProtocol { -> FBLPromise { let promise = Promise.pending() - if let error = expectedError { - promise.reject(error) + if let expectedError { + promise.reject(expectedError) } else { let token = expectedToken ?? AppCheckCoreToken( token: "dummy", diff --git a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift index d808af90..1a86f425 100644 --- a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift @@ -37,8 +37,8 @@ class MockBackoffWrapper: NSObject, GACAppCheckBackoffWrapperProtocol { let swiftPromise = Promise(error as Error) return swiftPromise.asObjCPromise() } - if let result = mockResult { - let swiftPromise = Promise(result as AnyObject) + if let mockResult { + let swiftPromise = Promise(mockResult as AnyObject) return swiftPromise.asObjCPromise() } return operationProvider() From 2f3eddf2ddd5608d33bc67d95197807d6fd417fb Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 12 May 2026 15:59:05 -0400 Subject: [PATCH 43/71] Adds a comment explaining the safety of the force cast in `RecaptchaEnterpriseCoreTokenGenerator`. --- .../Sources/RecaptchaEnterpriseCoreTokenGenerator.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index 4e681a93..44135155 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -87,6 +87,8 @@ final class RecaptchaEnterpriseTokenGenerator { ) return Promise(fblPromise).then { result in + // Force cast is safe because the operation provider above returns a String + // (the token) fulfilled as AnyObject to satisfy FBLPromise interop. result as! String } } From 16434e8f00be033445d1343daf5baef90ebb98d8 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 12 May 2026 16:15:43 -0400 Subject: [PATCH 44/71] consolidate error messaging --- AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m | 9 +++++---- .../Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h | 2 ++ .../Public/AppCheckCoreRecaptchaEnterpriseProvider.swift | 4 +--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m index e5916f27..a22a7322 100644 --- a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m +++ b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m @@ -24,6 +24,10 @@ #import "AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" +NSString *const kGACAppCheckMissingRecaptchaSDKMessage = + @"The reCAPTCHA Enterprise SDK is not linked. See " + @"https://cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment"; + @implementation GACAppCheckErrorUtil + (NSError *)publicDomainErrorWithError:(NSError *)error { @@ -109,11 +113,8 @@ + (NSError *)unsupportedAttestationProvider:(NSString *)providerName { } + (NSError *)missingRecaptchaSDKError { - NSString *failureReason = - @"The reCAPTCHA Enterprise SDK is not linked. See " - @"https://cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment"; return [self appCheckErrorWithCode:GACAppCheckErrorCodeUnsupported - failureReason:failureReason + failureReason:kGACAppCheckMissingRecaptchaSDKMessage underlyingError:nil]; } diff --git a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h index 0c7480a3..d1f27757 100644 --- a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h +++ b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h @@ -20,6 +20,8 @@ NS_ASSUME_NONNULL_BEGIN +extern NSString *const kGACAppCheckMissingRecaptchaSDKMessage NS_SWIFT_NAME(missingRecaptchaSDKMessage); + void GACAppCheckSetErrorToPointer(NSError *error, NSError **pointer); @interface GACAppCheckErrorUtil : NSObject diff --git a/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift index 773c1fd0..2ea4fda1 100644 --- a/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -28,8 +28,6 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo // This action name should never change without coordination with the backend. private static let appCheckActionName = "app_check_ios" private static let providerName = "RecaptchaEnterprise" - private static let missingSDKMessage = - "The reCAPTCHA Enterprise SDK is not linked. See https://cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment" private let tokenGenerator: RecaptchaEnterpriseTokenGenerator? private let apiService: RecaptchaEnterpriseAPIService @@ -59,7 +57,7 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo backoffWrapper: backoffWrapper ) } else { - assertionFailure(Self.missingSDKMessage) + assertionFailure(missingRecaptchaSDKMessage) tokenGenerator = nil } From 66858f1528fe49ca37302fbe5a5757d827ca3471 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 12 May 2026 16:22:51 -0400 Subject: [PATCH 45/71] restore comment --- .../Public/AppCheckCoreRecaptchaEnterpriseProvider.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift index 2ea4fda1..e6ab8a47 100644 --- a/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -57,6 +57,8 @@ public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCo backoffWrapper: backoffWrapper ) } else { + // Fail fast in Debug (-Onone) builds to alert the developer. + // In Release (-O) builds, a nil tokenGenerator falls back to returning an error in getToken. assertionFailure(missingRecaptchaSDKMessage) tokenGenerator = nil } From 03fb421a786af7c16007114d70229ff810352495 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 12 May 2026 17:22:38 -0400 Subject: [PATCH 46/71] Test cleanup --- ...CoreRecaptchaEnterpriseProviderTests.swift | 45 +++++++++++++++---- ...chaEnterpriseCoreTokenGeneratorTests.swift | 18 ++++---- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift index 4152fb8b..182436c0 100644 --- a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift @@ -87,9 +87,9 @@ final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { waitForExpectations(timeout: 1.0) } - func testGetTokenSuccess() { - // Arrange - let mockClient = MockRecaptchaClient(dummy: ()) + private func createProviderWithMocks(expectedToken: AppCheckCoreToken) + -> AppCheckCoreRecaptchaEnterpriseProvider { + let mockClient = MockRecaptchaClient() mockClient.mockToken = "valid-recaptcha-token" MockRecaptcha.mockClient = mockClient @@ -100,21 +100,26 @@ final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { ) let mockCoreAPIService = MockAppCheckCoreAPIService() - let expectedAppCheckToken = AppCheckCoreToken( - token: "app-check-token-456", - expirationDate: Date(timeIntervalSinceNow: 3600) - ) - mockCoreAPIService.expectedToken = expectedAppCheckToken + mockCoreAPIService.expectedToken = expectedToken let apiService = RecaptchaEnterpriseAPIService( apiService: mockCoreAPIService, resourceName: testResourceName ) - let providerWithMocks = AppCheckCoreRecaptchaEnterpriseProvider( + return AppCheckCoreRecaptchaEnterpriseProvider( tokenGenerator: tokenGenerator, apiService: apiService ) + } + + func testGetTokenSuccess() { + // Arrange + let expectedAppCheckToken = AppCheckCoreToken( + token: "app-check-token-456", + expirationDate: Date(timeIntervalSinceNow: 3600) + ) + let providerWithMocks = createProviderWithMocks(expectedToken: expectedAppCheckToken) let expectation = self.expectation(description: "Get token succeeds") @@ -129,4 +134,26 @@ final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { waitForExpectations(timeout: 1.0) } + + func testGetLimitedUseTokenSuccess() { + // Arrange + let expectedAppCheckToken = AppCheckCoreToken( + token: "app-check-token-456", + expirationDate: Date(timeIntervalSinceNow: 3600) + ) + let providerWithMocks = createProviderWithMocks(expectedToken: expectedAppCheckToken) + + let expectation = self.expectation(description: "Get limited use token succeeds") + + // Act + providerWithMocks.getLimitedUseToken { token, error in + // Assert + XCTAssertNotNil(token) + XCTAssertNil(error) + XCTAssertEqual(token?.token, expectedAppCheckToken.token) + expectation.fulfill() + } + + waitForExpectations(timeout: 1.0) + } } diff --git a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift index 1a86f425..9ad9bb72 100644 --- a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift @@ -62,7 +62,7 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { func testGetRecaptchaTokenSuccess() { // Arrange - let mockClient = MockRecaptchaClient(dummy: ()) + let mockClient = MockRecaptchaClient() mockClient.mockToken = "valid-recaptcha-token" MockRecaptcha.mockClient = mockClient @@ -114,7 +114,7 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { func testGetRecaptchaTokenExecutionFailure() { // Arrange - let mockClient = MockRecaptchaClient(dummy: ()) + let mockClient = MockRecaptchaClient() let expectedError = NSError(domain: "test", code: -2, userInfo: nil) mockClient.mockError = expectedError MockRecaptcha.mockClient = mockClient @@ -148,7 +148,7 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { func testGetRecaptchaTokenCallsBackoffWrapper() { // Arrange - let mockClient = MockRecaptchaClient(dummy: ()) + let mockClient = MockRecaptchaClient() mockClient.mockToken = "valid-recaptcha-token" MockRecaptcha.mockClient = mockClient @@ -177,7 +177,7 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { func testGetRecaptchaTokenBackoffWrapperError() { // Arrange - let mockClient = MockRecaptchaClient(dummy: ()) + let mockClient = MockRecaptchaClient() MockRecaptcha.mockClient = mockClient let mockBackoffWrapper = MockBackoffWrapper() @@ -209,7 +209,7 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { func testGetRecaptchaTokenMapsNetworkErrorToServerUnreachable() { // Arrange - let mockClient = MockRecaptchaClient(dummy: ()) + let mockClient = MockRecaptchaClient() let recaptchaError = NSError( domain: "RecaptchaErrorDomain", code: RecaptchaEnterpriseTokenGenerator.networkErrorCode, @@ -245,7 +245,7 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { func testGetRecaptchaTokenMapsInternalErrorToServerUnreachable() { // Arrange - let mockClient = MockRecaptchaClient(dummy: ()) + let mockClient = MockRecaptchaClient() let recaptchaError = NSError( domain: "RecaptchaErrorDomain", code: RecaptchaEnterpriseTokenGenerator.internalErrorCode, @@ -281,7 +281,7 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { func testErrorHandlerTriggersBackoffForServerUnreachable() { // Arrange - let mockClient = MockRecaptchaClient(dummy: ()) + let mockClient = MockRecaptchaClient() mockClient.mockToken = "valid-recaptcha-token" MockRecaptcha.mockClient = mockClient @@ -319,7 +319,7 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { func testErrorHandlerDoesNotTriggerBackoffForOtherErrors() { // Arrange - let mockClient = MockRecaptchaClient(dummy: ()) + let mockClient = MockRecaptchaClient() mockClient.mockToken = "valid-recaptcha-token" MockRecaptcha.mockClient = mockClient @@ -357,7 +357,7 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { func testGetRecaptchaTokenExecutionNilNilFallback() { // Arrange - let mockClient = MockRecaptchaClient(dummy: ()) + let mockClient = MockRecaptchaClient() MockRecaptcha.mockClient = mockClient let generator = RecaptchaEnterpriseTokenGenerator( From f8d4016f72cde22bfdbb5a73aaae74d865407940 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 12 May 2026 17:32:40 -0400 Subject: [PATCH 47/71] fixes --- .../Tests/MockRecaptchaSupport.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift b/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift index 95af2e1c..2b3d1c30 100644 --- a/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift +++ b/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift @@ -37,7 +37,9 @@ final class MockRecaptcha: NSObject, RCARecaptchaProtocol { static var mockClient: MockRecaptchaClient? static var mockError: Error? - init(dummy: Void = ()) { + // This initializer bypasses the unavailable `init()` in the protocol. + // The unlabeled `Void` parameter carries no data and is purely to change the signature. + init(_: Void = ()) { super.init() } @@ -55,7 +57,9 @@ final class MockRecaptchaClient: NSObject, RCARecaptchaClientProtocol { var mockToken: String? var mockError: Error? - init(dummy: Void = ()) { + // This initializer bypasses the unavailable `init()` in the protocol. + // The unlabeled `Void` parameter carries no data and is purely to change the signature. + init(_: Void = ()) { super.init() } @@ -121,7 +125,7 @@ class MockAppCheckCoreAPIService: NSObject, AppCheckCoreAPIServiceProtocol { promise.reject(expectedError) } else { let token = expectedToken ?? AppCheckCoreToken( - token: "dummy", + token: "placeholder_app_check_token", expirationDate: Date() ) promise.fulfill(token) From 42270e4083075d6122d33c3eb5607efb58eff5c6 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 12 May 2026 17:42:40 -0400 Subject: [PATCH 48/71] fixes --- .../Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift | 1 - .../Sources/RecaptchaEnterpriseCoreTokenGenerator.swift | 1 + RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift b/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift index e6ab8a47..c37becc4 100644 --- a/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift @@ -27,7 +27,6 @@ import RecaptchaInterop public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCoreProvider { // This action name should never change without coordination with the backend. private static let appCheckActionName = "app_check_ios" - private static let providerName = "RecaptchaEnterprise" private let tokenGenerator: RecaptchaEnterpriseTokenGenerator? private let apiService: RecaptchaEnterpriseAPIService diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index 44135155..e695e1ef 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -94,6 +94,7 @@ final class RecaptchaEnterpriseTokenGenerator { } } + // This fallback path is useful for testing when we don't want to involve the backoff wrapper. private func getRecaptchaTokenNoBackoff() -> Promise { recaptchaClient.then { client in Promise { fulfill, reject in diff --git a/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift b/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift index 2b3d1c30..014fbe2e 100644 --- a/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift +++ b/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift @@ -22,6 +22,7 @@ import RecaptchaInterop class MockRCAAction: NSObject, RCAActionProtocol { var action: String { return customAction } + // The following properties are required by RCAActionProtocol but not used in these tests. static var login: RCAActionProtocol { return MockRCAAction(customAction: "login") } static var signup: RCAActionProtocol { return MockRCAAction(customAction: "signup") } From d6a65fb41eaa0d5219ace5b7c96c99395b53e2b9 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 12 May 2026 17:45:25 -0400 Subject: [PATCH 49/71] cleanup --- .../Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift index 182436c0..5fa380ad 100644 --- a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift @@ -22,7 +22,6 @@ final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { private var provider: AppCheckCoreRecaptchaEnterpriseProvider! private let testSiteKey = "test-site-key" private let testResourceName = "projects/test-project/apps/test-app" - private let testAPIKey = "test-api-key" override func setUp() { super.setUp() From b812295b920576cedcc7952bad0f41d559affa19 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 12 May 2026 18:02:04 -0400 Subject: [PATCH 50/71] refactor: remove test-specific code path in RecaptchaEnterpriseCoreTokenGenerator --- ...ecaptchaEnterpriseCoreTokenGenerator.swift | 25 ++--------- ...CoreRecaptchaEnterpriseProviderTests.swift | 3 +- .../Tests/MockRecaptchaSupport.swift | 29 +++++++++++++ ...chaEnterpriseCoreTokenGeneratorTests.swift | 41 ++++--------------- 4 files changed, 42 insertions(+), 56 deletions(-) diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift index e695e1ef..7c238ffa 100644 --- a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift +++ b/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift @@ -33,11 +33,11 @@ final class RecaptchaEnterpriseTokenGenerator { private let recaptchaClient: Promise - private let backoffWrapper: GACAppCheckBackoffWrapperProtocol? + private let backoffWrapper: GACAppCheckBackoffWrapperProtocol init(siteKey: String, recaptchaAction: RCAActionProtocol, recaptchaClass: RCARecaptchaProtocol.Type, - backoffWrapper: GACAppCheckBackoffWrapperProtocol? = nil) { + backoffWrapper: GACAppCheckBackoffWrapperProtocol) { self.siteKey = siteKey self.recaptchaAction = recaptchaAction self.backoffWrapper = backoffWrapper @@ -54,10 +54,6 @@ final class RecaptchaEnterpriseTokenGenerator { } func getRecaptchaToken() -> Promise { - guard let backoffWrapper else { - return getRecaptchaTokenNoBackoff() - } - return recaptchaClient.then { client in let operationProvider: GACAppCheckBackoffOperationProvider = { let swiftPromise = Promise { fulfill, reject in @@ -81,7 +77,7 @@ final class RecaptchaEnterpriseTokenGenerator { return .typeNone } - let fblPromise = backoffWrapper.applyBackoff( + let fblPromise = self.backoffWrapper.applyBackoff( toOperation: operationProvider, errorHandler: errorHandler ) @@ -94,21 +90,6 @@ final class RecaptchaEnterpriseTokenGenerator { } } - // This fallback path is useful for testing when we don't want to involve the backoff wrapper. - private func getRecaptchaTokenNoBackoff() -> Promise { - recaptchaClient.then { client in - Promise { fulfill, reject in - client.execute(withAction: self.recaptchaAction) { token, error in - if let token { - fulfill(token) - } else { - reject(self.mapRecaptchaError(error)) - } - } - } - } - } - private func mapRecaptchaError(_ error: Error?) -> Error { guard let error = error as NSError? else { return GACAppCheckErrorUtil.error(withFailureReason: "Failed to execute Recaptcha action") diff --git a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift index 5fa380ad..5b3cfc8e 100644 --- a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift @@ -95,7 +95,8 @@ final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { let tokenGenerator = RecaptchaEnterpriseTokenGenerator( siteKey: testSiteKey, recaptchaAction: MockRCAAction(customAction: "app_check_ios"), - recaptchaClass: MockRecaptcha.self + recaptchaClass: MockRecaptcha.self, + backoffWrapper: MockBackoffWrapper() ) let mockCoreAPIService = MockAppCheckCoreAPIService() diff --git a/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift b/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift index 014fbe2e..536bd588 100644 --- a/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift +++ b/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift @@ -135,3 +135,32 @@ class MockAppCheckCoreAPIService: NSObject, AppCheckCoreAPIServiceProtocol { return promise.asObjCPromise() } } + +class MockBackoffWrapper: NSObject, GACAppCheckBackoffWrapperProtocol { + var applyBackoffCalled = false + var shouldReturnError = false + var mockError: NSError? + var mockResult: Any? + var capturedErrorHandler: GACAppCheckBackoffErrorHandler? + + func applyBackoff(toOperation operationProvider: @escaping GACAppCheckBackoffOperationProvider, + errorHandler: @escaping GACAppCheckBackoffErrorHandler) + -> FBLPromise { + applyBackoffCalled = true + capturedErrorHandler = errorHandler + if shouldReturnError { + let error = mockError ?? NSError(domain: "MockBackoffWrapper", code: -1, userInfo: nil) + let swiftPromise = Promise(error as Error) + return swiftPromise.asObjCPromise() + } + if let mockResult { + let swiftPromise = Promise(mockResult as AnyObject) + return swiftPromise.asObjCPromise() + } + return operationProvider() + } + + func defaultAppCheckProviderErrorHandler() -> GACAppCheckBackoffErrorHandler { + return { error in .typeExponential } + } +} diff --git a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift index 9ad9bb72..6ed8bfa9 100644 --- a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift +++ b/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift @@ -20,35 +20,6 @@ import Promises @testable import RecaptchaEnterpriseProvider import RecaptchaInterop -class MockBackoffWrapper: NSObject, GACAppCheckBackoffWrapperProtocol { - var applyBackoffCalled = false - var shouldReturnError = false - var mockError: NSError? - var mockResult: Any? - var capturedErrorHandler: GACAppCheckBackoffErrorHandler? - - func applyBackoff(toOperation operationProvider: @escaping GACAppCheckBackoffOperationProvider, - errorHandler: @escaping GACAppCheckBackoffErrorHandler) - -> FBLPromise { - applyBackoffCalled = true - capturedErrorHandler = errorHandler - if shouldReturnError { - let error = mockError ?? NSError(domain: "MockBackoffWrapper", code: -1, userInfo: nil) - let swiftPromise = Promise(error as Error) - return swiftPromise.asObjCPromise() - } - if let mockResult { - let swiftPromise = Promise(mockResult as AnyObject) - return swiftPromise.asObjCPromise() - } - return operationProvider() - } - - func defaultAppCheckProviderErrorHandler() -> GACAppCheckBackoffErrorHandler { - return { error in .typeExponential } - } -} - final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { private let testSiteKey = "test-site-key" private var mockAction: MockRCAAction! @@ -69,7 +40,8 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { let generator = RecaptchaEnterpriseTokenGenerator( siteKey: testSiteKey, recaptchaAction: mockAction, - recaptchaClass: MockRecaptcha.self + recaptchaClass: MockRecaptcha.self, + backoffWrapper: MockBackoffWrapper() ) let expectation = self.expectation(description: "Generates token successfully") @@ -94,7 +66,8 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { let generator = RecaptchaEnterpriseTokenGenerator( siteKey: testSiteKey, recaptchaAction: mockAction, - recaptchaClass: MockRecaptcha.self + recaptchaClass: MockRecaptcha.self, + backoffWrapper: MockBackoffWrapper() ) let expectation = self.expectation(description: "Fails when fetchClient fails") @@ -122,7 +95,8 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { let generator = RecaptchaEnterpriseTokenGenerator( siteKey: testSiteKey, recaptchaAction: mockAction, - recaptchaClass: MockRecaptcha.self + recaptchaClass: MockRecaptcha.self, + backoffWrapper: MockBackoffWrapper() ) let expectation = self.expectation(description: "Fails when execute fails") @@ -363,7 +337,8 @@ final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { let generator = RecaptchaEnterpriseTokenGenerator( siteKey: testSiteKey, recaptchaAction: mockAction, - recaptchaClass: MockRecaptcha.self + recaptchaClass: MockRecaptcha.self, + backoffWrapper: MockBackoffWrapper() ) let expectation = self From eab77db58e8839605e6378dc7d01b0e713e561d8 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Wed, 13 May 2026 15:38:30 -0400 Subject: [PATCH 51/71] refactor names --- AppCheckCore.podspec | 2 +- .../AppCheckRecaptchaEnterpriseProvider.swift | 2 +- .../Sources/RecaptchaEnterpriseAPIService.swift | 0 .../RecaptchaEnterpriseTokenGenerator.swift | 0 .../AppCheckRecaptchaEnterpriseProviderTests.swift | 12 ++++++------ .../Tests/MockRecaptchaSupport.swift | 2 +- .../Tests/RecaptchaEnterpriseAPIServiceTests.swift | 4 ++-- .../RecaptchaEnterpriseTokenGeneratorTests.swift | 4 ++-- Package.swift | 14 +++++++------- 9 files changed, 20 insertions(+), 20 deletions(-) rename RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift => AppCheckRecaptchaEnterpriseProvider/Sources/Public/AppCheckRecaptchaEnterpriseProvider.swift (98%) rename RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreAPIService.swift => AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseAPIService.swift (100%) rename RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift => AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseTokenGenerator.swift (100%) rename RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift => AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift (93%) rename {RecaptchaEnterpriseProvider => AppCheckRecaptchaEnterpriseProvider}/Tests/MockRecaptchaSupport.swift (98%) rename RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift => AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift (97%) rename RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift => AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift (98%) diff --git a/AppCheckCore.podspec b/AppCheckCore.podspec index 76e67bb1..b3cb5409 100644 --- a/AppCheckCore.podspec +++ b/AppCheckCore.podspec @@ -38,7 +38,7 @@ Pod::Spec.new do |s| base_dir + 'Sources/**/*.[mh]', ] s.ios.source_files = [ - 'RecaptchaEnterpriseProvider/Sources/**/*.swift', + 'AppCheckRecaptchaEnterpriseProvider/Sources/**/*.swift', ] s.public_header_files = base_dir + 'Sources/Public/AppCheckCore/*.h' diff --git a/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift b/AppCheckRecaptchaEnterpriseProvider/Sources/Public/AppCheckRecaptchaEnterpriseProvider.swift similarity index 98% rename from RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift rename to AppCheckRecaptchaEnterpriseProvider/Sources/Public/AppCheckRecaptchaEnterpriseProvider.swift index c37becc4..c945bd3c 100644 --- a/RecaptchaEnterpriseProvider/Sources/Public/AppCheckCoreRecaptchaEnterpriseProvider.swift +++ b/AppCheckRecaptchaEnterpriseProvider/Sources/Public/AppCheckRecaptchaEnterpriseProvider.swift @@ -24,7 +24,7 @@ import RecaptchaInterop /// API. This class is available on all platforms for select OS versions. See /// https://firebase.google.com/docs/ios/learn-more for more details. @objc(GACRecaptchaEnterpriseProvider) -public final class AppCheckCoreRecaptchaEnterpriseProvider: NSObject, AppCheckCoreProvider { +public final class AppCheckRecaptchaEnterpriseProvider: NSObject, AppCheckCoreProvider { // This action name should never change without coordination with the backend. private static let appCheckActionName = "app_check_ios" diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreAPIService.swift b/AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseAPIService.swift similarity index 100% rename from RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreAPIService.swift rename to AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseAPIService.swift diff --git a/RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift b/AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseTokenGenerator.swift similarity index 100% rename from RecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseCoreTokenGenerator.swift rename to AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseTokenGenerator.swift diff --git a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift b/AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift similarity index 93% rename from RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift rename to AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift index 5b3cfc8e..bfc5a146 100644 --- a/RecaptchaEnterpriseProvider/Tests/AppCheckCoreRecaptchaEnterpriseProviderTests.swift +++ b/AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift @@ -15,11 +15,11 @@ import XCTest @testable import AppCheckCore +@testable import AppCheckRecaptchaEnterpriseProvider import Promises -@testable import RecaptchaEnterpriseProvider -final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { - private var provider: AppCheckCoreRecaptchaEnterpriseProvider! +final class AppCheckRecaptchaEnterpriseProviderTests: XCTestCase { + private var provider: AppCheckRecaptchaEnterpriseProvider! private let testSiteKey = "test-site-key" private let testResourceName = "projects/test-project/apps/test-app" @@ -30,7 +30,7 @@ final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { apiService: mockCoreAPIService, resourceName: testResourceName ) - provider = AppCheckCoreRecaptchaEnterpriseProvider( + provider = AppCheckRecaptchaEnterpriseProvider( tokenGenerator: nil, apiService: apiService ) @@ -87,7 +87,7 @@ final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { } private func createProviderWithMocks(expectedToken: AppCheckCoreToken) - -> AppCheckCoreRecaptchaEnterpriseProvider { + -> AppCheckRecaptchaEnterpriseProvider { let mockClient = MockRecaptchaClient() mockClient.mockToken = "valid-recaptcha-token" MockRecaptcha.mockClient = mockClient @@ -107,7 +107,7 @@ final class AppCheckCoreRecaptchaEnterpriseProviderTests: XCTestCase { resourceName: testResourceName ) - return AppCheckCoreRecaptchaEnterpriseProvider( + return AppCheckRecaptchaEnterpriseProvider( tokenGenerator: tokenGenerator, apiService: apiService ) diff --git a/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift b/AppCheckRecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift similarity index 98% rename from RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift rename to AppCheckRecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift index 536bd588..c02f3191 100644 --- a/RecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift +++ b/AppCheckRecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift @@ -13,10 +13,10 @@ // limitations under the License. @testable import AppCheckCore +@testable import AppCheckRecaptchaEnterpriseProvider import FBLPromises import Foundation import Promises -@testable import RecaptchaEnterpriseProvider import RecaptchaInterop class MockRCAAction: NSObject, RCAActionProtocol { diff --git a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift b/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift similarity index 97% rename from RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift rename to AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift index 8f2e116e..156a1274 100644 --- a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreAPIServiceTests.swift +++ b/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift @@ -15,10 +15,10 @@ import XCTest @testable import AppCheckCore +@testable import AppCheckRecaptchaEnterpriseProvider import FBLPromises -@testable import RecaptchaEnterpriseProvider -final class RecaptchaEnterpriseCoreAPIServiceTests: XCTestCase { +final class RecaptchaEnterpriseAPIServiceTests: XCTestCase { private var apiService: RecaptchaEnterpriseAPIService! private var mockCoreAPIService: MockAppCheckCoreAPIService! private let testResourceName = "projects/test-project/apps/test-app" diff --git a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift b/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift similarity index 98% rename from RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift rename to AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift index 6ed8bfa9..ca313ea7 100644 --- a/RecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseCoreTokenGeneratorTests.swift +++ b/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift @@ -15,12 +15,12 @@ import XCTest @testable import AppCheckCore +@testable import AppCheckRecaptchaEnterpriseProvider import FBLPromises import Promises -@testable import RecaptchaEnterpriseProvider import RecaptchaInterop -final class RecaptchaEnterpriseCoreTokenGeneratorTests: XCTestCase { +final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { private let testSiteKey = "test-site-key" private var mockAction: MockRCAAction! diff --git a/Package.swift b/Package.swift index e2761de7..00befb7d 100644 --- a/Package.swift +++ b/Package.swift @@ -27,8 +27,8 @@ let package = Package( ), .library( - name: "RecaptchaEnterpriseProvider", - targets: ["RecaptchaEnterpriseProvider"] + name: "AppCheckRecaptchaEnterpriseProvider", + targets: ["AppCheckRecaptchaEnterpriseProvider"] ), ], @@ -68,13 +68,13 @@ let package = Package( .when(platforms: [.iOS, .macCatalyst, .macOS, .tvOS, .appCheckVisionOS]) ), ]), - .target(name: "RecaptchaEnterpriseProvider", + .target(name: "AppCheckRecaptchaEnterpriseProvider", dependencies: [ "AppCheckCore", .product(name: "RecaptchaInterop", package: "interop-ios-for-google-sdks"), .product(name: "Promises", package: "Promises"), ], - path: "RecaptchaEnterpriseProvider/Sources"), + path: "AppCheckRecaptchaEnterpriseProvider/Sources"), .testTarget( name: "AppCheckCoreUnit", dependencies: [ @@ -103,12 +103,12 @@ let package = Package( ] ), .testTarget( - name: "RecaptchaEnterpriseProviderUnit", + name: "AppCheckRecaptchaEnterpriseProviderUnit", dependencies: [ - "RecaptchaEnterpriseProvider", + "AppCheckRecaptchaEnterpriseProvider", .product(name: "OCMock", package: "ocmock"), ], - path: "RecaptchaEnterpriseProvider/Tests", + path: "AppCheckRecaptchaEnterpriseProvider/Tests", cSettings: [ .headerSearchPath("../.."), ] From 046b20626aa1b8741672f923d3e3af1c28025924 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 21 May 2026 13:27:53 -0400 Subject: [PATCH 52/71] add availability checks that mirro RecaptchaEnterprise --- .../Public/AppCheckRecaptchaEnterpriseProvider.swift | 5 +++++ .../Sources/RecaptchaEnterpriseAPIService.swift | 6 +++++- .../Sources/RecaptchaEnterpriseTokenGenerator.swift | 5 +++++ .../Tests/AppCheckRecaptchaEnterpriseProviderTests.swift | 1 + .../Tests/RecaptchaEnterpriseAPIServiceTests.swift | 1 + .../Tests/RecaptchaEnterpriseTokenGeneratorTests.swift | 1 + 6 files changed, 18 insertions(+), 1 deletion(-) diff --git a/AppCheckRecaptchaEnterpriseProvider/Sources/Public/AppCheckRecaptchaEnterpriseProvider.swift b/AppCheckRecaptchaEnterpriseProvider/Sources/Public/AppCheckRecaptchaEnterpriseProvider.swift index c945bd3c..b30947d8 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Sources/Public/AppCheckRecaptchaEnterpriseProvider.swift +++ b/AppCheckRecaptchaEnterpriseProvider/Sources/Public/AppCheckRecaptchaEnterpriseProvider.swift @@ -23,6 +23,11 @@ import RecaptchaInterop /// [reCAPTCHA Enterprise](https://cloud.google.com/recaptcha/docs/instrument-ios-apps) /// API. This class is available on all platforms for select OS versions. See /// https://firebase.google.com/docs/ios/learn-more for more details. +@available(iOS 15.0, visionOS 1.0, *) +@available(macOS, unavailable) +@available(macCatalyst, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) @objc(GACRecaptchaEnterpriseProvider) public final class AppCheckRecaptchaEnterpriseProvider: NSObject, AppCheckCoreProvider { // This action name should never change without coordination with the backend. diff --git a/AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseAPIService.swift b/AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseAPIService.swift index 1b5030be..e7242f7c 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseAPIService.swift +++ b/AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseAPIService.swift @@ -28,7 +28,11 @@ private enum Constants { static let httpMethodPost = "POST" } -@objc(GACRecaptchaEnterpriseAPIService) +@available(iOS 15.0, visionOS 1.0, *) +@available(macOS, unavailable) +@available(macCatalyst, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) final class RecaptchaEnterpriseAPIService: NSObject { private let apiService: AppCheckCoreAPIServiceProtocol private let resourceName: String diff --git a/AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseTokenGenerator.swift b/AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseTokenGenerator.swift index 7c238ffa..53042a4e 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseTokenGenerator.swift +++ b/AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseTokenGenerator.swift @@ -20,6 +20,11 @@ import Foundation import Promises import RecaptchaInterop +@available(iOS 15.0, visionOS 1.0, *) +@available(macOS, unavailable) +@available(macCatalyst, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) final class RecaptchaEnterpriseTokenGenerator { // Corresponds to RecaptchaErrorNetworkError. These codes are not in the interop. // See https://docs.cloud.google.com/recaptcha/docs/reference/ios/client/api/Enums/RecaptchaErrorCode.html#recaptchaerrornetworkerror diff --git a/AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift b/AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift index bfc5a146..df3ebae0 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift +++ b/AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import AppCheckRecaptchaEnterpriseProvider import Promises +@available(iOS 15.0, visionOS 1.0, *) final class AppCheckRecaptchaEnterpriseProviderTests: XCTestCase { private var provider: AppCheckRecaptchaEnterpriseProvider! private let testSiteKey = "test-site-key" diff --git a/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift b/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift index 156a1274..51b420e3 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift +++ b/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift @@ -18,6 +18,7 @@ import XCTest @testable import AppCheckRecaptchaEnterpriseProvider import FBLPromises +@available(iOS 15.0, visionOS 1.0, *) final class RecaptchaEnterpriseAPIServiceTests: XCTestCase { private var apiService: RecaptchaEnterpriseAPIService! private var mockCoreAPIService: MockAppCheckCoreAPIService! diff --git a/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift b/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift index ca313ea7..ff634919 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift +++ b/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift @@ -20,6 +20,7 @@ import FBLPromises import Promises import RecaptchaInterop +@available(iOS 15.0, visionOS 1.0, *) final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { private let testSiteKey = "test-site-key" private var mockAction: MockRCAAction! From d03149edfb64e1c0cbe9666808bfec5725e65242 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 21 May 2026 16:16:10 -0400 Subject: [PATCH 53/71] add availability attributes to test suites --- .../Tests/AppCheckRecaptchaEnterpriseProviderTests.swift | 4 ++++ .../Tests/RecaptchaEnterpriseAPIServiceTests.swift | 4 ++++ .../Tests/RecaptchaEnterpriseTokenGeneratorTests.swift | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift b/AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift index df3ebae0..50764bb0 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift +++ b/AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift @@ -19,6 +19,10 @@ import XCTest import Promises @available(iOS 15.0, visionOS 1.0, *) +@available(macOS, unavailable) +@available(macCatalyst, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) final class AppCheckRecaptchaEnterpriseProviderTests: XCTestCase { private var provider: AppCheckRecaptchaEnterpriseProvider! private let testSiteKey = "test-site-key" diff --git a/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift b/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift index 51b420e3..b8e84385 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift +++ b/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift @@ -19,6 +19,10 @@ import XCTest import FBLPromises @available(iOS 15.0, visionOS 1.0, *) +@available(macOS, unavailable) +@available(macCatalyst, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) final class RecaptchaEnterpriseAPIServiceTests: XCTestCase { private var apiService: RecaptchaEnterpriseAPIService! private var mockCoreAPIService: MockAppCheckCoreAPIService! diff --git a/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift b/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift index ff634919..3308d859 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift +++ b/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift @@ -21,6 +21,10 @@ import Promises import RecaptchaInterop @available(iOS 15.0, visionOS 1.0, *) +@available(macOS, unavailable) +@available(macCatalyst, unavailable) +@available(tvOS, unavailable) +@available(watchOS, unavailable) final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { private let testSiteKey = "test-site-key" private var mockAction: MockRCAAction! From 4ddab298738c8afa107b3d9262132f69f14e64ae Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 22 May 2026 15:07:49 -0400 Subject: [PATCH 54/71] remove unneeded dep --- Package.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Package.swift b/Package.swift index 00befb7d..53a3c385 100644 --- a/Package.swift +++ b/Package.swift @@ -106,7 +106,6 @@ let package = Package( name: "AppCheckRecaptchaEnterpriseProviderUnit", dependencies: [ "AppCheckRecaptchaEnterpriseProvider", - .product(name: "OCMock", package: "ocmock"), ], path: "AppCheckRecaptchaEnterpriseProvider/Tests", cSettings: [ From 5db821ca47aedf3b948553994a6e03728b81ec4e Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 22 May 2026 15:17:02 -0400 Subject: [PATCH 55/71] remove unneeded target stuff --- Package.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 53a3c385..05cb7573 100644 --- a/Package.swift +++ b/Package.swift @@ -107,10 +107,7 @@ let package = Package( dependencies: [ "AppCheckRecaptchaEnterpriseProvider", ], - path: "AppCheckRecaptchaEnterpriseProvider/Tests", - cSettings: [ - .headerSearchPath("../.."), - ] + path: "AppCheckRecaptchaEnterpriseProvider/Tests" ), ] ) From 23bd67d4778b3a17ce6ee56f1641a0e066369b12 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 28 May 2026 14:25:26 -0400 Subject: [PATCH 56/71] refactor: Rename RecaptchaEnterprise to Recaptcha Rename all types and files with the RecaptchaEnterprise substring to instead be Recaptcha. --- AppCheckCore.podspec | 2 +- .../Public/AppCheckRecaptchaProvider.swift | 22 +++++++-------- .../Sources/RecaptchaAPIService.swift | 2 +- .../Sources/RecaptchaTokenGenerator.swift | 2 +- .../AppCheckRecaptchaProviderTests.swift | 18 ++++++------ .../Tests/MockRecaptchaSupport.swift | 2 +- .../Tests/RecaptchaAPIServiceTests.swift | 8 +++--- .../Tests/RecaptchaTokenGeneratorTests.swift | 28 +++++++++---------- Package.swift | 14 +++++----- 9 files changed, 49 insertions(+), 49 deletions(-) rename AppCheckRecaptchaEnterpriseProvider/Sources/Public/AppCheckRecaptchaEnterpriseProvider.swift => AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift (89%) rename AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseAPIService.swift => AppCheckRecaptchaProvider/Sources/RecaptchaAPIService.swift (98%) rename AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseTokenGenerator.swift => AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift (98%) rename AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift => AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift (90%) rename {AppCheckRecaptchaEnterpriseProvider => AppCheckRecaptchaProvider}/Tests/MockRecaptchaSupport.swift (98%) rename AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift => AppCheckRecaptchaProvider/Tests/RecaptchaAPIServiceTests.swift (95%) rename AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift => AppCheckRecaptchaProvider/Tests/RecaptchaTokenGeneratorTests.swift (93%) diff --git a/AppCheckCore.podspec b/AppCheckCore.podspec index b3cb5409..e2b9a786 100644 --- a/AppCheckCore.podspec +++ b/AppCheckCore.podspec @@ -38,7 +38,7 @@ Pod::Spec.new do |s| base_dir + 'Sources/**/*.[mh]', ] s.ios.source_files = [ - 'AppCheckRecaptchaEnterpriseProvider/Sources/**/*.swift', + 'AppCheckRecaptchaProvider/Sources/**/*.swift', ] s.public_header_files = base_dir + 'Sources/Public/AppCheckCore/*.h' diff --git a/AppCheckRecaptchaEnterpriseProvider/Sources/Public/AppCheckRecaptchaEnterpriseProvider.swift b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift similarity index 89% rename from AppCheckRecaptchaEnterpriseProvider/Sources/Public/AppCheckRecaptchaEnterpriseProvider.swift rename to AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift index b30947d8..ec8941d0 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Sources/Public/AppCheckRecaptchaEnterpriseProvider.swift +++ b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift @@ -28,13 +28,13 @@ import RecaptchaInterop @available(macCatalyst, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -@objc(GACRecaptchaEnterpriseProvider) -public final class AppCheckRecaptchaEnterpriseProvider: NSObject, AppCheckCoreProvider { +@objc(GACRecaptchaProvider) +public final class AppCheckRecaptchaProvider: NSObject, AppCheckCoreProvider { // This action name should never change without coordination with the backend. private static let appCheckActionName = "app_check_ios" - private let tokenGenerator: RecaptchaEnterpriseTokenGenerator? - private let apiService: RecaptchaEnterpriseAPIService + private let tokenGenerator: RecaptchaTokenGenerator? + private let apiService: RecaptchaAPIService /// The default initializer. /// - Parameters: @@ -49,12 +49,12 @@ public final class AppCheckRecaptchaEnterpriseProvider: NSObject, AppCheckCorePr @objc public convenience init(siteKey: String, resourceName: String, APIKey: String, requestHooks: [@convention(block) (NSMutableURLRequest) -> Void]? = nil) { - let tokenGenerator: RecaptchaEnterpriseTokenGenerator? + let tokenGenerator: RecaptchaTokenGenerator? - if let sdk = RecaptchaEnterpriseSDK(customAction: Self.appCheckActionName) { + if let sdk = RecaptchaSDK(customAction: Self.appCheckActionName) { let backoffWrapper = GACAppCheckBackoffWrapper() - tokenGenerator = RecaptchaEnterpriseTokenGenerator( + tokenGenerator = RecaptchaTokenGenerator( siteKey: siteKey, recaptchaAction: sdk.action, recaptchaClass: sdk.recaptchaClass, @@ -72,7 +72,7 @@ public final class AppCheckRecaptchaEnterpriseProvider: NSObject, AppCheckCorePr baseURL: nil, apiKey: APIKey, requestHooks: requestHooks) - let apiService = RecaptchaEnterpriseAPIService( + let apiService = RecaptchaAPIService( apiService: appCheckAPIService, resourceName: resourceName ) @@ -80,8 +80,8 @@ public final class AppCheckRecaptchaEnterpriseProvider: NSObject, AppCheckCorePr self.init(tokenGenerator: tokenGenerator, apiService: apiService) } - init(tokenGenerator: RecaptchaEnterpriseTokenGenerator?, - apiService: RecaptchaEnterpriseAPIService) { + init(tokenGenerator: RecaptchaTokenGenerator?, + apiService: RecaptchaAPIService) { self.tokenGenerator = tokenGenerator self.apiService = apiService super.init() @@ -122,7 +122,7 @@ public final class AppCheckRecaptchaEnterpriseProvider: NSObject, AppCheckCorePr } } -private struct RecaptchaEnterpriseSDK { +private struct RecaptchaSDK { // This symbol is specified in the RecaptchaEnterprise SDK. // See https://github.com/GoogleCloudPlatform/recaptcha-enterprise-mobile-sdk/blob/18.9.0/Sources/RecaptchaEnterprise/RecaptchaInteropBidings.swift private static let recaptchaActionClassName = "RecaptchaEnterprise.RCAAction" diff --git a/AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseAPIService.swift b/AppCheckRecaptchaProvider/Sources/RecaptchaAPIService.swift similarity index 98% rename from AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseAPIService.swift rename to AppCheckRecaptchaProvider/Sources/RecaptchaAPIService.swift index e7242f7c..46ff6278 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseAPIService.swift +++ b/AppCheckRecaptchaProvider/Sources/RecaptchaAPIService.swift @@ -33,7 +33,7 @@ private enum Constants { @available(macCatalyst, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -final class RecaptchaEnterpriseAPIService: NSObject { +final class RecaptchaAPIService: NSObject { private let apiService: AppCheckCoreAPIServiceProtocol private let resourceName: String diff --git a/AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseTokenGenerator.swift b/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift similarity index 98% rename from AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseTokenGenerator.swift rename to AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift index 53042a4e..1cba0a16 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Sources/RecaptchaEnterpriseTokenGenerator.swift +++ b/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift @@ -25,7 +25,7 @@ import RecaptchaInterop @available(macCatalyst, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -final class RecaptchaEnterpriseTokenGenerator { +final class RecaptchaTokenGenerator { // Corresponds to RecaptchaErrorNetworkError. These codes are not in the interop. // See https://docs.cloud.google.com/recaptcha/docs/reference/ios/client/api/Enums/RecaptchaErrorCode.html#recaptchaerrornetworkerror static let networkErrorCode = 1 diff --git a/AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift b/AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift similarity index 90% rename from AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift rename to AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift index 50764bb0..1b8fe681 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Tests/AppCheckRecaptchaEnterpriseProviderTests.swift +++ b/AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift @@ -15,7 +15,7 @@ import XCTest @testable import AppCheckCore -@testable import AppCheckRecaptchaEnterpriseProvider +@testable import AppCheckRecaptchaProvider import Promises @available(iOS 15.0, visionOS 1.0, *) @@ -23,19 +23,19 @@ import Promises @available(macCatalyst, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -final class AppCheckRecaptchaEnterpriseProviderTests: XCTestCase { - private var provider: AppCheckRecaptchaEnterpriseProvider! +final class AppCheckRecaptchaProviderTests: XCTestCase { + private var provider: AppCheckRecaptchaProvider! private let testSiteKey = "test-site-key" private let testResourceName = "projects/test-project/apps/test-app" override func setUp() { super.setUp() let mockCoreAPIService = MockAppCheckCoreAPIService() - let apiService = RecaptchaEnterpriseAPIService( + let apiService = RecaptchaAPIService( apiService: mockCoreAPIService, resourceName: testResourceName ) - provider = AppCheckRecaptchaEnterpriseProvider( + provider = AppCheckRecaptchaProvider( tokenGenerator: nil, apiService: apiService ) @@ -92,12 +92,12 @@ final class AppCheckRecaptchaEnterpriseProviderTests: XCTestCase { } private func createProviderWithMocks(expectedToken: AppCheckCoreToken) - -> AppCheckRecaptchaEnterpriseProvider { + -> AppCheckRecaptchaProvider { let mockClient = MockRecaptchaClient() mockClient.mockToken = "valid-recaptcha-token" MockRecaptcha.mockClient = mockClient - let tokenGenerator = RecaptchaEnterpriseTokenGenerator( + let tokenGenerator = RecaptchaTokenGenerator( siteKey: testSiteKey, recaptchaAction: MockRCAAction(customAction: "app_check_ios"), recaptchaClass: MockRecaptcha.self, @@ -107,12 +107,12 @@ final class AppCheckRecaptchaEnterpriseProviderTests: XCTestCase { let mockCoreAPIService = MockAppCheckCoreAPIService() mockCoreAPIService.expectedToken = expectedToken - let apiService = RecaptchaEnterpriseAPIService( + let apiService = RecaptchaAPIService( apiService: mockCoreAPIService, resourceName: testResourceName ) - return AppCheckRecaptchaEnterpriseProvider( + return AppCheckRecaptchaProvider( tokenGenerator: tokenGenerator, apiService: apiService ) diff --git a/AppCheckRecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift b/AppCheckRecaptchaProvider/Tests/MockRecaptchaSupport.swift similarity index 98% rename from AppCheckRecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift rename to AppCheckRecaptchaProvider/Tests/MockRecaptchaSupport.swift index c02f3191..b91d5bd4 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Tests/MockRecaptchaSupport.swift +++ b/AppCheckRecaptchaProvider/Tests/MockRecaptchaSupport.swift @@ -13,7 +13,7 @@ // limitations under the License. @testable import AppCheckCore -@testable import AppCheckRecaptchaEnterpriseProvider +@testable import AppCheckRecaptchaProvider import FBLPromises import Foundation import Promises diff --git a/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift b/AppCheckRecaptchaProvider/Tests/RecaptchaAPIServiceTests.swift similarity index 95% rename from AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift rename to AppCheckRecaptchaProvider/Tests/RecaptchaAPIServiceTests.swift index b8e84385..cd62549b 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseAPIServiceTests.swift +++ b/AppCheckRecaptchaProvider/Tests/RecaptchaAPIServiceTests.swift @@ -15,7 +15,7 @@ import XCTest @testable import AppCheckCore -@testable import AppCheckRecaptchaEnterpriseProvider +@testable import AppCheckRecaptchaProvider import FBLPromises @available(iOS 15.0, visionOS 1.0, *) @@ -23,8 +23,8 @@ import FBLPromises @available(macCatalyst, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -final class RecaptchaEnterpriseAPIServiceTests: XCTestCase { - private var apiService: RecaptchaEnterpriseAPIService! +final class RecaptchaAPIServiceTests: XCTestCase { + private var apiService: RecaptchaAPIService! private var mockCoreAPIService: MockAppCheckCoreAPIService! private let testResourceName = "projects/test-project/apps/test-app" private let testRecaptchaToken = "recaptcha-token-123" @@ -32,7 +32,7 @@ final class RecaptchaEnterpriseAPIServiceTests: XCTestCase { override func setUp() { super.setUp() mockCoreAPIService = MockAppCheckCoreAPIService() - apiService = RecaptchaEnterpriseAPIService( + apiService = RecaptchaAPIService( apiService: mockCoreAPIService, resourceName: testResourceName ) diff --git a/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift b/AppCheckRecaptchaProvider/Tests/RecaptchaTokenGeneratorTests.swift similarity index 93% rename from AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift rename to AppCheckRecaptchaProvider/Tests/RecaptchaTokenGeneratorTests.swift index 3308d859..2363dccb 100644 --- a/AppCheckRecaptchaEnterpriseProvider/Tests/RecaptchaEnterpriseTokenGeneratorTests.swift +++ b/AppCheckRecaptchaProvider/Tests/RecaptchaTokenGeneratorTests.swift @@ -15,7 +15,7 @@ import XCTest @testable import AppCheckCore -@testable import AppCheckRecaptchaEnterpriseProvider +@testable import AppCheckRecaptchaProvider import FBLPromises import Promises import RecaptchaInterop @@ -25,7 +25,7 @@ import RecaptchaInterop @available(macCatalyst, unavailable) @available(tvOS, unavailable) @available(watchOS, unavailable) -final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { +final class RecaptchaTokenGeneratorTests: XCTestCase { private let testSiteKey = "test-site-key" private var mockAction: MockRCAAction! @@ -42,7 +42,7 @@ final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { mockClient.mockToken = "valid-recaptcha-token" MockRecaptcha.mockClient = mockClient - let generator = RecaptchaEnterpriseTokenGenerator( + let generator = RecaptchaTokenGenerator( siteKey: testSiteKey, recaptchaAction: mockAction, recaptchaClass: MockRecaptcha.self, @@ -68,7 +68,7 @@ final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { let expectedError = NSError(domain: "test", code: -1, userInfo: nil) MockRecaptcha.mockError = expectedError - let generator = RecaptchaEnterpriseTokenGenerator( + let generator = RecaptchaTokenGenerator( siteKey: testSiteKey, recaptchaAction: mockAction, recaptchaClass: MockRecaptcha.self, @@ -97,7 +97,7 @@ final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { mockClient.mockError = expectedError MockRecaptcha.mockClient = mockClient - let generator = RecaptchaEnterpriseTokenGenerator( + let generator = RecaptchaTokenGenerator( siteKey: testSiteKey, recaptchaAction: mockAction, recaptchaClass: MockRecaptcha.self, @@ -133,7 +133,7 @@ final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { let mockBackoffWrapper = MockBackoffWrapper() - let generator = RecaptchaEnterpriseTokenGenerator( + let generator = RecaptchaTokenGenerator( siteKey: testSiteKey, recaptchaAction: mockAction, recaptchaClass: MockRecaptcha.self, @@ -164,7 +164,7 @@ final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { let expectedError = NSError(domain: "test", code: -3, userInfo: nil) mockBackoffWrapper.mockError = expectedError - let generator = RecaptchaEnterpriseTokenGenerator( + let generator = RecaptchaTokenGenerator( siteKey: testSiteKey, recaptchaAction: mockAction, recaptchaClass: MockRecaptcha.self, @@ -191,7 +191,7 @@ final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { let mockClient = MockRecaptchaClient() let recaptchaError = NSError( domain: "RecaptchaErrorDomain", - code: RecaptchaEnterpriseTokenGenerator.networkErrorCode, + code: RecaptchaTokenGenerator.networkErrorCode, userInfo: nil ) mockClient.mockError = recaptchaError @@ -199,7 +199,7 @@ final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { let mockBackoffWrapper = MockBackoffWrapper() - let generator = RecaptchaEnterpriseTokenGenerator( + let generator = RecaptchaTokenGenerator( siteKey: testSiteKey, recaptchaAction: mockAction, recaptchaClass: MockRecaptcha.self, @@ -227,7 +227,7 @@ final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { let mockClient = MockRecaptchaClient() let recaptchaError = NSError( domain: "RecaptchaErrorDomain", - code: RecaptchaEnterpriseTokenGenerator.internalErrorCode, + code: RecaptchaTokenGenerator.internalErrorCode, userInfo: nil ) mockClient.mockError = recaptchaError @@ -235,7 +235,7 @@ final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { let mockBackoffWrapper = MockBackoffWrapper() - let generator = RecaptchaEnterpriseTokenGenerator( + let generator = RecaptchaTokenGenerator( siteKey: testSiteKey, recaptchaAction: mockAction, recaptchaClass: MockRecaptcha.self, @@ -266,7 +266,7 @@ final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { let mockBackoffWrapper = MockBackoffWrapper() - let generator = RecaptchaEnterpriseTokenGenerator( + let generator = RecaptchaTokenGenerator( siteKey: testSiteKey, recaptchaAction: mockAction, recaptchaClass: MockRecaptcha.self, @@ -304,7 +304,7 @@ final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { let mockBackoffWrapper = MockBackoffWrapper() - let generator = RecaptchaEnterpriseTokenGenerator( + let generator = RecaptchaTokenGenerator( siteKey: testSiteKey, recaptchaAction: mockAction, recaptchaClass: MockRecaptcha.self, @@ -339,7 +339,7 @@ final class RecaptchaEnterpriseTokenGeneratorTests: XCTestCase { let mockClient = MockRecaptchaClient() MockRecaptcha.mockClient = mockClient - let generator = RecaptchaEnterpriseTokenGenerator( + let generator = RecaptchaTokenGenerator( siteKey: testSiteKey, recaptchaAction: mockAction, recaptchaClass: MockRecaptcha.self, diff --git a/Package.swift b/Package.swift index 05cb7573..8254c4af 100644 --- a/Package.swift +++ b/Package.swift @@ -27,8 +27,8 @@ let package = Package( ), .library( - name: "AppCheckRecaptchaEnterpriseProvider", - targets: ["AppCheckRecaptchaEnterpriseProvider"] + name: "AppCheckRecaptchaProvider", + targets: ["AppCheckRecaptchaProvider"] ), ], @@ -68,13 +68,13 @@ let package = Package( .when(platforms: [.iOS, .macCatalyst, .macOS, .tvOS, .appCheckVisionOS]) ), ]), - .target(name: "AppCheckRecaptchaEnterpriseProvider", + .target(name: "AppCheckRecaptchaProvider", dependencies: [ "AppCheckCore", .product(name: "RecaptchaInterop", package: "interop-ios-for-google-sdks"), .product(name: "Promises", package: "Promises"), ], - path: "AppCheckRecaptchaEnterpriseProvider/Sources"), + path: "AppCheckRecaptchaProvider/Sources"), .testTarget( name: "AppCheckCoreUnit", dependencies: [ @@ -103,11 +103,11 @@ let package = Package( ] ), .testTarget( - name: "AppCheckRecaptchaEnterpriseProviderUnit", + name: "AppCheckRecaptchaProviderUnit", dependencies: [ - "AppCheckRecaptchaEnterpriseProvider", + "AppCheckRecaptchaProvider", ], - path: "AppCheckRecaptchaEnterpriseProvider/Tests" + path: "AppCheckRecaptchaProvider/Tests" ), ] ) From edc0083aa6b27a4909bbe0b0f0ff8afa2a8b8669 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 28 May 2026 17:46:33 -0400 Subject: [PATCH 57/71] fix(ci): install simulators for requested platforms, skipping macOS --- .github/workflows/app_check_core.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/app_check_core.yml b/.github/workflows/app_check_core.yml index 5db50fb0..e89b0d3d 100644 --- a/.github/workflows/app_check_core.yml +++ b/.github/workflows/app_check_core.yml @@ -32,6 +32,15 @@ jobs: run: ./scripts/setup_bundler.sh - name: Select Xcode version run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Install simulators in case they are missing. + if: contains(matrix.target, 'macos') == false + uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3 + with: + timeout_minutes: 15 + max_attempts: 5 + retry_wait_seconds: 120 + continue_on_error: true + command: xcodebuild -downloadPlatform ${{ matrix.target }} - name: Build and test run: | scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb AppCheckCore.podspec \ From 26c88aad207e669950d07ac1c592119828a39f4a Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 28 May 2026 18:17:27 -0400 Subject: [PATCH 58/71] review #3 --- .../Sources/RecaptchaAPIService.swift | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/AppCheckRecaptchaProvider/Sources/RecaptchaAPIService.swift b/AppCheckRecaptchaProvider/Sources/RecaptchaAPIService.swift index 46ff6278..809308f7 100644 --- a/AppCheckRecaptchaProvider/Sources/RecaptchaAPIService.swift +++ b/AppCheckRecaptchaProvider/Sources/RecaptchaAPIService.swift @@ -50,43 +50,40 @@ final class RecaptchaAPIService: NSObject { .error(withFailureReason: "Invalid URL string: \(urlString)")) } - return httpBody(with: recaptchaToken, limitedUse: limitedUse) - .then { httpBody in - Promise(self.apiService.sendRequest(with: url, - httpMethod: Constants - .httpMethodPost, - body: httpBody, - additionalHeaders: [Constants - .contentTypeKey: Constants - .jsonContentType])) - }.then { response in + let httpBody: Data + do { + httpBody = try self.httpBody(with: recaptchaToken, limitedUse: limitedUse) + } catch { + return Promise(error) + } + + return Promise(apiService.sendRequest(with: url, + httpMethod: Constants + .httpMethodPost, + body: httpBody, + additionalHeaders: [Constants + .contentTypeKey: Constants + .jsonContentType])) + .then { response in Promise(self.apiService.appCheckToken(withAPIResponse: response)) } } private func httpBody(with recaptchaToken: String, - limitedUse: Bool) -> Promise { + limitedUse: Bool) throws -> Data { guard !recaptchaToken.isEmpty else { - return Promise(GACAppCheckErrorUtil - .error(withFailureReason: "Recaptcha token cannot be empty")) + throw GACAppCheckErrorUtil.error(withFailureReason: "Recaptcha token cannot be empty") } - return Promise(on: backgroundQueue()) { - let payload: [String: Any] = [ - Constants.recaptchaTokenField: recaptchaToken, - Constants.limitedUseField: limitedUse, - ] + let payload: [String: Any] = [ + Constants.recaptchaTokenField: recaptchaToken, + Constants.limitedUseField: limitedUse, + ] - do { - let jsonData = try JSONSerialization.data(withJSONObject: payload, options: []) - return jsonData - } catch { - throw GACAppCheckErrorUtil.jsonSerializationError(error) - } + do { + return try JSONSerialization.data(withJSONObject: payload, options: []) + } catch { + throw GACAppCheckErrorUtil.jsonSerializationError(error) } } - - private func backgroundQueue() -> DispatchQueue { - return DispatchQueue.global(qos: .utility) - } } From aff324cb959ef3bdca8b8a566c244de3f7b3e874 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 28 May 2026 18:26:49 -0400 Subject: [PATCH 59/71] review 1 --- .../Sources/RecaptchaTokenGenerator.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift b/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift index 1cba0a16..fce80c56 100644 --- a/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift +++ b/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift @@ -88,9 +88,13 @@ final class RecaptchaTokenGenerator { ) return Promise(fblPromise).then { result in - // Force cast is safe because the operation provider above returns a String - // (the token) fulfilled as AnyObject to satisfy FBLPromise interop. - result as! String + guard let token = result as? String else { + throw GACAppCheckErrorUtil + .error( + withFailureReason: "Unexpected result type from reCAPTCHA token exchange: \(type(of: result)). Expected String." + ) + } + return token } } } From 11b09cf3aa1d7fce81df937c36d32fab3b39bff6 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 28 May 2026 18:47:25 -0400 Subject: [PATCH 60/71] refactor(recaptcha): rename RecaptchaSDK to RecaptchaEnterpriseSDKLoader --- .../Sources/Public/AppCheckRecaptchaProvider.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift index ec8941d0..33731992 100644 --- a/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift +++ b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift @@ -51,7 +51,7 @@ public final class AppCheckRecaptchaProvider: NSObject, AppCheckCoreProvider { nil) { let tokenGenerator: RecaptchaTokenGenerator? - if let sdk = RecaptchaSDK(customAction: Self.appCheckActionName) { + if let sdk = RecaptchaEnterpriseSDKLoader(customAction: Self.appCheckActionName) { let backoffWrapper = GACAppCheckBackoffWrapper() tokenGenerator = RecaptchaTokenGenerator( @@ -122,7 +122,7 @@ public final class AppCheckRecaptchaProvider: NSObject, AppCheckCoreProvider { } } -private struct RecaptchaSDK { +private struct RecaptchaEnterpriseSDKLoader { // This symbol is specified in the RecaptchaEnterprise SDK. // See https://github.com/GoogleCloudPlatform/recaptcha-enterprise-mobile-sdk/blob/18.9.0/Sources/RecaptchaEnterprise/RecaptchaInteropBidings.swift private static let recaptchaActionClassName = "RecaptchaEnterprise.RCAAction" From 065572ab3fe66f6f4adcca61a59651886fd22fa9 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 29 May 2026 15:49:07 -0400 Subject: [PATCH 61/71] refactor, move out exception handling --- .../API/GACAppAttestAPIService.h | 6 +- .../API/GACAppAttestAPIService.m | 31 +++---- .../API/GACAppAttestAttestationResponse.m | 13 +-- .../Errors/GACAppAttestRejectionError.m | 2 +- .../AppAttestProvider/GACAppAttestProvider.m | 31 +++---- .../Storage/GACAppAttestArtifactStorage.m | 8 +- .../Storage/GACAppAttestKeyIDStorage.m | 4 +- .../Core/APIService/GACAppCheckAPIService.m | 26 +++--- .../APIService/GACAppCheckToken+APIResponse.h | 2 +- .../APIService/GACAppCheckToken+APIResponse.m | 13 +-- .../APIService/GACURLSessionDataResponse.m | 2 +- .../APIService/NSURLSession+GACPromises.h | 4 +- .../APIService/NSURLSession+GACPromises.m | 6 +- .../Core/Backoff/GACAppCheckBackoffWrapper.m | 8 +- .../Core/Errors/GACAppCheckErrorUtil.m | 2 +- AppCheckCore/Sources/Core/GACAppCheck.m | 8 +- .../Sources/Core/Storage/GACAppCheckStorage.m | 8 +- .../API/GACAppCheckDebugProviderAPIService.h | 6 +- .../API/GACAppCheckDebugProviderAPIService.m | 14 +-- .../DebugProvider/GACAppCheckDebugProvider.m | 10 +-- .../API/GACDeviceCheckAPIService.h | 6 +- .../API/GACDeviceCheckAPIService.m | 14 +-- .../GACDeviceCheckProvider.m | 22 ++--- .../Public/AppCheckCore/AppCheckCore.h | 4 + .../AppCheckCore/GACAppCheckAPIService.h | 50 +---------- .../AppCheckCore/GACAppCheckBackoffWrapper.h | 72 +-------------- .../AppCheckCore/GACAppCheckErrorUtil.h | 57 +----------- .../AppCheckCore/GACURLSessionDataResponse.h | 16 +--- .../AppCheckCore/_GACAppCheckAPIService.h | 68 ++++++++++++++ .../AppCheckCore/_GACAppCheckBackoffWrapper.h | 90 +++++++++++++++++++ .../AppCheckCore/_GACAppCheckErrorUtil.h | 75 ++++++++++++++++ .../AppCheckCore/_GACURLSessionDataResponse.h | 34 +++++++ .../GACDeviceCheckAPIServiceE2ETests.m | 10 +-- .../GACAppAttestAPIServiceTests.m | 66 +++++++------- .../GACAppAttestProviderTests.m | 18 ++-- .../GACAppAttestArtifactStorageTests.m | 8 +- .../Storage/GACAppAttestKeyIDStorageTests.m | 4 +- .../Unit/Core/GACAppCheckAPIServiceTests.m | 70 +++++++-------- .../Core/GACAppCheckBackoffWrapperTests.m | 10 +-- .../Tests/Unit/Core/GACAppCheckStorageTests.m | 8 +- .../Tests/Unit/Core/GACAppCheckTests.m | 4 +- .../GACAppCheckDebugProviderAPIServiceTests.m | 16 ++-- .../GACDeviceCheckAPIServiceTests.m | 16 ++-- .../GACDeviceCheckProviderTests.m | 6 +- .../GACAppCheckBackoffWrapperFake.h | 2 +- .../Public/AppCheckRecaptchaProvider.swift | 44 ++++----- .../Sources/RecaptchaAPIService.swift | 24 ++--- .../Sources/RecaptchaTokenGenerator.swift | 12 +-- .../AppCheckRecaptchaProviderTests.swift | 14 +++ .../Tests/MockRecaptchaSupport.swift | 14 +-- 50 files changed, 581 insertions(+), 477 deletions(-) create mode 100644 AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h create mode 100644 AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckBackoffWrapper.h create mode 100644 AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h create mode 100644 AppCheckCore/Sources/Public/AppCheckCore/_GACURLSessionDataResponse.h diff --git a/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.h b/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.h index 8b249721..dad0a1a4 100644 --- a/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.h +++ b/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.h @@ -19,7 +19,7 @@ @class FBLPromise; @class GACAppAttestAttestationResponse; @class GACAppCheckToken; -@protocol GACAppCheckAPIServiceProtocol; +@protocol _GACAppCheckAPIServiceProtocol; NS_ASSUME_NONNULL_BEGIN @@ -55,11 +55,11 @@ NS_ASSUME_NONNULL_BEGIN /// Default initializer. /// -/// @param APIService An instance implementing `GACAppCheckAPIServiceProtocol` to be used to send +/// @param APIService An instance implementing `_GACAppCheckAPIServiceProtocol` to be used to send /// network requests to the App Check backend. /// @param resourceName The name of the resource protected by App Check; for a Firebase App this is /// "projects/{project_id}/apps/{app_id}". -- (instancetype)initWithAPIService:(id)APIService +- (instancetype)initWithAPIService:(id<_GACAppCheckAPIServiceProtocol>)APIService resourceName:(NSString *)resourceName NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; diff --git a/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.m b/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.m index 68bb7350..04cf7043 100644 --- a/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.m +++ b/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.m @@ -24,8 +24,8 @@ #import "AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN @@ -46,7 +46,7 @@ @interface GACAppAttestAPIService () -@property(nonatomic, readonly) id APIService; +@property(nonatomic, readonly) id<_GACAppCheckAPIServiceProtocol> APIService; @property(nonatomic, readonly) NSString *resourceName; @@ -54,7 +54,7 @@ @interface GACAppAttestAPIService () @implementation GACAppAttestAPIService -- (instancetype)initWithAPIService:(id)APIService +- (instancetype)initWithAPIService:(id<_GACAppCheckAPIServiceProtocol>)APIService resourceName:(NSString *)resourceName { self = [super init]; if (self) { @@ -76,13 +76,13 @@ - (instancetype)initWithAPIService:(id)APIService challenge:challenge assertion:assertion limitedUse:limitedUse] - .then(^FBLPromise *(NSData *HTTPBody) { + .then(^FBLPromise<_GACURLSessionDataResponse *> *(NSData *HTTPBody) { return [self.APIService sendRequestWithURL:URL HTTPMethod:kHTTPMethodPost body:HTTPBody additionalHeaders:@{kContentTypeKey : kJSONContentType}]; }) - .then(^id _Nullable(GACURLSessionDataResponse *_Nullable response) { + .then(^id _Nullable(_GACURLSessionDataResponse *_Nullable response) { return [self.APIService appCheckTokenWithAPIResponse:response]; }); } @@ -99,14 +99,14 @@ - (instancetype)initWithAPIService:(id)APIService body:nil additionalHeaders:nil]; }] - .then(^id _Nullable(GACURLSessionDataResponse *_Nullable response) { + .then(^id _Nullable(_GACURLSessionDataResponse *_Nullable response) { return [self randomChallengeWithAPIResponse:response]; }); } #pragma mark - Challenge response parsing -- (FBLPromise *)randomChallengeWithAPIResponse:(GACURLSessionDataResponse *)response { +- (FBLPromise *)randomChallengeWithAPIResponse:(_GACURLSessionDataResponse *)response { return [FBLPromise onQueue:[self backgroundQueue] do:^id _Nullable { NSError *error; @@ -122,7 +122,7 @@ - (instancetype)initWithAPIService:(id)APIService - (nullable NSData *)randomChallengeFromResponseBody:(NSData *)response error:(NSError **)outError { if (response.length <= 0) { GACAppCheckSetErrorToPointer( - [GACAppCheckErrorUtil errorWithFailureReason:@"Empty server response body."], outError); + [_GACAppCheckErrorUtil errorWithFailureReason:@"Empty server response body."], outError); return nil; } @@ -132,14 +132,15 @@ - (nullable NSData *)randomChallengeFromResponseBody:(NSData *)response error:(N error:&JSONError]; if (![responseDict isKindOfClass:[NSDictionary class]]) { - GACAppCheckSetErrorToPointer([GACAppCheckErrorUtil JSONSerializationError:JSONError], outError); + GACAppCheckSetErrorToPointer([_GACAppCheckErrorUtil JSONSerializationError:JSONError], + outError); return nil; } NSString *challenge = responseDict[@"challenge"]; if (![challenge isKindOfClass:[NSString class]]) { GACAppCheckSetErrorToPointer( - [GACAppCheckErrorUtil appCheckTokenResponseErrorWithMissingField:@"challenge"], outError); + [_GACAppCheckErrorUtil appCheckTokenResponseErrorWithMissingField:@"challenge"], outError); return nil; } @@ -159,14 +160,14 @@ - (nullable NSData *)randomChallengeFromResponseBody:(NSData *)response error:(N keyID:keyID challenge:challenge limitedUse:limitedUse] - .then(^FBLPromise *(NSData *HTTPBody) { + .then(^FBLPromise<_GACURLSessionDataResponse *> *(NSData *HTTPBody) { return [self.APIService sendRequestWithURL:URL HTTPMethod:kHTTPMethodPost body:HTTPBody additionalHeaders:@{kContentTypeKey : kJSONContentType}]; }) .thenOn( - [self backgroundQueue], ^id _Nullable(GACURLSessionDataResponse *_Nullable URLResponse) { + [self backgroundQueue], ^id _Nullable(_GACURLSessionDataResponse *_Nullable URLResponse) { NSError *error; __auto_type response = @@ -186,7 +187,7 @@ - (nullable NSData *)randomChallengeFromResponseBody:(NSData *)response error:(N limitedUse:(BOOL)limitedUse { if (artifact.length <= 0 || challenge.length <= 0 || assertion.length <= 0) { FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; - [rejectedPromise reject:[GACAppCheckErrorUtil + [rejectedPromise reject:[_GACAppCheckErrorUtil errorWithFailureReason:@"Missing or empty request parameter."]]; return rejectedPromise; } @@ -210,7 +211,7 @@ - (nullable NSData *)randomChallengeFromResponseBody:(NSData *)response error:(N limitedUse:(BOOL)limitedUse { if (attestation.length <= 0 || keyID.length <= 0 || challenge.length <= 0) { FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; - [rejectedPromise reject:[GACAppCheckErrorUtil + [rejectedPromise reject:[_GACAppCheckErrorUtil errorWithFailureReason:@"Missing or empty request parameter."]]; return rejectedPromise; } @@ -237,7 +238,7 @@ - (nullable NSData *)randomChallengeFromResponseBody:(NSData *)response error:(N if (payloadJSON) { [HTTPBodyPromise fulfill:payloadJSON]; } else { - [HTTPBodyPromise reject:[GACAppCheckErrorUtil JSONSerializationError:encodingError]]; + [HTTPBodyPromise reject:[_GACAppCheckErrorUtil JSONSerializationError:encodingError]]; } return HTTPBodyPromise; } diff --git a/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.m b/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.m index eeb59635..906d5ae4 100644 --- a/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.m +++ b/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.m @@ -17,7 +17,7 @@ #import "AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.h" #import "AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" static NSString *const kResponseFieldAppCheckTokenDict = @"appCheckToken"; static NSString *const kResponseFieldArtifact = @"artifact"; @@ -38,7 +38,7 @@ - (nullable instancetype)initWithResponseData:(NSData *)response error:(NSError **)outError { if (response.length <= 0) { GACAppCheckSetErrorToPointer( - [GACAppCheckErrorUtil + [_GACAppCheckErrorUtil errorWithFailureReason: @"Failed to parse the initial handshake response. Empty server response body."], outError); @@ -51,14 +51,15 @@ - (nullable instancetype)initWithResponseData:(NSData *)response error:&JSONError]; if (![responseDict isKindOfClass:[NSDictionary class]]) { - GACAppCheckSetErrorToPointer([GACAppCheckErrorUtil JSONSerializationError:JSONError], outError); + GACAppCheckSetErrorToPointer([_GACAppCheckErrorUtil JSONSerializationError:JSONError], + outError); return nil; } NSString *artifactBase64String = responseDict[kResponseFieldArtifact]; if (![artifactBase64String isKindOfClass:[NSString class]]) { GACAppCheckSetErrorToPointer( - [GACAppCheckErrorUtil + [_GACAppCheckErrorUtil appAttestAttestationResponseErrorWithMissingField:kResponseFieldArtifact], outError); return nil; @@ -67,7 +68,7 @@ - (nullable instancetype)initWithResponseData:(NSData *)response options:0]; if (artifactData == nil) { GACAppCheckSetErrorToPointer( - [GACAppCheckErrorUtil + [_GACAppCheckErrorUtil appAttestAttestationResponseErrorWithMissingField:kResponseFieldArtifact], outError); return nil; @@ -76,7 +77,7 @@ - (nullable instancetype)initWithResponseData:(NSData *)response NSDictionary *appCheckTokenDict = responseDict[kResponseFieldAppCheckTokenDict]; if (![appCheckTokenDict isKindOfClass:[NSDictionary class]]) { GACAppCheckSetErrorToPointer( - [GACAppCheckErrorUtil + [_GACAppCheckErrorUtil appAttestAttestationResponseErrorWithMissingField:kResponseFieldAppCheckTokenDict], outError); return nil; diff --git a/AppCheckCore/Sources/AppAttestProvider/Errors/GACAppAttestRejectionError.m b/AppCheckCore/Sources/AppAttestProvider/Errors/GACAppAttestRejectionError.m index 7429af77..c637546d 100644 --- a/AppCheckCore/Sources/AppAttestProvider/Errors/GACAppAttestRejectionError.m +++ b/AppCheckCore/Sources/AppAttestProvider/Errors/GACAppAttestRejectionError.m @@ -18,7 +18,7 @@ #import "AppCheckCore/Sources/AppAttestProvider/Errors/GACAppAttestRejectionError.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" @implementation GACAppAttestRejectionError diff --git a/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m b/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m index 78a0cf73..df3d5f28 100644 --- a/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m +++ b/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m @@ -39,8 +39,8 @@ #import "AppCheckCore/Sources/AppAttestProvider/Errors/GACAppAttestRejectionError.h" #import "AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN @@ -108,7 +108,7 @@ @interface GACAppAttestProvider () @property(nonatomic, readonly) id appAttestService; @property(nonatomic, readonly) id keyIDStorage; @property(nonatomic, readonly) id artifactStorage; -@property(nonatomic, readonly) id backoffWrapper; +@property(nonatomic, readonly) id<_GACAppCheckBackoffWrapperProtocol> backoffWrapper; @property(nonatomic, nullable) FBLPromise *ongoingGetTokenOperation; @property(nonatomic, assign) BOOL ongoingGetTokenOperationLimitedUse; @@ -123,7 +123,7 @@ - (instancetype)initWithAppAttestService:(id)appAttestServi APIService:(id)APIService keyIDStorage:(id)keyIDStorage artifactStorage:(id)artifactStorage - backoffWrapper:(id)backoffWrapper { + backoffWrapper:(id<_GACAppCheckBackoffWrapperProtocol>)backoffWrapper { self = [super init]; if (self) { _appAttestService = appAttestService; @@ -151,11 +151,11 @@ - (instancetype)initWithServiceName:(NSString *)serviceName GACAppAttestKeyIDStorage *keyIDStorage = [[GACAppAttestKeyIDStorage alloc] initWithKeySuffix:storageKeySuffix]; - GACAppCheckAPIService *APIService = - [[GACAppCheckAPIService alloc] initWithURLSession:URLSession - baseURL:baseURL - APIKey:APIKey - requestHooks:requestHooks]; + _GACAppCheckAPIService *APIService = + [[_GACAppCheckAPIService alloc] initWithURLSession:URLSession + baseURL:baseURL + APIKey:APIKey + requestHooks:requestHooks]; GACAppAttestAPIService *appAttestAPIService = [[GACAppAttestAPIService alloc] initWithAPIService:APIService resourceName:resourceName]; @@ -164,7 +164,7 @@ - (instancetype)initWithServiceName:(NSString *)serviceName [[GACAppAttestArtifactStorage alloc] initWithKeySuffix:storageKeySuffix accessGroup:accessGroup]; - GACAppCheckBackoffWrapper *backoffWrapper = [[GACAppCheckBackoffWrapper alloc] init]; + _GACAppCheckBackoffWrapper *backoffWrapper = [[_GACAppCheckBackoffWrapper alloc] init]; return [self initWithAppAttestService:DCAppAttestService.sharedService APIService:appAttestAPIService @@ -341,9 +341,10 @@ - (void)getTokenWithLimitedUse:(BOOL)limitedUse completionHandler:handler]; }] .recoverOn(self.queue, ^id(NSError *error) { - return [GACAppCheckErrorUtil appAttestAttestKeyFailedWithError:error - keyId:keyID - clientDataHash:challengeHash]; + return + [_GACAppCheckErrorUtil appAttestAttestKeyFailedWithError:error + keyId:keyID + clientDataHash:challengeHash]; }); }) .thenOn(self.queue, ^FBLPromise *(NSData *attestation) { @@ -488,7 +489,7 @@ - (void)getTokenWithLimitedUse:(BOOL)limitedUse completionHandler:handler]; }] .recoverOn(self.queue, ^id(NSError *appAttestError) { - NSError *error = [GACAppCheckErrorUtil + NSError *error = [_GACAppCheckErrorUtil appAttestGenerateAssertionFailedWithError:appAttestError keyId:keyID clientDataHash:statementHash]; @@ -570,7 +571,7 @@ - (void)getTokenWithLimitedUse:(BOOL)limitedUse if (self.appAttestService.isSupported) { return [FBLPromise resolvedWith:[NSNull null]]; } else { - NSError *error = [GACAppCheckErrorUtil unsupportedAttestationProvider:@"AppAttestProvider"]; + NSError *error = [_GACAppCheckErrorUtil unsupportedAttestationProvider:@"AppAttestProvider"]; FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; [rejectedPromise reject:error]; return rejectedPromise; @@ -596,7 +597,7 @@ - (void)getTokenWithLimitedUse:(BOOL)limitedUse }] .recoverOn(self.queue, ^id(NSError *error) { - return [GACAppCheckErrorUtil appAttestGenerateKeyFailedWithError:error]; + return [_GACAppCheckErrorUtil appAttestGenerateKeyFailedWithError:error]; }) .thenOn(self.queue, ^FBLPromise *(NSString *keyID) { return [self.keyIDStorage setAppAttestKeyID:keyID]; diff --git a/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestArtifactStorage.m b/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestArtifactStorage.m index b4318524..1f91ea97 100644 --- a/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestArtifactStorage.m +++ b/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestArtifactStorage.m @@ -25,7 +25,7 @@ #import #import "AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestStoredArtifact.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN @@ -78,14 +78,14 @@ - (instancetype)initWithKeySuffix:(NSString *)keySuffix } }) .recover(^NSError *(NSError *error) { - return [GACAppCheckErrorUtil keychainErrorWithError:error]; + return [_GACAppCheckErrorUtil keychainErrorWithError:error]; }); } - (FBLPromise *)setArtifact:(nullable NSData *)artifact forKey:(nonnull NSString *)keyID { if (artifact) { return [self storeArtifact:artifact forKey:keyID].recover(^NSError *(NSError *error) { - return [GACAppCheckErrorUtil keychainErrorWithError:error]; + return [_GACAppCheckErrorUtil keychainErrorWithError:error]; }); } else { return [FBLPromise wrapErrorCompletion:^(FBLPromiseErrorCompletion _Nonnull handler) { @@ -97,7 +97,7 @@ - (instancetype)initWithKeySuffix:(NSString *)keySuffix return nil; }) .recover(^NSError *(NSError *error) { - return [GACAppCheckErrorUtil keychainErrorWithError:error]; + return [_GACAppCheckErrorUtil keychainErrorWithError:error]; }); } } diff --git a/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestKeyIDStorage.m b/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestKeyIDStorage.m index 87e04831..a87f6e07 100644 --- a/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestKeyIDStorage.m +++ b/AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestKeyIDStorage.m @@ -24,7 +24,7 @@ #import -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" /// The `GULUserDefaults` suite name for the storage location of the app attest key ID. static NSString *const kKeyIDStorageDefaultsSuiteName = @"com.firebase.GACAppAttestKeyIDStorage"; @@ -59,7 +59,7 @@ - (instancetype)initWithKeySuffix:(NSString *)keySuffix { if (appAttestKeyID) { return [FBLPromise resolvedWith:appAttestKeyID]; } else { - NSError *error = [GACAppCheckErrorUtil appAttestKeyIDNotFound]; + NSError *error = [_GACAppCheckErrorUtil appAttestKeyIDNotFound]; FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; [rejectedPromise reject:error]; return rejectedPromise; diff --git a/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m b/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m index 814c1543..d3162405 100644 --- a/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m +++ b/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m @@ -24,10 +24,10 @@ #import "AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.h" #import "AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.h" #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckLogger.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN @@ -36,7 +36,7 @@ static NSString *const kDefaultBaseURL = @"https://firebaseappcheck.googleapis.com/v1"; -@interface GACAppCheckAPIService () +@interface _GACAppCheckAPIService () @property(nonatomic, readonly) NSURLSession *URLSession; @property(nonatomic, readonly, nullable) NSString *APIKey; @@ -44,7 +44,7 @@ @interface GACAppCheckAPIService () @end -@implementation GACAppCheckAPIService +@implementation _GACAppCheckAPIService // Synthesize properties declared in a protocol. @synthesize baseURL = _baseURL; @@ -63,7 +63,7 @@ - (instancetype)initWithURLSession:(NSURLSession *)session return self; } -- (FBLPromise *) +- (FBLPromise<_GACURLSessionDataResponse *> *) sendRequestWithURL:(NSURL *)requestURL HTTPMethod:(NSString *)HTTPMethod body:(nullable NSData *)body @@ -75,7 +75,7 @@ - (instancetype)initWithURLSession:(NSURLSession *)session .then(^id _Nullable(NSURLRequest *_Nullable request) { return [self sendURLRequest:request]; }) - .then(^id _Nullable(GACURLSessionDataResponse *_Nullable response) { + .then(^id _Nullable(_GACURLSessionDataResponse *_Nullable response) { return [self validateHTTPResponseStatusCode:response]; }); } @@ -116,19 +116,19 @@ - (instancetype)initWithURLSession:(NSURLSession *)session }]; } -- (FBLPromise *)sendURLRequest:(NSURLRequest *)request { +- (FBLPromise<_GACURLSessionDataResponse *> *)sendURLRequest:(NSURLRequest *)request { return [self.URLSession gac_dataTaskPromiseWithRequest:request] .recover(^id(NSError *networkError) { // Wrap raw network error into App Check domain error. - return [GACAppCheckErrorUtil APIErrorWithNetworkError:networkError]; + return [_GACAppCheckErrorUtil APIErrorWithNetworkError:networkError]; }) - .then(^id _Nullable(GACURLSessionDataResponse *response) { + .then(^id _Nullable(_GACURLSessionDataResponse *response) { return [self validateHTTPResponseStatusCode:response]; }); } -- (FBLPromise *)validateHTTPResponseStatusCode: - (GACURLSessionDataResponse *)response { +- (FBLPromise<_GACURLSessionDataResponse *> *)validateHTTPResponseStatusCode: + (_GACURLSessionDataResponse *)response { NSInteger statusCode = response.HTTPResponse.statusCode; return [FBLPromise do:^id _Nullable { if (statusCode < 200 || statusCode >= 300) { @@ -137,15 +137,15 @@ - (instancetype)initWithURLSession:(NSURLSession *)session [[NSString alloc] initWithData:response.HTTPBody encoding:NSUTF8StringEncoding]]; GACAppCheckLogDebug(GACLoggerAppCheckMessageCodeUnexpectedHTTPCode, logMessage); - return [GACAppCheckErrorUtil APIErrorWithHTTPResponse:response.HTTPResponse - data:response.HTTPBody]; + return [_GACAppCheckErrorUtil APIErrorWithHTTPResponse:response.HTTPResponse + data:response.HTTPBody]; } return response; }]; } - (FBLPromise *)appCheckTokenWithAPIResponse: - (GACURLSessionDataResponse *)response { + (_GACURLSessionDataResponse *)response { return [FBLPromise onQueue:[self defaultQueue] do:^id _Nullable { NSError *error; diff --git a/AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.h b/AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.h index c4831a91..3298f05d 100644 --- a/AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.h +++ b/AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.h @@ -17,7 +17,7 @@ #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" @class FBLPromise; -@class GACURLSessionDataResponse; +@class _GACURLSessionDataResponse; NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.m b/AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.m index be907380..1ceba87b 100644 --- a/AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.m +++ b/AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.m @@ -22,7 +22,7 @@ #import "FBLPromises.h" #endif -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" static NSString *const kResponseFieldToken = @"token"; static NSString *const kResponseFieldTTL = @"ttl"; @@ -34,7 +34,7 @@ - (nullable instancetype)initWithTokenExchangeResponse:(NSData *)response error:(NSError **)outError { if (response.length <= 0) { GACAppCheckSetErrorToPointer( - [GACAppCheckErrorUtil errorWithFailureReason:@"Empty server response body."], outError); + [_GACAppCheckErrorUtil errorWithFailureReason:@"Empty server response body."], outError); return nil; } @@ -44,7 +44,8 @@ - (nullable instancetype)initWithTokenExchangeResponse:(NSData *)response error:&JSONError]; if (![responseDict isKindOfClass:[NSDictionary class]]) { - GACAppCheckSetErrorToPointer([GACAppCheckErrorUtil JSONSerializationError:JSONError], outError); + GACAppCheckSetErrorToPointer([_GACAppCheckErrorUtil JSONSerializationError:JSONError], + outError); return nil; } @@ -57,7 +58,7 @@ - (nullable instancetype)initWithResponseDict:(NSDictionary *)re NSString *token = responseDict[kResponseFieldToken]; if (![token isKindOfClass:[NSString class]]) { GACAppCheckSetErrorToPointer( - [GACAppCheckErrorUtil appCheckTokenResponseErrorWithMissingField:kResponseFieldToken], + [_GACAppCheckErrorUtil appCheckTokenResponseErrorWithMissingField:kResponseFieldToken], outError); return nil; } @@ -65,7 +66,7 @@ - (nullable instancetype)initWithResponseDict:(NSDictionary *)re NSString *timeToLiveString = responseDict[kResponseFieldTTL]; if (![token isKindOfClass:[NSString class]] || token.length <= 0) { GACAppCheckSetErrorToPointer( - [GACAppCheckErrorUtil appCheckTokenResponseErrorWithMissingField:kResponseFieldTTL], + [_GACAppCheckErrorUtil appCheckTokenResponseErrorWithMissingField:kResponseFieldTTL], outError); return nil; } @@ -77,7 +78,7 @@ - (nullable instancetype)initWithResponseDict:(NSDictionary *)re if (secondsToLive == 0) { GACAppCheckSetErrorToPointer( - [GACAppCheckErrorUtil appCheckTokenResponseErrorWithMissingField:kResponseFieldTTL], + [_GACAppCheckErrorUtil appCheckTokenResponseErrorWithMissingField:kResponseFieldTTL], outError); return nil; } diff --git a/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.m b/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.m index 51b61eae..77e976ee 100644 --- a/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.m +++ b/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.m @@ -16,7 +16,7 @@ #import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" -@implementation GACURLSessionDataResponse +@implementation _GACURLSessionDataResponse - (instancetype)initWithResponse:(NSHTTPURLResponse *)response HTTPBody:(NSData *)body { self = [super init]; diff --git a/AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.h b/AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.h index a79586c3..fb3a08f4 100644 --- a/AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.h +++ b/AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.h @@ -17,7 +17,7 @@ #import @class FBLPromise; -@class GACURLSessionDataResponse; +@class _GACURLSessionDataResponse; NS_ASSUME_NONNULL_BEGIN @@ -29,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN * @return A promise that is fulfilled when an HTTP response is received (with any response code), * or is rejected with the error passed to the task completion. */ -- (FBLPromise *)gac_dataTaskPromiseWithRequest: +- (FBLPromise<_GACURLSessionDataResponse *> *)gac_dataTaskPromiseWithRequest: (NSURLRequest *)URLRequest; @end diff --git a/AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.m b/AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.m index 377bf319..f9a79b03 100644 --- a/AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.m +++ b/AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.m @@ -22,11 +22,11 @@ #import "FBLPromises.h" #endif -#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACURLSessionDataResponse.h" @implementation NSURLSession (GACPromises) -- (FBLPromise *)gac_dataTaskPromiseWithRequest: +- (FBLPromise<_GACURLSessionDataResponse *> *)gac_dataTaskPromiseWithRequest: (NSURLRequest *)URLRequest { return [FBLPromise async:^(FBLPromiseFulfillBlock fulfill, FBLPromiseRejectBlock reject) { [[self dataTaskWithRequest:URLRequest @@ -35,7 +35,7 @@ @implementation NSURLSession (GACPromises) if (error) { reject(error); } else { - fulfill([[GACURLSessionDataResponse alloc] + fulfill([[_GACURLSessionDataResponse alloc] initWithResponse:(NSHTTPURLResponse *)response HTTPBody:data]); } diff --git a/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m b/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m index 626df8ec..1988c6b5 100644 --- a/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m +++ b/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m @@ -99,7 +99,7 @@ + (instancetype)nextRetryFailureWithFailure: @end -@interface GACAppCheckBackoffWrapper () +@interface _GACAppCheckBackoffWrapper () /// Current date provider. Is used instead of `+[NSDate date]` for testability. @property(nonatomic, readonly) GACAppCheckDateProvider dateProvider; @@ -109,10 +109,10 @@ @interface GACAppCheckBackoffWrapper () @end -@implementation GACAppCheckBackoffWrapper +@implementation _GACAppCheckBackoffWrapper - (instancetype)init { - return [self initWithDateProvider:[GACAppCheckBackoffWrapper currentDateProvider]]; + return [self initWithDateProvider:[_GACAppCheckBackoffWrapper currentDateProvider]]; } - (instancetype)initWithDateProvider:(GACAppCheckDateProvider)dateProvider { @@ -202,7 +202,7 @@ - (FBLPromise *)promiseWithRetryDisallowedError:(NSError *)error { NSString *reason = [NSString stringWithFormat:@"Too many attempts. Underlying error: %@", error.localizedDescription ?: error.localizedFailureReason]; - NSError *retryDisallowedError = [GACAppCheckErrorUtil errorWithFailureReason:reason]; + NSError *retryDisallowedError = [_GACAppCheckErrorUtil errorWithFailureReason:reason]; FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; [rejectedPromise reject:retryDisallowedError]; return rejectedPromise; diff --git a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m index a22a7322..914c17b4 100644 --- a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m +++ b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m @@ -28,7 +28,7 @@ @"The reCAPTCHA Enterprise SDK is not linked. See " @"https://cloud.google.com/recaptcha/docs/instrument-ios-apps#prepare-environment"; -@implementation GACAppCheckErrorUtil +@implementation _GACAppCheckErrorUtil + (NSError *)publicDomainErrorWithError:(NSError *)error { if ([error.domain isEqualToString:GACAppCheckErrorDomain]) { diff --git a/AppCheckCore/Sources/Core/GACAppCheck.m b/AppCheckCore/Sources/Core/GACAppCheck.m index e990efe9..4492e327 100644 --- a/AppCheckCore/Sources/Core/GACAppCheck.m +++ b/AppCheckCore/Sources/Core/GACAppCheck.m @@ -33,7 +33,7 @@ #import "AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.h" #import "AppCheckCore/Sources/Core/TokenRefresh/GACAppCheckTokenRefreshResult.h" #import "AppCheckCore/Sources/Core/TokenRefresh/GACAppCheckTokenRefresher.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN @@ -166,19 +166,19 @@ - (void)limitedUseTokenWithCompletion:(GACAppCheckTokenHandler)handler { - (FBLPromise *)getCachedValidTokenForcingRefresh:(BOOL)forcingRefresh { if (forcingRefresh) { FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; - [rejectedPromise reject:[GACAppCheckErrorUtil cachedTokenNotFound]]; + [rejectedPromise reject:[_GACAppCheckErrorUtil cachedTokenNotFound]]; return rejectedPromise; } return [self.storage getToken].then(^id(GACAppCheckToken *_Nullable token) { if (token == nil) { - return [GACAppCheckErrorUtil cachedTokenNotFound]; + return [_GACAppCheckErrorUtil cachedTokenNotFound]; } BOOL isTokenExpiredOrExpiresSoon = [token.expirationDate timeIntervalSinceNow] < kTokenExpirationThreshold; if (isTokenExpiredOrExpiresSoon) { - return [GACAppCheckErrorUtil cachedTokenExpired]; + return [_GACAppCheckErrorUtil cachedTokenExpired]; } return token; diff --git a/AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.m b/AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.m index c5ec970d..b26b52fb 100644 --- a/AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.m +++ b/AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.m @@ -25,7 +25,7 @@ #import #import "AppCheckCore/Sources/Core/Storage/GACAppCheckStoredToken+GACAppCheckToken.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN @@ -75,14 +75,14 @@ - (instancetype)initWithTokenKey:(NSString *)tokenKey accessGroup:(nullable NSSt } }) .recover(^NSError *(NSError *error) { - return [GACAppCheckErrorUtil keychainErrorWithError:error]; + return [_GACAppCheckErrorUtil keychainErrorWithError:error]; }); } - (FBLPromise *)setToken:(nullable GACAppCheckToken *)token { if (token) { return [self storeToken:token].recover(^NSError *(NSError *error) { - return [GACAppCheckErrorUtil keychainErrorWithError:error]; + return [_GACAppCheckErrorUtil keychainErrorWithError:error]; }); } else { return [FBLPromise wrapErrorCompletion:^(FBLPromiseErrorCompletion _Nonnull handler) { @@ -94,7 +94,7 @@ - (instancetype)initWithTokenKey:(NSString *)tokenKey accessGroup:(nullable NSSt return token; }) .recover(^NSError *(NSError *error) { - return [GACAppCheckErrorUtil keychainErrorWithError:error]; + return [_GACAppCheckErrorUtil keychainErrorWithError:error]; }); } } diff --git a/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.h b/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.h index ac4c5e75..a75ad75c 100644 --- a/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.h +++ b/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.h @@ -18,7 +18,7 @@ @class FBLPromise; @class GACAppCheckToken; -@protocol GACAppCheckAPIServiceProtocol; +@protocol _GACAppCheckAPIServiceProtocol; NS_ASSUME_NONNULL_BEGIN @@ -33,12 +33,12 @@ NS_ASSUME_NONNULL_BEGIN : NSObject /// Default initializer. -/// @param APIService An instance implementing `GACAppCheckAPIServiceProtocol` to be used to send +/// @param APIService An instance implementing `_GACAppCheckAPIServiceProtocol` to be used to send /// network requests to the App Check backend. /// @param resourceName The name of the resource protected by App Check; for a Firebase App this is /// "projects/{project_id}/apps/{app_id}". See https://google.aip.dev/122 for more details about /// resource names. -- (instancetype)initWithAPIService:(id)APIService +- (instancetype)initWithAPIService:(id<_GACAppCheckAPIServiceProtocol>)APIService resourceName:(NSString *)resourceName; @end diff --git a/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.m b/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.m index d4921ece..d7cd748d 100644 --- a/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.m +++ b/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.m @@ -26,7 +26,7 @@ #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN @@ -37,7 +37,7 @@ @interface GACAppCheckDebugProviderAPIService () -@property(nonatomic, readonly) id APIService; +@property(nonatomic, readonly) id<_GACAppCheckAPIServiceProtocol> APIService; @property(nonatomic, readonly) NSString *resourceName; @@ -45,7 +45,7 @@ @interface GACAppCheckDebugProviderAPIService () @implementation GACAppCheckDebugProviderAPIService -- (instancetype)initWithAPIService:(id)APIService +- (instancetype)initWithAPIService:(id<_GACAppCheckAPIServiceProtocol>)APIService resourceName:(NSString *)resourceName { self = [super init]; if (self) { @@ -64,13 +64,13 @@ - (instancetype)initWithAPIService:(id)APIService NSURL *URL = [NSURL URLWithString:URLString]; return [self HTTPBodyWithDebugToken:debugToken limitedUse:limitedUse] - .then(^FBLPromise *(NSData *HTTPBody) { + .then(^FBLPromise<_GACURLSessionDataResponse *> *(NSData *HTTPBody) { return [self.APIService sendRequestWithURL:URL HTTPMethod:@"POST" body:HTTPBody additionalHeaders:@{kContentTypeKey : kJSONContentType}]; }) - .then(^id _Nullable(GACURLSessionDataResponse *_Nullable response) { + .then(^id _Nullable(_GACURLSessionDataResponse *_Nullable response) { return [self.APIService appCheckTokenWithAPIResponse:response]; }); } @@ -82,7 +82,7 @@ - (instancetype)initWithAPIService:(id)APIService if (debugToken.length <= 0) { FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; [rejectedPromise - reject:[GACAppCheckErrorUtil errorWithFailureReason:@"Debug token must not be empty."]]; + reject:[_GACAppCheckErrorUtil errorWithFailureReason:@"Debug token must not be empty."]]; return rejectedPromise; } @@ -100,7 +100,7 @@ - (instancetype)initWithAPIService:(id)APIService if (payloadJSON != nil) { return payloadJSON; } else { - return [GACAppCheckErrorUtil JSONSerializationError:encodingError]; + return [_GACAppCheckErrorUtil JSONSerializationError:encodingError]; } }]; } diff --git a/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m b/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m index 38908969..854b4d84 100644 --- a/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m +++ b/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m @@ -60,11 +60,11 @@ - (instancetype)initWithServiceName:(NSString *)serviceName NSURLSession *URLSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]]; - GACAppCheckAPIService *APIService = - [[GACAppCheckAPIService alloc] initWithURLSession:URLSession - baseURL:baseURL - APIKey:APIKey - requestHooks:requestHooks]; + _GACAppCheckAPIService *APIService = + [[_GACAppCheckAPIService alloc] initWithURLSession:URLSession + baseURL:baseURL + APIKey:APIKey + requestHooks:requestHooks]; GACAppCheckDebugProviderAPIService *debugAPIService = [[GACAppCheckDebugProviderAPIService alloc] initWithAPIService:APIService diff --git a/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.h b/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.h index 079a208c..cbc4468e 100644 --- a/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.h +++ b/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.h @@ -18,7 +18,7 @@ @class FBLPromise; @class GACAppCheckToken; -@protocol GACAppCheckAPIServiceProtocol; +@protocol _GACAppCheckAPIServiceProtocol; NS_ASSUME_NONNULL_BEGIN @@ -32,12 +32,12 @@ NS_ASSUME_NONNULL_BEGIN @interface GACDeviceCheckAPIService : NSObject /// Default initializer. -/// @param APIService An instance implementing `GACAppCheckAPIServiceProtocol` to be used to send +/// @param APIService An instance implementing `_GACAppCheckAPIServiceProtocol` to be used to send /// network requests to the App Check backend. /// @param resourceName The name of the resource protected by App Check; for a Firebase App this is /// "projects/{project_id}/apps/{app_id}". See https://google.aip.dev/122 for more details about /// resource names. -- (instancetype)initWithAPIService:(id)APIService +- (instancetype)initWithAPIService:(id<_GACAppCheckAPIServiceProtocol>)APIService resourceName:(NSString *)resourceName; @end diff --git a/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.m b/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.m index 1dee9c92..9fa9c510 100644 --- a/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.m +++ b/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.m @@ -26,7 +26,7 @@ #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN @@ -37,7 +37,7 @@ @interface GACDeviceCheckAPIService () -@property(nonatomic, readonly) id APIService; +@property(nonatomic, readonly) id<_GACAppCheckAPIServiceProtocol> APIService; @property(nonatomic, readonly) NSString *resourceName; @@ -45,7 +45,7 @@ @interface GACDeviceCheckAPIService () @implementation GACDeviceCheckAPIService -- (instancetype)initWithAPIService:(id)APIService +- (instancetype)initWithAPIService:(id<_GACAppCheckAPIServiceProtocol>)APIService resourceName:(NSString *)resourceName { self = [super init]; if (self) { @@ -64,13 +64,13 @@ - (instancetype)initWithAPIService:(id)APIService NSURL *URL = [NSURL URLWithString:URLString]; return [self HTTPBodyWithDeviceToken:deviceToken limitedUse:limitedUse] - .then(^FBLPromise *(NSData *HTTPBody) { + .then(^FBLPromise<_GACURLSessionDataResponse *> *(NSData *HTTPBody) { return [self.APIService sendRequestWithURL:URL HTTPMethod:@"POST" body:HTTPBody additionalHeaders:@{kContentTypeKey : kJSONContentType}]; }) - .then(^id _Nullable(GACURLSessionDataResponse *_Nullable response) { + .then(^id _Nullable(_GACURLSessionDataResponse *_Nullable response) { return [self.APIService appCheckTokenWithAPIResponse:response]; }); } @@ -79,7 +79,7 @@ - (instancetype)initWithAPIService:(id)APIService limitedUse:(BOOL)limitedUse { if (deviceToken.length <= 0) { FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; - [rejectedPromise reject:[GACAppCheckErrorUtil + [rejectedPromise reject:[_GACAppCheckErrorUtil errorWithFailureReason:@"DeviceCheck token must not be empty."]]; return rejectedPromise; } @@ -100,7 +100,7 @@ - (instancetype)initWithAPIService:(id)APIService if (payloadJSON != nil) { return payloadJSON; } else { - return [GACAppCheckErrorUtil JSONSerializationError:encodingError]; + return [_GACAppCheckErrorUtil JSONSerializationError:encodingError]; } }]; } diff --git a/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m b/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m index ba61674a..1d14a4c4 100644 --- a/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m +++ b/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m @@ -31,19 +31,19 @@ #import "AppCheckCore/Sources/DeviceCheckProvider/DCDevice+GACDeviceCheckTokenGenerator.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN @interface GACDeviceCheckProvider () @property(nonatomic, readonly) id APIService; @property(nonatomic, readonly) id deviceTokenGenerator; -@property(nonatomic, readonly) id backoffWrapper; +@property(nonatomic, readonly) id<_GACAppCheckBackoffWrapperProtocol> backoffWrapper; - (instancetype)initWithAPIService:(id)APIService deviceTokenGenerator:(id)deviceTokenGenerator - backoffWrapper:(id)backoffWrapper + backoffWrapper:(id<_GACAppCheckBackoffWrapperProtocol>)backoffWrapper NS_DESIGNATED_INITIALIZER; @end @@ -52,7 +52,7 @@ @implementation GACDeviceCheckProvider - (instancetype)initWithAPIService:(id)APIService deviceTokenGenerator:(id)deviceTokenGenerator - backoffWrapper:(id)backoffWrapper { + backoffWrapper:(id<_GACAppCheckBackoffWrapperProtocol>)backoffWrapper { self = [super init]; if (self) { _APIService = APIService; @@ -63,7 +63,7 @@ - (instancetype)initWithAPIService:(id)APIServ } - (instancetype)initWithAPIService:(id)APIService { - GACAppCheckBackoffWrapper *backoffWrapper = [[GACAppCheckBackoffWrapper alloc] init]; + _GACAppCheckBackoffWrapper *backoffWrapper = [[_GACAppCheckBackoffWrapper alloc] init]; return [self initWithAPIService:APIService deviceTokenGenerator:[DCDevice currentDevice] backoffWrapper:backoffWrapper]; @@ -76,11 +76,11 @@ - (instancetype)initWithServiceName:(NSString *)serviceName NSURLSession *URLSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]]; - GACAppCheckAPIService *APIService = - [[GACAppCheckAPIService alloc] initWithURLSession:URLSession - baseURL:nil - APIKey:APIKey - requestHooks:requestHooks]; + _GACAppCheckAPIService *APIService = + [[_GACAppCheckAPIService alloc] initWithURLSession:URLSession + baseURL:nil + APIKey:APIKey + requestHooks:requestHooks]; GACDeviceCheckAPIService *deviceCheckAPIService = [[GACDeviceCheckAPIService alloc] initWithAPIService:APIService resourceName:resourceName]; @@ -146,7 +146,7 @@ - (void)getTokenWithLimitedUse:(BOOL)limitedUse if (self.deviceTokenGenerator.isSupported) { return [FBLPromise resolvedWith:[NSNull null]]; } else { - NSError *error = [GACAppCheckErrorUtil unsupportedAttestationProvider:@"DeviceCheckProvider"]; + NSError *error = [_GACAppCheckErrorUtil unsupportedAttestationProvider:@"DeviceCheckProvider"]; FBLPromise *rejectedPromise = [FBLPromise pendingPromise]; [rejectedPromise reject:error]; return rejectedPromise; diff --git a/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h b/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h index d97d0650..9de0aff4 100644 --- a/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h +++ b/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h @@ -37,3 +37,7 @@ #import "GACAppCheckBackoffWrapper.h" #import "GACAppCheckErrorUtil.h" #import "GACURLSessionDataResponse.h" +#import "_GACAppCheckAPIService.h" +#import "_GACAppCheckBackoffWrapper.h" +#import "_GACAppCheckErrorUtil.h" +#import "_GACURLSessionDataResponse.h" diff --git a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h index ba9bcf00..b60626be 100644 --- a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h +++ b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h @@ -14,52 +14,4 @@ * limitations under the License. */ -#import - -#import "GACAppCheckProvider.h" - -@class FBLPromise; -@class GACURLSessionDataResponse; -@class GACAppCheckToken; - -NS_ASSUME_NONNULL_BEGIN -NS_SWIFT_NAME(AppCheckCoreAPIServiceProtocol) -@protocol GACAppCheckAPIServiceProtocol - -@property(nonatomic, readonly) NSString *baseURL; - -- (FBLPromise *) - sendRequestWithURL:(NSURL *)requestURL - HTTPMethod:(NSString *)HTTPMethod - body:(nullable NSData *)body - additionalHeaders:(nullable NSDictionary *)additionalHeaders; - -- (FBLPromise *)appCheckTokenWithAPIResponse: - (GACURLSessionDataResponse *)response; - -@end -NS_SWIFT_NAME(AppCheckCoreAPIService) -@interface GACAppCheckAPIService : NSObject - -/** - * The default initializer. - * @param session The URL session used to make network requests. - * @param baseURL The base URL for the App Check service, e.g., - * `https://firebaseappcheck.googleapis.com/v1`. - * @param APIKey The Google Cloud Platform API key, if needed, or nil. - * @param requestHooks Hooks that will be invoked on requests through this service. - */ -- (instancetype)initWithURLSession:(NSURLSession *)session - baseURL:(nullable NSString *)baseURL - APIKey:(nullable NSString *)APIKey - requestHooks:(nullable NSArray *)requestHooks - NS_DESIGNATED_INITIALIZER; - -- (instancetype)init NS_UNAVAILABLE; - -- (FBLPromise *)appCheckTokenWithAPIResponse: - (GACURLSessionDataResponse *)response; - -@end - -NS_ASSUME_NONNULL_END +#import "_GACAppCheckAPIService.h" diff --git a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h index 8b34e298..4dd1c5b6 100644 --- a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h +++ b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h @@ -14,74 +14,4 @@ * limitations under the License. */ -#import - -@class FBLPromise; - -NS_ASSUME_NONNULL_BEGIN - -/// Backoff type. Backoff interval calculation depends on the type. -typedef NS_ENUM(NSUInteger, GACAppCheckBackoffType) { - /// No backoff. Another retry is allowed straight away. - GACAppCheckBackoffTypeNone, - - /// Next retry will be allowed in 1 day (24 hours) after the failure. - GACAppCheckBackoffType1Day, - - /// A small backoff interval that exponentially increases after each consequent failure. - GACAppCheckBackoffTypeExponential -}; - -/// Creates a promise for an operation to apply the backoff to. -typedef FBLPromise *_Nonnull (^GACAppCheckBackoffOperationProvider)(void); - -/// Converts an error to a backoff type. -typedef GACAppCheckBackoffType (^GACAppCheckBackoffErrorHandler)(NSError *error); - -/// A block returning a date. Is used instead of `+[NSDate date]` for better testability of logic -/// dependent on the current time. -typedef NSDate *_Nonnull (^GACAppCheckDateProvider)(void); - -/// Defines API for an object that conditionally applies backoff to a given operation based on the -/// history of previous operation failures. -@protocol GACAppCheckBackoffWrapperProtocol - -/// Conditionally applies backoff to the given operation. -/// @param operationProvider A block that returns a new promise. The block will be called only when -/// the operation is allowed. -/// NOTE: We cannot accept just a promise because the operation will be started once the -/// promise has been instantiated, so we need to have a way to instantiate the promise only -/// when the operation is good to go. The provider block is the way we use. -/// @param errorHandler A block that receives an operation error as an input and returns the -/// appropriate backoff type. `defaultErrorHandler` provides a default implementation for Firebase -/// services. -/// @return A promise that is either: -/// - a promise returned by the promise provider if no backoff is required -/// - rejected if the backoff is needed -- (FBLPromise *)applyBackoffToOperation:(GACAppCheckBackoffOperationProvider)operationProvider - errorHandler:(GACAppCheckBackoffErrorHandler)errorHandler; - -/// The default Firebase services error handler. It keeps track of network errors and -/// `GACAppCheckHTTPError.HTTPResponse.statusCode.statusCode` value to return the appropriate -/// backoff type for the standard Firebase App Check backend response codes. -- (GACAppCheckBackoffErrorHandler)defaultAppCheckProviderErrorHandler; - -@end - -/// Provides a backoff implementation. Keeps track of the operation successes and failures to either -/// create and perform the operation promise or fails with a backoff error when the backoff is -/// needed. -@interface GACAppCheckBackoffWrapper : NSObject - -/// Initializes the wrapper with `+[GACAppCheckBackoffWrapper currentDateProvider]`. -- (instancetype)init; - -- (instancetype)initWithDateProvider:(GACAppCheckDateProvider)dateProvider - NS_DESIGNATED_INITIALIZER; - -/// A date provider that returns `+[NSDate date]`. -+ (GACAppCheckDateProvider)currentDateProvider; - -@end - -NS_ASSUME_NONNULL_END +#import "_GACAppCheckBackoffWrapper.h" diff --git a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h index d1f27757..1039cbf0 100644 --- a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h +++ b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h @@ -14,59 +14,4 @@ * limitations under the License. */ -#import - -@class GACAppCheckHTTPError; - -NS_ASSUME_NONNULL_BEGIN - -extern NSString *const kGACAppCheckMissingRecaptchaSDKMessage NS_SWIFT_NAME(missingRecaptchaSDKMessage); - -void GACAppCheckSetErrorToPointer(NSError *error, NSError **pointer); - -@interface GACAppCheckErrorUtil : NSObject - -+ (NSError *)publicDomainErrorWithError:(NSError *)error; - -// MARK: - Internal errors - -+ (NSError *)cachedTokenNotFound; - -+ (NSError *)cachedTokenExpired; - -+ (NSError *)keychainErrorWithError:(NSError *)error; - -+ (GACAppCheckHTTPError *)APIErrorWithHTTPResponse:(NSHTTPURLResponse *)HTTPResponse - data:(nullable NSData *)data; - -+ (NSError *)APIErrorWithNetworkError:(NSError *)networkError; - -+ (NSError *)appCheckTokenResponseErrorWithMissingField:(NSString *)fieldName; - -+ (NSError *)appAttestAttestationResponseErrorWithMissingField:(NSString *)fieldName; - -+ (NSError *)JSONSerializationError:(NSError *)error; - -+ (NSError *)errorWithFailureReason:(NSString *)failureReason; - -+ (NSError *)unsupportedAttestationProvider:(NSString *)providerName; - -+ (NSError *)missingRecaptchaSDKError; - -// MARK: - App Attest Errors - -+ (NSError *)appAttestKeyIDNotFound; - -+ (NSError *)appAttestGenerateKeyFailedWithError:(NSError *)error; - -+ (NSError *)appAttestAttestKeyFailedWithError:(NSError *)error - keyId:(NSString *)keyId - clientDataHash:(NSData *)clientDataHash; - -+ (NSError *)appAttestGenerateAssertionFailedWithError:(NSError *)error - keyId:(NSString *)keyId - clientDataHash:(NSData *)clientDataHash; - -@end - -NS_ASSUME_NONNULL_END +#import "_GACAppCheckErrorUtil.h" diff --git a/AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h b/AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h index e5482d45..b77abdfb 100644 --- a/AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h +++ b/AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h @@ -14,18 +14,4 @@ * limitations under the License. */ -#import - -NS_ASSUME_NONNULL_BEGIN - -/** The class represents HTTP response received from `NSURLSession`. */ -@interface GACURLSessionDataResponse : NSObject - -@property(nonatomic, readonly) NSHTTPURLResponse *HTTPResponse; -@property(nonatomic, nullable, readonly) NSData *HTTPBody; - -- (instancetype)initWithResponse:(NSHTTPURLResponse *)response HTTPBody:(nullable NSData *)body; - -@end - -NS_ASSUME_NONNULL_END +#import "_GACURLSessionDataResponse.h" diff --git a/AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h b/AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h new file mode 100644 index 00000000..5ff0f27f --- /dev/null +++ b/AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import "GACAppCheckProvider.h" + +@class FBLPromise; +@class _GACURLSessionDataResponse; +@class GACAppCheckToken; + +NS_ASSUME_NONNULL_BEGIN + +// This header is for internal use within Google SDKs (Firebase, Google Sign-In). +// It is not intended for use by external developers and may change without notice. + +@protocol _GACAppCheckAPIServiceProtocol + +@property(nonatomic, readonly) NSString *baseURL; + +- (FBLPromise<_GACURLSessionDataResponse *> *) + sendRequestWithURL:(NSURL *)requestURL + HTTPMethod:(NSString *)HTTPMethod + body:(nullable NSData *)body + additionalHeaders:(nullable NSDictionary *)additionalHeaders; + +- (FBLPromise *)appCheckTokenWithAPIResponse: + (_GACURLSessionDataResponse *)response; + +@end + +@interface _GACAppCheckAPIService : NSObject <_GACAppCheckAPIServiceProtocol> + +/** + * The default initializer. + * @param session The URL session used to make network requests. + * @param baseURL The base URL for the App Check service, e.g., + * `https://firebaseappcheck.googleapis.com/v1`. + * @param APIKey The Google Cloud Platform API key, if needed, or nil. + * @param requestHooks Hooks that will be invoked on requests through this service. + */ +- (instancetype)initWithURLSession:(NSURLSession *)session + baseURL:(nullable NSString *)baseURL + APIKey:(nullable NSString *)APIKey + requestHooks:(nullable NSArray *)requestHooks + NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +- (FBLPromise *)appCheckTokenWithAPIResponse: + (_GACURLSessionDataResponse *)response; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckBackoffWrapper.h b/AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckBackoffWrapper.h new file mode 100644 index 00000000..4fc5c0e8 --- /dev/null +++ b/AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckBackoffWrapper.h @@ -0,0 +1,90 @@ +/* + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +@class FBLPromise; + +NS_ASSUME_NONNULL_BEGIN + +// This header is for internal use within Google SDKs (Firebase, Google Sign-In). +// It is not intended for use by external developers and may change without notice. + +/// Backoff type. Backoff interval calculation depends on the type. +typedef NS_ENUM(NSUInteger, GACAppCheckBackoffType) { + /// No backoff. Another retry is allowed straight away. + GACAppCheckBackoffTypeNone, + + /// Next retry will be allowed in 1 day (24 hours) after the failure. + GACAppCheckBackoffType1Day, + + /// A small backoff interval that exponentially increases after each consequent failure. + GACAppCheckBackoffTypeExponential +}; + +/// Creates a promise for an operation to apply the backoff to. +typedef FBLPromise *_Nonnull (^GACAppCheckBackoffOperationProvider)(void); + +/// Converts an error to a backoff type. +typedef GACAppCheckBackoffType (^GACAppCheckBackoffErrorHandler)(NSError *error); + +/// A block returning a date. Is used instead of `+[NSDate date]` for better testability of logic +/// dependent on the current time. +typedef NSDate *_Nonnull (^GACAppCheckDateProvider)(void); + +/// Defines API for an object that conditionally applies backoff to a given operation based on the +/// history of previous operation failures. +@protocol _GACAppCheckBackoffWrapperProtocol + +/// Conditionally applies backoff to the given operation. +/// @param operationProvider A block that returns a new promise. The block will be called only when +/// the operation is allowed. +/// NOTE: We cannot accept just a promise because the operation will be started once the +/// promise has been instantiated, so we need to have a way to instantiate the promise only +/// when the operation is good to go. The provider block is the way we use. +/// @param errorHandler A block that receives an operation error as an input and returns the +/// appropriate backoff type. `defaultErrorHandler` provides a default implementation for Firebase +/// services. +/// @return A promise that is either: +/// - a promise returned by the promise provider if no backoff is required +/// - rejected if the backoff is needed +- (FBLPromise *)applyBackoffToOperation:(GACAppCheckBackoffOperationProvider)operationProvider + errorHandler:(GACAppCheckBackoffErrorHandler)errorHandler; + +/// The default Firebase services error handler. It keeps track of network errors and +/// `GACAppCheckHTTPError.HTTPResponse.statusCode.statusCode` value to return the appropriate +/// backoff type for the standard Firebase App Check backend response codes. +- (GACAppCheckBackoffErrorHandler)defaultAppCheckProviderErrorHandler; + +@end + +/// Provides a backoff implementation. Keeps track of the operation successes and failures to either +/// create and perform the operation promise or fails with a backoff error when the backoff is +/// needed. +@interface _GACAppCheckBackoffWrapper : NSObject <_GACAppCheckBackoffWrapperProtocol> + +/// Initializes the wrapper with `+[GACAppCheckBackoffWrapper currentDateProvider]`. +- (instancetype)init; + +- (instancetype)initWithDateProvider:(GACAppCheckDateProvider)dateProvider + NS_DESIGNATED_INITIALIZER; + +/// A date provider that returns `+[NSDate date]`. ++ (GACAppCheckDateProvider)currentDateProvider; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h b/AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h new file mode 100644 index 00000000..811cf0ed --- /dev/null +++ b/AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h @@ -0,0 +1,75 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +@class GACAppCheckHTTPError; + +NS_ASSUME_NONNULL_BEGIN + +// This header is for internal use within Google SDKs (Firebase, Google Sign-In). +// It is not intended for use by external developers and may change without notice. + +extern NSString *const kGACAppCheckMissingRecaptchaSDKMessage NS_SWIFT_NAME(missingRecaptchaSDKMessage); + +void GACAppCheckSetErrorToPointer(NSError *error, NSError **pointer); + +@interface _GACAppCheckErrorUtil : NSObject + ++ (NSError *)publicDomainErrorWithError:(NSError *)error; + +// MARK: - Internal errors + ++ (NSError *)cachedTokenNotFound; + ++ (NSError *)cachedTokenExpired; + ++ (NSError *)keychainErrorWithError:(NSError *)error; + ++ (GACAppCheckHTTPError *)APIErrorWithHTTPResponse:(NSHTTPURLResponse *)HTTPResponse + data:(nullable NSData *)data; + ++ (NSError *)APIErrorWithNetworkError:(NSError *)networkError; + ++ (NSError *)appCheckTokenResponseErrorWithMissingField:(NSString *)fieldName; + ++ (NSError *)appAttestAttestationResponseErrorWithMissingField:(NSString *)fieldName; + ++ (NSError *)JSONSerializationError:(NSError *)error; + ++ (NSError *)errorWithFailureReason:(NSString *)failureReason; + ++ (NSError *)unsupportedAttestationProvider:(NSString *)providerName; + ++ (NSError *)missingRecaptchaSDKError; + +// MARK: - App Attest Errors + ++ (NSError *)appAttestKeyIDNotFound; + ++ (NSError *)appAttestGenerateKeyFailedWithError:(NSError *)error; + ++ (NSError *)appAttestAttestKeyFailedWithError:(NSError *)error + keyId:(NSString *)keyId + clientDataHash:(NSData *)clientDataHash; + ++ (NSError *)appAttestGenerateAssertionFailedWithError:(NSError *)error + keyId:(NSString *)keyId + clientDataHash:(NSData *)clientDataHash; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AppCheckCore/Sources/Public/AppCheckCore/_GACURLSessionDataResponse.h b/AppCheckCore/Sources/Public/AppCheckCore/_GACURLSessionDataResponse.h new file mode 100644 index 00000000..07aeb335 --- /dev/null +++ b/AppCheckCore/Sources/Public/AppCheckCore/_GACURLSessionDataResponse.h @@ -0,0 +1,34 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +// This header is for internal use within Google SDKs (Firebase, Google Sign-In). +// It is not intended for use by external developers and may change without notice. + +/** The class represents HTTP response received from `NSURLSession`. */ +@interface _GACURLSessionDataResponse : NSObject + +@property(nonatomic, readonly) NSHTTPURLResponse *HTTPResponse; +@property(nonatomic, nullable, readonly) NSData *HTTPBody; + +- (instancetype)initWithResponse:(NSHTTPURLResponse *)response HTTPBody:(nullable NSData *)body; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AppCheckCore/Tests/Integration/GACDeviceCheckAPIServiceE2ETests.m b/AppCheckCore/Tests/Integration/GACDeviceCheckAPIServiceE2ETests.m index 23bae1d3..f9e0143b 100644 --- a/AppCheckCore/Tests/Integration/GACDeviceCheckAPIServiceE2ETests.m +++ b/AppCheckCore/Tests/Integration/GACDeviceCheckAPIServiceE2ETests.m @@ -38,7 +38,7 @@ @interface GACDeviceCheckAPIServiceE2ETests : XCTestCase @property(nonatomic) GACDeviceCheckAPIService *deviceCheckAPIService; -@property(nonatomic) GACAppCheckAPIService *APIService; +@property(nonatomic) _GACAppCheckAPIService *APIService; @property(nonatomic) NSURLSession *URLSession; @end @@ -50,10 +50,10 @@ - (void)setUp { self.URLSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; - self.APIService = [[GACAppCheckAPIService alloc] initWithURLSession:self.URLSession - baseURL:nil - APIKey:nil - requestHooks:nil]; + self.APIService = [[_GACAppCheckAPIService alloc] initWithURLSession:self.URLSession + baseURL:nil + APIKey:nil + requestHooks:nil]; self.deviceCheckAPIService = [[GACDeviceCheckAPIService alloc] initWithAPIService:self.APIService resourceName:kResourceName]; } diff --git a/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestAPIServiceTests.m b/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestAPIServiceTests.m index 04538d52..5212adb1 100644 --- a/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestAPIServiceTests.m +++ b/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestAPIServiceTests.m @@ -22,11 +22,11 @@ #import "AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.h" #import "AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.h" #import "AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACURLSessionDataResponse.h" #import "AppCheckCore/Tests/Unit/Utils/GACFixtureLoader.h" #import "AppCheckCore/Tests/Utils/Date/GACDateTestUtils.h" @@ -48,7 +48,7 @@ @implementation GACAppAttestAPIServiceTests - (void)setUp { [super setUp]; - self.mockAPIService = OCMProtocolMock(@protocol(GACAppCheckAPIServiceProtocol)); + self.mockAPIService = OCMProtocolMock(@protocol(_GACAppCheckAPIServiceProtocol)); OCMStub([self.mockAPIService baseURL]).andReturn(kBaseURL); self.appAttestAPIService = [[GACAppAttestAPIService alloc] initWithAPIService:self.mockAPIService @@ -68,8 +68,8 @@ - (void)tearDown { - (void)testGetRandomChallengeWhenAPIResponseValid { // 1. Prepare API response. NSData *responseBody = [GACFixtureLoader loadFixtureNamed:@"AppAttestResponseSuccess.json"]; - GACURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 - responseBody:responseBody]; + _GACURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; // 2. Stub API Service Request to return prepared API response. [self stubMockAPIServiceRequestForChallengeRequestWithResponse:validAPIResponse]; @@ -93,11 +93,11 @@ - (void)testGetRandomChallengeWhenAPIError { // 1. Prepare API response. NSString *responseBodyString = @"Generate challenge failed with invalid format."; NSData *responseBody = [responseBodyString dataUsingEncoding:NSUTF8StringEncoding]; - GACURLSessionDataResponse *invalidAPIResponse = [self APIResponseWithCode:300 - responseBody:responseBody]; + _GACURLSessionDataResponse *invalidAPIResponse = [self APIResponseWithCode:300 + responseBody:responseBody]; GACAppCheckHTTPError *APIError = - [GACAppCheckErrorUtil APIErrorWithHTTPResponse:invalidAPIResponse.HTTPResponse - data:invalidAPIResponse.HTTPBody]; + [_GACAppCheckErrorUtil APIErrorWithHTTPResponse:invalidAPIResponse.HTTPResponse + data:invalidAPIResponse.HTTPBody]; // 2. Stub API Service Request to return prepared API response. [self stubMockAPIServiceRequestForChallengeRequestWithResponse:APIError]; @@ -123,8 +123,8 @@ - (void)testGetRandomChallengeWhenAPIError { - (void)testGetRandomChallengeWhenAPIResponseEmpty { // 1. Prepare API response. NSData *responseBody = [NSData data]; - GACURLSessionDataResponse *emptyAPIResponse = [self APIResponseWithCode:200 - responseBody:responseBody]; + _GACURLSessionDataResponse *emptyAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; // 2. Stub API Service Request to return prepared API response. [self stubMockAPIServiceRequestForChallengeRequestWithResponse:emptyAPIResponse]; @@ -146,8 +146,8 @@ - (void)testGetRandomChallengeWhenAPIResponseInvalidFormat { // 1. Prepare API response. NSString *responseBodyString = @"Generate challenge failed with invalid format."; NSData *responseBody = [responseBodyString dataUsingEncoding:NSUTF8StringEncoding]; - GACURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 - responseBody:responseBody]; + _GACURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; // 2. Stub API Service Request to return prepared API response. [self stubMockAPIServiceRequestForChallengeRequestWithResponse:validAPIResponse]; @@ -174,8 +174,8 @@ - (void)assertMissingFieldErrorWithFixture:(NSString *)fixtureName missingField:(NSString *)fieldName { // 1. Prepare API response. NSData *missingFieldBody = [GACFixtureLoader loadFixtureNamed:fixtureName]; - GACURLSessionDataResponse *incompleteAPIResponse = [self APIResponseWithCode:200 - responseBody:missingFieldBody]; + _GACURLSessionDataResponse *incompleteAPIResponse = [self APIResponseWithCode:200 + responseBody:missingFieldBody]; // 2. Stub API Service Request to return prepared API response. [self stubMockAPIServiceRequestForChallengeRequestWithResponse:incompleteAPIResponse]; @@ -216,8 +216,8 @@ - (void)testGetAppCheckTokenSuccessWithLimitedUse:(BOOL)limitedUse { // 1. Prepare response. NSData *responseBody = [GACFixtureLoader loadFixtureNamed:@"FACTokenExchangeResponseSuccess.json"]; - GACURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 - responseBody:responseBody]; + _GACURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; // 2. Stub API Service // 2.1. Return prepared response. @@ -260,8 +260,8 @@ - (void)testGetAppCheckTokenNetworkError { // 1. Prepare response. NSData *responseBody = [GACFixtureLoader loadFixtureNamed:@"FACTokenExchangeResponseSuccess.json"]; - GACURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 - responseBody:responseBody]; + _GACURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; // 2. Stub API Service // 2.1. Return prepared response. @@ -296,8 +296,8 @@ - (void)testGetAppCheckTokenUnexpectedResponse { // 1. Prepare response. NSData *responseBody = [GACFixtureLoader loadFixtureNamed:@"DeviceCheckResponseMissingToken.json"]; - GACURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 - responseBody:responseBody]; + _GACURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; // 2. Stub API Service // 2.1. Return prepared response. @@ -343,8 +343,8 @@ - (void)testAttestKeySuccessWithLimitedUse:(BOOL)limitedUse { // 1. Prepare response. NSData *responseBody = [GACFixtureLoader loadFixtureNamed:@"AppAttestAttestationResponseSuccess.json"]; - GACURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 - responseBody:responseBody]; + _GACURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; // 2. Stub API Service // 2.1. Return prepared response. @@ -418,8 +418,8 @@ - (void)testAttestKeyUnexpectedResponse { // 1. Prepare unexpected response. NSData *responseBody = [GACFixtureLoader loadFixtureNamed:@"FACTokenExchangeResponseSuccess.json"]; - GACURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 - responseBody:responseBody]; + _GACURLSessionDataResponse *validAPIResponse = [self APIResponseWithCode:200 + responseBody:responseBody]; // 2. Stub API Service // 2.1. Return prepared response. @@ -448,12 +448,12 @@ - (void)testAttestKeyUnexpectedResponse { #pragma mark - Helpers -- (GACURLSessionDataResponse *)APIResponseWithCode:(NSInteger)code - responseBody:(NSData *)responseBody { +- (_GACURLSessionDataResponse *)APIResponseWithCode:(NSInteger)code + responseBody:(NSData *)responseBody { XCTAssertNotNil(responseBody); NSHTTPURLResponse *HTTPResponse = [GACURLSessionOCMockStub HTTPResponseWithCode:code]; - GACURLSessionDataResponse *APIResponse = - [[GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody]; + _GACURLSessionDataResponse *APIResponse = + [[_GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody]; return APIResponse; } @@ -493,7 +493,7 @@ - (void)expectTokenAPIRequestWithArtifact:(NSData *)attestation challenge:(NSData *)challenge assertion:(NSData *)assertion limitedUse:(BOOL)limitedUse - response:(nullable GACURLSessionDataResponse *)response + response:(nullable _GACURLSessionDataResponse *)response error:(nullable NSError *)error { id URLValidationArg = [self URLValidationArgumentWithCustomMethod:@"exchangeAppAttestAssertion"]; @@ -549,7 +549,7 @@ - (void)expectTokenAPIRequestWithArtifact:(NSData *)attestation .andReturn(responsePromise); } -- (void)expectTokenWithAPIReponse:(nonnull GACURLSessionDataResponse *)response +- (void)expectTokenWithAPIReponse:(nonnull _GACURLSessionDataResponse *)response toReturnToken:(nullable GACAppCheckToken *)token { FBLPromise *tokenPromise = [FBLPromise pendingPromise]; if (token) { @@ -565,7 +565,7 @@ - (void)expectAttestAPIRequestWithAttestation:(NSData *)attestation keyID:(NSString *)keyID challenge:(NSData *)challenge limitedUse:(BOOL)limitedUse - response:(nullable GACURLSessionDataResponse *)response + response:(nullable _GACURLSessionDataResponse *)response error:(nullable NSError *)error { id URLValidationArg = [self URLValidationArgumentWithCustomMethod:@"exchangeAppAttestAttestation"]; diff --git a/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestProviderTests.m b/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestProviderTests.m index 5684e7de..377ad606 100644 --- a/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestProviderTests.m +++ b/AppCheckCore/Tests/Unit/AppAttestProvider/GACAppAttestProviderTests.m @@ -32,7 +32,7 @@ #import "AppCheckCore/Sources/AppAttestProvider/Errors/GACAppAttestRejectionError.h" #import "AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" #import "AppCheckCore/Tests/Utils/AppCheckBackoffWrapperFake/GACAppCheckBackoffWrapperFake.h" @@ -42,7 +42,7 @@ - (instancetype)initWithAppAttestService:(id)appAttestServi APIService:(id)APIService keyIDStorage:(id)keyIDStorage artifactStorage:(id)artifactStorage - backoffWrapper:(id)backoffWrapper; + backoffWrapper:(id<_GACAppCheckBackoffWrapperProtocol>)backoffWrapper; @end GAC_APP_ATTEST_PROVIDER_AVAILABILITY @@ -101,7 +101,7 @@ - (void)tearDown { - (void)testGetTokenWhenAppAttestIsNotSupported { NSError *expectedError = - [GACAppCheckErrorUtil unsupportedAttestationProvider:@"AppAttestProvider"]; + [_GACAppCheckErrorUtil unsupportedAttestationProvider:@"AppAttestProvider"]; // 0.1. Expect backoff wrapper to be used. self.fakeBackoffWrapper.backoffExpectation = [self expectationWithDescription:@"Backoff"]; @@ -317,9 +317,9 @@ - (void)testGetToken_WhenUnregisteredKeyAndKeyAttestationError { code:0 userInfo:nil]; NSError *expectedError = - [GACAppCheckErrorUtil appAttestAttestKeyFailedWithError:attestationError - keyId:existingKeyID - clientDataHash:self.randomChallengeHash]; + [_GACAppCheckErrorUtil appAttestAttestKeyFailedWithError:attestationError + keyId:existingKeyID + clientDataHash:self.randomChallengeHash]; id attestCompletionArg = [OCMArg invokeBlockWithArgs:[NSNull null], attestationError, nil]; OCMExpect([self.mockAppAttestService attestKey:existingKeyID clientDataHash:self.randomChallengeHash @@ -651,9 +651,9 @@ - (void)testGetToken_WhenKeyRegisteredAndGenerateAssertionError { [statementForAssertion appendData:self.randomChallenge]; NSData *clientDataHash = [GACAppCheckCryptoUtils sha256HashFromData:[statementForAssertion copy]]; NSError *expectedError = - [GACAppCheckErrorUtil appAttestGenerateAssertionFailedWithError:generateAssertionError - keyId:existingKeyID - clientDataHash:clientDataHash]; + [_GACAppCheckErrorUtil appAttestGenerateAssertionFailedWithError:generateAssertionError + keyId:existingKeyID + clientDataHash:clientDataHash]; id completionBlockArg = [OCMArg invokeBlockWithArgs:[NSNull null], generateAssertionError, nil]; OCMExpect([self.mockAppAttestService generateAssertion:existingKeyID diff --git a/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestArtifactStorageTests.m b/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestArtifactStorageTests.m index fa79f154..c755b555 100644 --- a/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestArtifactStorageTests.m +++ b/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestArtifactStorageTests.m @@ -34,7 +34,7 @@ #import "FBLPromise+Testing.h" #import "AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestArtifactStorage.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" static NSString *const kAppName = @"GACAppAttestArtifactStorageTests"; static NSString *const kAppID = @"1:100000000000:ios:aaaaaaaaaaaaaaaaaaaaaaaa"; @@ -152,7 +152,7 @@ - (void)testGetArtifact_KeychainError { XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertNotNil(getPromise.error); XCTAssertEqualObjects(getPromise.error, - [GACAppCheckErrorUtil keychainErrorWithError:gulsKeychainError]); + [_GACAppCheckErrorUtil keychainErrorWithError:gulsKeychainError]); // 4. Verify storage mock. OCMVerifyAll(mockKeychainStorage); @@ -179,7 +179,7 @@ - (void)testSetArtifact_KeychainError { XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertNotNil(setPromise.error); XCTAssertEqualObjects(setPromise.error, - [GACAppCheckErrorUtil keychainErrorWithError:gulsKeychainError]); + [_GACAppCheckErrorUtil keychainErrorWithError:gulsKeychainError]); // 4. Verify storage mock. OCMVerifyAll(mockKeychainStorage); @@ -205,7 +205,7 @@ - (void)testRemoveArtifact_KeychainError { XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertNotNil(setPromise.error); XCTAssertEqualObjects(setPromise.error, - [GACAppCheckErrorUtil keychainErrorWithError:gulsKeychainError]); + [_GACAppCheckErrorUtil keychainErrorWithError:gulsKeychainError]); // 4. Verify storage mock. OCMVerifyAll(mockKeychainStorage); diff --git a/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestKeyIDStorageTests.m b/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestKeyIDStorageTests.m index a0a74082..17cd5c2e 100644 --- a/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestKeyIDStorageTests.m +++ b/AppCheckCore/Tests/Unit/AppAttestProvider/Storage/GACAppAttestKeyIDStorageTests.m @@ -20,7 +20,7 @@ #import "AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestKeyIDStorage.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" static NSString *const kAppName = @"GACAppAttestKeyIDStorageTestsApp"; static NSString *const kAppID = @"app_id"; @@ -77,7 +77,7 @@ - (void)testGetAppAttestKeyID_WhenAppAttestKeyIDNotFoundError { __auto_type getPromise = [self.storage getAppAttestKeyID]; XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertNotNil(getPromise.error); - XCTAssertEqualObjects(getPromise.error, [GACAppCheckErrorUtil appAttestKeyIDNotFound]); + XCTAssertEqualObjects(getPromise.error, [_GACAppCheckErrorUtil appAttestKeyIDNotFound]); } - (void)testSetGetAppAttestKeyIDPerApp { diff --git a/AppCheckCore/Tests/Unit/Core/GACAppCheckAPIServiceTests.m b/AppCheckCore/Tests/Unit/Core/GACAppCheckAPIServiceTests.m index 45f10a36..f02b7ddf 100644 --- a/AppCheckCore/Tests/Unit/Core/GACAppCheckAPIServiceTests.m +++ b/AppCheckCore/Tests/Unit/Core/GACAppCheckAPIServiceTests.m @@ -20,11 +20,11 @@ #import "FBLPromise+Testing.h" #import "AppCheckCore/Sources/Core/APIService/NSURLSession+GACPromises.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACURLSessionDataResponse.h" #import "AppCheckCore/Tests/Unit/Utils/GACFixtureLoader.h" #import "AppCheckCore/Tests/Utils/Date/GACDateTestUtils.h" @@ -36,11 +36,11 @@ static NSString *const kTestHeaderKey = @"X-test-header"; static NSString *const kTestHeaderValue = @"TEST_HEADER_VALUE"; -#pragma mark - GACAppCheckAPIServiceTests +#pragma mark - _GACAppCheckAPIServiceTests -@interface GACAppCheckAPIServiceTests : XCTestCase +@interface _GACAppCheckAPIServiceTests : XCTestCase -@property(nonatomic) GACAppCheckAPIService *APIService; +@property(nonatomic) _GACAppCheckAPIService *APIService; @property(nonatomic) id mockURLSession; @@ -48,7 +48,7 @@ @interface GACAppCheckAPIServiceTests : XCTestCase @end -@implementation GACAppCheckAPIServiceTests +@implementation _GACAppCheckAPIServiceTests - (void)setUp { [super setUp]; @@ -58,10 +58,10 @@ - (void)setUp { self.expectedHTTPHeaderFields = [NSMutableDictionary dictionaryWithDictionary:@{kBundleIDHeaderKey : [[NSBundle mainBundle] bundleIdentifier]}]; - self.APIService = [[GACAppCheckAPIService alloc] initWithURLSession:self.mockURLSession - baseURL:nil - APIKey:nil - requestHooks:nil]; + self.APIService = [[_GACAppCheckAPIService alloc] initWithURLSession:self.mockURLSession + baseURL:nil + APIKey:nil + requestHooks:nil]; } - (void)tearDown { @@ -75,11 +75,11 @@ - (void)tearDown { #pragma mark - Init - (void)testInitDefaultBaseURL { - GACAppCheckAPIService *APIService = - [[GACAppCheckAPIService alloc] initWithURLSession:self.mockURLSession - baseURL:nil - APIKey:nil - requestHooks:nil]; + _GACAppCheckAPIService *APIService = + [[_GACAppCheckAPIService alloc] initWithURLSession:self.mockURLSession + baseURL:nil + APIKey:nil + requestHooks:nil]; XCTAssertNotNil(APIService); XCTAssertEqualObjects(APIService.baseURL, @"https://firebaseappcheck.googleapis.com/v1"); @@ -88,11 +88,11 @@ - (void)testInitDefaultBaseURL { - (void)testInitCustomBaseURL { NSString *customBaseURL = @"https://custom.example.com/v1beta"; - GACAppCheckAPIService *APIService = - [[GACAppCheckAPIService alloc] initWithURLSession:self.mockURLSession - baseURL:customBaseURL - APIKey:nil - requestHooks:nil]; + _GACAppCheckAPIService *APIService = + [[_GACAppCheckAPIService alloc] initWithURLSession:self.mockURLSession + baseURL:customBaseURL + APIKey:nil + requestHooks:nil]; XCTAssertNotNil(APIService); XCTAssertEqualObjects(APIService.baseURL, customBaseURL); @@ -187,7 +187,7 @@ - (void)testDataRequestWithRequestHooks { request.allowsCellularAccess = NO; }; - self.APIService = [[GACAppCheckAPIService alloc] + self.APIService = [[_GACAppCheckAPIService alloc] initWithURLSession:self.mockURLSession baseURL:nil APIKey:nil @@ -282,10 +282,10 @@ - (void)testDataRequestWithAPIKey { NSData *requestBody = [@"Request body" dataUsingEncoding:NSUTF8StringEncoding]; [self.expectedHTTPHeaderFields setObject:kAPIKeyHeaderValue forKey:kAPIKeyHeaderKey]; - self.APIService = [[GACAppCheckAPIService alloc] initWithURLSession:self.mockURLSession - baseURL:nil - APIKey:kAPIKeyHeaderValue - requestHooks:nil]; + self.APIService = [[_GACAppCheckAPIService alloc] initWithURLSession:self.mockURLSession + baseURL:nil + APIKey:kAPIKeyHeaderValue + requestHooks:nil]; // 1. Stub URL session. FIRRequestValidationBlock requestValidation = ^BOOL(NSURLRequest *request) { @@ -332,8 +332,8 @@ - (void)testAppCheckTokenWithAPIResponseValidResponse { [GACFixtureLoader loadFixtureNamed:@"FACTokenExchangeResponseSuccess.json"]; XCTAssertNotNil(responseBody); NSHTTPURLResponse *HTTPResponse = [GACURLSessionOCMockStub HTTPResponseWithCode:200]; - GACURLSessionDataResponse *APIResponse = - [[GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody]; + _GACURLSessionDataResponse *APIResponse = + [[_GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody]; // 2. Expected result. NSString *expectedFACToken = @"valid_app_check_token"; @@ -358,8 +358,8 @@ - (void)testAppCheckTokenWithAPIResponseInvalidFormat { NSString *responseBodyString = @"Token verification failed."; NSData *responseBody = [responseBodyString dataUsingEncoding:NSUTF8StringEncoding]; NSHTTPURLResponse *HTTPResponse = [GACURLSessionOCMockStub HTTPResponseWithCode:200]; - GACURLSessionDataResponse *APIResponse = - [[GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody]; + _GACURLSessionDataResponse *APIResponse = + [[_GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody]; // 2. Parse API response. __auto_type tokenPromise = [self.APIService appCheckTokenWithAPIResponse:APIResponse]; @@ -393,8 +393,8 @@ - (void)assertMissingFieldErrorWithFixture:(NSString *)fixtureName XCTAssertNotNil(missingFiledBody); NSHTTPURLResponse *HTTPResponse = [GACURLSessionOCMockStub HTTPResponseWithCode:200]; - GACURLSessionDataResponse *APIResponse = - [[GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:missingFiledBody]; + _GACURLSessionDataResponse *APIResponse = + [[_GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:missingFiledBody]; // 2. Parse API response. __auto_type tokenPromise = [self.APIService appCheckTokenWithAPIResponse:APIResponse]; @@ -434,10 +434,10 @@ - (void)stubURLSessionDataTaskPromiseWithResponse:(NSHTTPURLResponse *)HTTPRespo id URLRequestValidationArg = [OCMArg checkWithBlock:nonOptionalRequestValidationBlock]; // Result promise. - FBLPromise *result = [FBLPromise pendingPromise]; + FBLPromise<_GACURLSessionDataResponse *> *result = [FBLPromise pendingPromise]; if (error == nil) { - GACURLSessionDataResponse *response = - [[GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:body]; + _GACURLSessionDataResponse *response = + [[_GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:body]; [result fulfill:response]; } else { [result reject:error]; diff --git a/AppCheckCore/Tests/Unit/Core/GACAppCheckBackoffWrapperTests.m b/AppCheckCore/Tests/Unit/Core/GACAppCheckBackoffWrapperTests.m index e96dc6a8..51b1f01b 100644 --- a/AppCheckCore/Tests/Unit/Core/GACAppCheckBackoffWrapperTests.m +++ b/AppCheckCore/Tests/Unit/Core/GACAppCheckBackoffWrapperTests.m @@ -23,13 +23,13 @@ #import "FBLPromises.h" #endif -#import +#import #import "AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.h" -@interface GACAppCheckBackoffWrapperTests : XCTestCase +@interface _GACAppCheckBackoffWrapperTests : XCTestCase -@property(nonatomic, nullable) GACAppCheckBackoffWrapper *backoffWrapper; +@property(nonatomic, nullable) _GACAppCheckBackoffWrapper *backoffWrapper; @property(nonatomic) NSDate *currentDate; @@ -50,13 +50,13 @@ @interface GACAppCheckBackoffWrapperTests : XCTestCase @end -@implementation GACAppCheckBackoffWrapperTests +@implementation _GACAppCheckBackoffWrapperTests - (void)setUp { [super setUp]; __auto_type __weak weakSelf = self; - self.backoffWrapper = [[GACAppCheckBackoffWrapper alloc] initWithDateProvider:^NSDate *_Nonnull { + self.backoffWrapper = [[_GACAppCheckBackoffWrapper alloc] initWithDateProvider:^NSDate *_Nonnull { return weakSelf.currentDate ?: [NSDate date]; }]; } diff --git a/AppCheckCore/Tests/Unit/Core/GACAppCheckStorageTests.m b/AppCheckCore/Tests/Unit/Core/GACAppCheckStorageTests.m index 51f05129..f34892ab 100644 --- a/AppCheckCore/Tests/Unit/Core/GACAppCheckStorageTests.m +++ b/AppCheckCore/Tests/Unit/Core/GACAppCheckStorageTests.m @@ -35,8 +35,8 @@ #import "AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" static NSString *const kAppName = @"GACAppCheckStorageTestsApp"; static NSString *const kGoogleAppID = @"1:100000000000:ios:aaaaaaaaaaaaaaaaaaaaaaaa"; @@ -109,7 +109,7 @@ - (void)testGetToken_KeychainError { XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertNotNil(getPromise.error); XCTAssertEqualObjects(getPromise.error, - [GACAppCheckErrorUtil keychainErrorWithError:gulsKeychainError]); + [_GACAppCheckErrorUtil keychainErrorWithError:gulsKeychainError]); // 4. Verify storage mock. OCMVerifyAll(mockKeychainStorage); @@ -138,7 +138,7 @@ - (void)testSetToken_KeychainError { XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertNotNil(getPromise.error); XCTAssertEqualObjects(getPromise.error, - [GACAppCheckErrorUtil keychainErrorWithError:gulsKeychainError]); + [_GACAppCheckErrorUtil keychainErrorWithError:gulsKeychainError]); // 4. Verify storage mock. OCMVerifyAll(mockKeychainStorage); @@ -162,7 +162,7 @@ - (void)testRemoveToken_KeychainError { XCTAssert(FBLWaitForPromisesWithTimeout(0.5)); XCTAssertNotNil(getPromise.error); XCTAssertEqualObjects(getPromise.error, - [GACAppCheckErrorUtil keychainErrorWithError:gulsKeychainError]); + [_GACAppCheckErrorUtil keychainErrorWithError:gulsKeychainError]); // 4. Verify storage mock. OCMVerifyAll(mockKeychainStorage); diff --git a/AppCheckCore/Tests/Unit/Core/GACAppCheckTests.m b/AppCheckCore/Tests/Unit/Core/GACAppCheckTests.m index 450f27b5..7710b059 100644 --- a/AppCheckCore/Tests/Unit/Core/GACAppCheckTests.m +++ b/AppCheckCore/Tests/Unit/Core/GACAppCheckTests.m @@ -28,10 +28,10 @@ #import "AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.h" #import "AppCheckCore/Sources/Core/TokenRefresh/GACAppCheckTokenRefreshResult.h" #import "AppCheckCore/Sources/Core/TokenRefresh/GACAppCheckTokenRefresher.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckTokenDelegate.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckTokenResult.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" /// The placeholder token value returned when an error occurs: `{"error":"UNKNOWN_ERROR"}` encoded /// as base64 @@ -359,7 +359,7 @@ - (void)testLimitedUseToken_WhenTokenGenerationErrors { OCMReject([self.mockStorage getToken]); // 2. Expect error when requesting token from app check provider. - NSError *providerError = [GACAppCheckErrorUtil keychainErrorWithError:[self internalError]]; + NSError *providerError = [_GACAppCheckErrorUtil keychainErrorWithError:[self internalError]]; id completionArg = [OCMArg invokeBlockWithArgs:[NSNull null], providerError, nil]; OCMExpect([self.mockAppCheckProvider getLimitedUseTokenWithCompletion:completionArg]); diff --git a/AppCheckCore/Tests/Unit/DebugProvider/GACAppCheckDebugProviderAPIServiceTests.m b/AppCheckCore/Tests/Unit/DebugProvider/GACAppCheckDebugProviderAPIServiceTests.m index 5bdc8cd6..02b4b846 100644 --- a/AppCheckCore/Tests/Unit/DebugProvider/GACAppCheckDebugProviderAPIServiceTests.m +++ b/AppCheckCore/Tests/Unit/DebugProvider/GACAppCheckDebugProviderAPIServiceTests.m @@ -23,9 +23,9 @@ #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" #import "AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACURLSessionDataResponse.h" #import "AppCheckCore/Tests/Utils/URLSession/GACURLSessionOCMockStub.h" @@ -42,7 +42,7 @@ @implementation GACAppCheckDebugProviderAPIServiceTests - (void)setUp { [super setUp]; - self.mockAPIService = OCMProtocolMock(@protocol(GACAppCheckAPIServiceProtocol)); + self.mockAPIService = OCMProtocolMock(@protocol(_GACAppCheckAPIServiceProtocol)); OCMStub([self.mockAPIService baseURL]).andReturn(@"https://test.appcheck.url.com/alpha"); self.debugAPIService = @@ -84,8 +84,8 @@ - (void)testAppCheckTokenSuccessWithLimitedUse:(BOOL)limitedUse { limitedUse:limitedUse]; NSData *fakeResponseData = [@"fake response" dataUsingEncoding:NSUTF8StringEncoding]; NSHTTPURLResponse *HTTPResponse = [GACURLSessionOCMockStub HTTPResponseWithCode:200]; - GACURLSessionDataResponse *APIResponse = - [[GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:fakeResponseData]; + _GACURLSessionDataResponse *APIResponse = + [[_GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:fakeResponseData]; OCMExpect([self.mockAPIService sendRequestWithURL:URLValidationArg HTTPMethod:@"POST" @@ -132,8 +132,8 @@ - (void)testAppCheckTokenResponseParsingError { id HTTPBodyValidationArg = [self HTTPBodyValidationArgWithDebugToken:debugToken limitedUse:NO]; NSData *fakeResponseData = [@"fake response" dataUsingEncoding:NSUTF8StringEncoding]; NSHTTPURLResponse *HTTPResponse = [GACURLSessionOCMockStub HTTPResponseWithCode:200]; - GACURLSessionDataResponse *APIResponse = - [[GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:fakeResponseData]; + _GACURLSessionDataResponse *APIResponse = + [[_GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:fakeResponseData]; OCMExpect([self.mockAPIService sendRequestWithURL:URLValidationArg HTTPMethod:@"POST" diff --git a/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckAPIServiceTests.m b/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckAPIServiceTests.m index 3acd2c37..3694aeb5 100644 --- a/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckAPIServiceTests.m +++ b/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckAPIServiceTests.m @@ -20,11 +20,11 @@ #import "FBLPromise+Testing.h" #import "AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACURLSessionDataResponse.h" #import "AppCheckCore/Tests/Unit/Utils/GACFixtureLoader.h" #import "AppCheckCore/Tests/Utils/URLSession/GACURLSessionOCMockStub.h" @@ -45,7 +45,7 @@ @implementation GACDeviceCheckAPIServiceTests - (void)setUp { [super setUp]; - self.mockAPIService = OCMProtocolMock(@protocol(GACAppCheckAPIServiceProtocol)); + self.mockAPIService = OCMProtocolMock(@protocol(_GACAppCheckAPIServiceProtocol)); OCMStub([self.mockAPIService baseURL]).andReturn(@"https://test.appcheck.url.com/alpha"); self.APIService = [[GACDeviceCheckAPIService alloc] initWithAPIService:self.mockAPIService @@ -91,8 +91,8 @@ - (void)testAppCheckTokenSuccessWithLimitedUse:(BOOL)limitedUse { XCTAssertNotNil(responseBody); NSHTTPURLResponse *HTTPResponse = [GACURLSessionOCMockStub HTTPResponseWithCode:200]; - GACURLSessionDataResponse *APIResponse = - [[GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody]; + _GACURLSessionDataResponse *APIResponse = + [[_GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody]; OCMExpect([self.mockAPIService sendRequestWithURL:URLValidationArg HTTPMethod:@"POST" @@ -144,8 +144,8 @@ - (void)testAppCheckTokenResponseParsingError { XCTAssertNotNil(responseBody); NSHTTPURLResponse *HTTPResponse = [GACURLSessionOCMockStub HTTPResponseWithCode:200]; - GACURLSessionDataResponse *APIResponse = - [[GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody]; + _GACURLSessionDataResponse *APIResponse = + [[_GACURLSessionDataResponse alloc] initWithResponse:HTTPResponse HTTPBody:responseBody]; OCMExpect([self.mockAPIService sendRequestWithURL:URLValidationArg HTTPMethod:@"POST" diff --git a/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckProviderTests.m b/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckProviderTests.m index d014c559..d8210b9d 100644 --- a/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckProviderTests.m +++ b/AppCheckCore/Tests/Unit/DeviceCheckProvider/GACDeviceCheckProviderTests.m @@ -21,9 +21,9 @@ #import "AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.h" #import "AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckTokenGenerator.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACDeviceCheckProvider.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" #import "AppCheckCore/Tests/Utils/AppCheckBackoffWrapperFake/GACAppCheckBackoffWrapperFake.h" @@ -32,7 +32,7 @@ @interface GACDeviceCheckProvider (Tests) - (instancetype)initWithAPIService:(id)APIService deviceTokenGenerator:(id)deviceTokenGenerator - backoffWrapper:(id)backoffWrapper; + backoffWrapper:(id<_GACAppCheckBackoffWrapperProtocol>)backoffWrapper; @end @@ -120,7 +120,7 @@ - (void)testGetTokenSuccess { - (void)testGetTokenWhenDeviceCheckIsNotSupported { NSError *expectedError = - [GACAppCheckErrorUtil unsupportedAttestationProvider:@"DeviceCheckProvider"]; + [_GACAppCheckErrorUtil unsupportedAttestationProvider:@"DeviceCheckProvider"]; // 0.1. Expect backoff wrapper to be used. self.fakeBackoffWrapper.backoffExpectation = [self expectationWithDescription:@"Backoff"]; diff --git a/AppCheckCore/Tests/Utils/AppCheckBackoffWrapperFake/GACAppCheckBackoffWrapperFake.h b/AppCheckCore/Tests/Utils/AppCheckBackoffWrapperFake/GACAppCheckBackoffWrapperFake.h index 1c38bfe4..333fc2fb 100644 --- a/AppCheckCore/Tests/Utils/AppCheckBackoffWrapperFake/GACAppCheckBackoffWrapperFake.h +++ b/AppCheckCore/Tests/Utils/AppCheckBackoffWrapperFake/GACAppCheckBackoffWrapperFake.h @@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface GACAppCheckBackoffWrapperFake : NSObject +@interface GACAppCheckBackoffWrapperFake : NSObject <_GACAppCheckBackoffWrapperProtocol> /// If `YES` then the next operation passed to `[backoff:errorHandler:]` method will be performed. /// If `NO` then it will fail with a backoff error. diff --git a/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift index 33731992..5eda2f3f 100644 --- a/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift +++ b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift @@ -33,6 +33,14 @@ public final class AppCheckRecaptchaProvider: NSObject, AppCheckCoreProvider { // This action name should never change without coordination with the backend. private static let appCheckActionName = "app_check_ios" + // TODO(ncooke3): Consider if this check should be expanded to handle runtime OS availability checking. + @objc public static func isRecaptchaEnterpriseSDKLinked() -> Bool { + let actionObj: AnyClass? = NSClassFromString("RecaptchaEnterprise.RCAAction") + let recaptchaObj: AnyClass? = NSClassFromString("RecaptchaEnterprise.RCARecaptcha") + return actionObj as? RCAActionProtocol.Type != nil && recaptchaObj as? RCARecaptchaProtocol + .Type != nil + } + private let tokenGenerator: RecaptchaTokenGenerator? private let apiService: RecaptchaAPIService @@ -46,29 +54,23 @@ public final class AppCheckRecaptchaProvider: NSObject, AppCheckCoreProvider { // `@convention(block)` is required because the Swift compiler cannot automatically // bridge collections of closures (like an Array) to Objective-C blocks. This attribute // changes the closure's representation to match the Objective-C block heap layout. - @objc public convenience init(siteKey: String, resourceName: String, APIKey: String, - requestHooks: [@convention(block) (NSMutableURLRequest) -> Void]? = - nil) { - let tokenGenerator: RecaptchaTokenGenerator? - - if let sdk = RecaptchaEnterpriseSDKLoader(customAction: Self.appCheckActionName) { - let backoffWrapper = GACAppCheckBackoffWrapper() - - tokenGenerator = RecaptchaTokenGenerator( - siteKey: siteKey, - recaptchaAction: sdk.action, - recaptchaClass: sdk.recaptchaClass, - backoffWrapper: backoffWrapper - ) - } else { - // Fail fast in Debug (-Onone) builds to alert the developer. - // In Release (-O) builds, a nil tokenGenerator falls back to returning an error in getToken. - assertionFailure(missingRecaptchaSDKMessage) - tokenGenerator = nil + @objc public convenience init?(siteKey: String, resourceName: String, APIKey: String, + requestHooks: [@convention(block) (NSMutableURLRequest) -> Void]? = + nil) { + guard let sdk = RecaptchaEnterpriseSDKLoader(customAction: Self.appCheckActionName) else { + return nil } + let backoffWrapper = _GACAppCheckBackoffWrapper() + let tokenGenerator = RecaptchaTokenGenerator( + siteKey: siteKey, + recaptchaAction: sdk.action, + recaptchaClass: sdk.recaptchaClass, + backoffWrapper: backoffWrapper + ) + let urlSession = URLSession(configuration: .ephemeral) - let appCheckAPIService = AppCheckCoreAPIService(urlSession: urlSession, + let appCheckAPIService = _GACAppCheckAPIService(urlSession: urlSession, baseURL: nil, apiKey: APIKey, requestHooks: requestHooks) @@ -110,7 +112,7 @@ public final class AppCheckRecaptchaProvider: NSObject, AppCheckCoreProvider { private func getToken(limitedUse: Bool) -> Promise { guard let tokenGenerator else { - return Promise(GACAppCheckErrorUtil.missingRecaptchaSDKError()) + return Promise(_GACAppCheckErrorUtil.missingRecaptchaSDKError()) } return tokenGenerator.getRecaptchaToken() .then { recaptchaToken in diff --git a/AppCheckRecaptchaProvider/Sources/RecaptchaAPIService.swift b/AppCheckRecaptchaProvider/Sources/RecaptchaAPIService.swift index 809308f7..1e5d501a 100644 --- a/AppCheckRecaptchaProvider/Sources/RecaptchaAPIService.swift +++ b/AppCheckRecaptchaProvider/Sources/RecaptchaAPIService.swift @@ -34,10 +34,10 @@ private enum Constants { @available(tvOS, unavailable) @available(watchOS, unavailable) final class RecaptchaAPIService: NSObject { - private let apiService: AppCheckCoreAPIServiceProtocol + private let apiService: _GACAppCheckAPIServiceProtocol private let resourceName: String - init(apiService: AppCheckCoreAPIServiceProtocol, resourceName: String) { + init(apiService: _GACAppCheckAPIServiceProtocol, resourceName: String) { self.apiService = apiService self.resourceName = resourceName } @@ -46,7 +46,7 @@ final class RecaptchaAPIService: NSObject { limitedUse: Bool) -> Promise { let urlString = "\(apiService.baseURL)/\(resourceName):\(Constants.exchangeEndpoint)" guard let url = URL(string: urlString) else { - return Promise(GACAppCheckErrorUtil + return Promise(_GACAppCheckErrorUtil .error(withFailureReason: "Invalid URL string: \(urlString)")) } @@ -57,13 +57,13 @@ final class RecaptchaAPIService: NSObject { return Promise(error) } - return Promise(apiService.sendRequest(with: url, - httpMethod: Constants - .httpMethodPost, - body: httpBody, - additionalHeaders: [Constants - .contentTypeKey: Constants - .jsonContentType])) + return Promise<_GACURLSessionDataResponse>(apiService.sendRequest(with: url, + httpMethod: Constants + .httpMethodPost, + body: httpBody, + additionalHeaders: [Constants + .contentTypeKey: Constants + .jsonContentType])) .then { response in Promise(self.apiService.appCheckToken(withAPIResponse: response)) } @@ -72,7 +72,7 @@ final class RecaptchaAPIService: NSObject { private func httpBody(with recaptchaToken: String, limitedUse: Bool) throws -> Data { guard !recaptchaToken.isEmpty else { - throw GACAppCheckErrorUtil.error(withFailureReason: "Recaptcha token cannot be empty") + throw _GACAppCheckErrorUtil.error(withFailureReason: "Recaptcha token cannot be empty") } let payload: [String: Any] = [ @@ -83,7 +83,7 @@ final class RecaptchaAPIService: NSObject { do { return try JSONSerialization.data(withJSONObject: payload, options: []) } catch { - throw GACAppCheckErrorUtil.jsonSerializationError(error) + throw _GACAppCheckErrorUtil.jsonSerializationError(error) } } } diff --git a/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift b/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift index fce80c56..9e825b00 100644 --- a/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift +++ b/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift @@ -38,11 +38,11 @@ final class RecaptchaTokenGenerator { private let recaptchaClient: Promise - private let backoffWrapper: GACAppCheckBackoffWrapperProtocol + private let backoffWrapper: _GACAppCheckBackoffWrapperProtocol init(siteKey: String, recaptchaAction: RCAActionProtocol, recaptchaClass: RCARecaptchaProtocol.Type, - backoffWrapper: GACAppCheckBackoffWrapperProtocol) { + backoffWrapper: _GACAppCheckBackoffWrapperProtocol) { self.siteKey = siteKey self.recaptchaAction = recaptchaAction self.backoffWrapper = backoffWrapper @@ -51,7 +51,7 @@ final class RecaptchaTokenGenerator { if let client { fulfill(client) } else { - reject(error ?? GACAppCheckErrorUtil + reject(error ?? _GACAppCheckErrorUtil .error(withFailureReason: "Failed to fetch Recaptcha client")) } } @@ -89,7 +89,7 @@ final class RecaptchaTokenGenerator { return Promise(fblPromise).then { result in guard let token = result as? String else { - throw GACAppCheckErrorUtil + throw _GACAppCheckErrorUtil .error( withFailureReason: "Unexpected result type from reCAPTCHA token exchange: \(type(of: result)). Expected String." ) @@ -101,13 +101,13 @@ final class RecaptchaTokenGenerator { private func mapRecaptchaError(_ error: Error?) -> Error { guard let error = error as NSError? else { - return GACAppCheckErrorUtil.error(withFailureReason: "Failed to execute Recaptcha action") + return _GACAppCheckErrorUtil.error(withFailureReason: "Failed to execute Recaptcha action") } // Map RecaptchaErrorNetworkError and RecaptchaErrorCodeInternalError. // See https://docs.cloud.google.com/recaptcha/docs/reference/ios/client/api/Enums/RecaptchaErrorCode.html if error.code == Self.networkErrorCode || error.code == Self.internalErrorCode { - return GACAppCheckErrorUtil.apiError(withNetworkError: error) + return _GACAppCheckErrorUtil.apiError(withNetworkError: error) } // Preserve underlying error for others diff --git a/AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift b/AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift index 1b8fe681..a266623c 100644 --- a/AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift +++ b/AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift @@ -46,6 +46,10 @@ final class AppCheckRecaptchaProviderTests: XCTestCase { super.tearDown() } + func testIsRecaptchaEnterpriseSDKLinkedReturnsFalse() { + XCTAssertFalse(AppCheckRecaptchaProvider.isRecaptchaEnterpriseSDKLinked()) + } + func testGetTokenWithoutRecaptchaSDK() { // When the Recaptcha SDK is not linked, the tokenGenerator will be nil. // We should expect an unsupported attestation provider error. @@ -91,6 +95,16 @@ final class AppCheckRecaptchaProviderTests: XCTestCase { waitForExpectations(timeout: 1.0) } + func testInitReturnsNilWithoutRecaptchaSDK() { + // When the Recaptcha SDK is not linked, the convenience initializer should return nil. + let provider = AppCheckRecaptchaProvider( + siteKey: testSiteKey, + resourceName: testResourceName, + APIKey: "test-api-key" + ) + XCTAssertNil(provider) + } + private func createProviderWithMocks(expectedToken: AppCheckCoreToken) -> AppCheckRecaptchaProvider { let mockClient = MockRecaptchaClient() diff --git a/AppCheckRecaptchaProvider/Tests/MockRecaptchaSupport.swift b/AppCheckRecaptchaProvider/Tests/MockRecaptchaSupport.swift index b91d5bd4..e62162a6 100644 --- a/AppCheckRecaptchaProvider/Tests/MockRecaptchaSupport.swift +++ b/AppCheckRecaptchaProvider/Tests/MockRecaptchaSupport.swift @@ -79,7 +79,7 @@ final class MockRecaptchaClient: NSObject, RCARecaptchaClientProtocol { } } -class MockAppCheckCoreAPIService: NSObject, AppCheckCoreAPIServiceProtocol { +class MockAppCheckCoreAPIService: NSObject, _GACAppCheckAPIServiceProtocol { var baseURL: String = "https://test.com" struct RequestData { @@ -90,12 +90,12 @@ class MockAppCheckCoreAPIService: NSObject, AppCheckCoreAPIServiceProtocol { } var lastRequest: RequestData? - var expectedResponse: GACURLSessionDataResponse? + var expectedResponse: _GACURLSessionDataResponse? var expectedToken: AppCheckCoreToken? var expectedError: Error? func sendRequest(with url: URL, httpMethod: String, body: Data?, - additionalHeaders: [String: String]?) -> FBLPromise { + additionalHeaders: [String: String]?) -> FBLPromise<_GACURLSessionDataResponse> { lastRequest = RequestData( url: url, httpMethod: httpMethod, @@ -103,12 +103,12 @@ class MockAppCheckCoreAPIService: NSObject, AppCheckCoreAPIServiceProtocol { additionalHeaders: additionalHeaders ) - let promise = Promise.pending() + let promise = Promise<_GACURLSessionDataResponse>.pending() if let expectedError { promise.reject(expectedError) } else { - let response = expectedResponse ?? GACURLSessionDataResponse( + let response = expectedResponse ?? _GACURLSessionDataResponse( response: HTTPURLResponse(), httpBody: Data() ) @@ -118,7 +118,7 @@ class MockAppCheckCoreAPIService: NSObject, AppCheckCoreAPIServiceProtocol { return promise.asObjCPromise() } - func appCheckToken(withAPIResponse response: GACURLSessionDataResponse) + func appCheckToken(withAPIResponse response: _GACURLSessionDataResponse) -> FBLPromise { let promise = Promise.pending() @@ -136,7 +136,7 @@ class MockAppCheckCoreAPIService: NSObject, AppCheckCoreAPIServiceProtocol { } } -class MockBackoffWrapper: NSObject, GACAppCheckBackoffWrapperProtocol { +class MockBackoffWrapper: NSObject, _GACAppCheckBackoffWrapperProtocol { var applyBackoffCalled = false var shouldReturnError = false var mockError: NSError? From f67b54fc0d58f13a0ccb2936e781868f2075192b Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 29 May 2026 16:27:54 -0400 Subject: [PATCH 62/71] deletes --- .../Public/AppCheckCore/GACAppCheckAPIService.h | 17 ----------------- .../AppCheckCore/GACAppCheckBackoffWrapper.h | 17 ----------------- .../Public/AppCheckCore/GACAppCheckErrorUtil.h | 17 ----------------- .../AppCheckCore/GACURLSessionDataResponse.h | 17 ----------------- 4 files changed, 68 deletions(-) delete mode 100644 AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h delete mode 100644 AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h delete mode 100644 AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h delete mode 100644 AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h diff --git a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h deleted file mode 100644 index b60626be..00000000 --- a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "_GACAppCheckAPIService.h" diff --git a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h deleted file mode 100644 index 4dd1c5b6..00000000 --- a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2021 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "_GACAppCheckBackoffWrapper.h" diff --git a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h deleted file mode 100644 index 1039cbf0..00000000 --- a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "_GACAppCheckErrorUtil.h" diff --git a/AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h b/AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h deleted file mode 100644 index b77abdfb..00000000 --- a/AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "_GACURLSessionDataResponse.h" From 5e158476cb70d567c42bf9b051cef74cfefd14e6 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 29 May 2026 16:29:16 -0400 Subject: [PATCH 63/71] finish moves --- .../Sources/AppAttestProvider/API/GACAppAttestAPIService.m | 4 ++-- AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m | 4 ++-- AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m | 4 ++-- .../Sources/Core/APIService/GACURLSessionDataResponse.m | 2 +- AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m | 4 ++-- AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m | 2 +- AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.m | 2 +- .../DebugProvider/API/GACAppCheckDebugProviderAPIService.m | 2 +- AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m | 2 +- .../DeviceCheckProvider/API/GACDeviceCheckAPIService.m | 2 +- .../Sources/DeviceCheckProvider/GACDeviceCheckProvider.m | 4 ++-- AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h | 4 ---- .../Tests/Integration/GACDeviceCheckAPIServiceE2ETests.m | 2 +- .../GACAppCheckBackoffWrapperFake.h | 2 +- 14 files changed, 18 insertions(+), 22 deletions(-) diff --git a/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.m b/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.m index 04cf7043..4743a82d 100644 --- a/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.m +++ b/AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAPIService.m @@ -23,9 +23,9 @@ #endif #import "AppCheckCore/Sources/AppAttestProvider/API/GACAppAttestAttestationResponse.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACURLSessionDataResponse.h" NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m b/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m index df3d5f28..d8a98eea 100644 --- a/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m +++ b/AppCheckCore/Sources/AppAttestProvider/GACAppAttestProvider.m @@ -31,9 +31,9 @@ #import "AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestArtifactStorage.h" #import "AppCheckCore/Sources/AppAttestProvider/Storage/GACAppAttestKeyIDStorage.h" #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckBackoffWrapper.h" #import "AppCheckCore/Sources/Core/Utils/GACAppCheckCryptoUtils.h" diff --git a/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m b/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m index d3162405..e481d7d6 100644 --- a/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m +++ b/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h" #if __has_include() #import @@ -26,8 +26,8 @@ #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckLogger.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" #import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACURLSessionDataResponse.h" NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.m b/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.m index 77e976ee..1842d1a5 100644 --- a/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.m +++ b/AppCheckCore/Sources/Core/APIService/GACURLSessionDataResponse.m @@ -14,7 +14,7 @@ * limitations under the License. */ -#import "AppCheckCore/Sources/Public/AppCheckCore/GACURLSessionDataResponse.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACURLSessionDataResponse.h" @implementation _GACURLSessionDataResponse diff --git a/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m b/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m index 1988c6b5..9241abcd 100644 --- a/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m +++ b/AppCheckCore/Sources/Core/Backoff/GACAppCheckBackoffWrapper.m @@ -14,7 +14,7 @@ * limitations under the License. */ -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckBackoffWrapper.h" #if __has_include() #import @@ -23,7 +23,7 @@ #endif #import "AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m index 914c17b4..6a491acc 100644 --- a/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m +++ b/AppCheckCore/Sources/Core/Errors/GACAppCheckErrorUtil.m @@ -14,7 +14,7 @@ * limitations under the License. */ -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" #import diff --git a/AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.m b/AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.m index d42d7f6a..3f79a408 100644 --- a/AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.m +++ b/AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.m @@ -16,8 +16,8 @@ #import "AppCheckCore/Sources/Core/Errors/GACAppCheckHTTPError.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrorUtil.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" @implementation GACAppCheckHTTPError diff --git a/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.m b/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.m index d7cd748d..3833ed66 100644 --- a/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.m +++ b/AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.m @@ -23,7 +23,7 @@ #endif #import "AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" #import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" diff --git a/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m b/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m index 854b4d84..16f3aa62 100644 --- a/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m +++ b/AppCheckCore/Sources/DebugProvider/GACAppCheckDebugProvider.m @@ -26,9 +26,9 @@ #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" #import "AppCheckCore/Sources/DebugProvider/API/GACAppCheckDebugProviderAPIService.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h" NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.m b/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.m index 9fa9c510..4ec9757e 100644 --- a/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.m +++ b/AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.m @@ -23,7 +23,7 @@ #endif #import "AppCheckCore/Sources/Core/APIService/GACAppCheckToken+APIResponse.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" #import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" diff --git a/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m b/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m index 1d14a4c4..7d7fecda 100644 --- a/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m +++ b/AppCheckCore/Sources/DeviceCheckProvider/GACDeviceCheckProvider.m @@ -29,9 +29,9 @@ #import "AppCheckCore/Sources/Core/GACAppCheckLogger+Internal.h" #import "AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.h" #import "AppCheckCore/Sources/DeviceCheckProvider/DCDevice+GACDeviceCheckTokenGenerator.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckBackoffWrapper.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckBackoffWrapper.h" #import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckErrorUtil.h" NS_ASSUME_NONNULL_BEGIN diff --git a/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h b/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h index 9de0aff4..3c8dd2fb 100644 --- a/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h +++ b/AppCheckCore/Sources/Public/AppCheckCore/AppCheckCore.h @@ -33,10 +33,6 @@ #import "GACAppAttestProvider.h" // Internal headers exposed for interop with the Swift implementation. -#import "GACAppCheckAPIService.h" -#import "GACAppCheckBackoffWrapper.h" -#import "GACAppCheckErrorUtil.h" -#import "GACURLSessionDataResponse.h" #import "_GACAppCheckAPIService.h" #import "_GACAppCheckBackoffWrapper.h" #import "_GACAppCheckErrorUtil.h" diff --git a/AppCheckCore/Tests/Integration/GACDeviceCheckAPIServiceE2ETests.m b/AppCheckCore/Tests/Integration/GACDeviceCheckAPIServiceE2ETests.m index f9e0143b..83db330b 100644 --- a/AppCheckCore/Tests/Integration/GACDeviceCheckAPIServiceE2ETests.m +++ b/AppCheckCore/Tests/Integration/GACDeviceCheckAPIServiceE2ETests.m @@ -30,8 +30,8 @@ #import "FBLPromise+Testing.h" #import "AppCheckCore/Sources/DeviceCheckProvider/API/GACDeviceCheckAPIService.h" -#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckAPIService.h" #import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/_GACAppCheckAPIService.h" // TODO: Replace with real resource name to run on CI static NSString *const kResourceName = @"projects/test-project-id/google-app-id"; diff --git a/AppCheckCore/Tests/Utils/AppCheckBackoffWrapperFake/GACAppCheckBackoffWrapperFake.h b/AppCheckCore/Tests/Utils/AppCheckBackoffWrapperFake/GACAppCheckBackoffWrapperFake.h index 333fc2fb..069a3876 100644 --- a/AppCheckCore/Tests/Utils/AppCheckBackoffWrapperFake/GACAppCheckBackoffWrapperFake.h +++ b/AppCheckCore/Tests/Utils/AppCheckBackoffWrapperFake/GACAppCheckBackoffWrapperFake.h @@ -24,7 +24,7 @@ #import "FBLPromises.h" #endif -#import +#import NS_ASSUME_NONNULL_BEGIN From 7a5798bbe4e2524f95be2ab80ebc05b4c4df5d79 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 29 May 2026 16:42:05 -0400 Subject: [PATCH 64/71] refactor: optimize recaptcha sdk loader and linking check --- .../Public/AppCheckRecaptchaProvider.swift | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift index 5eda2f3f..3c94b561 100644 --- a/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift +++ b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift @@ -35,10 +35,7 @@ public final class AppCheckRecaptchaProvider: NSObject, AppCheckCoreProvider { // TODO(ncooke3): Consider if this check should be expanded to handle runtime OS availability checking. @objc public static func isRecaptchaEnterpriseSDKLinked() -> Bool { - let actionObj: AnyClass? = NSClassFromString("RecaptchaEnterprise.RCAAction") - let recaptchaObj: AnyClass? = NSClassFromString("RecaptchaEnterprise.RCARecaptcha") - return actionObj as? RCAActionProtocol.Type != nil && recaptchaObj as? RCARecaptchaProtocol - .Type != nil + return RecaptchaEnterpriseSDKLoader.isLinked } private let tokenGenerator: RecaptchaTokenGenerator? @@ -125,22 +122,23 @@ public final class AppCheckRecaptchaProvider: NSObject, AppCheckCoreProvider { } private struct RecaptchaEnterpriseSDKLoader { - // This symbol is specified in the RecaptchaEnterprise SDK. + // These symbols are specified in the RecaptchaEnterprise SDK. // See https://github.com/GoogleCloudPlatform/recaptcha-enterprise-mobile-sdk/blob/18.9.0/Sources/RecaptchaEnterprise/RecaptchaInteropBidings.swift - private static let recaptchaActionClassName = "RecaptchaEnterprise.RCAAction" - // This symbol is specified in the RecaptchaEnterprise SDK. - // See https://github.com/GoogleCloudPlatform/recaptcha-enterprise-mobile-sdk/blob/18.9.0/Sources/RecaptchaEnterprise/RecaptchaInteropBidings.swift - private static let recaptchaClassName = "RecaptchaEnterprise.RCARecaptcha" + private static let actionClass = + NSClassFromString("RecaptchaEnterprise.RCAAction") as? RCAActionProtocol.Type + private static let recaptchaClass = + NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol.Type + + static var isLinked: Bool { + return actionClass != nil && recaptchaClass != nil + } let action: RCAActionProtocol let recaptchaClass: RCARecaptchaProtocol.Type init?(customAction: String) { - let actionObj: AnyClass? = NSClassFromString(Self.recaptchaActionClassName) - let recaptchaObj: AnyClass? = NSClassFromString(Self.recaptchaClassName) - - guard let actionClass = actionObj as? RCAActionProtocol.Type, - let recaptchaClass = recaptchaObj as? RCARecaptchaProtocol.Type else { + guard let actionClass = Self.actionClass, + let recaptchaClass = Self.recaptchaClass else { return nil } From 3e8e012e1291af01584fd2290419dbb4124bb1ae Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 29 May 2026 17:06:52 -0400 Subject: [PATCH 65/71] refactor(recaptcha): avoid NSNull in NSError userInfo during bridging --- .../Public/AppCheckRecaptchaProvider.swift | 15 ++++++++++++++- .../Sources/RecaptchaTokenGenerator.swift | 8 ++++---- .../Tests/AppCheckRecaptchaProviderTests.swift | 12 ++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift index 3c94b561..af0ff724 100644 --- a/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift +++ b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift @@ -54,7 +54,20 @@ public final class AppCheckRecaptchaProvider: NSObject, AppCheckCoreProvider { @objc public convenience init?(siteKey: String, resourceName: String, APIKey: String, requestHooks: [@convention(block) (NSMutableURLRequest) -> Void]? = nil) { - guard let sdk = RecaptchaEnterpriseSDKLoader(customAction: Self.appCheckActionName) else { + self.init( + siteKey: siteKey, + resourceName: resourceName, + APIKey: APIKey, + requestHooks: requestHooks, + actionName: Self.appCheckActionName + ) + } + + @objc public convenience init?(siteKey: String, resourceName: String, APIKey: String, + requestHooks: [@convention(block) (NSMutableURLRequest) -> Void]? = + nil, + actionName: String) { + guard let sdk = RecaptchaEnterpriseSDKLoader(customAction: actionName) else { return nil } diff --git a/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift b/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift index 9e825b00..a0ee0c91 100644 --- a/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift +++ b/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift @@ -111,10 +111,10 @@ final class RecaptchaTokenGenerator { } // Preserve underlying error for others - let userInfo: [String: Any] = [ - NSUnderlyingErrorKey: error, - NSLocalizedFailureReasonErrorKey: error.userInfo[NSLocalizedFailureReasonErrorKey] as Any, - ] + var userInfo: [String: Any] = [NSUnderlyingErrorKey: error] + if let reason = error.userInfo[NSLocalizedFailureReasonErrorKey] { + userInfo[NSLocalizedFailureReasonErrorKey] = reason + } return NSError( domain: AppCheckCoreErrorDomain, code: AppCheckCoreErrorCode.unknown.rawValue, diff --git a/AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift b/AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift index a266623c..0b86bbab 100644 --- a/AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift +++ b/AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift @@ -105,6 +105,18 @@ final class AppCheckRecaptchaProviderTests: XCTestCase { XCTAssertNil(provider) } + func testInitWithCustomActionNameReturnsNilWithoutRecaptchaSDK() { + // When the Recaptcha SDK is not linked, the convenience initializer should return nil + // even with a custom action name. + let provider = AppCheckRecaptchaProvider( + siteKey: testSiteKey, + resourceName: testResourceName, + APIKey: "test-api-key", + actionName: "custom_action" + ) + XCTAssertNil(provider) + } + private func createProviderWithMocks(expectedToken: AppCheckCoreToken) -> AppCheckRecaptchaProvider { let mockClient = MockRecaptchaClient() From cc573d4e145f82a0b791d984fe964d0da9219100 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 29 May 2026 17:13:17 -0400 Subject: [PATCH 66/71] sitekey prop removal --- AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift b/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift index a0ee0c91..4d1c4e80 100644 --- a/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift +++ b/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift @@ -33,7 +33,6 @@ final class RecaptchaTokenGenerator { // See https://docs.cloud.google.com/recaptcha/docs/reference/ios/client/api/Enums/RecaptchaErrorCode.html#recaptchaerrorcodeinternalerror static let internalErrorCode = 100 - private let siteKey: String private let recaptchaAction: RCAActionProtocol private let recaptchaClient: Promise @@ -43,7 +42,6 @@ final class RecaptchaTokenGenerator { init(siteKey: String, recaptchaAction: RCAActionProtocol, recaptchaClass: RCARecaptchaProtocol.Type, backoffWrapper: _GACAppCheckBackoffWrapperProtocol) { - self.siteKey = siteKey self.recaptchaAction = recaptchaAction self.backoffWrapper = backoffWrapper recaptchaClient = Promise { fulfill, reject in From 1bdf7a9a9c9813aa177136a8df392957b7e945c7 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 29 May 2026 17:17:43 -0400 Subject: [PATCH 67/71] chore: add clarifying comment for fetchClient --- .../Sources/RecaptchaTokenGenerator.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift b/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift index 4d1c4e80..9783157a 100644 --- a/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift +++ b/AppCheckRecaptchaProvider/Sources/RecaptchaTokenGenerator.swift @@ -44,6 +44,11 @@ final class RecaptchaTokenGenerator { backoffWrapper: _GACAppCheckBackoffWrapperProtocol) { self.recaptchaAction = recaptchaAction self.backoffWrapper = backoffWrapper + // Note: `fetchClient` is called only once and its result (including + // failure) is cached. reCAPTCHA engineers have confirmed that + // `fetchClient` handles transient errors internally and only fails on + // permanent integration errors (e.g., invalid site key). Therefore, + // retrying `fetchClient` on failure is unnecessary and not recommended. recaptchaClient = Promise { fulfill, reject in recaptchaClass.fetchClient(withSiteKey: siteKey) { client, error in if let client { From a27a56c0c333d5d4d55a231f284253f829bdd18e Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 8 Jun 2026 15:19:50 -0400 Subject: [PATCH 68/71] feat: add env var to toggle staging backend --- .../Core/APIService/GACAppCheckAPIService.m | 28 +++++++++++++- .../Public/AppCheckCore/GACAppCheckErrors.h | 1 + .../Unit/Core/GACAppCheckAPIServiceTests.m | 38 +++++++++++++++++++ .../Tests/Unit/Swift/AppCheckAPITests.swift | 1 + 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m b/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m index e481d7d6..3788b3b5 100644 --- a/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m +++ b/AppCheckCore/Sources/Core/APIService/GACAppCheckAPIService.m @@ -34,7 +34,13 @@ static NSString *const kAPIKeyHeaderKey = @"X-Goog-Api-Key"; static NSString *const kBundleIdKey = @"X-Ios-Bundle-Identifier"; -static NSString *const kDefaultBaseURL = @"https://firebaseappcheck.googleapis.com/v1"; +static NSString *const kProdBaseURL = @"https://firebaseappcheck.googleapis.com/v1"; + +#if !NDEBUG +static NSString *const kStagingBaseURL = + @"https://staging-firebaseappcheck.sandbox.googleapis.com/v1"; +static NSString *const kAppCheckUseStagingEnvKey = @"_AppCheckUseStaging"; +#endif @interface _GACAppCheckAPIService () @@ -58,7 +64,25 @@ - (instancetype)initWithURLSession:(NSURLSession *)session _URLSession = session; _APIKey = APIKey; _requestHooks = requestHooks ? [requestHooks copy] : @[]; - _baseURL = baseURL ?: kDefaultBaseURL; + + NSString *resolvedBaseURL = baseURL; + +#if !NDEBUG + if (resolvedBaseURL == nil) { + BOOL useStaging = + [[[NSProcessInfo processInfo] environment][kAppCheckUseStagingEnvKey] boolValue]; + if (useStaging) { + resolvedBaseURL = kStagingBaseURL; + NSString *logMessage = + [NSString stringWithFormat: + @"App Check staging environment enabled. API calls will be routed to %@.", + kStagingBaseURL]; + GACAppCheckLogInfo(GACLoggerAppCheckMessageCodeStagingModeEnabled, logMessage); + } + } +#endif + + _baseURL = resolvedBaseURL ?: kProdBaseURL; } return self; } diff --git a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h index 44b14b15..6a2e9e96 100644 --- a/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h +++ b/AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckErrors.h @@ -45,6 +45,7 @@ typedef NS_ENUM(NSInteger, GACAppCheckMessageCode) { // App Check GACLoggerAppCheckMessageCodeProviderIsMissing = 2002, + GACLoggerAppCheckMessageCodeStagingModeEnabled = 2003, GACLoggerAppCheckMessageCodeUnexpectedHTTPCode = 3001, // Debug Provider diff --git a/AppCheckCore/Tests/Unit/Core/GACAppCheckAPIServiceTests.m b/AppCheckCore/Tests/Unit/Core/GACAppCheckAPIServiceTests.m index f02b7ddf..f5aca01c 100644 --- a/AppCheckCore/Tests/Unit/Core/GACAppCheckAPIServiceTests.m +++ b/AppCheckCore/Tests/Unit/Core/GACAppCheckAPIServiceTests.m @@ -98,6 +98,44 @@ - (void)testInitCustomBaseURL { XCTAssertEqualObjects(APIService.baseURL, customBaseURL); } +- (void)testInitBaseURLStagingTriggeredByEnvVar { + NSString *stagingBaseURL = @"https://staging-firebaseappcheck.sandbox.googleapis.com/v1"; + + id processInfoMock = OCMPartialMock([NSProcessInfo processInfo]); + OCMExpect([processInfoMock processInfo]).andReturn(processInfoMock); + OCMExpect([processInfoMock environment]).andReturn(@{@"_AppCheckUseStaging" : @"YES"}); + + _GACAppCheckAPIService *APIService = + [[_GACAppCheckAPIService alloc] initWithURLSession:self.mockURLSession + baseURL:nil + APIKey:nil + requestHooks:nil]; + + XCTAssertNotNil(APIService); + XCTAssertEqualObjects(APIService.baseURL, stagingBaseURL); + + [processInfoMock stopMocking]; +} + +- (void)testInitBaseURLStagingNotTriggeredWhenEnvVarIsNo { + NSString *prodBaseURL = @"https://firebaseappcheck.googleapis.com/v1"; + + id processInfoMock = OCMPartialMock([NSProcessInfo processInfo]); + OCMExpect([processInfoMock processInfo]).andReturn(processInfoMock); + OCMExpect([processInfoMock environment]).andReturn(@{@"_AppCheckUseStaging" : @"NO"}); + + _GACAppCheckAPIService *APIService = + [[_GACAppCheckAPIService alloc] initWithURLSession:self.mockURLSession + baseURL:nil + APIKey:nil + requestHooks:nil]; + + XCTAssertNotNil(APIService); + XCTAssertEqualObjects(APIService.baseURL, prodBaseURL); + + [processInfoMock stopMocking]; +} + #pragma mark - Send Requests - (void)testDataRequestNetworkError { diff --git a/AppCheckCore/Tests/Unit/Swift/AppCheckAPITests.swift b/AppCheckCore/Tests/Unit/Swift/AppCheckAPITests.swift index 06205be5..a36fab5f 100644 --- a/AppCheckCore/Tests/Unit/Swift/AppCheckAPITests.swift +++ b/AppCheckCore/Tests/Unit/Swift/AppCheckAPITests.swift @@ -229,6 +229,7 @@ final class AppCheckAPITests { switch code! { case .loggerAppCheckMessageCodeUnknown: break case .loggerAppCheckMessageCodeProviderIsMissing: break + case .loggerAppCheckMessageCodeStagingModeEnabled: break case .loggerAppCheckMessageCodeUnexpectedHTTPCode: break case .loggerAppCheckMessageLocalDebugToken: break case .loggerAppCheckMessageEnvironmentVariableDebugToken: break From e44765d1d99c1e706988d4893d9182c9d8d2e869 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 8 Jun 2026 19:06:52 -0400 Subject: [PATCH 69/71] feat: replace isRecaptchaEnterpriseSDKLinked with isSupported --- .../Sources/Public/AppCheckRecaptchaProvider.swift | 3 +-- .../Tests/AppCheckRecaptchaProviderTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift index af0ff724..a28cb463 100644 --- a/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift +++ b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift @@ -33,8 +33,7 @@ public final class AppCheckRecaptchaProvider: NSObject, AppCheckCoreProvider { // This action name should never change without coordination with the backend. private static let appCheckActionName = "app_check_ios" - // TODO(ncooke3): Consider if this check should be expanded to handle runtime OS availability checking. - @objc public static func isRecaptchaEnterpriseSDKLinked() -> Bool { + @objc public static func isSupported() -> Bool { return RecaptchaEnterpriseSDKLoader.isLinked } diff --git a/AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift b/AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift index 0b86bbab..1b2d15a6 100644 --- a/AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift +++ b/AppCheckRecaptchaProvider/Tests/AppCheckRecaptchaProviderTests.swift @@ -46,8 +46,8 @@ final class AppCheckRecaptchaProviderTests: XCTestCase { super.tearDown() } - func testIsRecaptchaEnterpriseSDKLinkedReturnsFalse() { - XCTAssertFalse(AppCheckRecaptchaProvider.isRecaptchaEnterpriseSDKLinked()) + func testIsSupportedReturnsFalse() { + XCTAssertFalse(AppCheckRecaptchaProvider.isSupported()) } func testGetTokenWithoutRecaptchaSDK() { From 15fa108b76275c055933a0205ed3f80c294f4bd8 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:23:55 -0400 Subject: [PATCH 70/71] Update AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift Signed-off-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> --- .../Sources/Public/AppCheckRecaptchaProvider.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift index a28cb463..3239a289 100644 --- a/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift +++ b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift @@ -21,7 +21,7 @@ import RecaptchaInterop /// Firebase App Check provider that verifies app integrity using the /// [reCAPTCHA Enterprise](https://cloud.google.com/recaptcha/docs/instrument-ios-apps) -/// API. This class is available on all platforms for select OS versions. See +/// API. This class's platform and OS availability matches reCAPTCHA Enterprise's. See /// https://firebase.google.com/docs/ios/learn-more for more details. @available(iOS 15.0, visionOS 1.0, *) @available(macOS, unavailable) From c115953ea57eb9d0863867a0bc47e4aa0eee36c8 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:58:24 -0400 Subject: [PATCH 71/71] Update AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift Signed-off-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> --- .../Sources/Public/AppCheckRecaptchaProvider.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift index 3239a289..ccc66f41 100644 --- a/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift +++ b/AppCheckRecaptchaProvider/Sources/Public/AppCheckRecaptchaProvider.swift @@ -21,8 +21,8 @@ import RecaptchaInterop /// Firebase App Check provider that verifies app integrity using the /// [reCAPTCHA Enterprise](https://cloud.google.com/recaptcha/docs/instrument-ios-apps) -/// API. This class's platform and OS availability matches reCAPTCHA Enterprise's. See -/// https://firebase.google.com/docs/ios/learn-more for more details. +/// API. This class's platform and OS availability matches reCAPTCHA +/// Enterprise's. @available(iOS 15.0, visionOS 1.0, *) @available(macOS, unavailable) @available(macCatalyst, unavailable)