diff --git a/.gitignore b/.gitignore index d408c6f300d..1281d8072ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,16 @@ -Example/Auth/Sample/Sample.entitlements -Example/Auth/Sample/GoogleService-Info_multi.plist +Example/Auth/Sample/Application.plist Example/Auth/Sample/AuthCredentials.h +Example/Auth/Sample/GoogleService-Info_multi.plist Example/Auth/Sample/GoogleService-Info.plist -Example/Auth/Sample/Application.plist -Example/Auth/SwiftSample/GoogleService-Info.plist -Example/Auth/SwiftSample/Info.plist -Example/Auth/SwiftSample/AuthCredentials.swift +Example/Auth/Sample/Sample.entitlements Example/Auth/ApiTests/AuthCredentials.h Example/Database/App/GoogleService-Info.plist Example/Storage/App/GoogleService-Info.plist +Secrets.tar + # OS X .DS_Store @@ -33,6 +32,13 @@ DerivedData *.hmap *.ipa +# Swift Package Manager +*/.build +ZipBuilder/Packages +ZipBuilder/*.xcodeproj +ZipBuilder/Package.resolved + + # IntelliJ .idea @@ -75,3 +81,6 @@ Ninja # Visual Studio /.vs + +# CocoaPods generate +gen/ diff --git a/.travis.yml b/.travis.yml index a573308a69c..b0ab6b686f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,13 +13,7 @@ jobs: - brew install clang-format - brew install swiftformat script: - - ./scripts/check_whitespace.sh - - ./scripts/check_copyright.sh - - ./scripts/check_no_module_imports.sh - - ./scripts/check_test_inclusion.py - - ./scripts/style.sh test-only $TRAVIS_COMMIT_RANGE - # Google C++ style compliance - - ./scripts/lint.sh $TRAVIS_COMMIT_RANGE + - ./scripts/check.sh --test-only $TRAVIS_COMMIT_RANGE # The order of builds matters (even though they are run in parallel): # Travis will schedule them in the same order they are listed here. @@ -37,7 +31,17 @@ jobs: - stage: test env: - - PROJECT=InAppMessagingDisplay PLATFORM=iOS METHOD=xcodebuild + - PROJECT=Functions PLATFORM=iOS METHOD=pod-lib-lint + before_install: + - ./scripts/if_changed.sh ./scripts/install_prereqs.sh + script: + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseFunctions.podspec + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseFunctions.podspec --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseFunctions.podspec --use-modular-headers + + - stage: test + env: + - PROJECT=InAppMessaging PLATFORM=iOS METHOD=xcodebuild before_install: - ./scripts/if_changed.sh ./scripts/install_prereqs.sh script: @@ -65,9 +69,10 @@ jobs: - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseAuthInterop.podspec - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseDatabase.podspec - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseDynamicLinks.podspec + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseInstanceID.podspec - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseMessaging.podspec - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseStorage.podspec - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseFunctions.podspec + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseInAppMessaging.podspec - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseInAppMessagingDisplay.podspec - stage: test @@ -78,7 +83,7 @@ jobs: script: # Eliminate the one warning from BoringSSL when CocoaPods 1.6.0 is available. # The travis_wait is necessary because the command takes more than 10 minutes. - - travis_wait 45 ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseFirestore.podspec --allow-warnings --no-subspecs + - travis_wait 30 ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseFirestore.podspec --platforms=ios --allow-warnings --no-subspecs # pod lib lint to check build and warnings for static library build - only on cron jobs - stage: test @@ -94,10 +99,11 @@ jobs: - travis_retry ./scripts/if_cron.sh ./scripts/pod_lib_lint.sh FirebaseAuthInterop.podspec --use-libraries - travis_retry ./scripts/if_cron.sh ./scripts/pod_lib_lint.sh FirebaseDatabase.podspec --use-libraries - travis_retry ./scripts/if_cron.sh ./scripts/pod_lib_lint.sh FirebaseDynamicLinks.podspec --use-libraries + - travis_retry ./scripts/if_cron.sh ./scripts/pod_lib_lint.sh FirebaseInstanceID.podspec --use-libraries # The Protobuf dependency of FirebaseMessaging has warnings with --use-libraries - travis_retry ./scripts/if_cron.sh ./scripts/pod_lib_lint.sh FirebaseMessaging.podspec --use-libraries --allow-warnings - travis_retry ./scripts/if_cron.sh ./scripts/pod_lib_lint.sh FirebaseStorage.podspec --use-libraries - - travis_retry ./scripts/if_cron.sh ./scripts/pod_lib_lint.sh FirebaseFunctions.podspec --use-libraries + - travis_retry ./scripts/if_cron.sh ./scripts/pod_lib_lint.sh FirebaseInAppMessaging.podspec --use-libraries - travis_retry ./scripts/if_cron.sh ./scripts/pod_lib_lint.sh FirebaseInAppMessagingDisplay.podspec --use-libraries - stage: test @@ -121,44 +127,7 @@ jobs: # Alternative platforms - # Xcode 9 - - stage: test - osx_image: xcode9.4 - env: - - PROJECT=Firebase PLATFORM=iOS METHOD=xcodebuild - before_install: - - npm install ios-sim -g - - ios-sim start --devicetypeid "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus, 11.3" - - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - script: - - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM - - - stage: test - osx_image: xcode9.4 - env: - - PROJECT=InAppMessagingDisplay PLATFORM=iOS METHOD=xcodebuild - before_install: - - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - script: - - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM - - - stage: test - osx_image: xcode9.4 - env: - - PROJECT=Firestore PLATFORM=iOS METHOD=xcodebuild - before_install: - - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - script: - - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM $METHOD - - - stage: test - env: - - PROJECT=Firestore PLATFORM=macOS METHOD=cmake - before_install: - - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - script: - - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM $METHOD - + # Test Firestore on Xcode 8 to use old llvm to ensure C++ portability. - stage: test osx_image: xcode8.3 env: @@ -168,37 +137,6 @@ jobs: script: - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM $METHOD - # Xcode 9 may find lint errors that don't show up in Xcode 10 (#2081) - - stage: test - osx_image: xcode9.4 - env: - - PROJECT=Firebase PLATFORM=iOS METHOD=pod-lib-lint - before_install: - - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - script: - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh GoogleUtilities.podspec - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseCore.podspec - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseAnalyticsInterop.podspec - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseAuth.podspec - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseAuthInterop.podspec - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseDatabase.podspec - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseDynamicLinks.podspec - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseMessaging.podspec - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseStorage.podspec - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseFunctions.podspec - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseInAppMessagingDisplay.podspec - - - stage: test - osx_image: xcode9.4 - env: - - PROJECT=Firestore PLATFORM=iOS METHOD=pod-lib-lint - before_install: - - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - script: - # Eliminate the one warning from BoringSSL when CocoaPods 1.6.0 is available. - # The travis_wait is necessary because the command takes more than 10 minutes. - - travis_wait 45 ./scripts/if_changed.sh ./scripts/pod_lib_lint.sh FirebaseFirestore.podspec --allow-warnings --no-subspecs - # Community-supported platforms - stage: test @@ -278,6 +216,8 @@ jobs: - PROJECT=Firestore PLATFORM=iOS METHOD=xcodebuild SANITIZERS=asan - env: - PROJECT=Firestore PLATFORM=iOS METHOD=xcodebuild SANITIZERS=tsan + - env: + - PROJECT=InAppMessaging PLATFORM=iOS METHOD=xcodebuild # TODO(varconst): enable if it's possible to make this flag work on build # stages. It's supposed to avoid waiting for jobs that are allowed to fail diff --git a/Carthage.md b/Carthage.md index 1ca28b5542b..4a6c33bda3a 100644 --- a/Carthage.md +++ b/Carthage.md @@ -44,6 +44,7 @@ binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseInvitesBinary.jso binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseMessagingBinary.json" binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseMLModelInterpreterBinary.json" binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseMLNLLanguageIDBinary.json" +binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseMLNLSmartReplyBinary.json" binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseMLNaturalLanguageBinary.json" binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseMLVisionBarcodeModelBinary.json" binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseMLVisionBinary.json" @@ -65,7 +66,7 @@ binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseStorageBinary.jso into the Xcode project and make sure they're added to the `Copy Bundle Resources` Build Phase : - For Firestore: - - ./Carthage/Build/iOS/FirebaseFirestore.framework/gRPCCertificates-Firestore.bundle + - ./Carthage/Build/iOS/FirebaseFirestore.framework/gRPCCertificates.bundle - For Invites: - ./Carthage/Build/iOS/FirebaseInvites.framework/GoogleSignIn.bundle - ./Carthage/Build/iOS/FirebaseInvites.framework/GPPACLPickerResources.bundle diff --git a/DocumentReference+Codable.swift b/DocumentReference+Codable.swift new file mode 100644 index 00000000000..97ae1119731 --- /dev/null +++ b/DocumentReference+Codable.swift @@ -0,0 +1,36 @@ +// +// DocumentReference+Codable.swift +// BoringSSL-GRPC-iOS +// +// Created by 1amageek on 2019/03/26. +// + +import Foundation +import FirebaseFirestore + +/** + * A protocol describing the encodable properties of a Timestamp. + * + * Note: this protocol exists as a workaround for the Swift compiler: if the Timestamp class + * was extended directly to conform to Codable, the methods implementing the protocol would be need + * to be marked required but that can't be done in an extension. Declaring the extension on the + * protocol sidesteps this issue. + */ +private protocol CodableDocumentReference: Codable { + +} + + +extension CodableDocumentReference { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + _ = try container.decode(DocumentReference.self) + throw FirestoreDecodingError.decodingIsNotSupported + } + + public func encode(to encoder: Encoder) throws { +// var container = encoder.singleValueContainer() + } +} + +extension DocumentReference: CodableDocumentReference { } diff --git a/Example/Auth/ApiTests/AccountInfoTests.m b/Example/Auth/ApiTests/AccountInfoTests.m new file mode 100644 index 00000000000..227b1c2423e --- /dev/null +++ b/Example/Auth/ApiTests/AccountInfoTests.m @@ -0,0 +1,67 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRAuthApiTestsBase.h" + +/** The testing email address for testCreateAccountWithEmailAndPassword. */ +static NSString *const kOldUserEmail = @"olduseremail@iosapitests.com"; + +/** The testing email address for testUpdatingUsersEmail. */ +static NSString *const kNewUserEmail = @"newuseremail@iosapitests.com"; + +@interface AccountInfoTests : FIRAuthApiTestsBase + +@end + +@implementation AccountInfoTests + +- (void)testUpdatingUsersEmail { + SKIP_IF_ON_MOBILE_HARNESS + FIRAuth *auth = [FIRAuth auth]; + if (!auth) { + XCTFail(@"Could not obtain auth object."); + } + + __block NSError *apiError; + XCTestExpectation *expectation = + [self expectationWithDescription:@"Created account with email and password."]; + [auth createUserWithEmail:kOldUserEmail + password:@"password" + completion:^(FIRAuthDataResult *user, NSError *error) { + apiError = error; + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationsTimeout handler:nil]; + expectation = [self expectationWithDescription:@"Created account with email and password."]; + XCTAssertEqualObjects(auth.currentUser.email, kOldUserEmail); + XCTAssertNil(apiError); + + [auth.currentUser updateEmail:kNewUserEmail + completion:^(NSError *_Nullable error) { + apiError = error; + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationsTimeout handler:nil]; + XCTAssertNil(apiError); + XCTAssertEqualObjects(auth.currentUser.email, kNewUserEmail); + + // Clean up the created Firebase user for future runs. + [self deleteCurrentUser]; +} + +@end diff --git a/Example/Auth/ApiTests/AnonymousAuthTests.m b/Example/Auth/ApiTests/AnonymousAuthTests.m new file mode 100644 index 00000000000..90be13f0410 --- /dev/null +++ b/Example/Auth/ApiTests/AnonymousAuthTests.m @@ -0,0 +1,34 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRAuthApiTestsBase.h" + +@interface AnonymousAuthTests : FIRAuthApiTestsBase + +@end + +@implementation AnonymousAuthTests + +- (void)testSignInAnonymously { + [self signInAnonymously]; + XCTAssertTrue([FIRAuth auth].currentUser.anonymous); + + [self deleteCurrentUser]; +} + +@end diff --git a/Example/Auth/ApiTests/Auth_ApiTests-Bridging-Header.h b/Example/Auth/ApiTests/Auth_ApiTests-Bridging-Header.h new file mode 100644 index 00000000000..ab4a71c9709 --- /dev/null +++ b/Example/Auth/ApiTests/Auth_ApiTests-Bridging-Header.h @@ -0,0 +1,17 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRAuthApiTestsBase.h" diff --git a/Example/Auth/ApiTests/CustomAuthTests.m b/Example/Auth/ApiTests/CustomAuthTests.m new file mode 100644 index 00000000000..362d962d4ae --- /dev/null +++ b/Example/Auth/ApiTests/CustomAuthTests.m @@ -0,0 +1,191 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRAuthApiTestsBase.h" + +/** The user name string for Custom Auth testing account. */ +static NSString *const kCustomAuthTestingAccountUserID = KCUSTOM_AUTH_USER_ID; + +/** The url for obtaining a valid custom token string used to test Custom Auth. */ +static NSString *const kCustomTokenUrl = KCUSTOM_AUTH_TOKEN_URL; + +/** The url for obtaining an expired but valid custom token string used to test Custom Auth failure. + */ +static NSString *const kExpiredCustomTokenUrl = KCUSTOM_AUTH_TOKEN_EXPIRED_URL; + +/** The invalid custom token string for testing Custom Auth. */ +static NSString *const kInvalidCustomToken = @"invalid token."; + +/** Error message for invalid custom token sign in. */ +NSString *kInvalidTokenErrorMessage = + @"Invalid assertion format. 3 dot separated segments required."; + +@interface CustomAuthTests : FIRAuthApiTestsBase + +@end + +@implementation CustomAuthTests + +- (void)testSignInWithValidCustomAuthToken { + FIRAuth *auth = [FIRAuth auth]; + if (!auth) { + XCTFail(@"Could not obtain auth object."); + } + + NSError *error; + NSString *customToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:kCustomTokenUrl] + encoding:NSUTF8StringEncoding + error:&error]; + if (!customToken) { + XCTFail(@"There was an error retrieving the custom token: %@", error); + } + NSLog(@"The valid token is: %@", customToken); + + XCTestExpectation *expectation = + [self expectationWithDescription:@"CustomAuthToken sign-in finished."]; + + [auth signInWithCustomToken:customToken + completion:^(FIRAuthDataResult *_Nullable result, NSError *_Nullable error) { + if (error) { + NSLog(@"Valid token sign in error: %@", error); + } + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationsTimeout + handler:^(NSError *error) { + if (error != nil) { + XCTFail(@"Failed to wait for expectations " + @"in CustomAuthToken sign in. Error: %@", + error.localizedDescription); + } + }]; + + XCTAssertEqualObjects(auth.currentUser.uid, kCustomAuthTestingAccountUserID); +} + +- (void)testSignInWithValidCustomAuthExpiredToken { + FIRAuth *auth = [FIRAuth auth]; + if (!auth) { + XCTFail(@"Could not obtain auth object."); + } + + NSError *error; + NSString *customToken = + [NSString stringWithContentsOfURL:[NSURL URLWithString:kExpiredCustomTokenUrl] + encoding:NSUTF8StringEncoding + error:&error]; + if (!customToken) { + XCTFail(@"There was an error retrieving the custom token: %@", error); + } + XCTestExpectation *expectation = + [self expectationWithDescription:@"CustomAuthToken sign-in finished."]; + + __block NSError *apiError; + [auth signInWithCustomToken:customToken + completion:^(FIRAuthDataResult *_Nullable result, NSError *_Nullable error) { + if (error) { + apiError = error; + } + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationsTimeout + handler:^(NSError *error) { + if (error != nil) { + XCTFail(@"Failed to wait for expectations " + @"in CustomAuthToken sign in. Error: %@", + error.localizedDescription); + } + }]; + + XCTAssertNil(auth.currentUser); + XCTAssertEqual(apiError.code, FIRAuthErrorCodeInvalidCustomToken); +} + +- (void)testSignInWithInvalidCustomAuthToken { + FIRAuth *auth = [FIRAuth auth]; + if (!auth) { + XCTFail(@"Could not obtain auth object."); + } + XCTestExpectation *expectation = + [self expectationWithDescription:@"Invalid CustomAuthToken sign-in finished."]; + + [auth signInWithCustomToken:kInvalidCustomToken + completion:^(FIRAuthDataResult *_Nullable result, NSError *_Nullable error) { + XCTAssertEqualObjects(error.localizedDescription, kInvalidTokenErrorMessage); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationsTimeout + handler:^(NSError *error) { + if (error != nil) { + XCTFail(@"Failed to wait for expectations " + @"in CustomAuthToken sign in. Error: %@", + error.localizedDescription); + } + }]; +} + +- (void)testInMemoryUserAfterSignOut { + FIRAuth *auth = [FIRAuth auth]; + if (!auth) { + XCTFail(@"Could not obtain auth object."); + } + NSError *error; + NSString *customToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:kCustomTokenUrl] + encoding:NSUTF8StringEncoding + error:&error]; + if (!customToken) { + XCTFail(@"There was an error retrieving the custom token: %@", error); + } + XCTestExpectation *expectation = + [self expectationWithDescription:@"CustomAuthToken sign-in finished."]; + __block NSError *rpcError; + [auth signInWithCustomToken:customToken + completion:^(FIRAuthDataResult *_Nullable result, NSError *_Nullable error) { + if (error) { + rpcError = error; + } + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationsTimeout + handler:^(NSError *error) { + if (error != nil) { + XCTFail(@"Failed to wait for expectations " + @"in CustomAuthToken sign in. Error: %@", + error.localizedDescription); + } + }]; + XCTAssertEqualObjects(auth.currentUser.uid, kCustomAuthTestingAccountUserID); + XCTAssertNil(rpcError); + FIRUser *inMemoryUser = auth.currentUser; + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Profile data change."]; + [auth signOut:NULL]; + rpcError = nil; + NSString *newEmailAddress = [self fakeRandomEmail]; + XCTAssertNotEqualObjects(newEmailAddress, inMemoryUser.email); + [inMemoryUser updateEmail:newEmailAddress + completion:^(NSError *_Nullable error) { + rpcError = error; + [expectation1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationsTimeout handler:nil]; + XCTAssertEqualObjects(inMemoryUser.email, newEmailAddress); + XCTAssertNil(rpcError); + XCTAssertNil(auth.currentUser); +} + +@end diff --git a/Example/Auth/ApiTests/EmailPasswordAuthTests.m b/Example/Auth/ApiTests/EmailPasswordAuthTests.m new file mode 100644 index 00000000000..f6dade914ed --- /dev/null +++ b/Example/Auth/ApiTests/EmailPasswordAuthTests.m @@ -0,0 +1,93 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRAuthApiTestsBase.h" + +/** The testing email address for testCreateAccountWithEmailAndPassword. */ +static NSString *const kNewEmailToCreateUser = @"new_user@iosapitests.com"; + +/** The testing email address for testSignInExistingUserWithEmailAndPassword. */ +static NSString *const kExistingEmailToSignIn = @"existing_user@iosapitests.com"; + +/** The testing password for testSignInExistingUserWithEmailAndPassword. */ +static NSString *const kExistingPasswordToSignIn = @"password"; + +@interface EmailPasswordAuthTests : FIRAuthApiTestsBase + +@end + +@implementation EmailPasswordAuthTests + +- (void)testCreateAccountWithEmailAndPassword { + SKIP_IF_ON_MOBILE_HARNESS + FIRAuth *auth = [FIRAuth auth]; + if (!auth) { + XCTFail(@"Could not obtain auth object."); + } + XCTestExpectation *expectation = + [self expectationWithDescription:@"Created account with email and password."]; + [auth createUserWithEmail:kNewEmailToCreateUser + password:@"password" + completion:^(FIRAuthDataResult *result, NSError *error) { + if (error) { + NSLog(@"createUserWithEmail has error: %@", error); + } + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationsTimeout + handler:^(NSError *error) { + if (error != nil) { + XCTFail(@"Failed to wait for expectations " + @"in creating account. Error: %@", + error.localizedDescription); + } + }]; + + XCTAssertEqualObjects(auth.currentUser.email, kNewEmailToCreateUser); + + [self deleteCurrentUser]; +} + +- (void)testSignInExistingUserWithEmailAndPassword { + FIRAuth *auth = [FIRAuth auth]; + if (!auth) { + XCTFail(@"Could not obtain auth object."); + } + XCTestExpectation *expectation = + [self expectationWithDescription:@"Signed in existing account with email and password."]; + [auth signInWithEmail:kExistingEmailToSignIn + password:kExistingPasswordToSignIn + completion:^(FIRAuthDataResult *user, NSError *error) { + if (error) { + NSLog(@"Signing in existing account has error: %@", error); + } + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationsTimeout + handler:^(NSError *error) { + if (error != nil) { + XCTFail(@"Failed to wait for expectations " + @"in signing in existing account. Error: %@", + error.localizedDescription); + } + }]; + + XCTAssertEqualObjects(auth.currentUser.email, kExistingEmailToSignIn); +} + +@end diff --git a/Example/Auth/ApiTests/FIRAuthApiTestsBase.h b/Example/Auth/ApiTests/FIRAuthApiTestsBase.h new file mode 100644 index 00000000000..57b06d5d85c --- /dev/null +++ b/Example/Auth/ApiTests/FIRAuthApiTestsBase.h @@ -0,0 +1,60 @@ +/* + * Copyright 2019 Google + * + * 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 +#import "AuthCredentials.h" +#import "FirebaseAuth.h" + +#ifdef NO_NETWORK +#import "ITUIOSTestUtil.h" +#endif + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#ifdef NO_NETWORK +#define SKIP_IF_ON_MOBILE_HARNESS \ + if ([ITUIOSTestUtil isOnMobileHarness]) { \ + NSLog(@"Skipping '%@' on mobile harness", NSStringFromSelector(_cmd)); \ + return; \ + } +#else +#define SKIP_IF_ON_MOBILE_HARNESS +#endif + +static NSTimeInterval const kExpectationsTimeout = 10; + +@interface FIRAuthApiTestsBase : XCTestCase + +/** Sign in anonymously. */ +- (void)signInAnonymously; + +/** Sign out current account. */ +- (void)signOut; + +/** Clean up the created user for tests' future runs. */ +- (void)deleteCurrentUser; + +/** Generate fake random email address */ +- (NSString *)fakeRandomEmail; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/Auth/ApiTests/FIRAuthApiTestsBase.m b/Example/Auth/ApiTests/FIRAuthApiTestsBase.m new file mode 100644 index 00000000000..d67bb2d4720 --- /dev/null +++ b/Example/Auth/ApiTests/FIRAuthApiTestsBase.m @@ -0,0 +1,100 @@ +/* + * Copyright 2019 Google + * + * 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/LICENSE2.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 "FIRAuthApiTestsBase.h" + +@implementation FIRAuthApiTestsBase + +- (void)setUp { + [super setUp]; + + [self signOut]; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)signInAnonymously { + FIRAuth *auth = [FIRAuth auth]; + if (!auth) { + XCTFail(@"Could not obtain auth object."); + } + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Anonymousy sign-in finished."]; + [auth signInAnonymouslyWithCompletion:^(FIRAuthDataResult *result, NSError *error) { + if (error) { + NSLog(@"Anonymousy sign in error: %@", error); + } + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationsTimeout + handler:^(NSError *error) { + if (error != nil) { + XCTFail(@"Failed to wait for expectations " + @"in anonymousy sign in. Error: %@", + error.localizedDescription); + } + }]; +} + +- (void)signOut { + NSError *signOutError; + BOOL status = [[FIRAuth auth] signOut:&signOutError]; + + // Just log the error because we don't want to fail the test if signing out + // fails. + if (!status) { + NSLog(@"Error signing out: %@", signOutError); + } +} + +- (void)deleteCurrentUser { + FIRAuth *auth = [FIRAuth auth]; + if (!auth) { + NSLog(@"Could not obtain auth object."); + } + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Delete current user finished."]; + [auth.currentUser deleteWithCompletion:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Failed to delete user. Error: %@.", error); + } + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationsTimeout + handler:^(NSError *error) { + if (error != nil) { + XCTFail(@"Failed to wait for expectations " + @"in deleting user. Error: %@", + error.localizedDescription); + } + }]; +} + +- (NSString *)fakeRandomEmail { + NSMutableString *fakeEmail = [[NSMutableString alloc] init]; + for (int i = 0; i < 10; i++) { + [fakeEmail + appendString:[NSString stringWithFormat:@"%c", 'a' + arc4random_uniform('z' - 'a' + 1)]]; + } + [fakeEmail appendString:@"@gmail.com"]; + return fakeEmail; +} + +@end diff --git a/Example/Auth/ApiTests/FacebookAuthTests.m b/Example/Auth/ApiTests/FacebookAuthTests.m new file mode 100644 index 00000000000..ba865bbb5f0 --- /dev/null +++ b/Example/Auth/ApiTests/FacebookAuthTests.m @@ -0,0 +1,204 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRAuthApiTestsBase.h" + +/** Facebook app access token that will be used for Facebook Graph API, which is different from + * account access token. + */ +static NSString *const kFacebookAppAccessToken = KFACEBOOK_APP_ACCESS_TOKEN; + +/** Facebook app ID that will be used for Facebook Graph API. */ +static NSString *const kFacebookAppID = KFACEBOOK_APP_ID; + +static NSString *const kFacebookGraphApiAuthority = @"graph.facebook.com"; + +static NSString *const kFacebookTestAccountName = KFACEBOOK_USER_NAME; + +@interface FacebookAuthTests : FIRAuthApiTestsBase + +@end + +@implementation FacebookAuthTests + +- (void)DISABLE_testSignInWithFaceboook { + FIRAuth *auth = [FIRAuth auth]; + if (!auth) { + XCTFail(@"Could not obtain auth object."); + } + + NSDictionary *userInfoDict = [self createFacebookTestingAccount]; + NSString *facebookAccessToken = userInfoDict[@"access_token"]; + NSLog(@"Facebook testing account access token is: %@", facebookAccessToken); + NSString *facebookAccountId = userInfoDict[@"id"]; + NSLog(@"Facebook testing account id is: %@", facebookAccountId); + + FIRAuthCredential *credential = + [FIRFacebookAuthProvider credentialWithAccessToken:facebookAccessToken]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Facebook sign-in finished."]; + + [auth signInWithCredential:credential + completion:^(FIRUser *user, NSError *error) { + if (error) { + NSLog(@"Facebook sign in error: %@", error); + } + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kExpectationsTimeout + handler:^(NSError *error) { + if (error != nil) { + XCTFail(@"Failed to wait for expectations " + @"in Facebook sign in. Error: %@", + error.localizedDescription); + } + }]; + XCTAssertEqualObjects(auth.currentUser.displayName, kFacebookTestAccountName); + + // Clean up the created Firebase/Facebook user for future runs. + [self deleteCurrentUser]; + [self deleteFacebookTestingAccountbyId:facebookAccountId]; +} + +- (void)DISABLE_testLinkAnonymousAccountToFacebookAccount { + FIRAuth *auth = [FIRAuth auth]; + if (!auth) { + XCTFail(@"Could not obtain auth object."); + } + [self signInAnonymously]; + + NSDictionary *userInfoDict = [self createFacebookTestingAccount]; + NSString *facebookAccessToken = userInfoDict[@"access_token"]; + NSLog(@"Facebook testing account access token is: %@", facebookAccessToken); + NSString *facebookAccountId = userInfoDict[@"id"]; + NSLog(@"Facebook testing account id is: %@", facebookAccountId); + + FIRAuthCredential *credential = + [FIRFacebookAuthProvider credentialWithAccessToken:facebookAccessToken]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Facebook linking finished."]; + [auth.currentUser linkWithCredential:credential + completion:^(FIRUser *user, NSError *error) { + if (error) { + NSLog(@"Link to Facebok error: %@", error); + } + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kExpectationsTimeout + handler:^(NSError *error) { + if (error != nil) { + XCTFail(@"Failed to wait for expectations " + @"in linking to Facebook. Error: %@", + error.localizedDescription); + } + }]; + NSArray> *providerData = auth.currentUser.providerData; + XCTAssertEqual([providerData count], 1); + XCTAssertEqualObjects([providerData[0] providerID], @"facebook.com"); + + // Clean up the created Firebase/Facebook user for future runs. + [self deleteCurrentUser]; + [self deleteFacebookTestingAccountbyId:facebookAccountId]; +} + +/** Creates a Facebook testing account using Facebook Graph API and return a dictionary that + * constains "id", "access_token", "login_url", "email" and "password" of the created account. + */ +- (NSDictionary *)createFacebookTestingAccount { + // Build the URL. + NSString *urltoCreateTestUser = + [NSString stringWithFormat:@"https://%@/%@/accounts/test-users", kFacebookGraphApiAuthority, + kFacebookAppID]; + // Build the POST request. + NSString *bodyString = + [NSString stringWithFormat:@"installed=true&name=%@&permissions=read_stream&access_token=%@", + kFacebookTestAccountName, kFacebookAppAccessToken]; + NSData *postData = [bodyString dataUsingEncoding:NSUTF8StringEncoding]; + GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init]; + GTMSessionFetcher *fetcher = [service fetcherWithURLString:urltoCreateTestUser]; + fetcher.bodyData = postData; + [fetcher setRequestValue:@"text/plain" forHTTPHeaderField:@"Content-Type"]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Creating Facebook account finished."]; + __block NSData *data = nil; + [fetcher beginFetchWithCompletionHandler:^(NSData *receivedData, NSError *error) { + if (error) { + NSLog(@"Creating Facebook account finished with error: %@", error); + return; + } + data = receivedData; + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationsTimeout + handler:^(NSError *error) { + if (error != nil) { + XCTFail(@"Failed to wait for expectations " + @"in creating Facebook account. Error: %@", + error.localizedDescription); + } + }]; + NSString *userInfo = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSLog(@"The info of created Facebook testing account is: %@", userInfo); + // Parses the access token from the JSON data. + NSDictionary *userInfoDict = [NSJSONSerialization JSONObjectWithData:data + options:kNilOptions + error:nil]; + return userInfoDict; +} + +/** Delete a Facebook testing account by account Id using Facebook Graph API. */ +- (void)deleteFacebookTestingAccountbyId:(NSString *)accountId { + // Build the URL. + NSString *urltoDeleteTestUser = + [NSString stringWithFormat:@"https://%@/%@", kFacebookGraphApiAuthority, accountId]; + + // Build the POST request. + NSString *bodyString = + [NSString stringWithFormat:@"method=delete&access_token=%@", kFacebookAppAccessToken]; + NSData *postData = [bodyString dataUsingEncoding:NSUTF8StringEncoding]; + GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init]; + GTMSessionFetcher *fetcher = [service fetcherWithURLString:urltoDeleteTestUser]; + fetcher.bodyData = postData; + [fetcher setRequestValue:@"text/plain" forHTTPHeaderField:@"Content-Type"]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Deleting Facebook account finished."]; + [fetcher beginFetchWithCompletionHandler:^(NSData *receivedData, NSError *error) { + NSString *deleteResult = [[NSString alloc] initWithData:receivedData + encoding:NSUTF8StringEncoding]; + NSLog(@"The result of deleting Facebook account is: %@", deleteResult); + if (error) { + NSLog(@"Deleting Facebook account finished with error: %@", error); + } + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kExpectationsTimeout + handler:^(NSError *error) { + if (error != nil) { + XCTFail(@"Failed to wait for expectations " + @"in deleting Facebook account. Error: %@", + error.localizedDescription); + } + }]; +} + +@end diff --git a/Example/Auth/ApiTests/FirebaseAuthApiTests.m b/Example/Auth/ApiTests/FirebaseAuthApiTests.m deleted file mode 100644 index 5aa51708ba1..00000000000 --- a/Example/Auth/ApiTests/FirebaseAuthApiTests.m +++ /dev/null @@ -1,674 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#import -#import "FirebaseAuth.h" -#import "AuthCredentials.h" - -#ifdef NO_NETWORK -#import "ITUIOSTestUtil.h" -#endif - -#import -#import - -/** The user name string for Custom Auth testing account. */ -static NSString *const kCustomAuthTestingAccountUserID = KCUSTOM_AUTH_USER_ID; - -/** The url for obtaining a valid custom token string used to test Custom Auth. */ -static NSString *const kCustomTokenUrl = KCUSTOM_AUTH_TOKEN_URL; - -/** The url for obtaining an expired but valid custom token string used to test Custom Auth failure. - */ -static NSString *const kExpiredCustomTokenUrl = KCUSTOM_AUTH_TOKEN_EXPIRED_URL; - -/** Facebook app access token that will be used for Facebook Graph API, which is different from - * account access token. - */ -static NSString *const kFacebookAppAccessToken = KFACEBOOK_APP_ACCESS_TOKEN; - -/** Facebook app ID that will be used for Facebook Graph API. */ -static NSString *const kFacebookAppID = KFACEBOOK_APP_ID; - -static NSString *const kFacebookGraphApiAuthority = @"graph.facebook.com"; - -static NSString *const kFacebookTestAccountName = KFACEBOOK_USER_NAME; - -static NSString *const kGoogleTestAccountName = KGOOGLE_USER_NAME; - -/** The invalid custom token string for testing Custom Auth. */ -static NSString *const kInvalidCustomToken = @"invalid token."; - -/** The testing email address for testCreateAccountWithEmailAndPassword. */ -static NSString *const kTestingEmailToCreateUser = @"abc@xyz.com"; - -/** The testing email address for testSignInExistingUserWithEmailAndPassword. */ -static NSString *const kExistingTestingEmailToSignIn = @"456@abc.com"; - -/** The testing email address for testUpdatingUsersEmail. */ -static NSString *const kNewTestingEmail = @"updatedEmail@abc.com"; - -/** The testing password for testSignInExistingUserWithModifiedEmailAndPassword. */ -static NSString *const kNewTestingPasswordToSignIn = @"password_new"; - -/** Error message for invalid custom token sign in. */ -NSString *kInvalidTokenErrorMessage = - @"The custom token format is incorrect. Please check the documentation."; - -NSString *kGoogleCliendId = KGOOGLE_CLIENT_ID; - -/** Refresh token of Google test account to exchange for access token. Refresh token never expires - * unless user revokes it. If this refresh token expires, tests in record mode will fail and this - * token needs to be updated. - */ -NSString *kGoogleTestAccountRefreshToken = KGOOGLE_TEST_ACCOUNT_REFRESH_TOKEN; - -static NSTimeInterval const kExpectationsTimeout = 10; - -#ifdef NO_NETWORK -#define SKIP_IF_ON_MOBILE_HARNESS \ - if ([ITUIOSTestUtil isOnMobileHarness]) { \ - NSLog(@"Skipping '%@' on mobile harness", NSStringFromSelector(_cmd)); \ - return; \ - } -#else -#define SKIP_IF_ON_MOBILE_HARNESS -#endif - -@interface ApiTests : XCTestCase - -@end - -@implementation ApiTests - -/** To reset the app so that each test sees the app in a clean state. */ -- (void)setUp { - [super setUp]; - [self signOut]; -} - -#pragma mark - Tests - -/** - * This test runs in replay mode by default. To run in a different mode follow the instructions - * below. - * - * Blaze: --test_arg=\'--networkReplayMode=(replay|record|disabled|observe)\' - * - * Xcode: - * Update the following flag in the xcscheme. - * --networkReplayMode=(replay|record|disabled|observe) - */ -- (void)testCreateAccountWithEmailAndPassword { - SKIP_IF_ON_MOBILE_HARNESS - FIRAuth *auth = [FIRAuth auth]; - if (!auth) { - XCTFail(@"Could not obtain auth object."); - } - XCTestExpectation *expectation = - [self expectationWithDescription:@"Created account with email and password."]; - [auth createUserWithEmail:kTestingEmailToCreateUser - password:@"password" - completion:^(FIRAuthDataResult *result, NSError *error) { - if (error) { - NSLog(@"createUserWithEmail has error: %@", error); - } - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kExpectationsTimeout - handler:^(NSError *error) { - if (error != nil) { - XCTFail(@"Failed to wait for expectations " - @"in creating account. Error: %@", - error.localizedDescription); - } - }]; - - XCTAssertEqualObjects(auth.currentUser.email, kTestingEmailToCreateUser); - - // Clean up the created Firebase user for future runs. - [self deleteCurrentFirebaseUser]; -} - -- (void)testUpdatingUsersEmail { - SKIP_IF_ON_MOBILE_HARNESS - FIRAuth *auth = [FIRAuth auth]; - if (!auth) { - XCTFail(@"Could not obtain auth object."); - } - - __block NSError *apiError; - XCTestExpectation *expectation = - [self expectationWithDescription:@"Created account with email and password."]; - [auth createUserWithEmail:kTestingEmailToCreateUser - password:@"password" - completion:^(FIRAuthDataResult *user, NSError *error) { - apiError = error; - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kExpectationsTimeout handler:nil]; - expectation = [self expectationWithDescription:@"Created account with email and password."]; - XCTAssertEqualObjects(auth.currentUser.email, kTestingEmailToCreateUser); - XCTAssertNil(apiError); - [auth.currentUser updateEmail:kNewTestingEmail - completion:^(NSError *_Nullable error) { - apiError = error; - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kExpectationsTimeout handler:nil]; - XCTAssertNil(apiError); - XCTAssertEqualObjects(auth.currentUser.email, kNewTestingEmail); - // Clean up the created Firebase user for future runs. - [self deleteCurrentFirebaseUser]; -} - -- (void)testLinkAnonymousAccountToFacebookAccount { - FIRAuth *auth = [FIRAuth auth]; - if (!auth) { - XCTFail(@"Could not obtain auth object."); - } - [self signInAnonymously]; - - NSDictionary *userInfoDict = [self createFacebookTestingAccount]; - NSString *facebookAccessToken = userInfoDict[@"access_token"]; - NSLog(@"Facebook testing account access token is: %@", facebookAccessToken); - NSString *facebookAccountId = userInfoDict[@"id"]; - NSLog(@"Facebook testing account id is: %@", facebookAccountId); - - FIRAuthCredential *credential = - [FIRFacebookAuthProvider credentialWithAccessToken:facebookAccessToken]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Facebook linking finished."]; - [auth.currentUser linkWithCredential:credential - completion:^(FIRUser *user, NSError *error) { - if (error) { - NSLog(@"Link to Facebok error: %@", error); - } - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kExpectationsTimeout - handler:^(NSError *error) { - if (error != nil) { - XCTFail(@"Failed to wait for expectations " - @"in linking to Facebook. Error: %@", - error.localizedDescription); - } - }]; - NSArray> *providerData = auth.currentUser.providerData; - XCTAssertEqual([providerData count], 1); - XCTAssertEqualObjects([providerData[0] providerID], @"facebook.com"); - - // Clean up the created Firebase/Facebook user for future runs. - [self deleteCurrentFirebaseUser]; - [self deleteFacebookTestingAccountbyId:facebookAccountId]; -} - -- (void)testSignInAnonymously { - [self signInAnonymously]; - XCTAssertTrue([FIRAuth auth].currentUser.anonymous); - [self deleteCurrentFirebaseUser]; -} - -- (void)testSignInExistingUserWithEmailAndPassword { - FIRAuth *auth = [FIRAuth auth]; - if (!auth) { - XCTFail(@"Could not obtain auth object."); - } - XCTestExpectation *expectation = - [self expectationWithDescription:@"Signed in existing account with email and password."]; - [auth signInWithEmail:kExistingTestingEmailToSignIn - password:@"password" - completion:^(FIRAuthDataResult *user, NSError *error) { - if (error) { - NSLog(@"Signing in existing account has error: %@", error); - } - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kExpectationsTimeout - handler:^(NSError *error) { - if (error != nil) { - XCTFail(@"Failed to wait for expectations " - @"in signing in existing account. Error: %@", - error.localizedDescription); - } - }]; - - XCTAssertEqualObjects(auth.currentUser.email, kExistingTestingEmailToSignIn); -} - -- (void)testSignInWithValidCustomAuthToken { - FIRAuth *auth = [FIRAuth auth]; - if (!auth) { - XCTFail(@"Could not obtain auth object."); - } - - NSError *error; - NSString *customToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:kCustomTokenUrl] - encoding:NSUTF8StringEncoding - error:&error]; - if (!customToken) { - XCTFail(@"There was an error retrieving the custom token: %@", error); - } - NSLog(@"The valid token is: %@", customToken); - - XCTestExpectation *expectation = - [self expectationWithDescription:@"CustomAuthToken sign-in finished."]; - - [auth signInWithCustomToken:customToken - completion:^(FIRAuthDataResult *_Nullable result, NSError *_Nullable error) { - if (error) { - NSLog(@"Valid token sign in error: %@", error); - } - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kExpectationsTimeout - handler:^(NSError *error) { - if (error != nil) { - XCTFail(@"Failed to wait for expectations " - @"in CustomAuthToken sign in. Error: %@", - error.localizedDescription); - } - }]; - - XCTAssertEqualObjects(auth.currentUser.uid, kCustomAuthTestingAccountUserID); -} - -- (void)testSignInWithValidCustomAuthExpiredToken { - FIRAuth *auth = [FIRAuth auth]; - if (!auth) { - XCTFail(@"Could not obtain auth object."); - } - - NSError *error; - NSString *customToken = - [NSString stringWithContentsOfURL:[NSURL URLWithString:kExpiredCustomTokenUrl] - encoding:NSUTF8StringEncoding - error:&error]; - if (!customToken) { - XCTFail(@"There was an error retrieving the custom token: %@", error); - } - XCTestExpectation *expectation = - [self expectationWithDescription:@"CustomAuthToken sign-in finished."]; - - __block NSError *apiError; - [auth signInWithCustomToken:customToken - completion:^(FIRAuthDataResult *_Nullable result, NSError *_Nullable error) { - if (error) { - apiError = error; - } - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kExpectationsTimeout - handler:^(NSError *error) { - if (error != nil) { - XCTFail(@"Failed to wait for expectations " - @"in CustomAuthToken sign in. Error: %@", - error.localizedDescription); - } - }]; - - XCTAssertNil(auth.currentUser); - XCTAssertEqual(apiError.code, FIRAuthErrorCodeInvalidCustomToken); -} - -- (void)testInMemoryUserAfterSignOut { - FIRAuth *auth = [FIRAuth auth]; - if (!auth) { - XCTFail(@"Could not obtain auth object."); - } - NSError *error; - NSString *customToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:kCustomTokenUrl] - encoding:NSUTF8StringEncoding - error:&error]; - if (!customToken) { - XCTFail(@"There was an error retrieving the custom token: %@", error); - } - XCTestExpectation *expectation = - [self expectationWithDescription:@"CustomAuthToken sign-in finished."]; - __block NSError *rpcError; - [auth signInWithCustomToken:customToken - completion:^(FIRAuthDataResult *_Nullable result, NSError *_Nullable error) { - if (error) { - rpcError = error; - } - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kExpectationsTimeout - handler:^(NSError *error) { - if (error != nil) { - XCTFail(@"Failed to wait for expectations " - @"in CustomAuthToken sign in. Error: %@", - error.localizedDescription); - } - }]; - XCTAssertEqualObjects(auth.currentUser.uid, kCustomAuthTestingAccountUserID); - XCTAssertNil(rpcError); - FIRUser *inMemoryUser = auth.currentUser; - XCTestExpectation *expectation1 = [self expectationWithDescription:@"Profile data change."]; - [auth signOut:NULL]; - rpcError = nil; - NSString *newEmailAddress = [self fakeRandomEmail]; - XCTAssertNotEqualObjects(newEmailAddress, inMemoryUser.email); - [inMemoryUser updateEmail:newEmailAddress completion:^(NSError *_Nullable error) { - rpcError = error; - [expectation1 fulfill]; - }]; - [self waitForExpectationsWithTimeout:kExpectationsTimeout handler:nil]; - XCTAssertEqualObjects(inMemoryUser.email, newEmailAddress); - XCTAssertNil(rpcError); - XCTAssertNil(auth.currentUser); -} - -- (void)testSignInWithInvalidCustomAuthToken { - FIRAuth *auth = [FIRAuth auth]; - if (!auth) { - XCTFail(@"Could not obtain auth object."); - } - XCTestExpectation *expectation = - [self expectationWithDescription:@"Invalid CustomAuthToken sign-in finished."]; - - [auth signInWithCustomToken:kInvalidCustomToken - completion:^(FIRAuthDataResult *_Nullable result, NSError *_Nullable error) { - - XCTAssertEqualObjects(error.localizedDescription, kInvalidTokenErrorMessage); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kExpectationsTimeout - handler:^(NSError *error) { - if (error != nil) { - XCTFail(@"Failed to wait for expectations " - @"in CustomAuthToken sign in. Error: %@", - error.localizedDescription); - } - }]; -} - -- (void)testSignInWithFaceboook { - FIRAuth *auth = [FIRAuth auth]; - if (!auth) { - XCTFail(@"Could not obtain auth object."); - } - - NSDictionary *userInfoDict = [self createFacebookTestingAccount]; - NSString *facebookAccessToken = userInfoDict[@"access_token"]; - NSLog(@"Facebook testing account access token is: %@", facebookAccessToken); - NSString *facebookAccountId = userInfoDict[@"id"]; - NSLog(@"Facebook testing account id is: %@", facebookAccountId); - - FIRAuthCredential *credential = - [FIRFacebookAuthProvider credentialWithAccessToken:facebookAccessToken]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Facebook sign-in finished."]; - - [auth signInWithCredential:credential - completion:^(FIRUser *user, NSError *error) { - if (error) { - NSLog(@"Facebook sign in error: %@", error); - } - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kExpectationsTimeout - handler:^(NSError *error) { - if (error != nil) { - XCTFail(@"Failed to wait for expectations " - @"in Facebook sign in. Error: %@", - error.localizedDescription); - } - }]; - XCTAssertEqualObjects(auth.currentUser.displayName, kFacebookTestAccountName); - - // Clean up the created Firebase/Facebook user for future runs. - [self deleteCurrentFirebaseUser]; - [self deleteFacebookTestingAccountbyId:facebookAccountId]; -} - -- (void)testSignInWithGoogle { - FIRAuth *auth = [FIRAuth auth]; - if (!auth) { - XCTFail(@"Could not obtain auth object."); - } - NSDictionary *userInfoDict = [self getGoogleAccessToken]; - NSString *googleAccessToken = userInfoDict[@"access_token"]; - NSString *googleIdToken = userInfoDict[@"id_token"]; - FIRAuthCredential *credential = - [FIRGoogleAuthProvider credentialWithIDToken:googleIdToken accessToken:googleAccessToken]; - - XCTestExpectation *expectation = - [self expectationWithDescription:@"Signing in with Google finished."]; - [auth signInWithCredential:credential - completion:^(FIRUser *user, NSError *error) { - if (error) { - NSLog(@"Signing in with Google had error: %@", error); - } - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kExpectationsTimeout - handler:^(NSError *error) { - if (error != nil) { - XCTFail(@"Failed to wait for expectations " - @"in Signing in with Google. Error: %@", - error.localizedDescription); - } - }]; - XCTAssertEqualObjects(auth.currentUser.displayName, kGoogleTestAccountName); - - // Clean up the created Firebase/Facebook user for future runs. - [self deleteCurrentFirebaseUser]; -} - -#pragma mark - Helpers - -/** Generate fake random email address */ -- (NSString *)fakeRandomEmail { - NSMutableString *fakeEmail = [[NSMutableString alloc] init]; - for (int i=0; i<10; i++) { - [fakeEmail appendString: - [NSString stringWithFormat:@"%c", 'a' + arc4random_uniform('z' - 'a' + 1)]]; - } - [fakeEmail appendString:@"@gmail.com"]; - return fakeEmail; -} - -/** Sign out current account. */ -- (void)signOut { - NSError *signOutError; - BOOL status = [[FIRAuth auth] signOut:&signOutError]; - - // Just log the error because we don't want to fail the test if signing out - // fails. - if (!status) { - NSLog(@"Error signing out: %@", signOutError); - } -} - -/** Creates a Facebook testing account using Facebook Graph API and return a dictionary that - * constains "id", "access_token", "login_url", "email" and "password" of the created account. - */ -- (NSDictionary *)createFacebookTestingAccount { - // Build the URL. - NSString *urltoCreateTestUser = - [NSString stringWithFormat:@"https://%@/%@/accounts/test-users", kFacebookGraphApiAuthority, - kFacebookAppID]; - // Build the POST request. - NSString *bodyString = - [NSString stringWithFormat:@"installed=true&name=%@&permissions=read_stream&access_token=%@", - kFacebookTestAccountName, kFacebookAppAccessToken]; - NSData *postData = [bodyString dataUsingEncoding:NSUTF8StringEncoding]; - GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init]; - GTMSessionFetcher *fetcher = [service fetcherWithURLString:urltoCreateTestUser]; - fetcher.bodyData = postData; - [fetcher setRequestValue:@"text/plain" forHTTPHeaderField:@"Content-Type"]; - - XCTestExpectation *expectation = - [self expectationWithDescription:@"Creating Facebook account finished."]; - __block NSData *data = nil; - [fetcher beginFetchWithCompletionHandler:^(NSData *receivedData, NSError *error) { - if (error) { - NSLog(@"Creating Facebook account finished with error: %@", error); - return; - } - data = receivedData; - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kExpectationsTimeout - handler:^(NSError *error) { - if (error != nil) { - XCTFail(@"Failed to wait for expectations " - @"in creating Facebook account. Error: %@", - error.localizedDescription); - } - }]; - NSString *userInfo = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - NSLog(@"The info of created Facebook testing account is: %@", userInfo); - // Parses the access token from the JSON data. - NSDictionary *userInfoDict = - [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]; - return userInfoDict; -} - -/** Clean up the created user for tests' future runs. */ -- (void)deleteCurrentFirebaseUser { - FIRAuth *auth = [FIRAuth auth]; - if (!auth) { - NSLog(@"Could not obtain auth object."); - } - - XCTestExpectation *expectation = - [self expectationWithDescription:@"Delete current user finished."]; - [auth.currentUser deleteWithCompletion:^(NSError *_Nullable error) { - if (error) { - XCTFail(@"Failed to delete user. Error: %@.", error); - } - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kExpectationsTimeout - handler:^(NSError *error) { - if (error != nil) { - XCTFail(@"Failed to wait for expectations " - @"in deleting user. Error: %@", - error.localizedDescription); - } - }]; -} - -- (void)signInAnonymously { - FIRAuth *auth = [FIRAuth auth]; - if (!auth) { - XCTFail(@"Could not obtain auth object."); - } - - XCTestExpectation *expectation = - [self expectationWithDescription:@"Anonymousy sign-in finished."]; - [auth signInAnonymouslyWithCompletion:^(FIRAuthDataResult *result, NSError *error) { - if (error) { - NSLog(@"Anonymousy sign in error: %@", error); - } - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kExpectationsTimeout - handler:^(NSError *error) { - if (error != nil) { - XCTFail(@"Failed to wait for expectations " - @"in anonymousy sign in. Error: %@", - error.localizedDescription); - } - }]; -} - -/** Delete a Facebook testing account by account Id using Facebook Graph API. */ -- (void)deleteFacebookTestingAccountbyId:(NSString *)accountId { - // Build the URL. - NSString *urltoDeleteTestUser = - [NSString stringWithFormat:@"https://%@/%@", kFacebookGraphApiAuthority, accountId]; - - // Build the POST request. - NSString *bodyString = - [NSString stringWithFormat:@"method=delete&access_token=%@", kFacebookAppAccessToken]; - NSData *postData = [bodyString dataUsingEncoding:NSUTF8StringEncoding]; - GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init]; - GTMSessionFetcher *fetcher = [service fetcherWithURLString:urltoDeleteTestUser]; - fetcher.bodyData = postData; - [fetcher setRequestValue:@"text/plain" forHTTPHeaderField:@"Content-Type"]; - - XCTestExpectation *expectation = - [self expectationWithDescription:@"Deleting Facebook account finished."]; - [fetcher beginFetchWithCompletionHandler:^(NSData *receivedData, NSError *error) { - NSString *deleteResult = - [[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding]; - NSLog(@"The result of deleting Facebook account is: %@", deleteResult); - if (error) { - NSLog(@"Deleting Facebook account finished with error: %@", error); - } - [expectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:kExpectationsTimeout - handler:^(NSError *error) { - if (error != nil) { - XCTFail(@"Failed to wait for expectations " - @"in deleting Facebook account. Error: %@", - error.localizedDescription); - } - }]; -} - -/** Sends http request to Google OAuth2 token server to use refresh token to exchange for Google - * access token. Returns a dictionary that constains "access_token", "token_type", "expires_in" and - * "id_token". - */ -- (NSDictionary *)getGoogleAccessToken { - NSString *googleOauth2TokenServerUrl = @"https://www.googleapis.com/oauth2/v4/token"; - NSString *bodyString = - [NSString stringWithFormat:@"client_id=%@&grant_type=refresh_token&refresh_token=%@", - kGoogleCliendId, kGoogleTestAccountRefreshToken]; - NSData *postData = [bodyString dataUsingEncoding:NSUTF8StringEncoding]; - GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init]; - GTMSessionFetcher *fetcher = [service fetcherWithURLString:googleOauth2TokenServerUrl]; - fetcher.bodyData = postData; - [fetcher setRequestValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; - - XCTestExpectation *expectation = - [self expectationWithDescription:@"Exchanging Google account tokens finished."]; - __block NSData *data = nil; - [fetcher beginFetchWithCompletionHandler:^(NSData *receivedData, NSError *error) { - if (error) { - NSLog(@"Exchanging Google account tokens finished with error: %@", error); - return; - } - data = receivedData; - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kExpectationsTimeout - handler:^(NSError *error) { - if (error != nil) { - XCTFail(@"Failed to wait for expectations " - @"in exchanging Google account tokens. Error: %@", - error.localizedDescription); - } - }]; - NSString *userInfo = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - NSLog(@"The info of exchanged result is: %@", userInfo); - NSDictionary *userInfoDict = - [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]; - return userInfoDict; -} -@end diff --git a/Example/Auth/ApiTests/GoogleAuthTests.m b/Example/Auth/ApiTests/GoogleAuthTests.m new file mode 100644 index 00000000000..4611a1b8edf --- /dev/null +++ b/Example/Auth/ApiTests/GoogleAuthTests.m @@ -0,0 +1,114 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRAuthApiTestsBase.h" + +static NSString *kGoogleCliendId = KGOOGLE_CLIENT_ID; + +static NSString *const kGoogleTestAccountName = KGOOGLE_USER_NAME; + +/** Refresh token of Google test account to exchange for access token. Refresh token never expires + * unless user revokes it. If this refresh token expires, tests in record mode will fail and this + * token needs to be updated. + */ +NSString *kGoogleTestAccountRefreshToken = KGOOGLE_TEST_ACCOUNT_REFRESH_TOKEN; + +@interface GoogleAuthTests : FIRAuthApiTestsBase + +@end + +@implementation GoogleAuthTests + +- (void)testSignInWithGoogle { + FIRAuth *auth = [FIRAuth auth]; + if (!auth) { + XCTFail(@"Could not obtain auth object."); + } + NSDictionary *userInfoDict = [self getGoogleAccessToken]; + NSString *googleAccessToken = userInfoDict[@"access_token"]; + NSString *googleIdToken = userInfoDict[@"id_token"]; + FIRAuthCredential *credential = [FIRGoogleAuthProvider credentialWithIDToken:googleIdToken + accessToken:googleAccessToken]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Signing in with Google finished."]; + [auth signInWithCredential:credential + completion:^(FIRUser *user, NSError *error) { + if (error) { + NSLog(@"Signing in with Google had error: %@", error); + } + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kExpectationsTimeout + handler:^(NSError *error) { + if (error != nil) { + XCTFail(@"Failed to wait for expectations " + @"in Signing in with Google. Error: %@", + error.localizedDescription); + } + }]; + XCTAssertEqualObjects(auth.currentUser.displayName, kGoogleTestAccountName); + + // Clean up the created Firebase/Facebook user for future runs. + [self deleteCurrentUser]; +} + +/** Sends http request to Google OAuth2 token server to use refresh token to exchange for Google + * access token. Returns a dictionary that constains "access_token", "token_type", "expires_in" and + * "id_token". + */ +- (NSDictionary *)getGoogleAccessToken { + NSString *googleOauth2TokenServerUrl = @"https://www.googleapis.com/oauth2/v4/token"; + NSString *bodyString = + [NSString stringWithFormat:@"client_id=%@&grant_type=refresh_token&refresh_token=%@", + kGoogleCliendId, kGoogleTestAccountRefreshToken]; + NSData *postData = [bodyString dataUsingEncoding:NSUTF8StringEncoding]; + GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init]; + GTMSessionFetcher *fetcher = [service fetcherWithURLString:googleOauth2TokenServerUrl]; + fetcher.bodyData = postData; + [fetcher setRequestValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Exchanging Google account tokens finished."]; + __block NSData *data = nil; + [fetcher beginFetchWithCompletionHandler:^(NSData *receivedData, NSError *error) { + if (error) { + NSLog(@"Exchanging Google account tokens finished with error: %@", error); + return; + } + data = receivedData; + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationsTimeout + handler:^(NSError *error) { + if (error != nil) { + XCTFail(@"Failed to wait for expectations " + @"in exchanging Google account tokens. Error: %@", + error.localizedDescription); + } + }]; + NSString *userInfo = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSLog(@"The info of exchanged result is: %@", userInfo); + NSDictionary *userInfoDict = [NSJSONSerialization JSONObjectWithData:data + options:kNilOptions + error:nil]; + return userInfoDict; +} + +@end diff --git a/Example/Auth/ApiTests/GoogleAuthTests.swift b/Example/Auth/ApiTests/GoogleAuthTests.swift new file mode 100644 index 00000000000..9a2126ca614 --- /dev/null +++ b/Example/Auth/ApiTests/GoogleAuthTests.swift @@ -0,0 +1,91 @@ +/* + * Copyright 2019 Google + * + * 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 + +class GoogleAuthTestsSwift: FIRAuthApiTestsBase { + let kGoogleCliendId = KGOOGLE_CLIENT_ID + + let kGoogleTestAccountName = KGOOGLE_USER_NAME + + let kGoogleTestAccountRefreshToken = KGOOGLE_TEST_ACCOUNT_REFRESH_TOKEN + + func testSignInWithGoogle() { + let auth = Auth.auth() + let userInfoDictOptional = getGoogleAccessToken() + if userInfoDictOptional == nil { + XCTFail("Could not obtain Google access token.") + } + let userInfoDict = userInfoDictOptional! + let googleAccessToken = userInfoDict["access_token"] as! String + let googleIdToken = userInfoDict["id_token"] as! String + let credential = GoogleAuthProvider.credential(withIDToken: googleIdToken, accessToken: googleAccessToken) + + let expectation = self.expectation(description: "Signing in with Google finished.") + auth.signInAndRetrieveData(with: credential) { _, error in + if error != nil { + print("Signing in with Google had error: %@", error!) + } + expectation.fulfill() + } + + waitForExpectations(timeout: kExpectationsTimeout) { error in + if error != nil { + XCTFail(String(format: "Failed to wait for expectations in Signing in with Google. Error: %@", error!.localizedDescription)) + } + } + + XCTAssertEqual(auth.currentUser?.displayName, kGoogleTestAccountName) + + deleteCurrentUser() + } + + func getGoogleAccessToken() -> [String: Any]? { + let googleOauth2TokenServerUrl = "https://www.googleapis.com/oauth2/v4/token" + let bodyString = String(format: "client_id=%@&grant_type=refresh_token&refresh_token=%@", kGoogleCliendId, kGoogleTestAccountRefreshToken) + let postData = bodyString.data(using: .utf8) + let service = GTMSessionFetcherService() + let fetcher = service.fetcher(withURLString: googleOauth2TokenServerUrl) + fetcher.bodyData = postData + fetcher.setRequestValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + + let expectation = self.expectation(description: "Exchanging Google account tokens finished.") + var data: Data? + fetcher.beginFetch { receivedData, error in + if error != nil { + print("Exchanging Google account tokens finished with error: %@", error!) + return + } + data = receivedData + expectation.fulfill() + } + + waitForExpectations(timeout: kExpectationsTimeout) { error in + if error != nil { + XCTFail(String(format: "Failed to wait for expectations in exchanging Google account tokens. Error: %@", error!.localizedDescription)) + } + } + + let userInfo = String(data: data!, encoding: .utf8) + print("The info of exchanged result is: \(userInfo ?? "")") + let rawJsonObject = try? JSONSerialization.jsonObject(with: data!, options: []) + if let userInfoDict = rawJsonObject as? [String: Any] { + return userInfoDict + } else { + return nil + } + } +} diff --git a/Example/Auth/E2eTests/BYOAuthTests.m b/Example/Auth/E2eTests/BYOAuthTests.m new file mode 100644 index 00000000000..60f0904d2b0 --- /dev/null +++ b/Example/Auth/E2eTests/BYOAuthTests.m @@ -0,0 +1,86 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRAuthE2eTestsBase.h" + +/** The url for obtaining a valid custom token string used to test BYOAuth. */ +static NSString *const kCustomTokenUrl = @"https://fb-sa-1211.appspot.com/token"; + +/** The invalid custom token string for testing BYOAuth. */ +static NSString *const kInvalidCustomToken = @"invalid token."; + +/** The user name string for BYOAuth testing account. */ +static NSString *const kTestingAccountUserID = @"BYU_Test_User_ID"; + +@interface BYOAuthTests : FIRAuthE2eTestsBase + +@end + +@implementation BYOAuthTests + +/** Test sign in with a valid BYOAuth token retrived from a remote server. */ +- (void)testSignInWithValidBYOAuthToken { + NSError *error; + NSString *customToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:kCustomTokenUrl] + encoding:NSUTF8StringEncoding + error:&error]; + if (!customToken) { + GREYFail(@"There was an error retrieving the custom token: %@", error); + } + + [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(@"Sign In (BYOAuth)"), + grey_sufficientlyVisible(), nil)] + usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, kShortScrollDistance) + onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), + nil)] performAction:grey_tap()]; + + [[[EarlGrey selectElementWithMatcher:grey_kindOfClass([UITextView class])] + performAction:grey_replaceText(customToken)] assertWithMatcher:grey_text(customToken)]; + + [[EarlGrey selectElementWithMatcher:grey_text(@"Done")] performAction:grey_tap()]; + + [self waitForElementWithText:@"OK" withDelay:kWaitForElementTimeOut]; + + [[EarlGrey selectElementWithMatcher:grey_text(@"OK")] performAction:grey_tap()]; + + [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(kTestingAccountUserID), + grey_sufficientlyVisible(), nil)] + usingSearchAction:grey_scrollInDirection(kGREYDirectionUp, kShortScrollDistance) + onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), + nil)] assertWithMatcher:grey_sufficientlyVisible()]; +} + +- (void)testSignInWithInvalidBYOAuthToken { + [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(@"Sign In (BYOAuth)"), + grey_sufficientlyVisible(), nil)] + usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, kShortScrollDistance) + onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), + nil)] performAction:grey_tap()]; + + [[[EarlGrey selectElementWithMatcher:grey_kindOfClass([UITextView class])] + performAction:grey_replaceText(kInvalidCustomToken)] + assertWithMatcher:grey_text(kInvalidCustomToken)]; + + [[EarlGrey selectElementWithMatcher:grey_text(@"Done")] performAction:grey_tap()]; + + NSString *invalidTokenErrorMessage = @"Sign-In Error"; + + [self waitForElementWithText:invalidTokenErrorMessage withDelay:kWaitForElementTimeOut]; + + [[EarlGrey selectElementWithMatcher:grey_text(@"OK")] performAction:grey_tap()]; +} + +@end diff --git a/Example/Auth/E2eTests/FIRAuthE2eTests.m b/Example/Auth/E2eTests/FIRAuthE2eTests.m new file mode 100644 index 00000000000..f58e6bc850f --- /dev/null +++ b/Example/Auth/E2eTests/FIRAuthE2eTests.m @@ -0,0 +1,56 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRAuthE2eTestsBase.h" + +@interface FIRAuthE2eTests : FIRAuthE2eTestsBase + +@end + +@implementation FIRAuthE2eTests + +- (void)testSignInExistingUser { + NSString *email = @"123@abc.com"; + [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(@"Sign in with Email/Password"), + grey_sufficientlyVisible(), nil)] + usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, kShortScrollDistance) + onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), + nil)] performAction:grey_tap()]; + + id comfirmationButtonMatcher = + grey_allOf(grey_kindOfClass([UILabel class]), grey_accessibilityLabel(@"OK"), nil); + + [[EarlGrey selectElementWithMatcher: +#warning TODO Add accessibilityIdentifiers for the elements. + grey_kindOfClass(NSClassFromString(@"_UIAlertControllerView"))] + performAction:grey_typeText(email)]; + + [[EarlGrey selectElementWithMatcher:comfirmationButtonMatcher] performAction:grey_tap()]; + + [[EarlGrey + selectElementWithMatcher:grey_kindOfClass(NSClassFromString(@"_UIAlertControllerView"))] + performAction:grey_typeText(@"password")]; + + [[EarlGrey selectElementWithMatcher:comfirmationButtonMatcher] performAction:grey_tap()]; + + [[[EarlGrey + selectElementWithMatcher:grey_allOf(grey_text(email), grey_sufficientlyVisible(), nil)] + usingSearchAction:grey_scrollInDirection(kGREYDirectionUp, kShortScrollDistance) + onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), + nil)] assertWithMatcher:grey_sufficientlyVisible()]; +} + +@end diff --git a/Firestore/Source/Core/FSTListenSequence.mm b/Example/Auth/E2eTests/FIRAuthE2eTestsBase.h similarity index 51% rename from Firestore/Source/Core/FSTListenSequence.mm rename to Example/Auth/E2eTests/FIRAuthE2eTestsBase.h index f96568b6849..e1c97f5cdb1 100644 --- a/Firestore/Source/Core/FSTListenSequence.mm +++ b/Example/Auth/E2eTests/FIRAuthE2eTestsBase.h @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google + * Copyright 2019 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,38 +14,28 @@ * limitations under the License. */ -#import "Firestore/Source/Core/FSTListenSequence.h" +#import -using firebase::firestore::model::ListenSequenceNumber; +#import -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTListenSequence +#import "FirebaseAuth.h" -@interface FSTListenSequence () { - ListenSequenceNumber _previousSequenceNumber; -} +NS_ASSUME_NONNULL_BEGIN -@end +extern CGFloat const kShortScrollDistance; -@implementation FSTListenSequence +extern NSTimeInterval const kWaitForElementTimeOut; -#pragma mark - Constructors +/** Convenience function for EarlGrey tests. */ +id grey_scrollView(void); -- (instancetype)initStartingAfter:(ListenSequenceNumber)after { - self = [super init]; - if (self) { - _previousSequenceNumber = after; - } - return self; -} +@interface FIRAuthE2eTestsBase : XCTestCase -#pragma mark - Public methods +/** Sign out current account. */ +- (void)signOut; -- (ListenSequenceNumber)next { - _previousSequenceNumber++; - return _previousSequenceNumber; -} +/** Wait for an element with text to appear. */ +- (void)waitForElementWithText:(NSString *)text withDelay:(NSTimeInterval)maxDelay; @end diff --git a/Example/Auth/E2eTests/FIRAuthE2eTestsBase.m b/Example/Auth/E2eTests/FIRAuthE2eTestsBase.m new file mode 100644 index 00000000000..08fc7be79f8 --- /dev/null +++ b/Example/Auth/E2eTests/FIRAuthE2eTestsBase.m @@ -0,0 +1,70 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRAuthE2eTestsBase.h" + +CGFloat const kShortScrollDistance = 400; + +NSTimeInterval const kWaitForElementTimeOut = 15; + +/** Convenience function for EarlGrey tests. */ +id grey_scrollView(void) { + return [GREYMatchers matcherForKindOfClass:[UIScrollView class]]; +} + +@implementation FIRAuthE2eTestsBase + +/** To reset the app so that each test sees the app in a clean state. */ +- (void)setUp { + [super setUp]; + + [self signOut]; + + [[EarlGrey selectElementWithMatcher:grey_allOf(grey_scrollView(), + grey_kindOfClass([UITableView class]), nil)] + performAction:grey_scrollToContentEdge(kGREYContentEdgeTop)]; +} + +- (void)tearDown { + [super tearDown]; +} + +/** Sign out current account. */ +- (void)signOut { + NSError *signOutError; + BOOL status = [[FIRAuth auth] signOut:&signOutError]; + + // Just log the error because we don't want to fail the test if signing out fails. + if (!status) { + NSLog(@"Error signing out: %@", signOutError); + } +} + +/** Wait for an element with text to appear. */ +- (void)waitForElementWithText:(NSString *)text withDelay:(NSTimeInterval)maxDelay { + GREYCondition *displayed = + [GREYCondition conditionWithName:@"Wait for element" + block:^BOOL { + NSError *error = nil; + [[EarlGrey selectElementWithMatcher:grey_text(text)] + assertWithMatcher:grey_sufficientlyVisible() + error:&error]; + return !error; + }]; + GREYAssertTrue([displayed waitWithTimeout:maxDelay], @"Failed to wait for element '%@'.", text); +} + +@end diff --git a/Example/Auth/EarlGreyTests/Info.plist b/Example/Auth/E2eTests/Info.plist similarity index 100% rename from Example/Auth/EarlGreyTests/Info.plist rename to Example/Auth/E2eTests/Info.plist diff --git a/Example/Auth/E2eTests/VerifyIOSClientTests.m b/Example/Auth/E2eTests/VerifyIOSClientTests.m new file mode 100644 index 00000000000..131a56430de --- /dev/null +++ b/Example/Auth/E2eTests/VerifyIOSClientTests.m @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Google + * + * 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 "FIRAuthE2eTestsBase.h" + +@interface VerifyIOSClientTests : FIRAuthE2eTestsBase + +@end + +@implementation VerifyIOSClientTests + +/** Test verify ios client*/ +- (void)testVerifyIOSClient { + [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(@"Verify iOS client"), + grey_sufficientlyVisible(), nil)] + usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, kShortScrollDistance) + onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), + nil)] performAction:grey_tap()]; + + [self waitForElementWithText:@"OK" withDelay:kWaitForElementTimeOut]; + + [[EarlGrey selectElementWithMatcher:grey_text(@"OK")] performAction:grey_tap()]; +} + +@end diff --git a/Example/Auth/EarlGreyTests/FIRVerifyIOSClientTests.m b/Example/Auth/EarlGreyTests/FIRVerifyIOSClientTests.m deleted file mode 100644 index 1ccbd17b882..00000000000 --- a/Example/Auth/EarlGreyTests/FIRVerifyIOSClientTests.m +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2018 Google - * - * 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 -#import -#import "FirebaseAuth.h" - -static CGFloat const kShortScrollDistance = 100; - -static NSTimeInterval const kWaitForElementTimeOut = 15; - -@interface FIRVerifyIOSClientTests : XCTestCase -@end - -/** Convenience function for EarlGrey tests. */ -static id grey_scrollView(void) { - return [GREYMatchers matcherForKindOfClass:[UIScrollView class]]; -} - -@implementation FIRVerifyIOSClientTests - -/** To reset the app so that each test sees the app in a clean state. */ -- (void)setUp { - [super setUp]; - - [self signOut]; - - [[EarlGrey selectElementWithMatcher:grey_allOf(grey_scrollView(), - grey_kindOfClass([UITableView class]), nil)] - performAction:grey_scrollToContentEdge(kGREYContentEdgeTop)]; -} - -#pragma mark - Tests - -/** Test verify ios client*/ -- (void)testVerifyIOSClient { - [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(@"Verify iOS client"), - grey_sufficientlyVisible(), nil)] - usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, kShortScrollDistance) - onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), - nil)] performAction:grey_tap()]; - - [self waitForElementWithText:@"OK" withDelay:kWaitForElementTimeOut]; - - [[EarlGrey selectElementWithMatcher:grey_text(@"OK")] performAction:grey_tap()]; -} - -#pragma mark - Helpers - -/** Sign out current account. */ -- (void)signOut { - NSError *signOutError; - BOOL status = [[FIRAuth auth] signOut:&signOutError]; - - // Just log the error because we don't want to fail the test if signing out fails. - if (!status) { - NSLog(@"Error signing out: %@", signOutError); - } -} - -/** Wait for an element with text to appear. */ -- (void)waitForElementWithText:(NSString *)text withDelay:(NSTimeInterval)maxDelay { - GREYCondition *displayed = - [GREYCondition conditionWithName:@"Wait for element" - block:^BOOL { - NSError *error = nil; - [[EarlGrey selectElementWithMatcher:grey_text(text)] - assertWithMatcher:grey_sufficientlyVisible() - error:&error]; - return !error; - }]; - GREYAssertTrue([displayed waitWithTimeout:maxDelay], @"Failed to wait for element '%@'.", text); -} - -@end diff --git a/Example/Auth/EarlGreyTests/FirebaseAuthEarlGreyTests.m b/Example/Auth/EarlGreyTests/FirebaseAuthEarlGreyTests.m deleted file mode 100644 index d8130fe0a91..00000000000 --- a/Example/Auth/EarlGreyTests/FirebaseAuthEarlGreyTests.m +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 -#import - -#import -#import "FirebaseAuth.h" - -/** The url for obtaining a valid custom token string used to test BYOAuth. */ -static NSString *const kCustomTokenUrl = @"https://fb-sa-1211.appspot.com/token"; - -/** The invalid custom token string for testing BYOAuth. */ -static NSString *const kInvalidCustomToken = @"invalid token."; - -/** The user name string for BYOAuth testing account. */ -static NSString *const kTestingAccountUserID = @"BYU_Test_User_ID"; - -static CGFloat const kShortScrollDistance = 100; - -static NSTimeInterval const kWaitForElementTimeOut = 5; - -@interface BasicUITest :XCTestCase -@end - -/** Convenience function for EarlGrey tests. */ -static id grey_scrollView(void) { - return [GREYMatchers matcherForKindOfClass:[UIScrollView class]]; -} - -@implementation BasicUITest - -/** To reset the app so that each test sees the app in a clean state. */ -- (void)setUp { - [super setUp]; - - [self signOut]; - - [[EarlGrey selectElementWithMatcher:grey_allOf(grey_scrollView(), - grey_kindOfClass([UITableView class]), nil)] - performAction:grey_scrollToContentEdge(kGREYContentEdgeTop)]; -} - -#pragma mark - Tests - -/** - * This test runs in replay mode by default. To run in a different mode - * follow the instructions below. - * - * Blaze: - * --test_arg=\'--networkReplayMode=(replay|record|disabled|observe)\' - * - * Xcode: - * Update the following flag in the xcscheme. - * --networkReplayMode=(replay|record|disabled|observe) - */ -- (void)testSignInExistingUser { - NSString *email = @"123@abc.com"; - [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(@"Sign in with Email/Password"), - grey_sufficientlyVisible(), nil)] - usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, kShortScrollDistance) - onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), - nil)] performAction:grey_tap()]; - - id comfirmationButtonMatcher = - grey_allOf(grey_kindOfClass([UILabel class]), grey_accessibilityLabel(@"OK"), nil); - - [[EarlGrey selectElementWithMatcher: - #warning TODO Add accessibilityIdentifiers for the elements. - grey_kindOfClass(NSClassFromString(@"_UIAlertControllerView"))] - performAction:grey_typeText(email)]; - - [[EarlGrey selectElementWithMatcher:comfirmationButtonMatcher] performAction:grey_tap()]; - - [[EarlGrey - selectElementWithMatcher:grey_kindOfClass(NSClassFromString(@"_UIAlertControllerView"))] - performAction:grey_typeText(@"password")]; - - [[EarlGrey selectElementWithMatcher:comfirmationButtonMatcher] performAction:grey_tap()]; - - [[[EarlGrey - selectElementWithMatcher:grey_allOf(grey_text(email), grey_sufficientlyVisible(), nil)] - usingSearchAction:grey_scrollInDirection(kGREYDirectionUp, kShortScrollDistance) - onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), - nil)] assertWithMatcher:grey_sufficientlyVisible()]; -} - -/** Test sign in with a valid BYOAuth token retrived from a remote server. */ -- (void)testSignInWithValidBYOAuthToken { - NSError *error; - NSString *customToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:kCustomTokenUrl] - encoding:NSUTF8StringEncoding - error:&error]; - if (!customToken) { - GREYFail(@"There was an error retrieving the custom token: %@", error); - } - - [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(@"Sign In (BYOAuth)"), - grey_sufficientlyVisible(), nil)] - usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, kShortScrollDistance) - onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), - nil)] performAction:grey_tap()]; - - [[[EarlGrey selectElementWithMatcher:grey_kindOfClass([UITextView class])] - performAction:grey_replaceText(customToken)] assertWithMatcher:grey_text(customToken)]; - - [[EarlGrey selectElementWithMatcher:grey_text(@"Done")] performAction:grey_tap()]; - - [self waitForElementWithText:@"OK" withDelay:kWaitForElementTimeOut]; - - [[EarlGrey selectElementWithMatcher:grey_text(@"OK")] performAction:grey_tap()]; - - [[[EarlGrey - selectElementWithMatcher:grey_allOf(grey_text(kTestingAccountUserID), - grey_sufficientlyVisible(), nil)] - usingSearchAction:grey_scrollInDirection(kGREYDirectionUp, - kShortScrollDistance) - onElementWithMatcher:grey_allOf(grey_scrollView(), - grey_kindOfClass([UITableView class]), - nil)] - assertWithMatcher:grey_sufficientlyVisible()]; -} - -- (void)testSignInWithInvalidBYOAuthToken { - [[[EarlGrey selectElementWithMatcher:grey_allOf(grey_text(@"Sign In (BYOAuth)"), - grey_sufficientlyVisible(), nil)] - usingSearchAction:grey_scrollInDirection(kGREYDirectionDown, kShortScrollDistance) - onElementWithMatcher:grey_allOf(grey_scrollView(), grey_kindOfClass([UITableView class]), - nil)] performAction:grey_tap()]; - - [[[EarlGrey selectElementWithMatcher:grey_kindOfClass([UITextView class])] - performAction:grey_replaceText(kInvalidCustomToken)] - assertWithMatcher:grey_text(kInvalidCustomToken)]; - - [[EarlGrey selectElementWithMatcher:grey_text(@"Done")] performAction:grey_tap()]; - - NSString *invalidTokenErrorMessage = @"Sign-In Error"; - - [self waitForElementWithText:invalidTokenErrorMessage withDelay:kWaitForElementTimeOut]; - - [[EarlGrey selectElementWithMatcher:grey_text(@"OK")] performAction:grey_tap()]; -} - -#pragma mark - Helpers - -/** Sign out current account. */ -- (void)signOut { - NSError *signOutError; - BOOL status = [[FIRAuth auth] signOut:&signOutError]; - - // Just log the error because we don't want to fail the test if signing out fails. - if (!status) { - NSLog(@"Error signing out: %@", signOutError); - } -} - -/** Wait for an element with text to appear. */ -- (void)waitForElementWithText:(NSString *)text withDelay:(NSTimeInterval)maxDelay { - GREYCondition *displayed = - [GREYCondition conditionWithName:@"Wait for element" - block:^BOOL { - NSError *error = nil; - [[EarlGrey selectElementWithMatcher:grey_text(text)] - assertWithMatcher:grey_sufficientlyVisible() - error:&error]; - return !error; - }]; - GREYAssertTrue([displayed waitWithTimeout:maxDelay], @"Failed to wait for element '%@'.", text); -} - -@end diff --git a/Example/Auth/Sample/MainViewController.m b/Example/Auth/Sample/MainViewController.m index e8c04f606f2..6d74531b1b2 100644 --- a/Example/Auth/Sample/MainViewController.m +++ b/Example/Auth/Sample/MainViewController.m @@ -114,6 +114,11 @@ */ static NSString *const kSignInGoogleButtonText = @"Sign in with Google"; +/** @var kSignInGoogleProviderButtonText + @brief The text of the Google provider version of "Sign In With Provider" button. + */ +static NSString *const kSignInGoogleProviderButtonText = @"Sign in with provider:(Google)"; + /** @var kSignInWithEmailLink @brief The text of the "Sign in with Email Link" button. */ @@ -135,6 +140,16 @@ static NSString *const kSignInGoogleAndRetrieveDataButtonText = @"Sign in with Google and retrieve data"; +/** @var kSignInGoogleHeadfulLite + @brief The text of the "Sign in with Google (headful lite)" button. + */ +static NSString *const kSignInGoogleHeadfulLite = @"Sign in with Google (headful lite)"; + +/** @var kSignInMicrosoftHeadfulLite + @brief The text of the "Sign in with Microsoft (headful lite)" button. + */ +static NSString *const kSignInMicrosoftHeadfulLite = @"Sign in with Microsoft (headful lite)"; + /** @var kSignInFacebookButtonText @brief The text of the "Facebook SignIn" button. */ @@ -717,6 +732,16 @@ @implementation MainViewController { @brief The continue URL to be used in the next action code request. */ NSURL *_actionCodeContinueURL; + + /** @var _googleOAuthProvider + @brief OAuth provider instance for Google. + */ + FIROAuthProvider *_googleOAuthProvider; + + /** @var _microsoftOAuthProvider + @brief OAuth provider instance for Microsoft. + */ + FIROAuthProvider *_microsoftOAuthProvider; } /** @fn initWithNibName:bundle: @@ -729,6 +754,8 @@ - (id)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundl _actionCodeContinueURL = [NSURL URLWithString:KCONTINUE_URL]; _authStateDidChangeListeners = [NSMutableArray array]; _IDTokenDidChangeListeners = [NSMutableArray array]; + _googleOAuthProvider = [FIROAuthProvider providerWithProviderID:FIRGoogleAuthProviderID]; + _microsoftOAuthProvider = [FIROAuthProvider providerWithProviderID:@"microsoft.com"]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(authStateChangedForAuth:) name:FIRAuthStateDidChangeNotification @@ -819,6 +846,8 @@ - (void)updateTable { action:^{ [weakSelf createUserAuthDataResult]; }], [StaticContentTableViewCell cellWithTitle:kSignInGoogleButtonText action:^{ [weakSelf signInGoogle]; }], + [StaticContentTableViewCell cellWithTitle:kSignInGoogleProviderButtonText + action:^{ [weakSelf signInGoogleProvider]; }], [StaticContentTableViewCell cellWithTitle:kSignInWithEmailLink action:^{ [weakSelf signInWithEmailLink]; }], [StaticContentTableViewCell cellWithTitle:kVerifyEmailLinkAccount @@ -827,6 +856,10 @@ - (void)updateTable { action:^{ [weakSelf sendEmailSignInLink]; }], [StaticContentTableViewCell cellWithTitle:kSignInGoogleAndRetrieveDataButtonText action:^{ [weakSelf signInGoogleAndRetrieveData]; }], + [StaticContentTableViewCell cellWithTitle:kSignInGoogleHeadfulLite + action:^{ [weakSelf signInGoogleHeadfulLite]; }], + [StaticContentTableViewCell cellWithTitle:kSignInMicrosoftHeadfulLite + action:^{ [weakSelf signInMicrosoftHeadfulLite]; }], [StaticContentTableViewCell cellWithTitle:kSignInFacebookButtonText action:^{ [weakSelf signInFacebook]; }], [StaticContentTableViewCell cellWithTitle:kSignInFacebookAndRetrieveDataButtonText @@ -1760,6 +1793,106 @@ - (void)signInGoogleAndRetrieveData { [self signinWithProvider:[AuthProviders google] retrieveData:YES]; } +- (void)signInGoogleProvider { + FIROAuthProvider *provider = _googleOAuthProvider; + provider.customParameters = @{ + @"prompt" : @"consent", + }; + provider.scopes = @[ @"profile", @"email", @"https://www.googleapis.com/auth/plus.me" ]; + [self showSpinner:^{ + [[AppManager auth] signInWithProvider:provider + UIDelegate:nil + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + [self hideSpinner:^{ + if (error) { + [self logFailure:@"sign-in with provider (Google) failed" error:error]; + } else if (authResult.additionalUserInfo) { + [self logSuccess:[self stringWithAdditionalUserInfo:authResult.additionalUserInfo]]; + if (_isNewUserToggleOn) { + NSString *newUserString = authResult.additionalUserInfo.newUser ? + @"New user" : @"Existing user"; + [self showMessagePromptWithTitle:@"New or Existing" + message:newUserString + showCancelButton:NO + completion:nil]; + } + } + [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In Error" error:error]; + }]; + }]; + }]; +} + +/** @fn signInGoogleHeadfulLite + @brief Invoked when "Sign in with Google (headful-lite)" row is pressed. + */ +- (void)signInGoogleHeadfulLite { + FIROAuthProvider *provider = _googleOAuthProvider; + provider.customParameters = @{ + @"prompt" : @"consent", + }; + provider.scopes = @[ @"profile", @"email", @"https://www.googleapis.com/auth/plus.me" ]; + [self showSpinner:^{ + [provider getCredentialWithUIDelegate:nil completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) { + if (error) { + [self logFailure:@"sign-in with Google failed" error:error]; + return; + } + [[AppManager auth] signInAndRetrieveDataWithCredential:credential + completion:^(FIRAuthDataResult *_Nullable + authResult, + NSError *_Nullable error) { + [self hideSpinner:^{ + if (error) { + [self logFailure:@"sign-in with Google failed" error:error]; + return; + } else { + [self logSuccess:@"sign-in with Google (headful-lite) succeeded."]; + } + [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In Error" error:error]; + }]; + }]; + }]; + }]; +} + +/** @fn signInMicrosoftHeadfulLite + @brief Invoked when "Sign in with Microsoft (headful-lite)" row is pressed. + */ +- (void)signInMicrosoftHeadfulLite { + FIROAuthProvider *provider = _microsoftOAuthProvider; + provider.customParameters = @{ + @"prompt" : @"consent", + @"login_hint" : @"tu8731@gmail.com", + }; + provider.scopes = @[ @"user.readwrite,calendars.read" ]; + [self showSpinner:^{ + [provider getCredentialWithUIDelegate:nil completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) { + if (error) { + [self logFailure:@"sign-in with Microsoft failed" error:error]; + return; + } + [[AppManager auth] signInAndRetrieveDataWithCredential:credential + completion:^(FIRAuthDataResult *_Nullable + authResult, + NSError *_Nullable error) { + [self hideSpinner:^{ + if (error) { + [self logFailure:@"sign-in with Microsoft failed" error:error]; + return; + } else { + [self logSuccess:@"sign-in with Microsoft (headful-lite) succeeded."]; + } + [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In Error" error:error]; + }]; + }]; + }]; + }]; +} + /** @fn signInFacebook @brief Invoked when "Sign in with Facebook" row is pressed. */ @@ -3262,7 +3395,7 @@ - (void)linkPhoneNumber:(NSString *_Nullable)phoneNumber // provided credential. [self showSpinner:^{ FIRPhoneAuthCredential *credential = - error.userInfo[FIRAuthUpdatedCredentialKey]; + error.userInfo[FIRAuthErrorUserInfoUpdatedCredentialKey]; [[AppManager auth] signInWithCredential:credential completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { @@ -3356,7 +3489,7 @@ - (void)signInWithGitHub { if (!userPressedOK || !accessToken.length) { return; } - FIRAuthCredential *credential = + FIROAuthCredential *credential = [FIROAuthProvider credentialWithProviderID:FIRGitHubAuthProviderID accessToken:accessToken]; if (credential) { [[AppManager auth] signInWithCredential:credential diff --git a/Example/Auth/SwiftSample/AppDelegate.swift b/Example/Auth/SwiftSample/AppDelegate.swift deleted file mode 100644 index 52618fe5818..00000000000 --- a/Example/Auth/SwiftSample/AppDelegate.swift +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 UIKit - -import FirebaseCore -import GoogleSignIn - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - - /** @var kGoogleClientID - @brief The Google client ID. - */ - private let kGoogleClientID = AuthCredentials.GOOGLE_CLIENT_ID - - // TODO: add Facebook login support as well. - - /** @var kFacebookAppID - @brief The Facebook app ID. - */ - private let kFacebookAppID = AuthCredentials.FACEBOOK_APP_ID - - /** @var window - @brief The main window of the app. - */ - var window: UIWindow? - - func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { - FirebaseApp.configure() - GIDSignIn.sharedInstance().clientID = kGoogleClientID - return true - } - - @available(iOS 9.0, *) - func application(_ application: UIApplication, open url: URL, - options: [UIApplicationOpenURLOptionsKey : Any]) -> Bool { - return GIDSignIn.sharedInstance().handle(url, - sourceApplication: options[UIApplicationOpenURLOptionsKey.sourceApplication] as! String, - annotation: nil) - } - - func application(_ application: UIApplication, open url: URL, sourceApplication: String?, - annotation: Any) -> Bool { - return GIDSignIn.sharedInstance().handle(url, sourceApplication: sourceApplication, - annotation: annotation) - } -} diff --git a/Example/Auth/SwiftSample/AuthCredentialsTemplate.swift b/Example/Auth/SwiftSample/AuthCredentialsTemplate.swift deleted file mode 100644 index eea9335948c..00000000000 --- a/Example/Auth/SwiftSample/AuthCredentialsTemplate.swift +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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. - */ - - -/* - Some of the Auth Credentials needs to be populated for the Sample build to work. - - Please follow the following steps to populate the valid AuthCredentials - and copy it to AuthCredentials.swift file - - You will need to replace the following values: - $KGOOGLE_CLIENT_ID - Get the value of the CLIENT_ID key in the GoogleService-Info.plist file. - - $KFACEBOOK_APP_ID - FACEBOOK_APP_ID is the developer's Facebook app's ID, to be used to test the - 'Signing in with Facebook' feature of Firebase Auth. Follow the instructions - on the Facebook developer site: https://developers.facebook.com/docs/apps/register - to obtain the id - - */ - -import Foundation - - -struct AuthCredentials { - static let FACEBOOK_APP_ID = "$KFACEBOOK_APP_ID" - static let GOOGLE_CLIENT_ID = "$KGOOGLE_CLIENT_ID" -} diff --git a/Example/Auth/SwiftSample/Main.storyboard b/Example/Auth/SwiftSample/Main.storyboard deleted file mode 100644 index 3525a8bdf6a..00000000000 --- a/Example/Auth/SwiftSample/Main.storyboard +++ /dev/null @@ -1,227 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Example/Auth/SwiftSample/Stubs.swift b/Example/Auth/SwiftSample/Stubs.swift deleted file mode 100644 index edb574f024c..00000000000 --- a/Example/Auth/SwiftSample/Stubs.swift +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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. - */ - -/// This file contains a collection of stub functions to verify the Swift syntax of Firebase Auth -/// APIs in Swift for those that are not already covered by other parts of the app. -/// These functions are never executed, but just for passing compilation. - -import FirebaseAuth - -func actionCodeSettingsStubs() { - let actionCodeSettings = ActionCodeSettings() - actionCodeSettings.url = URL(string: "http://some.url/path/") - actionCodeSettings.setIOSBundleID("some.bundle.id") - actionCodeSettings.setAndroidPackageName("some.package.name", installIfNotAvailable: true, - minimumVersion: nil) - let _: String? = actionCodeSettings.iOSBundleID - let _: String? = actionCodeSettings.androidPackageName - let _: Bool = actionCodeSettings.androidInstallIfNotAvailable - let _: String? = actionCodeSettings.androidMinimumVersion - Auth.auth().sendPasswordReset(withEmail: "nobody@nowhere.com", - actionCodeSettings: actionCodeSettings) { (error: Error?) -> () in - } - Auth.auth().currentUser?.sendEmailVerification(with: actionCodeSettings) { - (error: Error?) -> () in - } -} - -func languageStubs() { - let _: String? = Auth.auth().languageCode - Auth.auth().languageCode = "asdf" - Auth.auth().useAppLanguage() -} - -func metadataStubs() { - let credential = OAuthProvider.credential(withProviderID: "fake", accessToken: "none") - Auth.auth().signInAndRetrieveData(with: credential) { result, error in - let _: Bool? = result!.additionalUserInfo!.isNewUser - let metadata: UserMetadata = result!.user.metadata - let _: Date? = metadata.lastSignInDate - let _: Date? = metadata.creationDate - } -} diff --git a/Example/Auth/SwiftSample/ViewController.swift b/Example/Auth/SwiftSample/ViewController.swift deleted file mode 100644 index 05b0dd266b5..00000000000 --- a/Example/Auth/SwiftSample/ViewController.swift +++ /dev/null @@ -1,708 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 UIKit - -import FirebaseAuth -import GoogleSignIn - -final class ViewController: UIViewController, UITextFieldDelegate, AuthUIDelegate { - /// The profile image for the currently signed-in user. - @IBOutlet weak var profileImage: UIImageView! - - /// The display name for the currently signed-in user. - @IBOutlet weak var displayNameLabel: UILabel! - - /// The email for the currently signed-in user. - @IBOutlet weak var emailLabel: UILabel! - - /// The ID for the currently signed-in user. - @IBOutlet weak var userIDLabel: UILabel! - - /// The list of providers for the currently signed-in user. - @IBOutlet weak var providerListLabel: UILabel! - - /// The picker for the list of action types. - @IBOutlet weak var actionTypePicker: UIPickerView! - - /// The picker for the list of actions. - @IBOutlet weak var actionPicker: UIPickerView! - - /// The picker for the list of credential types. - @IBOutlet weak var credentialTypePicker: UIPickerView! - - /// The label for the "email" text field. - @IBOutlet weak var emailInputLabel: UILabel! - - /// The "email" text field. - @IBOutlet weak var emailField: UITextField! - - /// The label for the "password" text field. - @IBOutlet weak var passwordInputLabel: UILabel! - - /// The "password" text field. - @IBOutlet weak var passwordField: UITextField! - - /// The "phone" text field. - @IBOutlet weak var phoneField: UITextField! - - /// The scroll view holding all content. - @IBOutlet weak var scrollView: UIScrollView! - - // The active keyboard input field. - var activeField: UITextField? - - /// The currently selected action type. - fileprivate var actionType = ActionType(rawValue: 0)! { - didSet { - if actionType != oldValue { - actionPicker.reloadAllComponents() - actionPicker.selectRow(actionType == .auth ? authAction.rawValue : userAction.rawValue, - inComponent: 0, animated: false) - } - } - } - - /// The currently selected auth action. - fileprivate var authAction = AuthAction(rawValue: 0)! - - /// The currently selected user action. - fileprivate var userAction = UserAction(rawValue: 0)! - - /// The currently selected credential. - fileprivate var credentialType = CredentialType(rawValue: 0)! - - /// The current Firebase user. - fileprivate var user: User? = nil { - didSet { - if user?.uid != oldValue?.uid { - actionTypePicker.reloadAllComponents() - actionType = ActionType(rawValue: actionTypePicker.selectedRow(inComponent: 0))! - } - } - } - - func registerForKeyboardNotifications() { - NotificationCenter.default.addObserver(self, - selector: - #selector(keyboardWillBeShown(notification:)), - name: NSNotification.Name.UIKeyboardWillShow, - object: nil) - NotificationCenter.default.addObserver(self, - selector: #selector(keyboardWillBeHidden(notification:)), - name: NSNotification.Name.UIKeyboardWillHide, - object: nil) - } - - func deregisterFromKeyboardNotifications() { - NotificationCenter.default.removeObserver(self, - name: NSNotification.Name.UIKeyboardWillShow, - object: nil) - NotificationCenter.default.removeObserver(self, - name: NSNotification.Name.UIKeyboardWillHide, - object: nil) - } - - func keyboardWillBeShown(notification: NSNotification) { - scrollView.isScrollEnabled = true - let info = notification.userInfo! - let keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size - let contentInsets : UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize!.height, 0.0) - - scrollView.contentInset = contentInsets - scrollView.scrollIndicatorInsets = contentInsets - - var aRect = self.view.frame - aRect.size.height -= keyboardSize!.height - if let activeField = activeField { - if (!aRect.contains(activeField.frame.origin)) { - scrollView.scrollRectToVisible(activeField.frame, animated: true) - } - } - } - - func keyboardWillBeHidden(notification: NSNotification){ - let info = notification.userInfo! - let keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size - let contentInsets : UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, -keyboardSize!.height, 0.0) - scrollView.contentInset = contentInsets - scrollView.scrollIndicatorInsets = contentInsets - self.view.endEditing(true) - scrollView.isScrollEnabled = false - } - - func textFieldDidBeginEditing(_ textField: UITextField) { - activeField = textField - } - - func textFieldDidEndEditing(_ textField: UITextField) { - activeField = nil - } - - func dismissKeyboard() { - view.endEditing(true) - } - - func verify(phoneNumber: String, completion: @escaping (PhoneAuthCredential?, Error?) -> Void) { - if #available(iOS 8.0, *) { - PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate:self) { - verificationID, error in - guard error == nil else { - completion(nil, error) - return - } - let codeAlertController = - UIAlertController(title: "Enter Code", message: nil, preferredStyle: .alert) - codeAlertController.addTextField { textfield in - textfield.placeholder = "SMS Code" - textfield.keyboardType = UIKeyboardType.numberPad - } - codeAlertController.addAction(UIAlertAction(title: "OK", - style: .default, - handler: { (UIAlertAction) in - let code = codeAlertController.textFields!.first!.text! - let phoneCredential = - PhoneAuthProvider.provider().credential(withVerificationID: verificationID ?? "", - verificationCode: code) - completion(phoneCredential, nil) - })) - self.present(codeAlertController, animated: true, completion: nil) - } - } - } - /// The user's photo URL used by the last network request for its contents. - fileprivate var lastPhotoURL: URL? = nil - - override func viewDidLoad() { - GIDSignIn.sharedInstance().uiDelegate = self - updateUserInfo(Auth.auth()) - NotificationCenter.default.addObserver(forName: .AuthStateDidChange, - object: Auth.auth(), queue: nil) { notification in - self.updateUserInfo(notification.object as? Auth) - } - phoneField.delegate = self - registerForKeyboardNotifications() - - let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) - scrollView.addGestureRecognizer(tap) - } - - override func viewWillDisappear(_ animated: Bool) { - deregisterFromKeyboardNotifications() - } - - /// Executes the action designated by the operator on the UI. - @IBAction func execute(_ sender: UIButton) { - switch actionType { - case .auth: - switch authAction { - case .fetchProviderForEmail: - Auth.auth().fetchProviders(forEmail: emailField.text!) { providers, error in - self.ifNoError(error) { - self.showAlert(title: "Providers", message: providers?.joined(separator: ", ")) - } - } - case .signInAnonymously: - Auth.auth().signInAnonymously() { user, error in - self.ifNoError(error) { - self.showAlert(title: "Signed In Anonymously") - } - } - case .signInWithCredential: - getCredential() { credential in - Auth.auth().signInAndRetrieveData(with: credential) { authData, error in - self.ifNoError(error) { - self.showAlert(title: "Signed In With Credential", - message: authData?.user.textDescription) - } - } - } - case .createUser: - Auth.auth().createUser(withEmail: emailField.text!, password: passwordField.text!) { - result, error in - self.ifNoError(error) { - self.showAlert(title: "Signed In With Credential", message: result?.user.textDescription) - } - } - case .signOut: - try! Auth.auth().signOut() - GIDSignIn.sharedInstance().signOut() - } - case .user: - switch userAction { - case .updateEmail: - user!.updateEmail(to: emailField.text!) { error in - self.ifNoError(error) { - self.showAlert(title: "Updated Email", message: self.user?.email) - } - } - case .updatePhone: - let phoneNumber = phoneField.text - self.verify(phoneNumber: phoneNumber!, completion: { (phoneAuthCredential, error) in - guard error == nil else { - self.showAlert(title: "Error", message: error!.localizedDescription) - return - } - self.user!.updatePhoneNumber(phoneAuthCredential!, completion: { error in - self.ifNoError(error) { - self.showAlert(title: "Updated Phone Number") - self.updateUserInfo(Auth.auth()) - } - }) - }) - case .updatePassword: - user!.updatePassword(to: passwordField.text!) { error in - self.ifNoError(error) { - self.showAlert(title: "Updated Password") - } - } - case .reload: - user!.reload() { error in - self.ifNoError(error) { - self.showAlert(title: "Reloaded", message: self.user?.textDescription) - } - } - case .reauthenticate: - getCredential() { credential in - self.user!.reauthenticateAndRetrieveData(with: credential) { authData, error in - self.ifNoError(error) { - if (authData?.user.uid != self.user?.uid) { - let message = "The reauthenticated user must be the same as the original user" - self.showAlert(title: "Reauthention error", - message: message) - return - } - self.showAlert(title: "Reauthenticated", message: self.user?.textDescription) - } - } - } - case .getToken: - user!.getIDToken() { token, error in - self.ifNoError(error) { - self.showAlert(title: "Got ID Token", message: token) - } - } - case .linkWithCredential: - getCredential() { credential in - self.user!.linkAndRetrieveData(with: credential) { authData, error in - self.ifNoError(error) { - self.showAlert(title: "Linked With Credential", - message: authData?.user.textDescription) - } - } - } - case .deleteAccount: - user!.delete() { error in - self.ifNoError(error) { - self.showAlert(title: "Deleted Account") - } - } - } - } - } - - /// Gets an AuthCredential potentially asynchronously. - private func getCredential(completion: @escaping (AuthCredential) -> Void) { - switch credentialType { - case .google: - GIDSignIn.sharedInstance().delegate = GoogleSignInDelegate(completion: { user, error in - self.ifNoError(error) { - completion(GoogleAuthProvider.credential( - withIDToken: user!.authentication.idToken, - accessToken: user!.authentication.accessToken)) - } - }) - GIDSignIn.sharedInstance().signIn() - case .password: - completion(EmailAuthProvider.credential(withEmail: emailField.text!, - password: passwordField.text!)) - case .phone: - let phoneNumber = phoneField.text - self.verify(phoneNumber: phoneNumber!, completion: { (phoneAuthCredential, error) in - guard error == nil else { - self.showAlert(title: "Error", message: error!.localizedDescription) - return - } - completion(phoneAuthCredential!) - }) - } - } - - /// Updates user's profile image and info text. - private func updateUserInfo(_ auth: Auth?) { - user = auth?.currentUser - displayNameLabel.text = user?.displayName - emailLabel.text = user?.email - userIDLabel.text = user?.uid - let providers = user?.providerData.map { userInfo in userInfo.providerID } - providerListLabel.text = providers?.joined(separator: ", ") - if let photoURL = user?.photoURL { - lastPhotoURL = photoURL - let queue: DispatchQueue - if #available(iOS 8.0, *) { - queue = DispatchQueue.global(qos: .background) - } else { - queue = DispatchQueue.global(priority: DispatchQueue.GlobalQueuePriority.background) - } - queue.async { - if let imageData = try? Data(contentsOf: photoURL) { - let image = UIImage(data: imageData) - DispatchQueue.main.async { - if self.lastPhotoURL == photoURL { - self.profileImage.image = image - } - } - } - } - } else { - lastPhotoURL = nil - self.profileImage.image = nil - } - updateControls() - } - - // Updates the states of the UI controls. - fileprivate func updateControls() { - let action: Action - switch actionType { - case .auth: - action = authAction - case .user: - action = userAction - } - let isCredentialEnabled = action.requiresCredential - credentialTypePicker.isUserInteractionEnabled = isCredentialEnabled - credentialTypePicker.alpha = isCredentialEnabled ? 1.0 : 0.6 - let isEmailEnabled = isCredentialEnabled && credentialType.requiresEmail || action.requiresEmail - emailInputLabel.alpha = isEmailEnabled ? 1.0 : 0.6 - emailField.isEnabled = isEmailEnabled - let isPasswordEnabled = isCredentialEnabled && credentialType.requiresPassword || - action.requiresPassword - passwordInputLabel.alpha = isPasswordEnabled ? 1.0 : 0.6 - passwordField.isEnabled = isPasswordEnabled - phoneField.isEnabled = credentialType.requiresPhone || action.requiresPhoneNumber - } - - fileprivate func showAlert(title: String, message: String? = "") { - if #available(iOS 8.0, *) { - let alertController = - UIAlertController(title: title, message: message, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: "OK", - style: .default, - handler: { (UIAlertAction) in - alertController.dismiss(animated: true, completion: nil) - })) - self.present(alertController, animated: true, completion: nil) - } else { - UIAlertView(title: title, - message: message ?? "(NULL)", - delegate: nil, - cancelButtonTitle: nil, - otherButtonTitles: "OK").show() - } - } - - private func ifNoError(_ error: Error?, execute: () -> Void) { - guard error == nil else { - showAlert(title: "Error", message: error!.localizedDescription) - return - } - execute() - } -} - -extension ViewController : GIDSignInUIDelegate { - func sign(_ signIn: GIDSignIn!, present viewController: UIViewController!) { - present(viewController, animated: true, completion: nil) - } - - func sign(_ signIn: GIDSignIn!, dismiss viewController: UIViewController!) { - dismiss(animated: true, completion: nil) - } -} - -extension ViewController : UIPickerViewDataSource { - func numberOfComponents(in pickerView: UIPickerView) -> Int { - return 1 - } - - func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - switch pickerView { - case actionTypePicker: - if Auth.auth().currentUser != nil { - return ActionType.countWithUser - } else { - return ActionType.countWithoutUser - } - case actionPicker: - switch actionType { - case .auth: - return AuthAction.count - case .user: - return UserAction.count - } - case credentialTypePicker: - return CredentialType.count - default: - return 0 - } - } -} - -extension ViewController : UIPickerViewDelegate { - func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) - -> String? { - switch pickerView { - case actionTypePicker: - return ActionType(rawValue: row)!.text - case actionPicker: - switch actionType { - case .auth: - return AuthAction(rawValue: row)!.text - case .user: - return UserAction(rawValue: row)!.text - } - case credentialTypePicker: - return CredentialType(rawValue: row)!.text - default: - return nil - } - } - - func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - switch pickerView { - case actionTypePicker: - actionType = ActionType(rawValue: row)! - case actionPicker: - switch actionType { - case .auth: - authAction = AuthAction(rawValue: row)! - case .user: - userAction = UserAction(rawValue: row)! - } - case credentialTypePicker: - credentialType = CredentialType(rawValue: row)! - default: - break - } - updateControls() - } -} - -/// An adapter class to pass GoogleSignIn delegate method to a block. -fileprivate final class GoogleSignInDelegate: NSObject, GIDSignInDelegate { - - private let completion: (GIDGoogleUser?, Error?) -> Void - private var retainedSelf: GoogleSignInDelegate? - - init(completion: @escaping (GIDGoogleUser?, Error?) -> Void) { - self.completion = completion - super.init() - retainedSelf = self - } - - func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser?, withError error: Error?) { - completion(user, error) - retainedSelf = nil - } -} - -/// The list of all possible action types. -fileprivate enum ActionType: Int { - - case auth, user - - // Count of action types when no user is signed in. - static var countWithoutUser: Int { - return ActionType.auth.rawValue + 1 - } - - // Count of action types when a user is signed in. - static var countWithUser: Int { - return ActionType.user.rawValue + 1 - } - - /// The text description for a particular enum value. - var text : String { - switch self { - case .auth: - return "Auth" - case .user: - return "User" - } - } -} - -fileprivate protocol Action { - /// The text description for the particular action. - var text: String { get } - - /// Whether or not the action requires a credential. - var requiresCredential : Bool { get } - - /// Whether or not the action requires an email. - var requiresEmail: Bool { get } - - /// Whether or not the credential requires a password. - var requiresPassword: Bool { get } - - /// Whether or not the credential requires a phone number. - var requiresPhoneNumber: Bool { get } -} - -/// The list of all possible actions the operator can take on the Auth object. -fileprivate enum AuthAction: Int, Action { - - case fetchProviderForEmail, signInAnonymously, signInWithCredential, createUser, signOut - - /// Total number of auth actions. - static var count: Int { - return AuthAction.signOut.rawValue + 1 - } - - var text : String { - switch self { - case .fetchProviderForEmail: - return "Fetch Provider ⬇️" - case .signInAnonymously: - return "Sign In Anonymously" - case .signInWithCredential: - return "Sign In w/ Credential ↙️" - case .createUser: - return "Create User ⬇️" - case .signOut: - return "Sign Out" - } - } - - var requiresCredential : Bool { - return self == .signInWithCredential - } - - var requiresEmail : Bool { - return self == .fetchProviderForEmail || self == .createUser - } - - var requiresPassword : Bool { - return self == .createUser - } - - var requiresPhoneNumber: Bool { - return false - } -} - -/// The list of all possible actions the operator can take on the User object. -fileprivate enum UserAction: Int, Action { - - case updateEmail, updatePhone, updatePassword, reload, reauthenticate, getToken, - linkWithCredential, deleteAccount - - /// Total number of user actions. - static var count: Int { - return UserAction.deleteAccount.rawValue + 1 - } - - var text : String { - switch self { - case .updateEmail: - return "Update Email ⬇️" - case .updatePhone: - if #available(iOS 8.0, *) { - return "Update Phone ⬇️" - } else { - return "-" - } - case .updatePassword: - return "Update Password ⬇️" - case .reload: - return "Reload" - case .reauthenticate: - return "Reauthenticate ↙️" - case .getToken: - return "Get Token" - case .linkWithCredential: - return "Link With Credential ↙️" - case .deleteAccount: - return "Delete Account" - } - } - - var requiresCredential : Bool { - return self == .reauthenticate || self == .linkWithCredential - } - - var requiresEmail : Bool { - return self == .updateEmail - } - - var requiresPassword : Bool { - return self == .updatePassword - } - - var requiresPhoneNumber : Bool { - return self == .updatePhone - } - -} - -/// The list of all possible credential types the operator can use to sign in or link. -fileprivate enum CredentialType: Int { - - case google, password, phone - - /// Total number of enum values. - static var count: Int { - return CredentialType.phone.rawValue + 1 - } - - /// The text description for a particular enum value. - var text : String { - switch self { - case .google: - return "Google" - case .password: - return "Password ➡️️" - case .phone: - if #available(iOS 8.0, *) { - return "Phone ➡️️" - } else { - return "-" - } - } - } - - /// Whether or not the credential requires an email. - var requiresEmail : Bool { - return self == .password - } - - /// Whether or not the credential requires a password. - var requiresPassword : Bool { - return self == .password - } - - /// Whether or not the credential requires a phone number. - var requiresPhone : Bool { - return self == .phone - } -} - -fileprivate extension User { - var textDescription: String { - return self.displayName ?? self.email ?? self.uid - } -} diff --git a/Example/Auth/Tests/FIRAuthAppDelegateProxyTests.m b/Example/Auth/Tests/FIRAuthAppDelegateProxyTests.m index b01ee26a3f0..92871c51e52 100644 --- a/Example/Auth/Tests/FIRAuthAppDelegateProxyTests.m +++ b/Example/Auth/Tests/FIRAuthAppDelegateProxyTests.m @@ -376,12 +376,12 @@ - (void)testEmptyDelegateOneHandler { XCTFail(@"Should not call completion handler."); }]; if (_isIOS9orLater) { - XCTAssertFalse([delegate application:_mockApplication openURL:_url options:@{}]); + XCTAssertNoThrow([delegate application:_mockApplication openURL:_url options:@{}]); } else { - XCTAssertFalse([delegate application:_mockApplication - openURL:_url - sourceApplication:@"sourceApplication" - annotation:@"annotaton"]); + XCTAssertNoThrow([delegate application:_mockApplication + openURL:_url + sourceApplication:@"sourceApplication" + annotation:@"annotaton"]); } } diff --git a/Example/Auth/Tests/FIRAuthLifeCycleTests.m b/Example/Auth/Tests/FIRAuthLifeCycleTests.m new file mode 100644 index 00000000000..daa77991bdb --- /dev/null +++ b/Example/Auth/Tests/FIRAuthLifeCycleTests.m @@ -0,0 +1,177 @@ +/* + * Copyright 2019 Google + * + * 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 +#import +#import +#import + +#import "FIRApp+FIRAuthUnitTests.h" +#import "FIRAuthRequestConfiguration.h" +#import "FIRAuth_Internal.h" + +/** @var kFirebaseAppName1 + @brief A fake Firebase app name. + */ +static NSString *const kFirebaseAppName1 = @"FIREBASE_APP_NAME_1"; + +/** @var kFirebaseAppName2 + @brief Another fake Firebase app name. + */ +static NSString *const kFirebaseAppName2 = @"FIREBASE_APP_NAME_2"; + +/** @var kAPIKey + @brief The fake API key. + */ +static NSString *const kAPIKey = @"FAKE_API_KEY"; + +/** @var kExpectationTimeout + @brief The maximum time waiting for expectations to fulfill. + */ +static const NSTimeInterval kExpectationTimeout = 2; + +/** @var kWaitInterval + @brief The time waiting for background tasks to finish before continue when necessary. + */ +static const NSTimeInterval kWaitInterval = .5; + +@interface FIRAuthLifeCycleTests : XCTestCase + +@end + +@implementation FIRAuthLifeCycleTests + +- (void)setUp { + [super setUp]; + + [FIRApp resetAppForAuthUnitTests]; +} + +- (void)tearDown { + [super tearDown]; +} + +/** @fn testSingleton + @brief Verifies the @c auth method behaves like a singleton. + */ +- (void)testSingleton { + FIRAuth *auth1 = [FIRAuth auth]; + XCTAssertNotNil(auth1); + FIRAuth *auth2 = [FIRAuth auth]; + XCTAssertEqual(auth1, auth2); +} + +/** @fn testDefaultAuth + @brief Verifies the @c auth method associates with the default Firebase app. + */ +- (void)testDefaultAuth { + FIRAuth *auth1 = [FIRAuth auth]; + FIRAuth *auth2 = [FIRAuth authWithApp:[FIRApp defaultApp]]; + XCTAssertEqual(auth1, auth2); + XCTAssertEqual(auth1.app, [FIRApp defaultApp]); +} + +/** @fn testNilAppException + @brief Verifies the @c auth method raises an exception if the default FIRApp is not configured. + */ +- (void)testNilAppException { + [FIRApp resetApps]; + XCTAssertThrows([FIRAuth auth]); +} + +/** @fn testAppAPIkey + @brief Verifies the API key is correctly copied from @c FIRApp to @c FIRAuth . + */ +- (void)testAppAPIkey { + FIRAuth *auth = [FIRAuth auth]; + XCTAssertEqualObjects(auth.requestConfiguration.APIKey, kAPIKey); +} + +/** @fn testAppAssociation + @brief Verifies each @c FIRApp instance associates with a @c FIRAuth . + */ +- (void)testAppAssociation { + FIRApp *app1 = [self app1]; + FIRAuth *auth1 = [FIRAuth authWithApp:app1]; + XCTAssertNotNil(auth1); + XCTAssertEqual(auth1.app, app1); + + FIRApp *app2 = [self app2]; + FIRAuth *auth2 = [FIRAuth authWithApp:app2]; + XCTAssertNotNil(auth2); + XCTAssertEqual(auth2.app, app2); + + XCTAssertNotEqual(auth1, auth2); +} + +/** @fn testLifeCycle + @brief Verifies the life cycle of @c FIRAuth is the same as its associated @c FIRApp . + */ +- (void)testLifeCycle { + __weak FIRApp *app; + __weak FIRAuth *auth; + @autoreleasepool { + FIRApp *app1 = [self app1]; + app = app1; + auth = [FIRAuth authWithApp:app1]; + // Verify that neither the app nor the auth is released yet, i.e., the app owns the auth + // because nothing else retains the auth. + XCTAssertNotNil(app); + XCTAssertNotNil(auth); + } + [self waitForTimeIntervel:kWaitInterval]; + // Verify that both the app and the auth are released upon exit of the autorelease pool, + // i.e., the app is the sole owner of the auth. + XCTAssertNil(app); + XCTAssertNil(auth); +} + +/** @fn app1 + @brief Creates a Firebase app. + @return A @c FIRApp with some name. + */ +- (FIRApp *)app1 { + return [FIRApp appForAuthUnitTestsWithName:kFirebaseAppName1]; +} + +/** @fn app2 + @brief Creates another Firebase app. + @return A @c FIRApp with some other name. + */ +- (FIRApp *)app2 { + return [FIRApp appForAuthUnitTestsWithName:kFirebaseAppName2]; +} + +/** @fn waitForTimeInterval: + @brief Wait for a particular time interval. + @remarks This method also waits for all other pending @c XCTestExpectation instances. + */ +- (void)waitForTimeIntervel:(NSTimeInterval)timeInterval { + static dispatch_queue_t queue; + static dispatch_once_t onceToken; + XCTestExpectation *expectation = [self expectationWithDescription:@"waitForTimeIntervel:"]; + dispatch_once(&onceToken, ^{ + queue = dispatch_queue_create("com.google.FIRAuthUnitTests.waitForTimeIntervel", NULL); + }); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeInterval * NSEC_PER_SEC), queue, ^() { + [expectation fulfill]; + }); + [self waitForExpectationsWithTimeout:timeInterval + kExpectationTimeout handler:nil]; +} + +@end diff --git a/Example/Auth/Tests/FIRAuthTests.m b/Example/Auth/Tests/FIRAuthTests.m index 23518cd51ac..557447a8a58 100644 --- a/Example/Auth/Tests/FIRAuthTests.m +++ b/Example/Auth/Tests/FIRAuthTests.m @@ -20,6 +20,11 @@ #import #import + +#import +#import +#import + #import #import @@ -41,7 +46,7 @@ #import "FIRGetAccountInfoResponse.h" #import "FIRGetOOBConfirmationCodeRequest.h" #import "FIRGetOOBConfirmationCodeResponse.h" -#import "FIRGoogleAuthProvider.h" +#import "FIROAuthProvider.h" #import "FIRSecureTokenRequest.h" #import "FIRSecureTokenResponse.h" #import "FIRResetPasswordRequest.h" @@ -59,25 +64,17 @@ #import "FIRVerifyPhoneNumberRequest.h" #import "FIRVerifyPhoneNumberResponse.h" #import "FIRApp+FIRAuthUnitTests.h" +#import "OAuth/FIROAuthCredential_Internal.h" #import "OCMStubRecorder+FIRAuthUnitTests.h" #import #import "FIRActionCodeSettings.h" #if TARGET_OS_IOS +#import "FIRAuthUIDelegate.h" #import "FIRPhoneAuthCredential.h" #import "FIRPhoneAuthProvider.h" #endif -/** @var kFirebaseAppName1 - @brief A fake Firebase app name. - */ -static NSString *const kFirebaseAppName1 = @"FIREBASE_APP_NAME_1"; - -/** @var kFirebaseAppName2 - @brief Another fake Firebase app name. - */ -static NSString *const kFirebaseAppName2 = @"FIREBASE_APP_NAME_2"; - /** @var kAPIKey @brief The fake API key. */ @@ -173,6 +170,21 @@ */ static NSString *const kVerificationID = @"55432"; +/** @var kOAuthRequestURI + @brief Fake OAuthRequest URI for testing. + */ +static NSString *const kOAuthRequestURI = @"requestURI"; + +/** @var kOAuthSessionID + @brief Fake session ID for testing. + */ +static NSString *const kOAuthSessionID = @"sessionID"; + +/** @var kFakeWebSignInUserInteractionFailureReason + @brief Fake reason for FIRAuthErrorCodeWebSignInUserInteractionFailure error while testing. + */ +static NSString *const kFakeWebSignInUserInteractionFailureReason = @"fake_reason"; + /** @var kContinueURL @brief Fake string value of continue url. */ @@ -288,83 +300,6 @@ - (void)tearDown { [super tearDown]; } -#pragma mark - Life Cycle Tests - -/** @fn testSingleton - @brief Verifies the @c auth method behaves like a singleton. - */ -- (void)testSingleton { - FIRAuth *auth1 = [FIRAuth auth]; - XCTAssertNotNil(auth1); - FIRAuth *auth2 = [FIRAuth auth]; - XCTAssertEqual(auth1, auth2); -} - -/** @fn testDefaultAuth - @brief Verifies the @c auth method associates with the default Firebase app. - */ -- (void)testDefaultAuth { - FIRAuth *auth1 = [FIRAuth auth]; - FIRAuth *auth2 = [FIRAuth authWithApp:[FIRApp defaultApp]]; - XCTAssertEqual(auth1, auth2); - XCTAssertEqual(auth1.app, [FIRApp defaultApp]); -} - -/** @fn testNilAppException - @brief Verifies the @c auth method raises an exception if the default FIRApp is not configured. - */ -- (void)testNilAppException { - [FIRApp resetApps]; - XCTAssertThrows([FIRAuth auth]); -} - -/** @fn testAppAPIkey - @brief Verifies the API key is correctly copied from @c FIRApp to @c FIRAuth . - */ -- (void)testAppAPIkey { - FIRAuth *auth = [FIRAuth auth]; - XCTAssertEqualObjects(auth.requestConfiguration.APIKey, kAPIKey); -} - -/** @fn testAppAssociation - @brief Verifies each @c FIRApp instance associates with a @c FIRAuth . - */ -- (void)testAppAssociation { - FIRApp *app1 = [self app1]; - FIRAuth *auth1 = [FIRAuth authWithApp:app1]; - XCTAssertNotNil(auth1); - XCTAssertEqual(auth1.app, app1); - - FIRApp *app2 = [self app2]; - FIRAuth *auth2 = [FIRAuth authWithApp:app2]; - XCTAssertNotNil(auth2); - XCTAssertEqual(auth2.app, app2); - - XCTAssertNotEqual(auth1, auth2); -} - -/** @fn testLifeCycle - @brief Verifies the life cycle of @c FIRAuth is the same as its associated @c FIRApp . - */ -- (void)testLifeCycle { - __weak FIRApp *app; - __weak FIRAuth *auth; - @autoreleasepool { - FIRApp *app1 = [self app1]; - app = app1; - auth = [FIRAuth authWithApp:app1]; - // Verify that neither the app nor the auth is released yet, i.e., the app owns the auth - // because nothing else retains the auth. - XCTAssertNotNil(app); - XCTAssertNotNil(auth); - } - [self waitForTimeIntervel:kWaitInterval]; - // Verify that both the app and the auth are released upon exit of the autorelease pool, - // i.e., the app is the sole owner of the auth. - XCTAssertNil(app); - XCTAssertNil(auth); -} - #pragma mark - Server API Tests /** @fn testFetchProvidersForEmailSuccess @@ -1203,6 +1138,91 @@ - (void)testSignInWithEmailCredentialEmptyPassword { [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; } +#if TARGET_OS_IOS +/** @fn testSignInWithProviderSuccess + @brief Tests a successful @c signInWithProvider:UIDelegate:completion: call with an OAuth + provider configured for Google. + */ +- (void)testSignInWithProviderSuccess { + OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request, + FIRVerifyAssertionResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.providerID, FIRGoogleAuthProviderID); + XCTAssertTrue(request.returnSecureToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVerifyAssertionResponse = OCMClassMock([FIRVerifyAssertionResponse class]); + OCMStub([mockVerifyAssertionResponse federatedID]).andReturn(kGoogleID); + OCMStub([mockVerifyAssertionResponse providerID]).andReturn(FIRGoogleAuthProviderID); + OCMStub([mockVerifyAssertionResponse localID]).andReturn(kLocalID); + OCMStub([mockVerifyAssertionResponse displayName]).andReturn(kGoogleDisplayName); + [self stubTokensWithMockResponse:mockVerifyAssertionResponse]; + callback(mockVerifyAssertionResponse, nil); + }); + }); + [self expectGetAccountInfoGoogle]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + id mockProvider = OCMClassMock([FIROAuthProvider class]); + OCMExpect([mockProvider getCredentialWithUIDelegate:[OCMArg any] completion:[OCMArg any]]) + .andCallBlock2(^(id delegate, FIRAuthCredentialCallback callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^(){ + FIROAuthCredential *credential = + [[FIROAuthCredential alloc] initWithProviderID:FIRGoogleAuthProviderID + sessionID:kOAuthSessionID + OAuthResponseURLString:kOAuthRequestURI]; + callback(credential, nil); + }); + }); + [[FIRAuth auth] signInWithProvider:mockProvider + UIDelegate:nil + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUserGoogle:authResult.user]; + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testSignInWithProviderFailure + @brief Tests a failed @c signInWithProvider:UIDelegate:completion: call with the error code + FIRAuthErrorCodeWebSignInUserInteractionFailure. + */ +- (void)testSignInWithProviderFailure { + OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]]) + .andDispatchError2([FIRAuthErrorUtils webSignInUserInteractionFailureWithReason: + kFakeWebSignInUserInteractionFailureReason]); + [[FIRAuth auth] signOut:NULL]; + id mockProvider = OCMClassMock([FIROAuthProvider class]); + OCMExpect([mockProvider getCredentialWithUIDelegate:[OCMArg any] completion:[OCMArg any]]) + .andCallBlock2(^(id delegate, FIRAuthCredentialCallback callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^(){ + FIROAuthCredential *credential = + [[FIROAuthCredential alloc] initWithProviderID:FIRGoogleAuthProviderID + sessionID:kOAuthSessionID + OAuthResponseURLString:kOAuthRequestURI]; + callback(credential, nil); + }); + }); + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signInWithProvider:mockProvider + UIDelegate:nil + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(authResult); + XCTAssertEqual(error.code, FIRAuthErrorCodeWebSignInUserInteractionFailure); + XCTAssertEqualObjects(error.userInfo[NSLocalizedFailureReasonErrorKey], + kFakeWebSignInUserInteractionFailureReason); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + /** @fn testSignInWithGoogleAccountExistsError @brief Tests the flow of a failed @c signInWithCredential:completion: with a Google credential where the backend returns a needs @needConfirmation equal to true. An @@ -1282,6 +1302,64 @@ - (void)testSignInWithGoogleCredentialSuccess { OCMVerifyAll(_mockBackend); } +/** @fn testSignInWithOAuthCredentialSuccess + @brief Tests the flow of a successful @c signInWithCredential:completion: call with a generic + OAuth credential (In this case, configured for the Google IDP). + */ +- (void)testSignInWithOAuthCredentialSuccess { + OCMExpect([_mockBackend verifyAssertion:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRVerifyAssertionRequest *_Nullable request, + FIRVerifyAssertionResponseCallback callback) { + XCTAssertEqualObjects(request.APIKey, kAPIKey); + XCTAssertEqualObjects(request.providerID, FIRGoogleAuthProviderID); + XCTAssertEqualObjects(request.requestURI, kOAuthRequestURI); + XCTAssertEqualObjects(request.sessionID, kOAuthSessionID); + XCTAssertTrue(request.returnSecureToken); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockVeriyAssertionResponse = OCMClassMock([FIRVerifyAssertionResponse class]); + OCMStub([mockVeriyAssertionResponse federatedID]).andReturn(kGoogleID); + OCMStub([mockVeriyAssertionResponse providerID]).andReturn(FIRGoogleAuthProviderID); + OCMStub([mockVeriyAssertionResponse localID]).andReturn(kLocalID); + OCMStub([mockVeriyAssertionResponse displayName]).andReturn(kGoogleDisplayName); + [self stubTokensWithMockResponse:mockVeriyAssertionResponse]; + callback(mockVeriyAssertionResponse, nil); + }); + }); + [self expectGetAccountInfoGoogle]; + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [[FIRAuth auth] signOut:NULL]; + id mockProvider = OCMClassMock([FIROAuthProvider class]); + OCMExpect([mockProvider getCredentialWithUIDelegate:[OCMArg any] completion:[OCMArg any]]) + .andCallBlock2(^(id delegate, FIRAuthCredentialCallback callback) { + dispatch_async(FIRAuthGlobalWorkQueue(), ^(){ + FIROAuthCredential *credential = + [[FIROAuthCredential alloc] initWithProviderID:FIRGoogleAuthProviderID + sessionID:kOAuthSessionID + OAuthResponseURLString:kOAuthRequestURI]; + callback(credential, nil); + }); + }); + [mockProvider getCredentialWithUIDelegate:nil + completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) { + XCTAssertTrue([credential isKindOfClass:[FIROAuthCredential class]]); + FIROAuthCredential *OAuthCredential = (FIROAuthCredential *)credential; + XCTAssertEqualObjects(OAuthCredential.OAuthResponseURLString, kOAuthRequestURI); + XCTAssertEqualObjects(OAuthCredential.sessionID, kOAuthSessionID); + [[FIRAuth auth] signInWithCredential:OAuthCredential completion:^(FIRUser *_Nullable user, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + [self assertUserGoogle:user]; + XCTAssertNil(error); + [expectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + [self assertUserGoogle:[FIRAuth auth].currentUser]; + OCMVerifyAll(_mockBackend); +} +#endif // TARGET_OS_IOS + /** @fn testSignInAndRetrieveDataWithCredentialSuccess @brief Tests the flow of a successful @c signInAndRetrieveDataWithCredential:completion: call with an Google Sign-In credential. @@ -2276,22 +2354,6 @@ - (void)enableAutoTokenRefresh { [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; } -/** @fn app1 - @brief Creates a Firebase app. - @return A @c FIRApp with some name. - */ -- (FIRApp *)app1 { - return [FIRApp appForAuthUnitTestsWithName:kFirebaseAppName1]; -} - -/** @fn app2 - @brief Creates another Firebase app. - @return A @c FIRApp with some other name. - */ -- (FIRApp *)app2 { - return [FIRApp appForAuthUnitTestsWithName:kFirebaseAppName2]; -} - /** @fn stubSecureTokensWithMockResponse @brief Creates stubs on the mock response object with access and refresh tokens @param mockResponse The mock response object. diff --git a/Example/Auth/Tests/FIROAuthProviderTests.m b/Example/Auth/Tests/FIROAuthProviderTests.m new file mode 100644 index 00000000000..3284704c804 --- /dev/null +++ b/Example/Auth/Tests/FIROAuthProviderTests.m @@ -0,0 +1,777 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import "FIRApp.h" +#import "FIRAuth_Internal.h" +#import "FIRAuthBackend.h" +#import "FIRAuthErrors.h" +#import "FIRAuthErrorUtils.h" +#import "FIRAuthGlobalWorkQueue.h" +#import "FIRAuthUIDelegate.h" +#import "FIRAuthURLPresenter.h" +#import "FIRAuthWebUtils.h" +#import "FIRAuthRequestConfiguration.h" +#import "FIRGetProjectConfigRequest.h" +#import "FIRGetProjectConfigResponse.h" +#import "FIROAuthProvider.h" +#import "FIROptions.h" +#import "OAuth/FIROAuthCredential_Internal.h" +#import "OCMStubRecorder+FIRAuthUnitTests.h" + +/** @var kExpectationTimeout + @brief The maximum time waiting for expectations to fulfill. + */ +static const NSTimeInterval kExpectationTimeout = 1; + +/** @var kFakeAuthorizedDomain + @brief A fake authorized domain for the app. + */ +static NSString *const kFakeAuthorizedDomain = @"test.firebaseapp.com"; + +/** @var kFakeBundleID + @brief A fake bundle ID. + */ +static NSString *const kFakeBundleID = @"com.firebaseapp.example"; + +/** @var kFakeAccessToken + @brief A fake access token for testing. + */ +static NSString *const kFakeAccessToken = @"fakeAccessToken"; + +/** @var kFakeIDToken + @brief A fake ID token for testing. + */ +static NSString *const kFakeIDToken = @"fakeIDToken"; + +/** @var kFakeProviderID + @brief A fake provider ID for testing. + */ +static NSString *const kFakeProviderID = @"fakeProviderID"; + +/** @var kFakeAPIKey + @brief A fake API key. + */ +static NSString *const kFakeAPIKey = @"asdfghjkl"; + +/** @var kFakeClientID + @brief A fake client ID. + */ +static NSString *const kFakeClientID = @"123456.apps.googleusercontent.com"; + +/** @var kFakeReverseClientID + @brief The dot-reversed version of the fake client ID. + */ +static NSString *const kFakeReverseClientID = @"com.googleusercontent.apps.123456"; + +/** @var kFakeOAuthResponseURL + @brief A fake OAuth response URL used in test. + */ +static NSString *const kFakeOAuthResponseURL = @"fakeOAuthResponseURL"; + +/** @var kFakeRedirectURLResponseURL + @brief A fake callback URL containing a fake response URL. + */ +static NSString *const kFakeRedirectURLResponseURL = @"com.googleusercontent.apps.123456://firebase" + "auth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcallback%3FauthType" + "%3DsignInWithRedirect%26link%3D"; + +/** @var kFakeRedirectURLBaseErrorString + @brief The base for a fake redirect URL string that contains an error. + */ +static NSString *const kFakeRedirectURLBaseErrorString = @"com.googleusercontent.apps.123456://fire" + "baseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcallback%3f"; + +/** @var kNetworkRequestFailedErrorString + @brief The error message returned if a network request failure occurs within the web context. + */ +static NSString *const kNetworkRequestFailedErrorString = @"firebaseError%3D%257B%2522code%2" + "522%253A%2522auth%252Fnetwork-request-failed%2522%252C%2522message%2522%253A%2522The%2520netwo" + "rk%2520request%2520failed%2520.%2522%257D%26authType%3DsignInWithRedirect"; + +/** @var kInvalidClientIDString + @brief The error message returned if the client ID used is invalid. + */ +static NSString *const kInvalidClientIDString = @"firebaseError%3D%257B%2522code%2522%253A%2522auth" + "%252Finvalid-oauth-client-id%2522%252C%2522message%2522%253A%2522The%2520OAuth%2520client%2520" + "ID%2520provided%2520is%2520either%2520invalid%2520or%2520does%2520not%2520match%2520the%2520sp" + "ecified%2520API%2520key.%2522%257D%26authType%3DsignInWithRedirect"; + +/** @var kInternalErrorString + @brief The error message returned if there is an internal error within the web context. + */ +static NSString *const kInternalErrorString = @"firebaseError%3D%257B%2522code%2522%253" + "A%2522auth%252Finternal-error%2522%252C%2522message%2522%253A%2522Internal%2520error%2520.%252" + "2%257D%26authType%3DsignInWithRedirect"; + +/** @var kUnknownErrorString + @brief The error message returned if an unknown error is returned from the web context. + */ +static NSString *const kUnknownErrorString = @"firebaseError%3D%257B%2522code%2522%253A%2522auth%2" + "52Funknown-error-id%2522%252C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%2520pr" + "ovided%2520is%2520either%2520invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2" + "520API%2520key.%2522%257D%26authType%3DsignInWithRedirect"; + +@interface FIROAuthProviderTests : XCTestCase + +@end + +@implementation FIROAuthProviderTests { + /** @var _mockBackend + @brief The mock @c FIRAuthBackendImplementation. + */ + id _mockBackend; + + /** @var _provider + @brief The @c FIROAuthProvider instance under test. + */ + FIROAuthProvider *_provider; + + /** @var _mockAuth + @brief The mock @c FIRAuth instance associated with @c _provider. + */ + id _mockAuth; + + /** @var _mockURLPresenter + @brief The mock @c FIRAuthURLPresenter instance associated with @c _mockAuth. + */ + id _mockURLPresenter; + + /** @var _mockApp + @brief The mock @c FIRApp instance associated with @c _mockAuth. + */ + id _mockApp; +} + +- (void)setUp { + [super setUp]; + _mockBackend = OCMProtocolMock(@protocol(FIRAuthBackendImplementation)); + [FIRAuthBackend setBackendImplementation:_mockBackend]; + _mockAuth = OCMClassMock([FIRAuth class]); + _mockApp = OCMClassMock([FIRApp class]); + OCMStub([_mockAuth app]).andReturn(_mockApp); + id mockOptions = OCMClassMock([FIROptions class]); + OCMStub([(FIRApp *)_mockApp options]).andReturn(mockOptions); + OCMStub([mockOptions clientID]).andReturn(kFakeClientID); + _mockURLPresenter = OCMClassMock([FIRAuthURLPresenter class]); + OCMStub([_mockAuth authURLPresenter]).andReturn(_mockURLPresenter); + id mockRequestConfiguration = OCMClassMock([FIRAuthRequestConfiguration class]); + OCMStub([_mockAuth requestConfiguration]).andReturn(mockRequestConfiguration); + OCMStub([mockRequestConfiguration APIKey]).andReturn(kFakeAPIKey); + _provider = [FIROAuthProvider providerWithProviderID:@"fake id" auth:_mockAuth]; +} + +/** @fn testObtainingOAuthCredentialNoIDToken + @brief Tests the correct creation of an OAuthCredential without an IDToken. + */ +- (void)testObtainingOAuthCredentialNoIDToken { + FIRAuthCredential *credential = + [FIROAuthProvider credentialWithProviderID:kFakeProviderID accessToken:kFakeAccessToken]; + XCTAssertTrue([credential isKindOfClass:[FIROAuthCredential class]]); + FIROAuthCredential *OAuthCredential = (FIROAuthCredential *)credential; + XCTAssertEqualObjects(OAuthCredential.accessToken, kFakeAccessToken); + XCTAssertEqualObjects(OAuthCredential.provider, kFakeProviderID); + XCTAssertNil(OAuthCredential.IDToken); +} + +/** @fn testObtainingOAuthCredentialWithIDToken + @brief Tests the correct creation of an OAuthCredential with an IDToken + */ +- (void)testObtainingOAuthCredentialWithIDToken { + FIRAuthCredential *credential = + [FIROAuthProvider credentialWithProviderID:kFakeProviderID + IDToken:kFakeIDToken + accessToken:kFakeAccessToken]; + XCTAssertTrue([credential isKindOfClass:[FIROAuthCredential class]]); + FIROAuthCredential *OAuthCredential = (FIROAuthCredential *)credential; + XCTAssertEqualObjects(OAuthCredential.accessToken, kFakeAccessToken); + XCTAssertEqualObjects(OAuthCredential.provider, kFakeProviderID); + XCTAssertEqualObjects(OAuthCredential.IDToken, kFakeIDToken); +} + +/** @fn testObtainingOAuthProvider + @brief Tests the correct creation of an FIROAuthProvider instance. + */ +- (void)testObtainingOAuthProvider { + id mockAuth = OCMClassMock([FIRAuth class]); + id mockApp = OCMClassMock([FIRApp class]); + OCMStub([mockAuth app]).andReturn(mockApp); + id mockOptions = OCMClassMock([FIROptions class]); + OCMStub([(FIRApp *)mockApp options]).andReturn(mockOptions); + FIROAuthProvider *OAuthProvider = + [FIROAuthProvider providerWithProviderID:kFakeProviderID auth:mockAuth]; + XCTAssertTrue([OAuthProvider isKindOfClass:[FIROAuthProvider class]]); + XCTAssertEqualObjects(OAuthProvider.providerID, kFakeProviderID); +} + +/** @fn testGetCredentialWithUIDelegate + @brief Tests a successful invocation of @c getCredentialWithUIDelegte:completion: + */ +- (void)testGetCredentialWithUIDelegate { + id mockBundle = OCMClassMock([NSBundle class]); + OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle); + OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]) + .andReturn(@[ @{ @"CFBundleURLSchemes" : @[ kFakeReverseClientID ] } ]); + OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID); + + OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetProjectConfigRequest *request, + FIRGetProjectConfigResponseCallback callback) { + XCTAssertNotNil(request); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockGetProjectConfigResponse = OCMClassMock([FIRGetProjectConfigResponse class]); + OCMStub([mockGetProjectConfigResponse authorizedDomains]). + andReturn(@[ kFakeAuthorizedDomain]); + callback(mockGetProjectConfigResponse, nil); + }); + }); + + id mockUIDelegate = OCMProtocolMock(@protocol(FIRAuthUIDelegate)); + + // Expect view controller presentation by UIDelegate. + OCMExpect([_mockURLPresenter presentURL:OCMOCK_ANY + UIDelegate:mockUIDelegate + callbackMatcher:OCMOCK_ANY + completion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained id unretainedArgument; + // Indices 0 and 1 indicate the hidden arguments self and _cmd. + // `presentURL` is at index 2. + [invocation getArgument:&unretainedArgument atIndex:2]; + NSURL *presentURL = unretainedArgument; + XCTAssertEqualObjects(presentURL.scheme, @"https"); + XCTAssertEqualObjects(presentURL.host, kFakeAuthorizedDomain); + XCTAssertEqualObjects(presentURL.path, @"/__/auth/handler"); + NSDictionary *params = [FIRAuthWebUtils dictionaryWithHttpArgumentsString:presentURL.query]; + XCTAssertEqualObjects(params[@"ibi"], kFakeBundleID); + XCTAssertEqualObjects(params[@"clientId"], kFakeClientID); + XCTAssertEqualObjects(params[@"apiKey"], kFakeAPIKey); + XCTAssertEqualObjects(params[@"authType"], @"signInWithRedirect"); + XCTAssertNotNil(params[@"v"]); + // `callbackMatcher` is at index 4 + [invocation getArgument:&unretainedArgument atIndex:4]; + FIRAuthURLCallbackMatcher callbackMatcher = unretainedArgument; + NSMutableString *redirectURL = [NSMutableString stringWithString:kFakeRedirectURLResponseURL]; + // Add fake OAuthResponse to callback. + [redirectURL appendString:kFakeOAuthResponseURL]; + // Verify that the URL is rejected by the callback matcher without the event ID. + XCTAssertFalse(callbackMatcher([NSURL URLWithString:redirectURL])); + [redirectURL appendString:@"%26eventId%3D"]; + [redirectURL appendString:params[@"eventId"]]; + NSURLComponents *originalComponents = [[NSURLComponents alloc] initWithString:redirectURL]; + // Verify that the URL is accepted by the callback matcher with the matching event ID. + XCTAssertTrue(callbackMatcher([originalComponents URL])); + NSURLComponents *components = [originalComponents copy]; + components.query = @"https"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.host = @"badhost"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.path = @"badpath"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.query = @"badquery"; + XCTAssertFalse(callbackMatcher([components URL])); + + // `completion` is at index 5 + [invocation getArgument:&unretainedArgument atIndex:5]; + FIRAuthURLPresentationCompletion completion = unretainedArgument; + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + completion(originalComponents.URL, nil); + }); + }); + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_provider getCredentialWithUIDelegate:mockUIDelegate + completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(error); + XCTAssertTrue([credential isKindOfClass:[FIROAuthCredential class]]); + FIROAuthCredential *OAuthCredential = (FIROAuthCredential *)credential; + XCTAssertEqualObjects(kFakeOAuthResponseURL, OAuthCredential.OAuthResponseURLString); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testGetCredentialWithUIDelegateUserCancellation + @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegte:completion: due to user + cancelation. + */ +- (void)testGetCredentialWithUIDelegateUserCancellation { + id mockBundle = OCMClassMock([NSBundle class]); + OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle); + OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]) + .andReturn(@[ @{ @"CFBundleURLSchemes" : @[ kFakeReverseClientID ] } ]); + OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID); + + OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetProjectConfigRequest *request, + FIRGetProjectConfigResponseCallback callback) { + XCTAssertNotNil(request); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockGetProjectConfigResponse = OCMClassMock([FIRGetProjectConfigResponse class]); + OCMStub([mockGetProjectConfigResponse authorizedDomains]). + andReturn(@[ kFakeAuthorizedDomain]); + callback(mockGetProjectConfigResponse, nil); + }); + }); + + id mockUIDelegate = OCMProtocolMock(@protocol(FIRAuthUIDelegate)); + + // Expect view controller presentation by UIDelegate. + OCMExpect([_mockURLPresenter presentURL:OCMOCK_ANY + UIDelegate:mockUIDelegate + callbackMatcher:OCMOCK_ANY + completion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained id unretainedArgument; + // Indices 0 and 1 indicate the hidden arguments self and _cmd. + // `presentURL` is at index 2. + [invocation getArgument:&unretainedArgument atIndex:2]; + NSURL *presentURL = unretainedArgument; + XCTAssertEqualObjects(presentURL.scheme, @"https"); + XCTAssertEqualObjects(presentURL.host, kFakeAuthorizedDomain); + XCTAssertEqualObjects(presentURL.path, @"/__/auth/handler"); + NSDictionary *params = [FIRAuthWebUtils dictionaryWithHttpArgumentsString:presentURL.query]; + XCTAssertEqualObjects(params[@"ibi"], kFakeBundleID); + XCTAssertEqualObjects(params[@"clientId"], kFakeClientID); + XCTAssertEqualObjects(params[@"apiKey"], kFakeAPIKey); + XCTAssertEqualObjects(params[@"authType"], @"signInWithRedirect"); + XCTAssertNotNil(params[@"v"]); + // `callbackMatcher` is at index 4 + [invocation getArgument:&unretainedArgument atIndex:4]; + FIRAuthURLCallbackMatcher callbackMatcher = unretainedArgument; + NSMutableString *redirectURL = [NSMutableString stringWithString:kFakeRedirectURLResponseURL]; + // Add fake OAuthResponse to callback. + [redirectURL appendString:kFakeOAuthResponseURL]; + // Verify that the URL is rejected by the callback matcher without the event ID. + XCTAssertFalse(callbackMatcher([NSURL URLWithString:redirectURL])); + [redirectURL appendString:@"%26eventId%3D"]; + [redirectURL appendString:params[@"eventId"]]; + + NSURLComponents *originalComponents = [[NSURLComponents alloc] initWithString:redirectURL]; + // Verify that the URL is accepted by the callback matcher with the matching event ID. + XCTAssertTrue(callbackMatcher([originalComponents URL])); + NSURLComponents *components = [originalComponents copy]; + components.query = @"https"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.host = @"badhost"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.path = @"badpath"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.query = @"badquery"; + XCTAssertFalse(callbackMatcher([components URL])); + + // `completion` is at index 5 + [invocation getArgument:&unretainedArgument atIndex:5]; + FIRAuthURLPresentationCompletion completion = unretainedArgument; + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + completion(nil, [FIRAuthErrorUtils webContextCancelledErrorWithMessage:nil]); + }); + }); + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_provider getCredentialWithUIDelegate:mockUIDelegate + completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(credential); + XCTAssertEqual(FIRAuthErrorCodeWebContextCancelled, error.code); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testGetCredentialWithUIDelegateNetworkRequestFailed + @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegte:completion: due to a + failed network request within the web context. + */ +- (void)testGetCredentialWithUIDelegateNetworkRequestFailed { + id mockBundle = OCMClassMock([NSBundle class]); + OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle); + OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]) + .andReturn(@[ @{ @"CFBundleURLSchemes" : @[ kFakeReverseClientID ] } ]); + OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID); + + OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetProjectConfigRequest *request, + FIRGetProjectConfigResponseCallback callback) { + XCTAssertNotNil(request); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockGetProjectConfigResponse = OCMClassMock([FIRGetProjectConfigResponse class]); + OCMStub([mockGetProjectConfigResponse authorizedDomains]). + andReturn(@[ kFakeAuthorizedDomain]); + callback(mockGetProjectConfigResponse, nil); + }); + }); + + id mockUIDelegate = OCMProtocolMock(@protocol(FIRAuthUIDelegate)); + + // Expect view controller presentation by UIDelegate. + OCMExpect([_mockURLPresenter presentURL:OCMOCK_ANY + UIDelegate:mockUIDelegate + callbackMatcher:OCMOCK_ANY + completion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained id unretainedArgument; + // Indices 0 and 1 indicate the hidden arguments self and _cmd. + // `presentURL` is at index 2. + [invocation getArgument:&unretainedArgument atIndex:2]; + NSURL *presentURL = unretainedArgument; + XCTAssertEqualObjects(presentURL.scheme, @"https"); + XCTAssertEqualObjects(presentURL.host, kFakeAuthorizedDomain); + XCTAssertEqualObjects(presentURL.path, @"/__/auth/handler"); + NSDictionary *params = [FIRAuthWebUtils dictionaryWithHttpArgumentsString:presentURL.query]; + XCTAssertEqualObjects(params[@"ibi"], kFakeBundleID); + XCTAssertEqualObjects(params[@"clientId"], kFakeClientID); + XCTAssertEqualObjects(params[@"apiKey"], kFakeAPIKey); + XCTAssertEqualObjects(params[@"authType"], @"signInWithRedirect"); + XCTAssertNotNil(params[@"v"]); + // `callbackMatcher` is at index 4 + [invocation getArgument:&unretainedArgument atIndex:4]; + FIRAuthURLCallbackMatcher callbackMatcher = unretainedArgument; + NSMutableString *redirectURL = + [NSMutableString stringWithString:kFakeRedirectURLBaseErrorString]; + [redirectURL appendString:kNetworkRequestFailedErrorString]; + // Verify that the URL is rejected by the callback matcher without the event ID. + XCTAssertFalse(callbackMatcher([NSURL URLWithString:redirectURL])); + [redirectURL appendString:@"%26eventId%3D"]; + [redirectURL appendString:params[@"eventId"]]; + + NSURLComponents *originalComponents = [[NSURLComponents alloc] initWithString:redirectURL]; + // Verify that the URL is accepted by the callback matcher with the matching event ID. + XCTAssertTrue(callbackMatcher([originalComponents URL])); + NSURLComponents *components = [originalComponents copy]; + components.query = @"https"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.host = @"badhost"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.path = @"badpath"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.query = @"badquery"; + XCTAssertFalse(callbackMatcher([components URL])); + + // `completion` is at index 5 + [invocation getArgument:&unretainedArgument atIndex:5]; + FIRAuthURLPresentationCompletion completion = unretainedArgument; + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + completion(originalComponents.URL, nil); + }); + }); + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_provider getCredentialWithUIDelegate:mockUIDelegate + completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(credential); + XCTAssertEqual(FIRAuthErrorCodeWebNetworkRequestFailed, error.code); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testGetCredentialWithUIDelegateInternalError + @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegte:completion: due to an + internal error within the web context. + */ +- (void)testGetCredentialWithUIDelegateInternalError { + id mockBundle = OCMClassMock([NSBundle class]); + OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle); + OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]) + .andReturn(@[ @{ @"CFBundleURLSchemes" : @[ kFakeReverseClientID ] } ]); + OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID); + + OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetProjectConfigRequest *request, + FIRGetProjectConfigResponseCallback callback) { + XCTAssertNotNil(request); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockGetProjectConfigResponse = OCMClassMock([FIRGetProjectConfigResponse class]); + OCMStub([mockGetProjectConfigResponse authorizedDomains]). + andReturn(@[ kFakeAuthorizedDomain]); + callback(mockGetProjectConfigResponse, nil); + }); + }); + + id mockUIDelegate = OCMProtocolMock(@protocol(FIRAuthUIDelegate)); + + // Expect view controller presentation by UIDelegate. + OCMExpect([_mockURLPresenter presentURL:OCMOCK_ANY + UIDelegate:mockUIDelegate + callbackMatcher:OCMOCK_ANY + completion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained id unretainedArgument; + // Indices 0 and 1 indicate the hidden arguments self and _cmd. + // `presentURL` is at index 2. + [invocation getArgument:&unretainedArgument atIndex:2]; + NSURL *presentURL = unretainedArgument; + XCTAssertEqualObjects(presentURL.scheme, @"https"); + XCTAssertEqualObjects(presentURL.host, kFakeAuthorizedDomain); + XCTAssertEqualObjects(presentURL.path, @"/__/auth/handler"); + NSDictionary *params = [FIRAuthWebUtils dictionaryWithHttpArgumentsString:presentURL.query]; + XCTAssertEqualObjects(params[@"ibi"], kFakeBundleID); + XCTAssertEqualObjects(params[@"clientId"], kFakeClientID); + XCTAssertEqualObjects(params[@"apiKey"], kFakeAPIKey); + XCTAssertEqualObjects(params[@"authType"], @"signInWithRedirect"); + XCTAssertNotNil(params[@"v"]); + // `callbackMatcher` is at index 4 + [invocation getArgument:&unretainedArgument atIndex:4]; + FIRAuthURLCallbackMatcher callbackMatcher = unretainedArgument; + NSMutableString *redirectURL = + [NSMutableString stringWithString:kFakeRedirectURLBaseErrorString]; + // Add internal error string to redirect URL. + [redirectURL appendString:kInternalErrorString]; + // Verify that the URL is rejected by the callback matcher without the event ID. + XCTAssertFalse(callbackMatcher([NSURL URLWithString:redirectURL])); + [redirectURL appendString:@"%26eventId%3D"]; + [redirectURL appendString:params[@"eventId"]]; + + NSURLComponents *originalComponents = [[NSURLComponents alloc] initWithString:redirectURL]; + // Verify that the URL is accepted by the callback matcher with the matching event ID. + XCTAssertTrue(callbackMatcher([originalComponents URL])); + NSURLComponents *components = [originalComponents copy]; + components.query = @"https"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.host = @"badhost"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.path = @"badpath"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.query = @"badquery"; + XCTAssertFalse(callbackMatcher([components URL])); + + // `completion` is at index 5 + [invocation getArgument:&unretainedArgument atIndex:5]; + FIRAuthURLPresentationCompletion completion = unretainedArgument; + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + completion(originalComponents.URL, nil); + }); + }); + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_provider getCredentialWithUIDelegate:mockUIDelegate + completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(credential); + XCTAssertEqual(FIRAuthErrorCodeWebInternalError, error.code); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testGetCredentialWithUIDelegateInvalidClientID + @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegte:completion: due to an + use of an invalid client ID. + */ +- (void)testGetCredentialWithUIDelegateInvalidClientID { + id mockBundle = OCMClassMock([NSBundle class]); + OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle); + OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]) + .andReturn(@[ @{ @"CFBundleURLSchemes" : @[ kFakeReverseClientID ] } ]); + OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID); + + OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetProjectConfigRequest *request, + FIRGetProjectConfigResponseCallback callback) { + XCTAssertNotNil(request); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockGetProjectConfigResponse = OCMClassMock([FIRGetProjectConfigResponse class]); + OCMStub([mockGetProjectConfigResponse authorizedDomains]). + andReturn(@[ kFakeAuthorizedDomain]); + callback(mockGetProjectConfigResponse, nil); + }); + }); + + id mockUIDelegate = OCMProtocolMock(@protocol(FIRAuthUIDelegate)); + + // Expect view controller presentation by UIDelegate. + OCMExpect([_mockURLPresenter presentURL:OCMOCK_ANY + UIDelegate:mockUIDelegate + callbackMatcher:OCMOCK_ANY + completion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained id unretainedArgument; + // Indices 0 and 1 indicate the hidden arguments self and _cmd. + // `presentURL` is at index 2. + [invocation getArgument:&unretainedArgument atIndex:2]; + NSURL *presentURL = unretainedArgument; + XCTAssertEqualObjects(presentURL.scheme, @"https"); + XCTAssertEqualObjects(presentURL.host, kFakeAuthorizedDomain); + XCTAssertEqualObjects(presentURL.path, @"/__/auth/handler"); + NSDictionary *params = [FIRAuthWebUtils dictionaryWithHttpArgumentsString:presentURL.query]; + XCTAssertEqualObjects(params[@"ibi"], kFakeBundleID); + XCTAssertEqualObjects(params[@"clientId"], kFakeClientID); + XCTAssertEqualObjects(params[@"apiKey"], kFakeAPIKey); + XCTAssertEqualObjects(params[@"authType"], @"signInWithRedirect"); + XCTAssertNotNil(params[@"v"]); + // `callbackMatcher` is at index 4 + [invocation getArgument:&unretainedArgument atIndex:4]; + FIRAuthURLCallbackMatcher callbackMatcher = unretainedArgument; + NSMutableString *redirectURL = + [NSMutableString stringWithString:kFakeRedirectURLBaseErrorString]; + // Add invalid client ID error to redirect URL. + [redirectURL appendString:kInvalidClientIDString]; + // Verify that the URL is rejected by the callback matcher without the event ID. + XCTAssertFalse(callbackMatcher([NSURL URLWithString:redirectURL])); + [redirectURL appendString:@"%26eventId%3D"]; + [redirectURL appendString:params[@"eventId"]]; + + NSURLComponents *originalComponents = [[NSURLComponents alloc] initWithString:redirectURL]; + // Verify that the URL is accepted by the callback matcher with the matching event ID. + XCTAssertTrue(callbackMatcher([originalComponents URL])); + NSURLComponents *components = [originalComponents copy]; + components.query = @"https"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.host = @"badhost"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.path = @"badpath"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.query = @"badquery"; + XCTAssertFalse(callbackMatcher([components URL])); + + // `completion` is at index 5 + [invocation getArgument:&unretainedArgument atIndex:5]; + FIRAuthURLPresentationCompletion completion = unretainedArgument; + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + completion(originalComponents.URL, nil); + }); + }); + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_provider getCredentialWithUIDelegate:mockUIDelegate + completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(credential); + XCTAssertEqual(FIRAuthErrorCodeInvalidClientID, error.code); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +/** @fn testGetCredentialWithUIDelegateUknownError + @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegte:completion: due to an + unknown error. + */ +- (void)testGetCredentialWithUIDelegateUknownError { + id mockBundle = OCMClassMock([NSBundle class]); + OCMStub(ClassMethod([mockBundle mainBundle])).andReturn(mockBundle); + OCMStub([mockBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]) + .andReturn(@[ @{ @"CFBundleURLSchemes" : @[ kFakeReverseClientID ] } ]); + OCMStub([mockBundle bundleIdentifier]).andReturn(kFakeBundleID); + + OCMExpect([_mockBackend getProjectConfig:[OCMArg any] callback:[OCMArg any]]) + .andCallBlock2(^(FIRGetProjectConfigRequest *request, + FIRGetProjectConfigResponseCallback callback) { + XCTAssertNotNil(request); + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + id mockGetProjectConfigResponse = OCMClassMock([FIRGetProjectConfigResponse class]); + OCMStub([mockGetProjectConfigResponse authorizedDomains]). + andReturn(@[ kFakeAuthorizedDomain]); + callback(mockGetProjectConfigResponse, nil); + }); + }); + + id mockUIDelegate = OCMProtocolMock(@protocol(FIRAuthUIDelegate)); + + // Expect view controller presentation by UIDelegate. + OCMExpect([_mockURLPresenter presentURL:OCMOCK_ANY + UIDelegate:mockUIDelegate + callbackMatcher:OCMOCK_ANY + completion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + __unsafe_unretained id unretainedArgument; + // Indices 0 and 1 indicate the hidden arguments self and _cmd. + // `presentURL` is at index 2. + [invocation getArgument:&unretainedArgument atIndex:2]; + NSURL *presentURL = unretainedArgument; + XCTAssertEqualObjects(presentURL.scheme, @"https"); + XCTAssertEqualObjects(presentURL.host, kFakeAuthorizedDomain); + XCTAssertEqualObjects(presentURL.path, @"/__/auth/handler"); + NSDictionary *params = [FIRAuthWebUtils dictionaryWithHttpArgumentsString:presentURL.query]; + XCTAssertEqualObjects(params[@"ibi"], kFakeBundleID); + XCTAssertEqualObjects(params[@"clientId"], kFakeClientID); + XCTAssertEqualObjects(params[@"apiKey"], kFakeAPIKey); + XCTAssertEqualObjects(params[@"authType"], @"signInWithRedirect"); + XCTAssertNotNil(params[@"v"]); + // `callbackMatcher` is at index 4 + [invocation getArgument:&unretainedArgument atIndex:4]; + FIRAuthURLCallbackMatcher callbackMatcher = unretainedArgument; + NSMutableString *redirectURL = + [NSMutableString stringWithString:kFakeRedirectURLBaseErrorString]; + // Add unknown error to redirect URL. + [redirectURL appendString:kUnknownErrorString]; + // Verify that the URL is rejected by the callback matcher without the event ID. + XCTAssertFalse(callbackMatcher([NSURL URLWithString:redirectURL])); + [redirectURL appendString:@"%26eventId%3D"]; + [redirectURL appendString:params[@"eventId"]]; + + NSURLComponents *originalComponents = [[NSURLComponents alloc] initWithString:redirectURL]; + // Verify that the URL is accepted by the callback matcher with the matching event ID. + XCTAssertTrue(callbackMatcher([originalComponents URL])); + NSURLComponents *components = [originalComponents copy]; + components.query = @"https"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.host = @"badhost"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.path = @"badpath"; + XCTAssertFalse(callbackMatcher([components URL])); + components = [originalComponents copy]; + components.query = @"badquery"; + XCTAssertFalse(callbackMatcher([components URL])); + + // `completion` is at index 5 + [invocation getArgument:&unretainedArgument atIndex:5]; + FIRAuthURLPresentationCompletion completion = unretainedArgument; + dispatch_async(FIRAuthGlobalWorkQueue(), ^() { + completion(originalComponents.URL, nil); + }); + }); + + XCTestExpectation *expectation = [self expectationWithDescription:@"callback"]; + [_provider getCredentialWithUIDelegate:mockUIDelegate + completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) { + XCTAssertTrue([NSThread isMainThread]); + XCTAssertNil(credential); + XCTAssertEqual(FIRAuthErrorCodeWebSignInUserInteractionFailure, error.code); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; + OCMVerifyAll(_mockBackend); +} + +@end diff --git a/Example/Auth/Tests/FIRUserTests.m b/Example/Auth/Tests/FIRUserTests.m index 5587e4addfd..e05e9a012df 100644 --- a/Example/Auth/Tests/FIRUserTests.m +++ b/Example/Auth/Tests/FIRUserTests.m @@ -2259,7 +2259,8 @@ - (void)testlinkPhoneCredentialAlreadyExistsError { providerID:FIRPhoneAuthProviderID]; callback(nil, [FIRAuthErrorUtils credentialAlreadyInUseErrorWithMessage:nil - credential:credential]); + credential:credential + email:nil]); }); }); }; @@ -2279,7 +2280,7 @@ - (void)testlinkPhoneCredentialAlreadyExistsError { NSError *_Nullable error) { XCTAssertNil(linkAuthResult); XCTAssertEqual(error.code, FIRAuthErrorCodeCredentialAlreadyInUse); - FIRPhoneAuthCredential *credential = error.userInfo[FIRAuthUpdatedCredentialKey]; + FIRPhoneAuthCredential *credential = error.userInfo[FIRAuthErrorUserInfoUpdatedCredentialKey]; XCTAssertEqual(credential.temporaryProof, kTemporaryProof); XCTAssertEqual(credential.phoneNumber, kPhoneNumber); [expectation fulfill]; diff --git a/Example/Auth/Tests/FIRVerifyAssertionRequestTests.m b/Example/Auth/Tests/FIRVerifyAssertionRequestTests.m index 79699dca7ae..d801da6e28d 100644 --- a/Example/Auth/Tests/FIRVerifyAssertionRequestTests.m +++ b/Example/Auth/Tests/FIRVerifyAssertionRequestTests.m @@ -79,10 +79,10 @@ */ static NSString *const kTestInputEmail = @"testInputEmail"; -/** @var kPendingIDTokenKey - @brief The key for the "pendingIdToken" value in the request. +/** @var kPendingTokenKey + @brief The key for the "pendingToken" value in the request. */ -static NSString *const kPendingIDTokenKey = @"pendingIdToken"; +static NSString *const kPendingTokenKey = @"pendingToken"; /** @var kTestPendingToken @brief Fake pending token used for testing. @@ -216,7 +216,7 @@ - (void)testVerifyAssertionRequestOptionalFields { request.providerAccessToken = kTestProviderAccessToken; request.accessToken = kTestAccessToken; request.inputEmail = kTestInputEmail; - request.pendingIDToken = kTestPendingToken; + request.pendingToken = kTestPendingToken; request.providerOAuthTokenSecret = kTestProviderOAuthTokenSecret; request.autoCreate = NO; diff --git a/Example/Auth/Tests/FIRVerifyAssertionResponseTests.m b/Example/Auth/Tests/FIRVerifyAssertionResponseTests.m index 923a9e52cdf..cc46d6f0e68 100644 --- a/Example/Auth/Tests/FIRVerifyAssertionResponseTests.m +++ b/Example/Auth/Tests/FIRVerifyAssertionResponseTests.m @@ -254,6 +254,7 @@ - (void)testUserDisabledError { XCTAssertEqual(RPCError.code, FIRAuthErrorCodeUserDisabled); } +#if TARGET_OS_IOS /** @fn testCredentialAlreadyInUseError @brief This test simulates a @c FIRAuthErrorCodeCredentialAlreadyInUse error. */ @@ -280,6 +281,7 @@ - (void)testCredentialAlreadyInUseError { XCTAssertNil(RPCResponse); XCTAssertEqual(RPCError.code, FIRAuthErrorCodeCredentialAlreadyInUse); } +#endif // TARGET_OS_IOS /** @fn testOperationNotAllowedError @brief This test simulates a @c FIRAuthErrorCodeOperationNotAllowed error. diff --git a/Example/Auth/Tests/FIRVerifyPhoneNumberResponseTests.m b/Example/Auth/Tests/FIRVerifyPhoneNumberResponseTests.m index 03637298953..801a7d276d5 100644 --- a/Example/Auth/Tests/FIRVerifyPhoneNumberResponseTests.m +++ b/Example/Auth/Tests/FIRVerifyPhoneNumberResponseTests.m @@ -278,7 +278,7 @@ - (void)testSuccessfulVerifyPhoneNumberResponseWithTemporaryProof { XCTAssert(callbackInvoked); XCTAssertNil(RPCResponse); - FIRPhoneAuthCredential *credential = RPCError.userInfo[FIRAuthUpdatedCredentialKey]; + FIRPhoneAuthCredential *credential = RPCError.userInfo[FIRAuthErrorUserInfoUpdatedCredentialKey]; XCTAssertEqualObjects(credential.temporaryProof, kFakeTemporaryProof); XCTAssertEqualObjects(credential.phoneNumber, kFakePhoneNumber); } diff --git a/Example/Core/Tests/FIRAppTest.m b/Example/Core/Tests/FIRAppTest.m index c144d6f438b..d688e3a0c88 100644 --- a/Example/Core/Tests/FIRAppTest.m +++ b/Example/Core/Tests/FIRAppTest.m @@ -389,11 +389,11 @@ - (void)testAppIDFormatInvalid { // Some direct tests of the validateAppIDFormat:withVersion: method. // Sanity checks first. NSString *const kGoodAppIDV1 = @"1:1337:ios:deadbeef"; - NSString *const kGoodVersionV1 = @"1:"; + NSString *const kGoodVersionV1 = @"1"; XCTAssertTrue([FIRApp validateAppIDFormat:kGoodAppIDV1 withVersion:kGoodVersionV1]); NSString *const kGoodAppIDV2 = @"2:1337:ios:5e18052ab54fbfec"; - NSString *const kGoodVersionV2 = @"2:"; + NSString *const kGoodVersionV2 = @"2"; XCTAssertTrue([FIRApp validateAppIDFormat:kGoodAppIDV2 withVersion:kGoodVersionV2]); // Version mismatch. @@ -440,18 +440,13 @@ - (void)testAppIDFingerprintInvalid { // Some direct tests of the validateAppIDFingerprint:withVersion: method. // Sanity checks first. NSString *const kGoodAppIDV1 = @"1:1337:ios:deadbeef"; - NSString *const kGoodVersionV1 = @"1:"; + NSString *const kGoodVersionV1 = @"1"; XCTAssertTrue([FIRApp validateAppIDFingerprint:kGoodAppIDV1 withVersion:kGoodVersionV1]); NSString *const kGoodAppIDV2 = @"2:1337:ios:5e18052ab54fbfec"; - NSString *const kGoodVersionV2 = @"2:"; + NSString *const kGoodVersionV2 = @"2"; XCTAssertTrue([FIRApp validateAppIDFormat:kGoodAppIDV2 withVersion:kGoodVersionV2]); - // Version mismatch. - XCTAssertFalse([FIRApp validateAppIDFingerprint:kGoodAppIDV2 withVersion:kGoodVersionV1]); - XCTAssertFalse([FIRApp validateAppIDFingerprint:kGoodAppIDV1 withVersion:kGoodVersionV2]); - XCTAssertFalse([FIRApp validateAppIDFingerprint:kGoodAppIDV1 withVersion:@"999:"]); - // Nil or empty strings. XCTAssertFalse([FIRApp validateAppIDFingerprint:kGoodAppIDV1 withVersion:nil]); XCTAssertFalse([FIRApp validateAppIDFingerprint:kGoodAppIDV1 withVersion:@""]); @@ -464,35 +459,19 @@ - (void)testAppIDFingerprintInvalid { XCTAssertFalse([FIRApp validateAppIDFingerprint:kGoodVersionV1 withVersion:kGoodVersionV1]); // The version is the entire app ID. XCTAssertFalse([FIRApp validateAppIDFingerprint:kGoodAppIDV1 withVersion:kGoodAppIDV1]); - - // Versions digits that may make a partial match. - XCTAssertFalse([FIRApp validateAppIDFingerprint:@"01:1337:ios:deadbeef" - withVersion:kGoodVersionV1]); - XCTAssertFalse([FIRApp validateAppIDFingerprint:@"10:1337:ios:deadbeef" - withVersion:kGoodVersionV1]); - XCTAssertFalse([FIRApp validateAppIDFingerprint:@"11:1337:ios:deadbeef" - withVersion:kGoodVersionV1]); - XCTAssertFalse([FIRApp validateAppIDFingerprint:@"21:1337:ios:5e18052ab54fbfec" - withVersion:kGoodVersionV2]); - XCTAssertFalse([FIRApp validateAppIDFingerprint:@"22:1337:ios:5e18052ab54fbfec" - withVersion:kGoodVersionV2]); - XCTAssertFalse([FIRApp validateAppIDFingerprint:@"02:1337:ios:5e18052ab54fbfec" - withVersion:kGoodVersionV2]); - XCTAssertFalse([FIRApp validateAppIDFingerprint:@"20:1337:ios:5e18052ab54fbfec" - withVersion:kGoodVersionV2]); - // Extra fields. - XCTAssertFalse([FIRApp validateAppIDFingerprint:@"ab:1:1337:ios:deadbeef" - withVersion:kGoodVersionV1]); - XCTAssertFalse([FIRApp validateAppIDFingerprint:@"1:ab:1337:ios:deadbeef" - withVersion:kGoodVersionV1]); - XCTAssertFalse([FIRApp validateAppIDFingerprint:@"1:1337:ab:ios:deadbeef" - withVersion:kGoodVersionV1]); - XCTAssertFalse([FIRApp validateAppIDFingerprint:@"1:1337:ios:ab:deadbeef" - withVersion:kGoodVersionV1]); - XCTAssertFalse([FIRApp validateAppIDFingerprint:@"1:1337:ios:deadbeef:ab" - withVersion:kGoodVersionV1]); } +// Uncomment if you need to measure performance of [FIRApp validateAppID:]. +// It is commented because measures are heavily dependent on a build agent configuration, +// so it cannot produce reliable resault on CI +//- (void)testAppIDFingerprintPerfomance { +// [self measureBlock:^{ +// for (NSInteger i = 0; i < 100; ++i) { +// [self testAppIDPrefix]; +// } +// }]; +//} + #pragma mark - Automatic Data Collection Tests - (void)testGlobalDataCollectionNoFlags { diff --git a/Example/Core/Tests/FIRBundleUtilTest.m b/Example/Core/Tests/FIRBundleUtilTest.m index 1204a8945fc..ad0669ddf54 100644 --- a/Example/Core/Tests/FIRBundleUtilTest.m +++ b/Example/Core/Tests/FIRBundleUtilTest.m @@ -15,6 +15,7 @@ #import "FIRTestCase.h" #import +#import static NSString *const kResultPath = @"resultPath"; static NSString *const kResourceName = @"resourceName"; @@ -69,16 +70,53 @@ - (void)testFindOptionsDictionaryPath_secondBundle { - (void)testBundleIdentifierExistsInBundles { NSString *bundleID = @"com.google.test"; [OCMStub([self.mockBundle bundleIdentifier]) andReturn:bundleID]; - XCTAssertTrue([FIRBundleUtil hasBundleIdentifier:bundleID inBundles:@[ self.mockBundle ]]); + XCTAssertTrue([FIRBundleUtil hasBundleIdentifierPrefix:bundleID inBundles:@[ self.mockBundle ]]); } - (void)testBundleIdentifierExistsInBundles_notExist { [OCMStub([self.mockBundle bundleIdentifier]) andReturn:@"com.google.test"]; - XCTAssertFalse([FIRBundleUtil hasBundleIdentifier:@"not-exist" inBundles:@[ self.mockBundle ]]); + XCTAssertFalse([FIRBundleUtil hasBundleIdentifierPrefix:@"not-exist" + inBundles:@[ self.mockBundle ]]); } - (void)testBundleIdentifierExistsInBundles_emptyBundlesArray { - XCTAssertFalse([FIRBundleUtil hasBundleIdentifier:@"com.google.test" inBundles:@[]]); + XCTAssertFalse([FIRBundleUtil hasBundleIdentifierPrefix:@"com.google.test" inBundles:@[]]); +} + +- (void)testBundleIdentifierHasPrefixInBundlesForExtension { + id environmentUtilsMock = [OCMockObject mockForClass:[GULAppEnvironmentUtil class]]; + [[[environmentUtilsMock stub] andReturnValue:@(YES)] isAppExtension]; + + [OCMStub([self.mockBundle bundleIdentifier]) andReturn:@"com.google.test"]; + XCTAssertTrue([FIRBundleUtil hasBundleIdentifierPrefix:@"com.google.test.someextension" + inBundles:@[ self.mockBundle ]]); + + [environmentUtilsMock stopMocking]; +} + +- (void)testBundleIdentifierHasPrefixInBundlesNotValidExtension { + id environmentUtilsMock = [OCMockObject mockForClass:[GULAppEnvironmentUtil class]]; + [[[environmentUtilsMock stub] andReturnValue:@(YES)] isAppExtension]; + + [OCMStub([self.mockBundle bundleIdentifier]) andReturn:@"com.google.test"]; + XCTAssertFalse([FIRBundleUtil hasBundleIdentifierPrefix:@"com.google.test.someextension.some" + inBundles:@[ self.mockBundle ]]); + + XCTAssertFalse([FIRBundleUtil hasBundleIdentifierPrefix:@"com.google.testsomeextension" + inBundles:@[ self.mockBundle ]]); + + XCTAssertFalse([FIRBundleUtil hasBundleIdentifierPrefix:@"com.google.testsomeextension.some" + inBundles:@[ self.mockBundle ]]); + + XCTAssertFalse([FIRBundleUtil hasBundleIdentifierPrefix:@"not-exist" + inBundles:@[ self.mockBundle ]]); + + // Should be NO, since if @"com.google.tests" is an app extension identifier, then the app bundle + // identifier is @"com.google" + XCTAssertFalse([FIRBundleUtil hasBundleIdentifierPrefix:@"com.google.tests" + inBundles:@[ self.mockBundle ]]); + + [environmentUtilsMock stopMocking]; } @end diff --git a/Example/DynamicLinks/Tests/FIRDLScionLoggingTest.m b/Example/DynamicLinks/Tests/FIRDLScionLoggingTest.m index 47778c34708..25f53658e6a 100644 --- a/Example/DynamicLinks/Tests/FIRDLScionLoggingTest.m +++ b/Example/DynamicLinks/Tests/FIRDLScionLoggingTest.m @@ -74,6 +74,20 @@ - (void)setUserPropertyWithOrigin:(nonnull NSString *)origin name:(nonnull NSString *)name value:(nonnull id)value { } + +- (void)checkLastNotificationForOrigin:(nonnull NSString *)origin + queue:(nonnull dispatch_queue_t)queue + callback:(nonnull void (^)(NSString *_Nullable)) + currentLastNotificationProperty { +} + +- (void)registerAnalyticsListener:(nonnull id)listener + withOrigin:(nonnull NSString *)origin { +} + +- (void)unregisterAnalyticsListenerWithOrigin:(nonnull NSString *)origin { +} + @end @interface FIRDLScionLoggingTest : XCTestCase diff --git a/Example/DynamicLinks/Tests/FIRDynamicLinksTest.m b/Example/DynamicLinks/Tests/FIRDynamicLinksTest.m index 74fc2373d78..1eb5e227585 100644 --- a/Example/DynamicLinks/Tests/FIRDynamicLinksTest.m +++ b/Example/DynamicLinks/Tests/FIRDynamicLinksTest.m @@ -21,6 +21,7 @@ #import #import #import "DynamicLinks/FIRDLRetrievalProcessFactory.h" +#import "DynamicLinks/FIRDLRetrievalProcessResult+Private.h" #import "DynamicLinks/FIRDynamicLink+Private.h" #import "DynamicLinks/FIRDynamicLinkNetworking+Private.h" #import "DynamicLinks/FIRDynamicLinks+FirstParty.h" @@ -76,6 +77,7 @@ - (BOOL)setUpWithLaunchOptions:(nullable NSDictionary *)launchOptions clientID:(NSString *)clientID urlScheme:(nullable NSString *)urlScheme userDefaults:(nullable NSUserDefaults *)userDefaults; +- (BOOL)canParseUniversalLinkURL:(nullable NSURL *)url; @end @interface FakeShortLinkResolver : FIRDynamicLinkNetworking @@ -773,13 +775,90 @@ - (void)testUniversalLinkWithSubdomain_DeepLinkWithParameters { XCTAssertEqualObjects(dynamicLink.url.absoluteString, parsedDeepLinkString); } -- (void)testMatchesUnversalLinkWithLongDurableLink { - NSString *urlString = - @"https://sample.page.link?link=https://google.com/test&ibi=com.google.sample&ius=79306483"; - NSURL *url = [NSURL URLWithString:urlString]; - BOOL matchesShort = [self.service matchesShortLinkFormat:url]; +- (void)testMatchesShortLinkFormat { + NSArray *urlStrings = + @[ @"https://test.app.goo.gl/xyz", @"https://test.app.goo.gl/xyz?link=" ]; + + for (NSString *urlString in urlStrings) { + NSURL *url = [NSURL URLWithString:urlString]; + BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; - XCTAssertFalse(matchesShort, @"Long Durable Link should not match short link format"); + XCTAssertTrue(matchesShortLinkFormat, + @"Non-DDL domain URL matched short link format with URL: %@", url); + } +} + +// Custom domain entries in plist file: +// https://google.com +// https://google.com/one +// https://a.firebase.com/mypath +- (void)testFailMatchesShortLinkFormatForCustomDomains { + NSArray *urlStrings = @[ + @"https://google.com", + @"https://google.com?link=", + @"https://a.firebase.com", + @"https://a.firebase.com/mypath?link=", + ]; + + for (NSString *urlString in urlStrings) { + NSURL *url = [NSURL URLWithString:urlString]; + BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; + + XCTAssertFalse(matchesShortLinkFormat, + @"Non-DDL domain URL matched short link format with URL: %@", url); + } +} + +// Custom domain entries in plist file: +// https://google.com +// https://google.com/one +// https://a.firebase.com/mypath +- (void)testPassMatchesShortLinkFormatForCustomDomains { + NSArray *urlStrings = @[ + @"https://google.com/xyz", @"https://google.com/xyz/?link=", @"https://google.com/xyz?link=", + @"https://google.com/one/xyz", @"https://google.com/one/xyz?link=", + @"https://google.com/one?utm_campaignlink=", @"https://google.com/mylink", + @"https://google.com/one/mylink", @"https://a.firebase.com/mypath/mylink" + ]; + + for (NSString *urlString in urlStrings) { + NSURL *url = [NSURL URLWithString:urlString]; + BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; + + XCTAssertTrue(matchesShortLinkFormat, + @"Non-DDL domain URL matched short link format with URL: %@", url); + } +} + +- (void)testPassMatchesShortLinkFormat { + NSArray *urlStrings = @[ + @"https://test.app.goo.gl/xyz", + @"https://test.app.goo.gl/xyz?link=", + ]; + + for (NSString *urlString in urlStrings) { + NSURL *url = [NSURL URLWithString:urlString]; + BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; + + XCTAssertTrue(matchesShortLinkFormat, + @"Non-DDL domain URL matched short link format with URL: %@", url); + } +} + +- (void)testFailMatchesShortLinkFormat { + NSArray *urlStrings = @[ + @"https://test.app.goo.gl", @"https://test.app.goo.gl?link=", @"https://test.app.goo.gl/", + @"https://sample.page.link?link=https://google.com/test&ibi=com.google.sample&ius=79306483" + @"https://sample.page.link/?link=https://google.com/test&ibi=com.google.sample&ius=79306483" + ]; + + for (NSString *urlString in urlStrings) { + NSURL *url = [NSURL URLWithString:urlString]; + BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; + + XCTAssertFalse(matchesShortLinkFormat, + @"Non-DDL domain URL matched short link format with URL: %@", url); + } } - (void)testMatchesUnversalLinkWithShortDurableLink { @@ -967,6 +1046,49 @@ - (void)testCheckForPendingDynamicLinkReturnsImmediatelyIfAlreadyRead { [mockService stopMocking]; } +- (void)testRetrievalProcessResultURLContainsAllParametersPassedToDynamicLinkInitializer { + NSDictionary *linkParameters = @{ + @"deep_link_id" : @"https://mmaksym.com/test-app1", + @"match_message" : @"Link is uniquely matched for this device.", + @"match_type" : @"unique", + @"utm_campaign" : @"Maksym M Test", + @"utm_medium" : @"test_medium", + @"utm_source" : @"test_source", + @"a_parameter" : @"a_value" + }; + + FIRDynamicLink *dynamicLink = + [[FIRDynamicLink alloc] initWithParametersDictionary:linkParameters]; + FIRDLRetrievalProcessResult *result = + [[FIRDLRetrievalProcessResult alloc] initWithDynamicLink:dynamicLink + error:nil + message:nil + matchSource:nil]; + + NSURL *customSchemeURL = [result URLWithCustomURLScheme:@"scheme"]; + XCTAssertNotNil(customSchemeURL); + + // Validate URL parameters + NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:customSchemeURL + resolvingAgainstBaseURL:NO]; + XCTAssertNotNil(urlComponents); + XCTAssertEqualObjects(urlComponents.scheme, @"scheme"); + + NSMutableDictionary *notEncodedParameters = [linkParameters mutableCopy]; + + for (NSURLQueryItem *queryItem in urlComponents.queryItems) { + NSString *expectedValue = notEncodedParameters[queryItem.name]; + XCTAssertNotNil(expectedValue, @"Extra parameter encoded: %@ = %@", queryItem.name, + queryItem.value); + + XCTAssertEqualObjects(queryItem.value, expectedValue); + [notEncodedParameters removeObjectForKey:queryItem.name]; + } + + XCTAssertEqual(notEncodedParameters.count, 0, @"The parameters must have been encoded: %@", + notEncodedParameters); +} + - (void)test_multipleRequestsToRetrievePendingDeepLinkShouldNotCrash { id mockService = OCMPartialMock(self.service); [[mockService expect] handlePendingDynamicLinkRetrievalFailureWithErrorCode:-1 @@ -1029,19 +1151,26 @@ - (void)testValidCustomDomainNames { NSArray *urlStrings = @[ @"https://google.com/mylink", // Short FDL starting with 'https://google.com' @"https://google.com/one", // Short FDL starting with 'https://google.com' - @"https://google.com/?link=abcd", // Long FDL starting with 'https://google.com' - @"https://google.com/one/mylink", // Long FDL starting with 'https://google.com/one' + @"https://google.com/one/mylink", // Short FDL starting with 'https://google.com/one' @"https://a.firebase.com/mypath/mylink", // Short FDL starting https://a.firebase.com/mypath + ]; + + NSArray *longFDLURLStrings = @[ @"https://a.firebase.com/mypath/?link=abcd&test=1", // Long FDL starting with // https://a.firebase.com/mypath + @"https://google.com/?link=abcd", // Long FDL starting with 'https://google.com' ]; - for (NSString *urlString in urlStrings) { NSURL *url = [NSURL URLWithString:urlString]; BOOL matchesShortLinkFormat = [self.service matchesShortLinkFormat:url]; - XCTAssertTrue(matchesShortLinkFormat, - @"Non-DDL domain URL matched short link format with URL: %@", url); + XCTAssertTrue(matchesShortLinkFormat, @"URL did not validate as short link: %@", url); + } + for (NSString *urlString in longFDLURLStrings) { + NSURL *url = [NSURL URLWithString:urlString]; + BOOL matchesLongLinkFormat = [self.service canParseUniversalLinkURL:url]; + + XCTAssertTrue(matchesLongLinkFormat, @"URL did not validate as long link: %@", url); } } diff --git a/Example/Firebase.xcodeproj/project.pbxproj b/Example/Firebase.xcodeproj/project.pbxproj index 7debc404cc0..be4844b799f 100644 --- a/Example/Firebase.xcodeproj/project.pbxproj +++ b/Example/Firebase.xcodeproj/project.pbxproj @@ -21,19 +21,6 @@ name = AllUnitTests_macOS; productName = AllTests; }; - DE26D2971F70668F004AE1D3 /* Auth_AllTests */ = { - isa = PBXAggregateTarget; - buildConfigurationList = DE26D2981F70668F004AE1D3 /* Build configuration list for PBXAggregateTarget "Auth_AllTests" */; - buildPhases = ( - ); - dependencies = ( - DE26D29E1F7066B0004AE1D3 /* PBXTargetDependency */, - DE26D2A01F7066B0004AE1D3 /* PBXTargetDependency */, - DE26D29C1F7066A7004AE1D3 /* PBXTargetDependency */, - ); - name = Auth_AllTests; - productName = Auth_AllTests; - }; DE3373891E73773400881891 /* AllUnitTests_iOS */ = { isa = PBXAggregateTarget; buildConfigurationList = DE33738A1E73773400881891 /* Build configuration list for PBXAggregateTarget "AllUnitTests_iOS" */; @@ -55,6 +42,7 @@ buildPhases = ( ); dependencies = ( + 510395E322270D8E0055A64F /* PBXTargetDependency */, DE545C881FBCA43200C637AE /* PBXTargetDependency */, DE545C861FBCA42C00C637AE /* PBXTargetDependency */, DE545C841FBCA41C00C637AE /* PBXTargetDependency */, @@ -116,8 +104,79 @@ 0672F2F31EBBA7D900818E87 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0672F2F11EBBA7D900818E87 /* GoogleService-Info.plist */; }; 069428831EC3B38C00F7BC69 /* 1mb.dat in Resources */ = {isa = PBXBuildFile; fileRef = 069428801EC3B35A00F7BC69 /* 1mb.dat */; }; 06C24A061EC39BCB005208CA /* FIRStorageIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 06121ECA1EC39A0B0008D70E /* FIRStorageIntegrationTests.m */; }; + 405EEF4C2216518B00B08FF4 /* FIRAuthLifeCycleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 405EEF4B2216518A00B08FF4 /* FIRAuthLifeCycleTests.m */; }; + 405EEF4D2216518B00B08FF4 /* FIRAuthLifeCycleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 405EEF4B2216518A00B08FF4 /* FIRAuthLifeCycleTests.m */; }; + 405EEF4E2216518B00B08FF4 /* FIRAuthLifeCycleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 405EEF4B2216518A00B08FF4 /* FIRAuthLifeCycleTests.m */; }; 408870AB21AE0218008AAE73 /* FIRSignInWithGameCenterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 408870AA21AE0218008AAE73 /* FIRSignInWithGameCenterTests.m */; }; - 409E1130219FA260000E6CFC /* FIRVerifyIOSClientTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 409E112F219FA260000E6CFC /* FIRVerifyIOSClientTests.m */; }; + 4090ADFD2217948D00547281 /* FIRAuthE2eTestsBase.m in Sources */ = {isa = PBXBuildFile; fileRef = 4090ADFC2217948D00547281 /* FIRAuthE2eTestsBase.m */; }; + 4090ADFF2217978300547281 /* BYOAuthTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4090ADFE2217978300547281 /* BYOAuthTests.m */; }; + 409E1130219FA260000E6CFC /* VerifyIOSClientTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 409E112F219FA260000E6CFC /* VerifyIOSClientTests.m */; }; + 40CC550D221B9B4700032423 /* FIRAuthApiTestsBase.m in Sources */ = {isa = PBXBuildFile; fileRef = 40CC5504221B9B4600032423 /* FIRAuthApiTestsBase.m */; }; + 40CC550E221B9B4700032423 /* GoogleAuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC5505221B9B4700032423 /* GoogleAuthTests.swift */; }; + 40CC550F221B9B4700032423 /* AccountInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 40CC5506221B9B4700032423 /* AccountInfoTests.m */; }; + 40CC5510221B9B4700032423 /* CustomAuthTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 40CC5508221B9B4700032423 /* CustomAuthTests.m */; }; + 40CC5511221B9B4700032423 /* GoogleAuthTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 40CC5509221B9B4700032423 /* GoogleAuthTests.m */; }; + 40CC5512221B9B4700032423 /* AnonymousAuthTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 40CC550A221B9B4700032423 /* AnonymousAuthTests.m */; }; + 40CC5513221B9B4700032423 /* FacebookAuthTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 40CC550B221B9B4700032423 /* FacebookAuthTests.m */; }; + 511DD2752225C4D00094D78D /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 511DD2742225C4D00094D78D /* AppDelegate.m */; }; + 511DD2782225C4D00094D78D /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 511DD2772225C4D00094D78D /* ViewController.m */; }; + 511DD27B2225C4D00094D78D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 511DD2792225C4D00094D78D /* Main.storyboard */; }; + 511DD27D2225C4D20094D78D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 511DD27C2225C4D20094D78D /* Assets.xcassets */; }; + 511DD2802225C4D20094D78D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 511DD27F2225C4D20094D78D /* main.m */; }; + 511DD2922225C8C40094D78D /* FIRInstanceIDWithFCMTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE8DB550221F5B470068BB0E /* FIRInstanceIDWithFCMTest.m */; }; + 511DD2932225C8C40094D78D /* FIRMessagingFakeConnection.h in Sources */ = {isa = PBXBuildFile; fileRef = DE9315C81E8738B70083EDBF /* FIRMessagingFakeConnection.h */; }; + 511DD2942225C8C40094D78D /* FIRMessagingFakeSocket.h in Sources */ = {isa = PBXBuildFile; fileRef = DE9315CA1E8738B70083EDBF /* FIRMessagingFakeSocket.h */; }; + 511DD2952225C8C40094D78D /* FIRMessagingTestNotificationUtilities.h in Sources */ = {isa = PBXBuildFile; fileRef = DE9315D61E8738B70083EDBF /* FIRMessagingTestNotificationUtilities.h */; }; + 511DD2962225C8C40094D78D /* FIRMessagingClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315C31E8738B70083EDBF /* FIRMessagingClientTest.m */; }; + 511DD2972225C8C40094D78D /* FIRMessagingCodedInputStreamTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315C41E8738B70083EDBF /* FIRMessagingCodedInputStreamTest.m */; }; + 511DD2982225C8C40094D78D /* FIRMessagingConnectionTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315C51E8738B70083EDBF /* FIRMessagingConnectionTest.m */; }; + 511DD2992225C8C40094D78D /* FIRMessagingContextManagerServiceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315C61E8738B70083EDBF /* FIRMessagingContextManagerServiceTest.m */; }; + 511DD29A2225C8C40094D78D /* FIRMessagingDataMessageManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315C71E8738B70083EDBF /* FIRMessagingDataMessageManagerTest.m */; }; + 511DD29B2225C8C40094D78D /* FIRMessagingFakeConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315C91E8738B70083EDBF /* FIRMessagingFakeConnection.m */; }; + 511DD29C2225C8C40094D78D /* FIRMessagingFakeSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315CB1E8738B70083EDBF /* FIRMessagingFakeSocket.m */; }; + 511DD29D2225C8C40094D78D /* FIRMessagingLinkHandlingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315CC1E8738B70083EDBF /* FIRMessagingLinkHandlingTest.m */; }; + 511DD29E2225C8C40094D78D /* FIRMessagingPendingTopicsListTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315CD1E8738B70083EDBF /* FIRMessagingPendingTopicsListTest.m */; }; + 511DD29F2225C8C40094D78D /* FIRMessagingPubSubTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315CE1E8738B70083EDBF /* FIRMessagingPubSubTest.m */; }; + 511DD2A02225C8C40094D78D /* FIRMessagingReceiverTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DEF61BFC216E8B1000A738D4 /* FIRMessagingReceiverTest.m */; }; + 511DD2A12225C8C40094D78D /* FIRMessagingRegistrarTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315CF1E8738B70083EDBF /* FIRMessagingRegistrarTest.m */; }; + 511DD2A22225C8C40094D78D /* FIRMessagingRemoteNotificationsProxyTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315D01E8738B70083EDBF /* FIRMessagingRemoteNotificationsProxyTest.m */; }; + 511DD2A32225C8C40094D78D /* FIRMessagingRmqManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315D11E8738B70083EDBF /* FIRMessagingRmqManagerTest.m */; }; + 511DD2A42225C8C40094D78D /* FIRMessagingSecureSocketTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315D21E8738B70083EDBF /* FIRMessagingSecureSocketTest.m */; }; + 511DD2A52225C8C40094D78D /* FIRMessagingServiceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315D31E8738B70083EDBF /* FIRMessagingServiceTest.m */; }; + 511DD2A62225C8C40094D78D /* FIRMessagingSyncMessageManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315D41E8738B70083EDBF /* FIRMessagingSyncMessageManagerTest.m */; }; + 511DD2A72225C8C40094D78D /* FIRMessagingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315D51E8738B70083EDBF /* FIRMessagingTest.m */; }; + 511DD2A82225C8C40094D78D /* FIRMessagingTestNotificationUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315D71E8738B70083EDBF /* FIRMessagingTestNotificationUtilities.m */; }; + 511DD2A92225C8C40094D78D /* FIRMessagingAnalyticsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE37C63A2163D5F30025D03E /* FIRMessagingAnalyticsTest.m */; }; + 511DD2AA2225C8D50094D78D /* FIRMessagingTestUtilities.h in Sources */ = {isa = PBXBuildFile; fileRef = EDF5242A21EA364600BB24C6 /* FIRMessagingTestUtilities.h */; }; + 511DD2AB2225C8D50094D78D /* FIRMessagingTestUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = EDF5242B21EA364600BB24C6 /* FIRMessagingTestUtilities.m */; }; + 518854D92230652900CA4141 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 518854D82230652900CA4141 /* AppDelegate.m */; }; + 518854DC2230652900CA4141 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 518854DB2230652900CA4141 /* ViewController.m */; }; + 518854DF2230652900CA4141 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 518854DD2230652900CA4141 /* Main.storyboard */; }; + 518854E12230652B00CA4141 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 518854E02230652B00CA4141 /* Assets.xcassets */; }; + 518854E42230652B00CA4141 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 518854E32230652B00CA4141 /* main.m */; }; + 518854F6223067E900CA4141 /* FIRInstanceIDAPNSInfoTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BDA21F7DF0B00E6C1C5 /* FIRInstanceIDAPNSInfoTest.m */; }; + 518854F7223067E900CA4141 /* FIRInstanceIDAuthKeyChainTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE521F7DF0C00E6C1C5 /* FIRInstanceIDAuthKeyChainTest.m */; }; + 518854F8223067E900CA4141 /* FIRInstanceIDAuthServiceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BDE21F7DF0C00E6C1C5 /* FIRInstanceIDAuthServiceTest.m */; }; + 518854F9223067E900CA4141 /* FIRInstanceIDBackupExcludedPlistTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE921F7DF0D00E6C1C5 /* FIRInstanceIDBackupExcludedPlistTest.m */; }; + 518854FA223067E900CA4141 /* FIRInstanceIDCheckinPreferencesTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BED21F7DF0D00E6C1C5 /* FIRInstanceIDCheckinPreferencesTest.m */; }; + 518854FB223067E900CA4141 /* FIRInstanceIDCheckinServiceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE721F7DF0D00E6C1C5 /* FIRInstanceIDCheckinServiceTest.m */; }; + 518854FC223067E900CA4141 /* FIRInstanceIDCheckinStoreTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BEB21F7DF0D00E6C1C5 /* FIRInstanceIDCheckinStoreTest.m */; }; + 518854FD223067E900CA4141 /* FIRInstanceIDFakeKeychain.h in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE321F7DF0C00E6C1C5 /* FIRInstanceIDFakeKeychain.h */; }; + 518854FE223067E900CA4141 /* FIRInstanceIDFakeKeychain.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BDC21F7DF0B00E6C1C5 /* FIRInstanceIDFakeKeychain.m */; }; + 518854FF223067E900CA4141 /* FIRInstanceIDKeyPairMigrationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BDF21F7DF0C00E6C1C5 /* FIRInstanceIDKeyPairMigrationTest.m */; }; + 51885500223067E900CA4141 /* FIRInstanceIDKeyPairStoreTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE421F7DF0C00E6C1C5 /* FIRInstanceIDKeyPairStoreTest.m */; }; + 51885501223067E900CA4141 /* FIRInstanceIDKeyPairTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE821F7DF0D00E6C1C5 /* FIRInstanceIDKeyPairTest.m */; }; + 51885502223067E900CA4141 /* FIRInstanceIDResultTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BDD21F7DF0C00E6C1C5 /* FIRInstanceIDResultTest.m */; }; + 51885503223067E900CA4141 /* FIRInstanceIDStoreTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BEA21F7DF0D00E6C1C5 /* FIRInstanceIDStoreTest.m */; }; + 51885504223067E900CA4141 /* FIRInstanceIDTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE021F7DF0C00E6C1C5 /* FIRInstanceIDTest.m */; }; + 51885505223067E900CA4141 /* FIRInstanceIDTokenInfoTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BEE21F7DF0D00E6C1C5 /* FIRInstanceIDTokenInfoTest.m */; }; + 51885506223067E900CA4141 /* FIRInstanceIDTokenManager+Test.h in Sources */ = {isa = PBXBuildFile; fileRef = DE958BD821F7DF0B00E6C1C5 /* FIRInstanceIDTokenManager+Test.h */; }; + 51885507223067E900CA4141 /* FIRInstanceIDTokenManager+Test.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE621F7DF0C00E6C1C5 /* FIRInstanceIDTokenManager+Test.m */; }; + 51885508223067E900CA4141 /* FIRInstanceIDTokenManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BD921F7DF0B00E6C1C5 /* FIRInstanceIDTokenManagerTest.m */; }; + 51885509223067E900CA4141 /* FIRInstanceIDTokenOperationsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BDB21F7DF0B00E6C1C5 /* FIRInstanceIDTokenOperationsTest.m */; }; + 5188550A223067E900CA4141 /* FIRInstanceIDUtilitiesTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE121F7DF0C00E6C1C5 /* FIRInstanceIDUtilitiesTest.m */; }; + 5188550C2230873000CA4141 /* FIRInstanceIDAPNSInfoTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BDA21F7DF0B00E6C1C5 /* FIRInstanceIDAPNSInfoTest.m */; }; + 7E21E0731F857DFC00D0AC1C /* FIROAuthProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E21E0721F857DFC00D0AC1C /* FIROAuthProviderTests.m */; }; 7E9485421F578AC4005A3939 /* FIRAuthURLPresenterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E94853F1F578A9D005A3939 /* FIRAuthURLPresenterTests.m */; }; 7EE21F7A1FE89193009B1370 /* FIREmailLinkRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7EE21F791FE89193009B1370 /* FIREmailLinkRequestTests.m */; }; 7EE21F7C1FE8919E009B1370 /* FIREmailLinkSignInResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7EE21F7B1FE8919D009B1370 /* FIREmailLinkSignInResponseTests.m */; }; @@ -341,15 +400,8 @@ DE26D2561F7040B2004AE1D3 /* UserInfoViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = DE26D1F51F70333E004AE1D3 /* UserInfoViewController.xib */; }; DE26D2571F7040E0004AE1D3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DE26D1DA1F70333E004AE1D3 /* Localizable.strings */; }; DE26D2581F7040E4004AE1D3 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DE26D1E41F70333E004AE1D3 /* Images.xcassets */; }; - DE26D2681F704A0C004AE1D3 /* FirebaseAuthApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE26D1C91F70330A004AE1D3 /* FirebaseAuthApiTests.m */; }; - DE26D2771F705CB5004AE1D3 /* FirebaseAuthEarlGreyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE26D1FA1F70333E004AE1D3 /* FirebaseAuthEarlGreyTests.m */; }; - DE26D28F1F705F34004AE1D3 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DE26D2001F70333E004AE1D3 /* GoogleService-Info.plist */; }; - DE26D2901F705F39004AE1D3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE26D2041F70333E004AE1D3 /* Main.storyboard */; }; - DE26D2911F705F3E004AE1D3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE26D2031F70333E004AE1D3 /* LaunchScreen.storyboard */; }; - DE26D2921F705F4A004AE1D3 /* Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE26D2061F70333E004AE1D3 /* Stubs.swift */; }; - DE26D2931F705F4D004AE1D3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE26D2071F70333E004AE1D3 /* ViewController.swift */; }; - DE26D2941F705F51004AE1D3 /* AuthCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE26D1FE1F70333E004AE1D3 /* AuthCredentials.swift */; }; - DE26D2951F705F53004AE1D3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE26D1FD1F70333E004AE1D3 /* AppDelegate.swift */; }; + DE26D2681F704A0C004AE1D3 /* EmailPasswordAuthTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE26D1C91F70330A004AE1D3 /* EmailPasswordAuthTests.m */; }; + DE26D2771F705CB5004AE1D3 /* FIRAuthE2eTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE26D1FA1F70333E004AE1D3 /* FIRAuthE2eTests.m */; }; DE37C63B2163D5F30025D03E /* FIRMessagingAnalyticsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE37C63A2163D5F30025D03E /* FIRMessagingAnalyticsTest.m */; }; DE47C0E2207AC87D00B1AEDF /* FIRSampleAppUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = AFC8BAA31EC257D800B8EEAE /* FIRSampleAppUtilities.m */; }; DE47C0E7207AC87D00B1AEDF /* Shared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AFAF36F41EC28C25004BDEE5 /* Shared.xcassets */; }; @@ -391,6 +443,7 @@ DE7B8DCC1E8EF23A009EB6DF /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DE7B8D371E8EF202009EB6DF /* main.m */; }; DE7B8DD01E8EF246009EB6DF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE7B8D2C1E8EF202009EB6DF /* LaunchScreen.storyboard */; }; DE7B8DD11E8EF24F009EB6DF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE7B8D2E1E8EF202009EB6DF /* Main.storyboard */; }; + DE8DB551221F5B480068BB0E /* FIRInstanceIDWithFCMTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE8DB550221F5B470068BB0E /* FIRInstanceIDWithFCMTest.m */; }; DE9037291FBA5F2400E239D3 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0672F2F11EBBA7D900818E87 /* GoogleService-Info.plist */; }; DE90372A1FBA5F8F00E239D3 /* FArraySortedDictionaryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4471EBA7AE200038A59 /* FArraySortedDictionaryTest.m */; }; DE90372B1FBA5F8F00E239D3 /* FCompoundHashTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 063CB4481EBA7AE200038A59 /* FCompoundHashTest.m */; }; @@ -488,6 +541,33 @@ DE9316031E8738E60083EDBF /* FIRMessagingSyncMessageManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315D41E8738B70083EDBF /* FIRMessagingSyncMessageManagerTest.m */; }; DE9316041E8738E60083EDBF /* FIRMessagingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315D51E8738B70083EDBF /* FIRMessagingTest.m */; }; DE9316051E8738E60083EDBF /* FIRMessagingTestNotificationUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9315D71E8738B70083EDBF /* FIRMessagingTestNotificationUtilities.m */; }; + DE958B6D21F7D13E00E6C1C5 /* Shared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AFAF36F41EC28C25004BDEE5 /* Shared.xcassets */; }; + DE958B8D21F7D15900E6C1C5 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; + DE958B8E21F7D15900E6C1C5 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 923F8250206C4DC500034974 /* UserNotifications.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + DE958B8F21F7D15900E6C1C5 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE45C6641E7DA8CB009E6ACD /* XCTest.framework */; }; + DE958BBE21F7D32300E6C1C5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE958B9F21F7D32200E6C1C5 /* Main.storyboard */; }; + DE958BBF21F7D32300E6C1C5 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BA121F7D32300E6C1C5 /* main.m */; }; + DE958BC021F7D32300E6C1C5 /* FIRAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BA221F7D32300E6C1C5 /* FIRAppDelegate.m */; }; + DE958BC121F7D32300E6C1C5 /* FIRViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BA321F7D32300E6C1C5 /* FIRViewController.m */; }; + DE958BD721F7D44200E6C1C5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE958B9D21F7D32200E6C1C5 /* LaunchScreen.storyboard */; }; + DE958BEF21F7DF0D00E6C1C5 /* FIRInstanceIDTokenManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BD921F7DF0B00E6C1C5 /* FIRInstanceIDTokenManagerTest.m */; }; + DE958BF121F7DF0D00E6C1C5 /* FIRInstanceIDTokenOperationsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BDB21F7DF0B00E6C1C5 /* FIRInstanceIDTokenOperationsTest.m */; }; + DE958BF221F7DF0D00E6C1C5 /* FIRInstanceIDFakeKeychain.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BDC21F7DF0B00E6C1C5 /* FIRInstanceIDFakeKeychain.m */; }; + DE958BF321F7DF0D00E6C1C5 /* FIRInstanceIDResultTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BDD21F7DF0C00E6C1C5 /* FIRInstanceIDResultTest.m */; }; + DE958BF421F7DF0D00E6C1C5 /* FIRInstanceIDAuthServiceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BDE21F7DF0C00E6C1C5 /* FIRInstanceIDAuthServiceTest.m */; }; + DE958BF521F7DF0D00E6C1C5 /* FIRInstanceIDKeyPairMigrationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BDF21F7DF0C00E6C1C5 /* FIRInstanceIDKeyPairMigrationTest.m */; }; + DE958BF621F7DF0E00E6C1C5 /* FIRInstanceIDTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE021F7DF0C00E6C1C5 /* FIRInstanceIDTest.m */; }; + DE958BF721F7DF0E00E6C1C5 /* FIRInstanceIDUtilitiesTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE121F7DF0C00E6C1C5 /* FIRInstanceIDUtilitiesTest.m */; }; + DE958BF921F7DF0E00E6C1C5 /* FIRInstanceIDKeyPairStoreTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE421F7DF0C00E6C1C5 /* FIRInstanceIDKeyPairStoreTest.m */; }; + DE958BFA21F7DF0E00E6C1C5 /* FIRInstanceIDAuthKeyChainTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE521F7DF0C00E6C1C5 /* FIRInstanceIDAuthKeyChainTest.m */; }; + DE958BFB21F7DF0E00E6C1C5 /* FIRInstanceIDTokenManager+Test.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE621F7DF0C00E6C1C5 /* FIRInstanceIDTokenManager+Test.m */; }; + DE958BFC21F7DF0E00E6C1C5 /* FIRInstanceIDCheckinServiceTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE721F7DF0D00E6C1C5 /* FIRInstanceIDCheckinServiceTest.m */; }; + DE958BFD21F7DF0E00E6C1C5 /* FIRInstanceIDKeyPairTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE821F7DF0D00E6C1C5 /* FIRInstanceIDKeyPairTest.m */; }; + DE958BFE21F7DF0E00E6C1C5 /* FIRInstanceIDBackupExcludedPlistTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BE921F7DF0D00E6C1C5 /* FIRInstanceIDBackupExcludedPlistTest.m */; }; + DE958BFF21F7DF0E00E6C1C5 /* FIRInstanceIDStoreTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BEA21F7DF0D00E6C1C5 /* FIRInstanceIDStoreTest.m */; }; + DE958C0021F7DF0E00E6C1C5 /* FIRInstanceIDCheckinStoreTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BEB21F7DF0D00E6C1C5 /* FIRInstanceIDCheckinStoreTest.m */; }; + DE958C0121F7DF0E00E6C1C5 /* FIRInstanceIDCheckinPreferencesTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BED21F7DF0D00E6C1C5 /* FIRInstanceIDCheckinPreferencesTest.m */; }; + DE958C0221F7DF0E00E6C1C5 /* FIRInstanceIDTokenInfoTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE958BEE21F7DF0D00E6C1C5 /* FIRInstanceIDTokenInfoTest.m */; }; DEA7795D207ACC8000245121 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DE47C107207AC94A00B1AEDF /* GoogleService-Info.plist */; }; DEAAD3C31FBA1CD90053BF48 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DEAAD3BD1FBA1CD80053BF48 /* main.m */; }; DEAAD3CE1FBA1EFA0053BF48 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DEAAD3C51FBA1EF90053BF48 /* Assets.xcassets */; }; @@ -553,7 +633,6 @@ DEE14D931E84468D006FA992 /* FIROptionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE14D7A1E844677006FA992 /* FIROptionsTest.m */; }; DEE14D941E84468D006FA992 /* FIRTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE14D7C1E844677006FA992 /* FIRTestCase.m */; }; DEF288411F9AB6E100D480CF /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DEF288401F9AB6E100D480CF /* Default-568h@2x.png */; }; - DEF288421F9AB6E100D480CF /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DEF288401F9AB6E100D480CF /* Default-568h@2x.png */; }; DEF61BFD216E8B1100A738D4 /* FIRMessagingReceiverTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DEF61BFC216E8B1000A738D4 /* FIRMessagingReceiverTest.m */; }; DEF6C30D1FBCE72F005D0740 /* FIRAuthDispatcherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9314FF1E86C6FF0083EDBF /* FIRAuthDispatcherTests.m */; }; DEF6C30F1FBCE775005D0740 /* FIRAdditionalUserInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE9314FA1E86C6FF0083EDBF /* FIRAdditionalUserInfoTests.m */; }; @@ -617,6 +696,7 @@ EDD53E2A211B08A300376BFF /* FIRComponentTestUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = EDD53E28211B08A300376BFF /* FIRComponentTestUtilities.m */; }; EDD53E2B211B08A300376BFF /* FIRComponentTestUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = EDD53E28211B08A300376BFF /* FIRComponentTestUtilities.m */; }; EDD53E2C211B08A300376BFF /* FIRComponentTestUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = EDD53E28211B08A300376BFF /* FIRComponentTestUtilities.m */; }; + EDF5242C21EA37AA00BB24C6 /* FIRMessagingTestUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = EDF5242B21EA364600BB24C6 /* FIRMessagingTestUtilities.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -634,6 +714,27 @@ remoteGlobalIDString = DE7B8D041E8EF077009EB6DF; remoteInfo = Database_Example; }; + 510395E222270D8E0055A64F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6003F582195388D10070C39A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 511DD2872225C5A80094D78D; + remoteInfo = Messaging_Tests_tvOS; + }; + 511DD28D2225C5A80094D78D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6003F582195388D10070C39A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 511DD2702225C4D00094D78D; + remoteInfo = Messaging_Example_tvOS; + }; + 518854F1223066BF00CA4141 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6003F582195388D10070C39A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 518854D42230652900CA4141; + remoteInfo = InstanceID_Example_tvOS; + }; AFD563111EB140E100EA2233 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6003F582195388D10070C39A /* Project object */; @@ -746,27 +847,6 @@ remoteGlobalIDString = DE26D22D1F70398A004AE1D3; remoteInfo = Auth_Sample; }; - DE26D29B1F7066A7004AE1D3 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6003F582195388D10070C39A /* Project object */; - proxyType = 1; - remoteGlobalIDString = DE9314DD1E86C6BE0083EDBF; - remoteInfo = Auth_Tests_iOS; - }; - DE26D29D1F7066B0004AE1D3 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6003F582195388D10070C39A /* Project object */; - proxyType = 1; - remoteGlobalIDString = DE26D25C1F7049F1004AE1D3; - remoteInfo = Auth_ApiTests; - }; - DE26D29F1F7066B0004AE1D3 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 6003F582195388D10070C39A /* Project object */; - proxyType = 1; - remoteGlobalIDString = DE26D26C1F705C35004AE1D3; - remoteInfo = Auth_EarlGreyTests; - }; DE3373971E73776F00881891 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6003F582195388D10070C39A /* Project object */; @@ -830,6 +910,13 @@ remoteGlobalIDString = DE9314DD1E86C6BE0083EDBF; remoteInfo = Auth_Tests_iOS; }; + DE958B9621F7D1F700E6C1C5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6003F582195388D10070C39A /* Project object */; + proxyType = 1; + remoteGlobalIDString = DE958B6421F7D13E00E6C1C5; + remoteInfo = InstanceID_Example_iOS; + }; DEAAD3961FBA11280053BF48 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6003F582195388D10070C39A /* Project object */; @@ -955,11 +1042,46 @@ 069428801EC3B35A00F7BC69 /* 1mb.dat */ = {isa = PBXFileReference; lastKnownFileType = file; path = 1mb.dat; sourceTree = ""; }; 0697B1201EC13D8A00542174 /* Base64.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Base64.h; sourceTree = ""; }; 0697B1211EC13D8A00542174 /* Base64.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Base64.m; sourceTree = ""; }; + 405EEF4B2216518A00B08FF4 /* FIRAuthLifeCycleTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthLifeCycleTests.m; sourceTree = ""; }; 408870AA21AE0218008AAE73 /* FIRSignInWithGameCenterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRSignInWithGameCenterTests.m; sourceTree = ""; }; - 409E112F219FA260000E6CFC /* FIRVerifyIOSClientTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRVerifyIOSClientTests.m; sourceTree = ""; }; + 4090ADFB2217948D00547281 /* FIRAuthE2eTestsBase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FIRAuthE2eTestsBase.h; sourceTree = ""; }; + 4090ADFC2217948D00547281 /* FIRAuthE2eTestsBase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRAuthE2eTestsBase.m; sourceTree = ""; }; + 4090ADFE2217978300547281 /* BYOAuthTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BYOAuthTests.m; sourceTree = ""; }; + 409E112F219FA260000E6CFC /* VerifyIOSClientTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VerifyIOSClientTests.m; sourceTree = ""; }; + 40CC5504221B9B4600032423 /* FIRAuthApiTestsBase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthApiTestsBase.m; sourceTree = ""; }; + 40CC5505221B9B4700032423 /* GoogleAuthTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleAuthTests.swift; sourceTree = ""; }; + 40CC5506221B9B4700032423 /* AccountInfoTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AccountInfoTests.m; sourceTree = ""; }; + 40CC5507221B9B4700032423 /* Auth_ApiTests-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Auth_ApiTests-Bridging-Header.h"; sourceTree = ""; }; + 40CC5508221B9B4700032423 /* CustomAuthTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CustomAuthTests.m; sourceTree = ""; }; + 40CC5509221B9B4700032423 /* GoogleAuthTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GoogleAuthTests.m; sourceTree = ""; }; + 40CC550A221B9B4700032423 /* AnonymousAuthTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AnonymousAuthTests.m; sourceTree = ""; }; + 40CC550B221B9B4700032423 /* FacebookAuthTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FacebookAuthTests.m; sourceTree = ""; }; + 40CC550C221B9B4700032423 /* FIRAuthApiTestsBase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRAuthApiTestsBase.h; sourceTree = ""; }; + 511DD2712225C4D00094D78D /* Messaging_Example_tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Messaging_Example_tvOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 511DD2732225C4D00094D78D /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 511DD2742225C4D00094D78D /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 511DD2762225C4D00094D78D /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 511DD2772225C4D00094D78D /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 511DD27A2225C4D00094D78D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 511DD27C2225C4D20094D78D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 511DD27E2225C4D20094D78D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 511DD27F2225C4D20094D78D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 511DD2882225C5A80094D78D /* Messaging_Tests_tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Messaging_Tests_tvOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 511DD2AC2226005D0094D78D /* FirebaseMessaging.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FirebaseMessaging.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 518854D52230652900CA4141 /* InstanceID_Example_tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InstanceID_Example_tvOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 518854D72230652900CA4141 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 518854D82230652900CA4141 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 518854DA2230652900CA4141 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 518854DB2230652900CA4141 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 518854DE2230652900CA4141 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 518854E02230652B00CA4141 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 518854E22230652B00CA4141 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 518854E32230652B00CA4141 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 518854EC223066BE00CA4141 /* InstanceID_Tests_tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InstanceID_Tests_tvOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 6003F58F195388D20070C39A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 6003F591195388D20070C39A /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 7E21E0721F857DFC00D0AC1C /* FIROAuthProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIROAuthProviderTests.m; sourceTree = ""; }; 7E94853F1F578A9D005A3939 /* FIRAuthURLPresenterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthURLPresenterTests.m; sourceTree = ""; }; 7EE21F791FE89193009B1370 /* FIREmailLinkRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIREmailLinkRequestTests.m; sourceTree = ""; }; 7EE21F7B1FE8919D009B1370 /* FIREmailLinkSignInResponseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIREmailLinkSignInResponseTests.m; sourceTree = ""; }; @@ -1044,7 +1166,7 @@ DE1FAE901FBCF5E100897AAA /* Auth_Example_tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Auth_Example_tvOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; DE26D1C71F70330A004AE1D3 /* AuthCredentials.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AuthCredentials.h; sourceTree = ""; }; DE26D1C81F70330A004AE1D3 /* AuthCredentialsTemplate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AuthCredentialsTemplate.h; sourceTree = ""; }; - DE26D1C91F70330A004AE1D3 /* FirebaseAuthApiTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FirebaseAuthApiTests.m; sourceTree = ""; }; + DE26D1C91F70330A004AE1D3 /* EmailPasswordAuthTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EmailPasswordAuthTests.m; sourceTree = ""; }; DE26D1CA1F70330A004AE1D3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DE26D1CE1F70333E004AE1D3 /* Application.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Application.plist; sourceTree = ""; }; DE26D1CF1F70333E004AE1D3 /* ApplicationDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ApplicationDelegate.h; sourceTree = ""; }; @@ -1088,23 +1210,11 @@ DE26D1F61F70333E004AE1D3 /* UserTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserTableViewCell.h; sourceTree = ""; }; DE26D1F71F70333E004AE1D3 /* UserTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UserTableViewCell.m; sourceTree = ""; }; DE26D1F81F70333E004AE1D3 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; - DE26D1FA1F70333E004AE1D3 /* FirebaseAuthEarlGreyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FirebaseAuthEarlGreyTests.m; sourceTree = ""; }; + DE26D1FA1F70333E004AE1D3 /* FIRAuthE2eTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAuthE2eTests.m; sourceTree = ""; }; DE26D1FB1F70333E004AE1D3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DE26D1FD1F70333E004AE1D3 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - DE26D1FE1F70333E004AE1D3 /* AuthCredentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthCredentials.swift; sourceTree = ""; }; - DE26D1FF1F70333E004AE1D3 /* AuthCredentialsTemplate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthCredentialsTemplate.swift; sourceTree = ""; }; - DE26D2001F70333E004AE1D3 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - DE26D2011F70333E004AE1D3 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DE26D2021F70333E004AE1D3 /* InfoTemplate.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = InfoTemplate.plist; sourceTree = ""; }; - DE26D2031F70333E004AE1D3 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; - DE26D2041F70333E004AE1D3 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; - DE26D2051F70333E004AE1D3 /* Sample.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Sample.entitlements; sourceTree = ""; }; - DE26D2061F70333E004AE1D3 /* Stubs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stubs.swift; sourceTree = ""; }; - DE26D2071F70333E004AE1D3 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; DE26D22E1F70398A004AE1D3 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; DE26D25D1F7049F1004AE1D3 /* Auth_ApiTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Auth_ApiTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - DE26D26D1F705C35004AE1D3 /* Auth_EarlGreyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Auth_EarlGreyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - DE26D27D1F705EC7004AE1D3 /* SwiftSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DE26D26D1F705C35004AE1D3 /* Auth_E2eTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Auth_E2eTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DE37C63A2163D5F30025D03E /* FIRMessagingAnalyticsTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingAnalyticsTest.m; sourceTree = ""; }; DE45C6641E7DA8CB009E6ACD /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; DE47C0ED207AC87D00B1AEDF /* Messaging_Sample_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Messaging_Sample_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1175,6 +1285,7 @@ DE7B8D8D1E8EF203009EB6DF /* SenTest+FWaiter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SenTest+FWaiter.m"; sourceTree = ""; }; DE7B8D8E1E8EF203009EB6DF /* syncPointSpec.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = syncPointSpec.json; sourceTree = ""; }; DE7B8DD21E8F1CA7009EB6DF /* Database-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Database-Info.plist"; sourceTree = ""; }; + DE8DB550221F5B470068BB0E /* FIRInstanceIDWithFCMTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDWithFCMTest.m; sourceTree = ""; }; DE9314C61E86C6BD0083EDBF /* Auth_Example_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Auth_Example_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; DE9314DE1E86C6BE0083EDBF /* Auth_Tests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Auth_Tests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DE9314EE1E86C6FF0083EDBF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -1252,6 +1363,37 @@ DE9315D61E8738B70083EDBF /* FIRMessagingTestNotificationUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRMessagingTestNotificationUtilities.h; sourceTree = ""; }; DE9315D71E8738B70083EDBF /* FIRMessagingTestNotificationUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingTestNotificationUtilities.m; sourceTree = ""; }; DE9315D81E8738B70083EDBF /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DE958B7221F7D13E00E6C1C5 /* InstanceID_Example_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InstanceID_Example_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DE958B9421F7D15900E6C1C5 /* InstanceID_Tests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InstanceID_Tests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DE958B9B21F7D32200E6C1C5 /* FIRAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRAppDelegate.h; sourceTree = ""; }; + DE958B9C21F7D32200E6C1C5 /* FIRViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRViewController.h; sourceTree = ""; }; + DE958B9E21F7D32200E6C1C5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + DE958BA021F7D32200E6C1C5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + DE958BA121F7D32300E6C1C5 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + DE958BA221F7D32300E6C1C5 /* FIRAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRAppDelegate.m; sourceTree = ""; }; + DE958BA321F7D32300E6C1C5 /* FIRViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRViewController.m; sourceTree = ""; }; + DE958BBA21F7D32300E6C1C5 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DE958BD821F7DF0B00E6C1C5 /* FIRInstanceIDTokenManager+Test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FIRInstanceIDTokenManager+Test.h"; sourceTree = ""; }; + DE958BD921F7DF0B00E6C1C5 /* FIRInstanceIDTokenManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDTokenManagerTest.m; sourceTree = ""; }; + DE958BDA21F7DF0B00E6C1C5 /* FIRInstanceIDAPNSInfoTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDAPNSInfoTest.m; sourceTree = ""; }; + DE958BDB21F7DF0B00E6C1C5 /* FIRInstanceIDTokenOperationsTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDTokenOperationsTest.m; sourceTree = ""; }; + DE958BDC21F7DF0B00E6C1C5 /* FIRInstanceIDFakeKeychain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDFakeKeychain.m; sourceTree = ""; }; + DE958BDD21F7DF0C00E6C1C5 /* FIRInstanceIDResultTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDResultTest.m; sourceTree = ""; }; + DE958BDE21F7DF0C00E6C1C5 /* FIRInstanceIDAuthServiceTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDAuthServiceTest.m; sourceTree = ""; }; + DE958BDF21F7DF0C00E6C1C5 /* FIRInstanceIDKeyPairMigrationTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDKeyPairMigrationTest.m; sourceTree = ""; }; + DE958BE021F7DF0C00E6C1C5 /* FIRInstanceIDTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDTest.m; sourceTree = ""; }; + DE958BE121F7DF0C00E6C1C5 /* FIRInstanceIDUtilitiesTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDUtilitiesTest.m; sourceTree = ""; }; + DE958BE321F7DF0C00E6C1C5 /* FIRInstanceIDFakeKeychain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FIRInstanceIDFakeKeychain.h; sourceTree = ""; }; + DE958BE421F7DF0C00E6C1C5 /* FIRInstanceIDKeyPairStoreTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDKeyPairStoreTest.m; sourceTree = ""; }; + DE958BE521F7DF0C00E6C1C5 /* FIRInstanceIDAuthKeyChainTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDAuthKeyChainTest.m; sourceTree = ""; }; + DE958BE621F7DF0C00E6C1C5 /* FIRInstanceIDTokenManager+Test.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "FIRInstanceIDTokenManager+Test.m"; sourceTree = ""; }; + DE958BE721F7DF0D00E6C1C5 /* FIRInstanceIDCheckinServiceTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDCheckinServiceTest.m; sourceTree = ""; }; + DE958BE821F7DF0D00E6C1C5 /* FIRInstanceIDKeyPairTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDKeyPairTest.m; sourceTree = ""; }; + DE958BE921F7DF0D00E6C1C5 /* FIRInstanceIDBackupExcludedPlistTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDBackupExcludedPlistTest.m; sourceTree = ""; }; + DE958BEA21F7DF0D00E6C1C5 /* FIRInstanceIDStoreTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDStoreTest.m; sourceTree = ""; }; + DE958BEB21F7DF0D00E6C1C5 /* FIRInstanceIDCheckinStoreTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDCheckinStoreTest.m; sourceTree = ""; }; + DE958BED21F7DF0D00E6C1C5 /* FIRInstanceIDCheckinPreferencesTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDCheckinPreferencesTest.m; sourceTree = ""; }; + DE958BEE21F7DF0D00E6C1C5 /* FIRInstanceIDTokenInfoTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRInstanceIDTokenInfoTest.m; sourceTree = ""; }; DEAAD3811FBA11270053BF48 /* Core_Example_tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Core_Example_tvOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; DEAAD3951FBA11270053BF48 /* Core_Tests_tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Core_Tests_tvOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DEAAD3BD1FBA1CD80053BF48 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; @@ -1345,6 +1487,8 @@ EDD53E24211A442D00376BFF /* FIRAuthInteropFake.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRAuthInteropFake.m; path = Shared/FIRAuthInteropFake.m; sourceTree = ""; }; EDD53E28211B08A300376BFF /* FIRComponentTestUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRComponentTestUtilities.m; path = Shared/FIRComponentTestUtilities.m; sourceTree = ""; }; EDD53E29211B08A300376BFF /* FIRComponentTestUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FIRComponentTestUtilities.h; path = Shared/FIRComponentTestUtilities.h; sourceTree = ""; }; + EDF5242A21EA364600BB24C6 /* FIRMessagingTestUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FIRMessagingTestUtilities.h; sourceTree = ""; }; + EDF5242B21EA364600BB24C6 /* FIRMessagingTestUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRMessagingTestUtilities.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1362,6 +1506,34 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 511DD26E2225C4D00094D78D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 511DD2852225C5A80094D78D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 518854D22230652900CA4141 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 518854E9223066BE00CA4141 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; AEC1C964E4211C8E5B7CD8C6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1505,13 +1677,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DE26D27A1F705EC7004AE1D3 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; DE47C0E4207AC87D00B1AEDF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1572,6 +1737,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DE958B6921F7D13E00E6C1C5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DE958B8C21F7D15900E6C1C5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DE958B8D21F7D15900E6C1C5 /* UIKit.framework in Frameworks */, + DE958B8E21F7D15900E6C1C5 /* UserNotifications.framework in Frameworks */, + DE958B8F21F7D15900E6C1C5 /* XCTest.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DEAAD37E1FBA11270053BF48 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1699,6 +1881,36 @@ path = third_party; sourceTree = ""; }; + 511DD2722225C4D00094D78D /* tvOS */ = { + isa = PBXGroup; + children = ( + 511DD2732225C4D00094D78D /* AppDelegate.h */, + 511DD2742225C4D00094D78D /* AppDelegate.m */, + 511DD2762225C4D00094D78D /* ViewController.h */, + 511DD2772225C4D00094D78D /* ViewController.m */, + 511DD2792225C4D00094D78D /* Main.storyboard */, + 511DD27C2225C4D20094D78D /* Assets.xcassets */, + 511DD27E2225C4D20094D78D /* Info.plist */, + 511DD27F2225C4D20094D78D /* main.m */, + ); + path = tvOS; + sourceTree = ""; + }; + 518854D62230652900CA4141 /* tvOS */ = { + isa = PBXGroup; + children = ( + 518854D72230652900CA4141 /* AppDelegate.h */, + 518854D82230652900CA4141 /* AppDelegate.m */, + 518854DA2230652900CA4141 /* ViewController.h */, + 518854DB2230652900CA4141 /* ViewController.m */, + 518854DD2230652900CA4141 /* Main.storyboard */, + 518854E02230652B00CA4141 /* Assets.xcassets */, + 518854E22230652B00CA4141 /* Info.plist */, + 518854E32230652B00CA4141 /* main.m */, + ); + path = tvOS; + sourceTree = ""; + }; 6003F581195388D10070C39A = { isa = PBXGroup; children = ( @@ -1707,11 +1919,13 @@ DEE14D661E844677006FA992 /* Core */, DE7B8D2A1E8EF202009EB6DF /* Database */, DEDFF0351FD6143000F7D466 /* DynamicLinks */, + DE958B9821F7D32200E6C1C5 /* InstanceID */, DE9315B41E8738B70083EDBF /* Messaging */, AFC8BAA01EC24B1600B8EEAE /* Shared */, DEB139B31E734D9D00AC236D /* Storage */, 6003F58C195388D20070C39A /* Frameworks */, 6003F58B195388D20070C39A /* Products */, + 73F3D26B690BF932AE286CD5 /* Pods */, ); sourceTree = ""; }; @@ -1742,8 +1956,7 @@ D01853C61EDAD364003A645C /* Auth_Tests_macOS.xctest */, DE26D22E1F70398A004AE1D3 /* Sample.app */, DE26D25D1F7049F1004AE1D3 /* Auth_ApiTests.xctest */, - DE26D26D1F705C35004AE1D3 /* Auth_EarlGreyTests.xctest */, - DE26D27D1F705EC7004AE1D3 /* SwiftSample.app */, + DE26D26D1F705C35004AE1D3 /* Auth_E2eTests.xctest */, DEAAD3811FBA11270053BF48 /* Core_Example_tvOS.app */, DEAAD3951FBA11270053BF48 /* Core_Tests_tvOS.xctest */, DEAAD3E11FBA46AA0053BF48 /* Storage_Example_tvOS.app */, @@ -1757,6 +1970,12 @@ DE17A2C8214215C1002A15ED /* DynamicLinks_Example_iOS.app */, DE17A39A21472F3A002A15ED /* FDLBuilderTestAppObjC.app */, DE4A62DD214816BD00670B27 /* FDLBuilderTestAppObjCEarlGrey.xctest */, + DE958B7221F7D13E00E6C1C5 /* InstanceID_Example_iOS.app */, + DE958B9421F7D15900E6C1C5 /* InstanceID_Tests_iOS.xctest */, + 511DD2712225C4D00094D78D /* Messaging_Example_tvOS.app */, + 511DD2882225C5A80094D78D /* Messaging_Tests_tvOS.xctest */, + 518854D52230652900CA4141 /* InstanceID_Example_tvOS.app */, + 518854EC223066BE00CA4141 /* InstanceID_Tests_tvOS.xctest */, ); name = Products; sourceTree = ""; @@ -1764,6 +1983,7 @@ 6003F58C195388D20070C39A /* Frameworks */ = { isa = PBXGroup; children = ( + 511DD2AC2226005D0094D78D /* FirebaseMessaging.framework */, 923F8250206C4DC500034974 /* UserNotifications.framework */, 923F824B206C4D8000034974 /* SafariServices.framework */, DE45C6641E7DA8CB009E6ACD /* XCTest.framework */, @@ -1784,6 +2004,13 @@ name = "Podspec Metadata"; sourceTree = ""; }; + 73F3D26B690BF932AE286CD5 /* Pods */ = { + isa = PBXGroup; + children = ( + ); + path = Pods; + sourceTree = ""; + }; AFC8BAA01EC24B1600B8EEAE /* Shared */ = { isa = PBXGroup; children = ( @@ -1969,9 +2196,18 @@ DE26D1C61F70330A004AE1D3 /* ApiTests */ = { isa = PBXGroup; children = ( + 40CC5506221B9B4700032423 /* AccountInfoTests.m */, + 40CC550A221B9B4700032423 /* AnonymousAuthTests.m */, + 40CC5507221B9B4700032423 /* Auth_ApiTests-Bridging-Header.h */, DE26D1C71F70330A004AE1D3 /* AuthCredentials.h */, DE26D1C81F70330A004AE1D3 /* AuthCredentialsTemplate.h */, - DE26D1C91F70330A004AE1D3 /* FirebaseAuthApiTests.m */, + 40CC5508221B9B4700032423 /* CustomAuthTests.m */, + DE26D1C91F70330A004AE1D3 /* EmailPasswordAuthTests.m */, + 40CC550B221B9B4700032423 /* FacebookAuthTests.m */, + 40CC550C221B9B4700032423 /* FIRAuthApiTestsBase.h */, + 40CC5504221B9B4600032423 /* FIRAuthApiTestsBase.m */, + 40CC5509221B9B4700032423 /* GoogleAuthTests.m */, + 40CC5505221B9B4700032423 /* GoogleAuthTests.swift */, DE26D1CA1F70330A004AE1D3 /* Info.plist */, ); path = ApiTests; @@ -2023,32 +2259,17 @@ path = Sample; sourceTree = ""; }; - DE26D1F91F70333E004AE1D3 /* EarlGreyTests */ = { + DE26D1F91F70333E004AE1D3 /* E2eTests */ = { isa = PBXGroup; children = ( - DE26D1FA1F70333E004AE1D3 /* FirebaseAuthEarlGreyTests.m */, - 409E112F219FA260000E6CFC /* FIRVerifyIOSClientTests.m */, + 4090ADFE2217978300547281 /* BYOAuthTests.m */, + DE26D1FA1F70333E004AE1D3 /* FIRAuthE2eTests.m */, + 4090ADFB2217948D00547281 /* FIRAuthE2eTestsBase.h */, + 4090ADFC2217948D00547281 /* FIRAuthE2eTestsBase.m */, + 409E112F219FA260000E6CFC /* VerifyIOSClientTests.m */, DE26D1FB1F70333E004AE1D3 /* Info.plist */, ); - path = EarlGreyTests; - sourceTree = ""; - }; - DE26D1FC1F70333E004AE1D3 /* SwiftSample */ = { - isa = PBXGroup; - children = ( - DE26D2051F70333E004AE1D3 /* Sample.entitlements */, - DE26D2001F70333E004AE1D3 /* GoogleService-Info.plist */, - DE26D2011F70333E004AE1D3 /* Info.plist */, - DE26D2021F70333E004AE1D3 /* InfoTemplate.plist */, - DE26D2031F70333E004AE1D3 /* LaunchScreen.storyboard */, - DE26D2041F70333E004AE1D3 /* Main.storyboard */, - DE26D1FD1F70333E004AE1D3 /* AppDelegate.swift */, - DE26D1FE1F70333E004AE1D3 /* AuthCredentials.swift */, - DE26D1FF1F70333E004AE1D3 /* AuthCredentialsTemplate.swift */, - DE26D2061F70333E004AE1D3 /* Stubs.swift */, - DE26D2071F70333E004AE1D3 /* ViewController.swift */, - ); - path = SwiftSample; + path = E2eTests; sourceTree = ""; }; DE47C106207AC94A00B1AEDF /* Sample */ = { @@ -2079,6 +2300,7 @@ DE47C131207ACAA900B1AEDF /* App */ = { isa = PBXGroup; children = ( + 511DD2722225C4D00094D78D /* tvOS */, DE47C133207ACAA900B1AEDF /* iOS */, ); path = App; @@ -2215,12 +2437,11 @@ DE9314EB1E86C6FF0083EDBF /* Auth */ = { isa = PBXGroup; children = ( - DE26D1F91F70333E004AE1D3 /* EarlGreyTests */, DE26D1CD1F70333E004AE1D3 /* Sample */, - DE26D1FC1F70333E004AE1D3 /* SwiftSample */, + DE9314F91E86C6FF0083EDBF /* Tests */, DE26D1C61F70330A004AE1D3 /* ApiTests */, + DE26D1F91F70333E004AE1D3 /* E2eTests */, DE9314EC1E86C6FF0083EDBF /* App */, - DE9314F91E86C6FF0083EDBF /* Tests */, ); path = Auth; sourceTree = ""; @@ -2252,6 +2473,7 @@ DE9314FF1E86C6FF0083EDBF /* FIRAuthDispatcherTests.m */, DE9315001E86C6FF0083EDBF /* FIRAuthGlobalWorkQueueTests.m */, DE9315011E86C6FF0083EDBF /* FIRAuthKeychainTests.m */, + 405EEF4B2216518A00B08FF4 /* FIRAuthLifeCycleTests.m */, DE750DB81EB3DD4000A75E47 /* FIRAuthNotificationManagerTests.m */, DE9315021E86C6FF0083EDBF /* FIRAuthSerialTaskQueueTests.m */, DE9315031E86C6FF0083EDBF /* FIRAuthTests.m */, @@ -2298,6 +2520,7 @@ DE9315231E86C6FF0083EDBF /* OCMStubRecorder+FIRAuthUnitTests.h */, DE9315241E86C6FF0083EDBF /* OCMStubRecorder+FIRAuthUnitTests.m */, DE9315251E86C6FF0083EDBF /* Tests-Info.plist */, + 7E21E0721F857DFC00D0AC1C /* FIROAuthProviderTests.m */, ); path = Tests; sourceTree = ""; @@ -2316,6 +2539,7 @@ DE9315C21E8738B70083EDBF /* Tests */ = { isa = PBXGroup; children = ( + DE8DB550221F5B470068BB0E /* FIRInstanceIDWithFCMTest.m */, DE9315C81E8738B70083EDBF /* FIRMessagingFakeConnection.h */, DE9315CA1E8738B70083EDBF /* FIRMessagingFakeSocket.h */, DE9315D61E8738B70083EDBF /* FIRMessagingTestNotificationUtilities.h */, @@ -2340,6 +2564,69 @@ DE9315D71E8738B70083EDBF /* FIRMessagingTestNotificationUtilities.m */, DE37C63A2163D5F30025D03E /* FIRMessagingAnalyticsTest.m */, DE9315D81E8738B70083EDBF /* Info.plist */, + EDF5242A21EA364600BB24C6 /* FIRMessagingTestUtilities.h */, + EDF5242B21EA364600BB24C6 /* FIRMessagingTestUtilities.m */, + ); + path = Tests; + sourceTree = ""; + }; + DE958B9821F7D32200E6C1C5 /* InstanceID */ = { + isa = PBXGroup; + children = ( + DE958B9921F7D32200E6C1C5 /* App */, + DE958BA421F7D32300E6C1C5 /* Tests */, + ); + path = InstanceID; + sourceTree = ""; + }; + DE958B9921F7D32200E6C1C5 /* App */ = { + isa = PBXGroup; + children = ( + 518854D62230652900CA4141 /* tvOS */, + DE958B9A21F7D32200E6C1C5 /* iOS */, + ); + path = App; + sourceTree = ""; + }; + DE958B9A21F7D32200E6C1C5 /* iOS */ = { + isa = PBXGroup; + children = ( + DE958B9B21F7D32200E6C1C5 /* FIRAppDelegate.h */, + DE958B9C21F7D32200E6C1C5 /* FIRViewController.h */, + DE958B9D21F7D32200E6C1C5 /* LaunchScreen.storyboard */, + DE958B9F21F7D32200E6C1C5 /* Main.storyboard */, + DE958BA121F7D32300E6C1C5 /* main.m */, + DE958BA221F7D32300E6C1C5 /* FIRAppDelegate.m */, + DE958BA321F7D32300E6C1C5 /* FIRViewController.m */, + ); + path = iOS; + sourceTree = ""; + }; + DE958BA421F7D32300E6C1C5 /* Tests */ = { + isa = PBXGroup; + children = ( + DE958BDA21F7DF0B00E6C1C5 /* FIRInstanceIDAPNSInfoTest.m */, + DE958BE521F7DF0C00E6C1C5 /* FIRInstanceIDAuthKeyChainTest.m */, + DE958BDE21F7DF0C00E6C1C5 /* FIRInstanceIDAuthServiceTest.m */, + DE958BE921F7DF0D00E6C1C5 /* FIRInstanceIDBackupExcludedPlistTest.m */, + DE958BED21F7DF0D00E6C1C5 /* FIRInstanceIDCheckinPreferencesTest.m */, + DE958BE721F7DF0D00E6C1C5 /* FIRInstanceIDCheckinServiceTest.m */, + DE958BEB21F7DF0D00E6C1C5 /* FIRInstanceIDCheckinStoreTest.m */, + DE958BE321F7DF0C00E6C1C5 /* FIRInstanceIDFakeKeychain.h */, + DE958BDC21F7DF0B00E6C1C5 /* FIRInstanceIDFakeKeychain.m */, + DE958BDF21F7DF0C00E6C1C5 /* FIRInstanceIDKeyPairMigrationTest.m */, + DE958BE421F7DF0C00E6C1C5 /* FIRInstanceIDKeyPairStoreTest.m */, + DE958BE821F7DF0D00E6C1C5 /* FIRInstanceIDKeyPairTest.m */, + DE958BDD21F7DF0C00E6C1C5 /* FIRInstanceIDResultTest.m */, + DE958BEA21F7DF0D00E6C1C5 /* FIRInstanceIDStoreTest.m */, + DE958BE021F7DF0C00E6C1C5 /* FIRInstanceIDTest.m */, + DE958BEE21F7DF0D00E6C1C5 /* FIRInstanceIDTokenInfoTest.m */, + DE958BD821F7DF0B00E6C1C5 /* FIRInstanceIDTokenManager+Test.h */, + DE958BE621F7DF0C00E6C1C5 /* FIRInstanceIDTokenManager+Test.m */, + DE958BD921F7DF0B00E6C1C5 /* FIRInstanceIDTokenManagerTest.m */, + DE958BDB21F7DF0B00E6C1C5 /* FIRInstanceIDTokenOperationsTest.m */, + DE958BE121F7DF0C00E6C1C5 /* FIRInstanceIDUtilitiesTest.m */, + DE958BBA21F7D32300E6C1C5 /* Info.plist */, ); path = Tests; sourceTree = ""; @@ -2535,6 +2822,76 @@ productReference = 0624F3E11EC0ECFA00E5940D /* Database_IntegrationTests_iOS.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 511DD2702225C4D00094D78D /* Messaging_Example_tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 511DD2812225C4D20094D78D /* Build configuration list for PBXNativeTarget "Messaging_Example_tvOS" */; + buildPhases = ( + 511DD26D2225C4D00094D78D /* Sources */, + 511DD26E2225C4D00094D78D /* Frameworks */, + 511DD26F2225C4D00094D78D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Messaging_Example_tvOS; + productName = tvOS; + productReference = 511DD2712225C4D00094D78D /* Messaging_Example_tvOS.app */; + productType = "com.apple.product-type.application"; + }; + 511DD2872225C5A80094D78D /* Messaging_Tests_tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 511DD28F2225C5A80094D78D /* Build configuration list for PBXNativeTarget "Messaging_Tests_tvOS" */; + buildPhases = ( + 511DD2842225C5A80094D78D /* Sources */, + 511DD2852225C5A80094D78D /* Frameworks */, + 511DD2862225C5A80094D78D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 511DD28E2225C5A80094D78D /* PBXTargetDependency */, + ); + name = Messaging_Tests_tvOS; + productName = Messaging_Tests_tvOS; + productReference = 511DD2882225C5A80094D78D /* Messaging_Tests_tvOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 518854D42230652900CA4141 /* InstanceID_Example_tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 518854E72230652B00CA4141 /* Build configuration list for PBXNativeTarget "InstanceID_Example_tvOS" */; + buildPhases = ( + 518854D12230652900CA4141 /* Sources */, + 518854D22230652900CA4141 /* Frameworks */, + 518854D32230652900CA4141 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = InstanceID_Example_tvOS; + productName = tvOS; + productReference = 518854D52230652900CA4141 /* InstanceID_Example_tvOS.app */; + productType = "com.apple.product-type.application"; + }; + 518854EB223066BE00CA4141 /* InstanceID_Tests_tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 518854F3223066BF00CA4141 /* Build configuration list for PBXNativeTarget "InstanceID_Tests_tvOS" */; + buildPhases = ( + 518854E8223066BE00CA4141 /* Sources */, + 518854E9223066BE00CA4141 /* Frameworks */, + 518854EA223066BE00CA4141 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 518854F2223066BF00CA4141 /* PBXTargetDependency */, + ); + name = InstanceID_Tests_tvOS; + productName = InstanceID_Tests_tvOS; + productReference = 518854EC223066BE00CA4141 /* InstanceID_Tests_tvOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; AFD562E41EB13C6D00EA2233 /* Messaging_Example_iOS */ = { isa = PBXNativeTarget; buildConfigurationList = AFD562F41EB13C6D00EA2233 /* Build configuration list for PBXNativeTarget "Messaging_Example_iOS" */; @@ -2849,9 +3206,9 @@ productReference = DE26D25D1F7049F1004AE1D3 /* Auth_ApiTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - DE26D26C1F705C35004AE1D3 /* Auth_EarlGreyTests */ = { + DE26D26C1F705C35004AE1D3 /* Auth_E2eTests */ = { isa = PBXNativeTarget; - buildConfigurationList = DE26D2741F705C35004AE1D3 /* Build configuration list for PBXNativeTarget "Auth_EarlGreyTests" */; + buildConfigurationList = DE26D2741F705C35004AE1D3 /* Build configuration list for PBXNativeTarget "Auth_E2eTests" */; buildPhases = ( DE26D2691F705C35004AE1D3 /* Sources */, DE26D26A1F705C35004AE1D3 /* Frameworks */, @@ -2862,28 +3219,11 @@ dependencies = ( DE26D2731F705C35004AE1D3 /* PBXTargetDependency */, ); - name = Auth_EarlGreyTests; + name = Auth_E2eTests; productName = Auth_EarlGreyTests; - productReference = DE26D26D1F705C35004AE1D3 /* Auth_EarlGreyTests.xctest */; + productReference = DE26D26D1F705C35004AE1D3 /* Auth_E2eTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - DE26D27C1F705EC7004AE1D3 /* Auth_SwiftSample */ = { - isa = PBXNativeTarget; - buildConfigurationList = DE26D28C1F705EC7004AE1D3 /* Build configuration list for PBXNativeTarget "Auth_SwiftSample" */; - buildPhases = ( - DE26D2791F705EC7004AE1D3 /* Sources */, - DE26D27A1F705EC7004AE1D3 /* Frameworks */, - DE26D27B1F705EC7004AE1D3 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Auth_SwiftSample; - productName = Auth_SwiftSample; - productReference = DE26D27D1F705EC7004AE1D3 /* SwiftSample.app */; - productType = "com.apple.product-type.application"; - }; DE47C0DC207AC87D00B1AEDF /* Messaging_Sample_iOS */ = { isa = PBXNativeTarget; buildConfigurationList = DE47C0EA207AC87D00B1AEDF /* Build configuration list for PBXNativeTarget "Messaging_Sample_iOS" */; @@ -3025,6 +3365,41 @@ productReference = DE9315A71E8738460083EDBF /* Messaging_Tests_iOS.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + DE958B6421F7D13E00E6C1C5 /* InstanceID_Example_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = DE958B6F21F7D13E00E6C1C5 /* Build configuration list for PBXNativeTarget "InstanceID_Example_iOS" */; + buildPhases = ( + DE958B6521F7D13E00E6C1C5 /* Sources */, + DE958B6921F7D13E00E6C1C5 /* Frameworks */, + DE958B6A21F7D13E00E6C1C5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = InstanceID_Example_iOS; + productName = Messaging_Example_iOS; + productReference = DE958B7221F7D13E00E6C1C5 /* InstanceID_Example_iOS.app */; + productType = "com.apple.product-type.application"; + }; + DE958B7421F7D15900E6C1C5 /* InstanceID_Tests_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = DE958B9121F7D15900E6C1C5 /* Build configuration list for PBXNativeTarget "InstanceID_Tests_iOS" */; + buildPhases = ( + DE958B7721F7D15900E6C1C5 /* Sources */, + DE958B8C21F7D15900E6C1C5 /* Frameworks */, + DE958B9021F7D15900E6C1C5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DE958B9721F7D1F700E6C1C5 /* PBXTargetDependency */, + ); + name = InstanceID_Tests_iOS; + productName = Messaging_Example_iOSTests; + productReference = DE958B9421F7D15900E6C1C5 /* InstanceID_Tests_iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; DEAAD3801FBA11270053BF48 /* Core_Example_tvOS */ = { isa = PBXNativeTarget; buildConfigurationList = DEAAD3A01FBA11280053BF48 /* Build configuration list for PBXNativeTarget "Core_Example_tvOS" */; @@ -3203,6 +3578,24 @@ ProvisioningStyle = Automatic; TestTargetID = DE7B8D041E8EF077009EB6DF; }; + 511DD2702225C4D00094D78D = { + CreatedOnToolsVersion = 9.4; + ProvisioningStyle = Automatic; + }; + 511DD2872225C5A80094D78D = { + CreatedOnToolsVersion = 9.4; + ProvisioningStyle = Automatic; + TestTargetID = 511DD2702225C4D00094D78D; + }; + 518854D42230652900CA4141 = { + CreatedOnToolsVersion = 10.0; + ProvisioningStyle = Automatic; + }; + 518854EB223066BE00CA4141 = { + CreatedOnToolsVersion = 10.0; + ProvisioningStyle = Automatic; + TestTargetID = 518854D42230652900CA4141; + }; AFD562E41EB13C6D00EA2233 = { CreatedOnToolsVersion = 8.3.2; LastSwiftMigration = 0830; @@ -3289,6 +3682,7 @@ DE26D25C1F7049F1004AE1D3 = { CreatedOnToolsVersion = 9.0; DevelopmentTeam = EQHXZ8M8AV; + LastSwiftMigration = 1010; ProvisioningStyle = Automatic; TestTargetID = DE26D22D1F70398A004AE1D3; }; @@ -3298,16 +3692,6 @@ ProvisioningStyle = Automatic; TestTargetID = DE26D22D1F70398A004AE1D3; }; - DE26D27C1F705EC7004AE1D3 = { - CreatedOnToolsVersion = 9.0; - DevelopmentTeam = EQHXZ8M8AV; - ProvisioningStyle = Automatic; - }; - DE26D2971F70668F004AE1D3 = { - CreatedOnToolsVersion = 9.0; - DevelopmentTeam = EQHXZ8M8AV; - ProvisioningStyle = Automatic; - }; DE3373891E73773400881891 = { CreatedOnToolsVersion = 8.2.1; DevelopmentTeam = EQHXZ8M8AV; @@ -3357,6 +3741,9 @@ ProvisioningStyle = Automatic; TestTargetID = AFD562E41EB13C6D00EA2233; }; + DE958B7421F7D15900E6C1C5 = { + TestTargetID = DE958B6421F7D13E00E6C1C5; + }; DEAAD3801FBA11270053BF48 = { CreatedOnToolsVersion = 9.1; DevelopmentTeam = EQHXZ8M8AV; @@ -3422,10 +3809,8 @@ DE1FAE8F1FBCF5E100897AAA /* Auth_Example_tvOS */, DE53893D1FBB62E100199FC2 /* Auth_Tests_tvOS */, DE26D22D1F70398A004AE1D3 /* Auth_Sample */, - DE26D27C1F705EC7004AE1D3 /* Auth_SwiftSample */, DE26D25C1F7049F1004AE1D3 /* Auth_ApiTests */, - DE26D26C1F705C35004AE1D3 /* Auth_EarlGreyTests */, - DE26D2971F70668F004AE1D3 /* Auth_AllTests */, + DE26D26C1F705C35004AE1D3 /* Auth_E2eTests */, DEE14D401E84464D006FA992 /* Core_Example_iOS */, DEE14D581E84464D006FA992 /* Core_Tests_iOS */, D064E6951ED9B1BF001956DF /* Core_Example_macOS */, @@ -3444,9 +3829,15 @@ DEDFF0121FD6135D00F7D466 /* DynamicLinks_Tests_iOS */, DE17A39921472F3A002A15ED /* FDLBuilderTestAppObjC */, DE4A62DC214816BD00670B27 /* FDLBuilderTestAppObjCEarlGrey */, + DE958B6421F7D13E00E6C1C5 /* InstanceID_Example_iOS */, + DE958B7421F7D15900E6C1C5 /* InstanceID_Tests_iOS */, + 518854D42230652900CA4141 /* InstanceID_Example_tvOS */, + 518854EB223066BE00CA4141 /* InstanceID_Tests_tvOS */, AFD562E41EB13C6D00EA2233 /* Messaging_Example_iOS */, DE9315A61E8738460083EDBF /* Messaging_Tests_iOS */, DE47C0DC207AC87D00B1AEDF /* Messaging_Sample_iOS */, + 511DD2702225C4D00094D78D /* Messaging_Example_tvOS */, + 511DD2872225C5A80094D78D /* Messaging_Tests_tvOS */, DEB139E01E73506A00AC236D /* Storage_Example_iOS */, DEB13A0A1E73507E00AC236D /* Storage_Tests_iOS */, 06121EBB1EC399C50008D70E /* Storage_IntegrationTests_iOS */, @@ -3477,6 +3868,38 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 511DD26F2225C4D00094D78D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 511DD27D2225C4D20094D78D /* Assets.xcassets in Resources */, + 511DD27B2225C4D00094D78D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 511DD2862225C5A80094D78D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 518854D32230652900CA4141 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 518854E12230652B00CA4141 /* Assets.xcassets in Resources */, + 518854DF2230652900CA4141 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 518854EA223066BE00CA4141 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; AFD562E31EB13C6D00EA2233 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -3652,17 +4075,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DE26D27B1F705EC7004AE1D3 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - DE26D28F1F705F34004AE1D3 /* GoogleService-Info.plist in Resources */, - DEF288421F9AB6E100D480CF /* Default-568h@2x.png in Resources */, - DE26D2901F705F39004AE1D3 /* Main.storyboard in Resources */, - DE26D2911F705F3E004AE1D3 /* LaunchScreen.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; DE47C0E5207AC87D00B1AEDF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -3737,6 +4149,23 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DE958B6A21F7D13E00E6C1C5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE958BD721F7D44200E6C1C5 /* LaunchScreen.storyboard in Resources */, + DE958B6D21F7D13E00E6C1C5 /* Shared.xcassets in Resources */, + DE958BBE21F7D32300E6C1C5 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DE958B9021F7D15900E6C1C5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; DEAAD37F1FBA11270053BF48 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -3854,21 +4283,102 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - AFD562E11EB13C6D00EA2233 /* Sources */ = { + 511DD26D2225C4D00094D78D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DE47C142207ACAA900B1AEDF /* main.m in Sources */, - DE47C143207ACAA900B1AEDF /* FIRAppDelegate.m in Sources */, - DE47C144207ACAA900B1AEDF /* FIRViewController.m in Sources */, + 511DD2782225C4D00094D78D /* ViewController.m in Sources */, + 511DD2802225C4D20094D78D /* main.m in Sources */, + 511DD2752225C4D00094D78D /* AppDelegate.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - D01853691EDAD084003A645C /* Sources */ = { + 511DD2842225C5A80094D78D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D01853831EDAD113003A645C /* FIRAppDelegate.m in Sources */, + 511DD2AA2225C8D50094D78D /* FIRMessagingTestUtilities.h in Sources */, + 511DD2AB2225C8D50094D78D /* FIRMessagingTestUtilities.m in Sources */, + 511DD2922225C8C40094D78D /* FIRInstanceIDWithFCMTest.m in Sources */, + 511DD2932225C8C40094D78D /* FIRMessagingFakeConnection.h in Sources */, + 511DD2942225C8C40094D78D /* FIRMessagingFakeSocket.h in Sources */, + 511DD2952225C8C40094D78D /* FIRMessagingTestNotificationUtilities.h in Sources */, + 511DD2962225C8C40094D78D /* FIRMessagingClientTest.m in Sources */, + 511DD2972225C8C40094D78D /* FIRMessagingCodedInputStreamTest.m in Sources */, + 511DD2982225C8C40094D78D /* FIRMessagingConnectionTest.m in Sources */, + 511DD2992225C8C40094D78D /* FIRMessagingContextManagerServiceTest.m in Sources */, + 511DD29A2225C8C40094D78D /* FIRMessagingDataMessageManagerTest.m in Sources */, + 511DD29B2225C8C40094D78D /* FIRMessagingFakeConnection.m in Sources */, + 511DD29C2225C8C40094D78D /* FIRMessagingFakeSocket.m in Sources */, + 511DD29D2225C8C40094D78D /* FIRMessagingLinkHandlingTest.m in Sources */, + 511DD29E2225C8C40094D78D /* FIRMessagingPendingTopicsListTest.m in Sources */, + 511DD29F2225C8C40094D78D /* FIRMessagingPubSubTest.m in Sources */, + 511DD2A02225C8C40094D78D /* FIRMessagingReceiverTest.m in Sources */, + 511DD2A12225C8C40094D78D /* FIRMessagingRegistrarTest.m in Sources */, + 511DD2A22225C8C40094D78D /* FIRMessagingRemoteNotificationsProxyTest.m in Sources */, + 511DD2A32225C8C40094D78D /* FIRMessagingRmqManagerTest.m in Sources */, + 511DD2A42225C8C40094D78D /* FIRMessagingSecureSocketTest.m in Sources */, + 511DD2A52225C8C40094D78D /* FIRMessagingServiceTest.m in Sources */, + 511DD2A62225C8C40094D78D /* FIRMessagingSyncMessageManagerTest.m in Sources */, + 511DD2A72225C8C40094D78D /* FIRMessagingTest.m in Sources */, + 511DD2A82225C8C40094D78D /* FIRMessagingTestNotificationUtilities.m in Sources */, + 511DD2A92225C8C40094D78D /* FIRMessagingAnalyticsTest.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 518854D12230652900CA4141 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 518854DC2230652900CA4141 /* ViewController.m in Sources */, + 518854E42230652B00CA4141 /* main.m in Sources */, + 518854D92230652900CA4141 /* AppDelegate.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 518854E8223066BE00CA4141 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 518854F6223067E900CA4141 /* FIRInstanceIDAPNSInfoTest.m in Sources */, + 518854F7223067E900CA4141 /* FIRInstanceIDAuthKeyChainTest.m in Sources */, + 518854F8223067E900CA4141 /* FIRInstanceIDAuthServiceTest.m in Sources */, + 518854F9223067E900CA4141 /* FIRInstanceIDBackupExcludedPlistTest.m in Sources */, + 518854FA223067E900CA4141 /* FIRInstanceIDCheckinPreferencesTest.m in Sources */, + 518854FB223067E900CA4141 /* FIRInstanceIDCheckinServiceTest.m in Sources */, + 518854FC223067E900CA4141 /* FIRInstanceIDCheckinStoreTest.m in Sources */, + 518854FD223067E900CA4141 /* FIRInstanceIDFakeKeychain.h in Sources */, + 518854FE223067E900CA4141 /* FIRInstanceIDFakeKeychain.m in Sources */, + 518854FF223067E900CA4141 /* FIRInstanceIDKeyPairMigrationTest.m in Sources */, + 51885500223067E900CA4141 /* FIRInstanceIDKeyPairStoreTest.m in Sources */, + 51885501223067E900CA4141 /* FIRInstanceIDKeyPairTest.m in Sources */, + 51885502223067E900CA4141 /* FIRInstanceIDResultTest.m in Sources */, + 51885503223067E900CA4141 /* FIRInstanceIDStoreTest.m in Sources */, + 51885504223067E900CA4141 /* FIRInstanceIDTest.m in Sources */, + 51885505223067E900CA4141 /* FIRInstanceIDTokenInfoTest.m in Sources */, + 51885506223067E900CA4141 /* FIRInstanceIDTokenManager+Test.h in Sources */, + 51885507223067E900CA4141 /* FIRInstanceIDTokenManager+Test.m in Sources */, + 51885508223067E900CA4141 /* FIRInstanceIDTokenManagerTest.m in Sources */, + 51885509223067E900CA4141 /* FIRInstanceIDTokenOperationsTest.m in Sources */, + 5188550A223067E900CA4141 /* FIRInstanceIDUtilitiesTest.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AFD562E11EB13C6D00EA2233 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE47C142207ACAA900B1AEDF /* main.m in Sources */, + DE47C143207ACAA900B1AEDF /* FIRAppDelegate.m in Sources */, + DE47C144207ACAA900B1AEDF /* FIRViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D01853691EDAD084003A645C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D01853831EDAD113003A645C /* FIRAppDelegate.m in Sources */, D01853841EDAD113003A645C /* FIRViewController.m in Sources */, D01853851EDAD113003A645C /* main.m in Sources */, ); @@ -3879,6 +4389,7 @@ buildActionMask = 2147483647; files = ( D018538D1EDAD364003A645C /* FIRGetOOBConfirmationCodeResponseTests.m in Sources */, + 405EEF4D2216518B00B08FF4 /* FIRAuthLifeCycleTests.m in Sources */, D018538E1EDAD364003A645C /* FIRGetAccountInfoRequestTests.m in Sources */, D018538F1EDAD364003A645C /* FIRSignUpNewUserResponseTests.m in Sources */, D01853901EDAD364003A645C /* FIRGetOOBConfirmationCodeRequestTests.m in Sources */, @@ -4188,7 +4699,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DE26D2681F704A0C004AE1D3 /* FirebaseAuthApiTests.m in Sources */, + 40CC550E221B9B4700032423 /* GoogleAuthTests.swift in Sources */, + 40CC5512221B9B4700032423 /* AnonymousAuthTests.m in Sources */, + 40CC550F221B9B4700032423 /* AccountInfoTests.m in Sources */, + 40CC5510221B9B4700032423 /* CustomAuthTests.m in Sources */, + DE26D2681F704A0C004AE1D3 /* EmailPasswordAuthTests.m in Sources */, + 40CC5513221B9B4700032423 /* FacebookAuthTests.m in Sources */, + 40CC5511221B9B4700032423 /* GoogleAuthTests.m in Sources */, + 40CC550D221B9B4700032423 /* FIRAuthApiTestsBase.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4196,19 +4714,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DE26D2771F705CB5004AE1D3 /* FirebaseAuthEarlGreyTests.m in Sources */, - 409E1130219FA260000E6CFC /* FIRVerifyIOSClientTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DE26D2791F705EC7004AE1D3 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - DE26D2921F705F4A004AE1D3 /* Stubs.swift in Sources */, - DE26D2941F705F51004AE1D3 /* AuthCredentials.swift in Sources */, - DE26D2951F705F53004AE1D3 /* AppDelegate.swift in Sources */, - DE26D2931F705F4D004AE1D3 /* ViewController.swift in Sources */, + 4090ADFF2217978300547281 /* BYOAuthTests.m in Sources */, + DE26D2771F705CB5004AE1D3 /* FIRAuthE2eTests.m in Sources */, + 4090ADFD2217948D00547281 /* FIRAuthE2eTestsBase.m in Sources */, + 409E1130219FA260000E6CFC /* VerifyIOSClientTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4253,6 +4762,7 @@ DEF6C3191FBCE775005D0740 /* FIRAuthKeychainTests.m in Sources */, DEF6C32A1FBCE775005D0740 /* FIRGitHubAuthProviderTests.m in Sources */, DEF6C3321FBCE775005D0740 /* FIRSignUpNewUserRequestTests.m in Sources */, + 405EEF4E2216518B00B08FF4 /* FIRAuthLifeCycleTests.m in Sources */, DEF6C30F1FBCE775005D0740 /* FIRAdditionalUserInfoTests.m in Sources */, DEF6C31B1FBCE775005D0740 /* FIRAuthSerialTaskQueueTests.m in Sources */, DEF6C3251FBCE775005D0740 /* FIRGetAccountInfoResponseTests.m in Sources */, @@ -4362,6 +4872,7 @@ DE9315741E86C71C0083EDBF /* FIRTwitterAuthProviderTests.m in Sources */, DE750DC01EB3DD6F00A75E47 /* FIRAuthNotificationManagerTests.m in Sources */, DE93156A1E86C71C0083EDBF /* FIRGitHubAuthProviderTests.m in Sources */, + 405EEF4C2216518B00B08FF4 /* FIRAuthLifeCycleTests.m in Sources */, DE9315761E86C71C0083EDBF /* FIRVerifyAssertionRequestTests.m in Sources */, DE9315781E86C71C0083EDBF /* FIRVerifyCustomTokenRequestTests.m in Sources */, DE93157C1E86C71C0083EDBF /* FIRVerifyPhoneNumberRequestTests.m in Sources */, @@ -4396,6 +4907,7 @@ DE9315711E86C71C0083EDBF /* FIRSetAccountInfoResponseTests.m in Sources */, DE93155F1E86C71C0083EDBF /* FIRAuthTests.m in Sources */, 7E9485421F578AC4005A3939 /* FIRAuthURLPresenterTests.m in Sources */, + 7E21E0731F857DFC00D0AC1C /* FIROAuthProviderTests.m in Sources */, DE750DBD1EB3DD5B00A75E47 /* FIRAuthAPNSTokenTests.m in Sources */, DE0E5BBE1EA7D93500FAA825 /* FIRAuthAppDelegateProxyTests.m in Sources */, 7EFA2E041F71C93300DD354F /* FIRUserMetadataTests.m in Sources */, @@ -4416,9 +4928,11 @@ DE9315FB1E8738E60083EDBF /* FIRMessagingLinkHandlingTest.m in Sources */, DE9315FC1E8738E60083EDBF /* FIRMessagingPendingTopicsListTest.m in Sources */, DE9316001E8738E60083EDBF /* FIRMessagingRmqManagerTest.m in Sources */, + DE8DB551221F5B480068BB0E /* FIRInstanceIDWithFCMTest.m in Sources */, DE9315F91E8738E60083EDBF /* FIRMessagingFakeConnection.m in Sources */, DE9316021E8738E60083EDBF /* FIRMessagingServiceTest.m in Sources */, DE9315FE1E8738E60083EDBF /* FIRMessagingRegistrarTest.m in Sources */, + EDF5242C21EA37AA00BB24C6 /* FIRMessagingTestUtilities.m in Sources */, DE9316031E8738E60083EDBF /* FIRMessagingSyncMessageManagerTest.m in Sources */, DE9315FF1E8738E60083EDBF /* FIRMessagingRemoteNotificationsProxyTest.m in Sources */, DEF61BFD216E8B1100A738D4 /* FIRMessagingReceiverTest.m in Sources */, @@ -4431,6 +4945,42 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DE958B6521F7D13E00E6C1C5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE958BC121F7D32300E6C1C5 /* FIRViewController.m in Sources */, + DE958BC021F7D32300E6C1C5 /* FIRAppDelegate.m in Sources */, + DE958BBF21F7D32300E6C1C5 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DE958B7721F7D15900E6C1C5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE958BF221F7DF0D00E6C1C5 /* FIRInstanceIDFakeKeychain.m in Sources */, + DE958BF721F7DF0E00E6C1C5 /* FIRInstanceIDUtilitiesTest.m in Sources */, + DE958C0121F7DF0E00E6C1C5 /* FIRInstanceIDCheckinPreferencesTest.m in Sources */, + DE958C0221F7DF0E00E6C1C5 /* FIRInstanceIDTokenInfoTest.m in Sources */, + DE958BFB21F7DF0E00E6C1C5 /* FIRInstanceIDTokenManager+Test.m in Sources */, + DE958BF621F7DF0E00E6C1C5 /* FIRInstanceIDTest.m in Sources */, + DE958BFE21F7DF0E00E6C1C5 /* FIRInstanceIDBackupExcludedPlistTest.m in Sources */, + DE958BF521F7DF0D00E6C1C5 /* FIRInstanceIDKeyPairMigrationTest.m in Sources */, + DE958BFD21F7DF0E00E6C1C5 /* FIRInstanceIDKeyPairTest.m in Sources */, + 5188550C2230873000CA4141 /* FIRInstanceIDAPNSInfoTest.m in Sources */, + DE958BF921F7DF0E00E6C1C5 /* FIRInstanceIDKeyPairStoreTest.m in Sources */, + DE958BEF21F7DF0D00E6C1C5 /* FIRInstanceIDTokenManagerTest.m in Sources */, + DE958BFF21F7DF0E00E6C1C5 /* FIRInstanceIDStoreTest.m in Sources */, + DE958BF121F7DF0D00E6C1C5 /* FIRInstanceIDTokenOperationsTest.m in Sources */, + DE958BF421F7DF0D00E6C1C5 /* FIRInstanceIDAuthServiceTest.m in Sources */, + DE958C0021F7DF0E00E6C1C5 /* FIRInstanceIDCheckinStoreTest.m in Sources */, + DE958BF321F7DF0D00E6C1C5 /* FIRInstanceIDResultTest.m in Sources */, + DE958BFC21F7DF0E00E6C1C5 /* FIRInstanceIDCheckinServiceTest.m in Sources */, + DE958BFA21F7DF0E00E6C1C5 /* FIRInstanceIDAuthKeyChainTest.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DEAAD37D1FBA11270053BF48 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -4560,6 +5110,21 @@ target = DE7B8D041E8EF077009EB6DF /* Database_Example_iOS */; targetProxy = 0624F3E61EC0ECFA00E5940D /* PBXContainerItemProxy */; }; + 510395E322270D8E0055A64F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 511DD2872225C5A80094D78D /* Messaging_Tests_tvOS */; + targetProxy = 510395E222270D8E0055A64F /* PBXContainerItemProxy */; + }; + 511DD28E2225C5A80094D78D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 511DD2702225C4D00094D78D /* Messaging_Example_tvOS */; + targetProxy = 511DD28D2225C5A80094D78D /* PBXContainerItemProxy */; + }; + 518854F2223066BF00CA4141 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 518854D42230652900CA4141 /* InstanceID_Example_tvOS */; + targetProxy = 518854F1223066BF00CA4141 /* PBXContainerItemProxy */; + }; AFD563121EB140E100EA2233 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = AFD562E41EB13C6D00EA2233 /* Messaging_Example_iOS */; @@ -4640,21 +5205,6 @@ target = DE26D22D1F70398A004AE1D3 /* Auth_Sample */; targetProxy = DE26D2721F705C35004AE1D3 /* PBXContainerItemProxy */; }; - DE26D29C1F7066A7004AE1D3 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DE9314DD1E86C6BE0083EDBF /* Auth_Tests_iOS */; - targetProxy = DE26D29B1F7066A7004AE1D3 /* PBXContainerItemProxy */; - }; - DE26D29E1F7066B0004AE1D3 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DE26D25C1F7049F1004AE1D3 /* Auth_ApiTests */; - targetProxy = DE26D29D1F7066B0004AE1D3 /* PBXContainerItemProxy */; - }; - DE26D2A01F7066B0004AE1D3 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DE26D26C1F705C35004AE1D3 /* Auth_EarlGreyTests */; - targetProxy = DE26D29F1F7066B0004AE1D3 /* PBXContainerItemProxy */; - }; DE3373981E73776F00881891 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DEB13A0A1E73507E00AC236D /* Storage_Tests_iOS */; @@ -4700,6 +5250,11 @@ target = DE9314DD1E86C6BE0083EDBF /* Auth_Tests_iOS */; targetProxy = DE9315861E86E9990083EDBF /* PBXContainerItemProxy */; }; + DE958B9721F7D1F700E6C1C5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DE958B6421F7D13E00E6C1C5 /* InstanceID_Example_iOS */; + targetProxy = DE958B9621F7D1F700E6C1C5 /* PBXContainerItemProxy */; + }; DEAAD3971FBA11280053BF48 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DEAAD3801FBA11270053BF48 /* Core_Example_tvOS */; @@ -4733,6 +5288,22 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + 511DD2792225C4D00094D78D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 511DD27A2225C4D00094D78D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 518854DD2230652900CA4141 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 518854DE2230652900CA4141 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; D018537B1EDAD0E6003A645C /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -4857,6 +5428,22 @@ name = Main.storyboard; sourceTree = ""; }; + DE958B9D21F7D32200E6C1C5 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + DE958B9E21F7D32200E6C1C5 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + DE958B9F21F7D32200E6C1C5 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + DE958BA021F7D32200E6C1C5 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; DEB61EB91E7C5DBB00C04B96 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -4997,30 +5584,351 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; HEADER_SEARCH_PATHS = ( "$(inherited)", - "\"${PODS_ROOT}/../../Firebase/Database/Utilities/Tuples\"", - "\"${PODS_ROOT}/../../Firebase/Database/Core\"", - "\"${PODS_ROOT}/../../Firebase/Database/Realtime\"", - "\"${PODS_ROOT}/../../Firebase/Database/third_party/SocketRocket\"", - "\"${PODS_ROOT}/../../Firebase/Database/Utilities\"", - "\"${PODS_ROOT}/../../Firebase/Database/Libraries\"", - "\"${PODS_ROOT}/../../Firebase/Database/Core/Utilities\"", - "\"${PODS_ROOT}/../../Firebase/Database/Api/Private\"", - "\"${PODS_ROOT}/../../Firebase/Database/Api\"", - "\"${PODS_ROOT}/../../Firebase/Database/Snapshot\"", - "\"${PODS_ROOT}/../../Firebase/Database/Login\"", - "\"${PODS_ROOT}/../../Firebase/Database/Constants\"", - "\"${PODS_ROOT}/../../Firebase/Database\"", - "\"${PODS_ROOT}/../../Firebase/Database/Persistence\"", - "\"${PODS_ROOT}/../../Firebase/Database/third_party/FImmutableSortedDictionary/FImmutableSortedDictionary\"", - "\"${PODS_ROOT}/../../Firebase/Database/Core/View\"", + "\"${PODS_ROOT}/../../Firebase/Database/Utilities/Tuples\"", + "\"${PODS_ROOT}/../../Firebase/Database/Core\"", + "\"${PODS_ROOT}/../../Firebase/Database/Realtime\"", + "\"${PODS_ROOT}/../../Firebase/Database/third_party/SocketRocket\"", + "\"${PODS_ROOT}/../../Firebase/Database/Utilities\"", + "\"${PODS_ROOT}/../../Firebase/Database/Libraries\"", + "\"${PODS_ROOT}/../../Firebase/Database/Core/Utilities\"", + "\"${PODS_ROOT}/../../Firebase/Database/Api/Private\"", + "\"${PODS_ROOT}/../../Firebase/Database/Api\"", + "\"${PODS_ROOT}/../../Firebase/Database/Snapshot\"", + "\"${PODS_ROOT}/../../Firebase/Database/Login\"", + "\"${PODS_ROOT}/../../Firebase/Database/Constants\"", + "\"${PODS_ROOT}/../../Firebase/Database\"", + "\"${PODS_ROOT}/../../Firebase/Database/Persistence\"", + "\"${PODS_ROOT}/../../Firebase/Database/third_party/FImmutableSortedDictionary/FImmutableSortedDictionary\"", + "\"${PODS_ROOT}/../../Firebase/Database/Core/View\"", + ); + INFOPLIST_FILE = "Database/Tests/FirebaseTests-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 10.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "com.google.Database-IntegrationTests-iOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Database_Example_iOS.app/Database_Example_iOS"; + }; + name = Release; + }; + 511DD2822225C4D20094D78D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_C_LANGUAGE_STANDARD = gnu11; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Public/FirebaseAnalyticsInterop\"", + "\"${PODS_ROOT}/Headers/Public/FirebaseAuthInterop\"", + ); + INFOPLIST_FILE = "$(SRCROOT)/Messaging/App/tvOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseMessagingSample.tvOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 11.4; + }; + name = Debug; + }; + 511DD2832225C4D20094D78D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "$(SRCROOT)/Messaging/App/tvOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseMessagingSample.tvOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 11.4; + }; + name = Release; + }; + 511DD2902225C5A80094D78D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Public/FirebaseAnalyticsInterop\"", + "\"${PODS_ROOT}/Headers/Public/FirebaseAuthInterop\"", + "\"${PODS_ROOT}/Headers/Private\"", + "\"${PODS_ROOT}/../../Firebase/Messaging\"", + ); + INFOPLIST_FILE = Messaging/Tests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseMessagingTests.tvOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Messaging_Example_tvOS.app/Messaging_Example_tvOS"; + TVOS_DEPLOYMENT_TARGET = 11.4; + }; + name = Debug; + }; + 511DD2912225C5A80094D78D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Public/FirebaseAnalyticsInterop\"", + "\"${PODS_ROOT}/Headers/Public/FirebaseAuthInterop\"", + "\"${PODS_ROOT}/../../Firebase/Messaging\"", + ); + INFOPLIST_FILE = Messaging/Tests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseMessagingTests.tvOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Messaging_Example_tvOS.app/Messaging_Example_tvOS"; + TVOS_DEPLOYMENT_TARGET = 11.4; + }; + name = Release; + }; + 518854E52230652B00CA4141 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "$(SRCROOT)/InstanceID/App/tvOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseInstanceIDSample.tvOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 12.0; + }; + name = Debug; + }; + 518854E62230652B00CA4141 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "$(SRCROOT)/InstanceID/App/tvOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseInstanceIDSample.tvOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 12.0; + }; + name = Release; + }; + 518854F4223066BF00CA4141 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Private\"", + "\"${SRCROOT}/../\"", + ); + INFOPLIST_FILE = InstanceID/Tests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseInstanceIDTests.tvOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InstanceID_Example_tvOS.app/InstanceID_Example_tvOS"; + TVOS_DEPLOYMENT_TARGET = 12.0; + }; + name = Debug; + }; + 518854F5223066BF00CA4141 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Private\"", + "\"${SRCROOT}/../\"", ); - INFOPLIST_FILE = "Database/Tests/FirebaseTests-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 10.2; + INFOPLIST_FILE = InstanceID/Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; - PRODUCT_BUNDLE_IDENTIFIER = "com.google.Database-IntegrationTests-iOS"; + PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseInstanceIDTests.tvOS; PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Database_Example_iOS.app/Database_Example_iOS"; + SDKROOT = appletvos; + TARGETED_DEVICE_FAMILY = 3; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InstanceID_Example_tvOS.app/InstanceID_Example_tvOS"; + TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Release; }; @@ -5754,7 +6662,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 4ANB9W7R3P; GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = $SRCROOT/DynamicLinks/FDLBuilderTestAppObjC/Info.plist; + INFOPLIST_FILE = "$SRCROOT/DynamicLinks/FDLBuilderTestAppObjC/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; @@ -5788,7 +6696,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 4ANB9W7R3P; GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = $SRCROOT/DynamicLinks/FDLBuilderTestAppObjC/Info.plist; + INFOPLIST_FILE = "$SRCROOT/DynamicLinks/FDLBuilderTestAppObjC/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; @@ -6111,6 +7019,7 @@ CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -6130,6 +7039,9 @@ MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.google.Auth-ApiTests"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Auth/ApiTests/Auth_ApiTests-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sample.app/Sample"; }; @@ -6142,6 +7054,7 @@ CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; @@ -6162,6 +7075,8 @@ MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.google.Auth-ApiTests"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Auth/ApiTests/Auth_ApiTests-Bridging-Header.h"; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sample.app/Sample"; }; @@ -6187,11 +7102,11 @@ DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = EQHXZ8M8AV; GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = Auth/EarlGreyTests/Info.plist; + INFOPLIST_FILE = Auth/E2eTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.google.Auth-EarlGreyTests"; + PRODUCT_BUNDLE_IDENTIFIER = "com.google.Auth-E2eTests"; PRODUCT_NAME = "$(TARGET_NAME)"; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sample.app/Sample"; @@ -6219,101 +7134,17 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = EQHXZ8M8AV; GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = Auth/EarlGreyTests/Info.plist; + INFOPLIST_FILE = Auth/E2eTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; - PRODUCT_BUNDLE_IDENTIFIER = "com.google.Auth-EarlGreyTests"; + PRODUCT_BUNDLE_IDENTIFIER = "com.google.Auth-E2eTests"; PRODUCT_NAME = "$(TARGET_NAME)"; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Sample.app/Sample"; }; name = Release; }; - DE26D28D1F705EC7004AE1D3 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = EQHXZ8M8AV; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = Auth/SwiftSample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MTL_ENABLE_DEBUG_INFO = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseExperimental2.dev; - PRODUCT_NAME = SwiftSample; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - DE26D28E1F705EC7004AE1D3 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = EQHXZ8M8AV; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = Auth/SwiftSample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MTL_ENABLE_DEBUG_INFO = NO; - PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseExperimental2.dev; - PRODUCT_NAME = SwiftSample; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - DE26D2991F70668F004AE1D3 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = EQHXZ8M8AV; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - DE26D29A1F70668F004AE1D3 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = EQHXZ8M8AV; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; DE33738B1E73773400881891 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -6452,6 +7283,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -6476,11 +7308,6 @@ INFOPLIST_FILE = "$(SRCROOT)/Auth/App/iOS/Auth-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; - OTHER_LDFLAGS = ( - "$(inherited)", - "-framework", - FirebaseAuth, - ); PRODUCT_BUNDLE_IDENTIFIER = "com.google.Auth-Example-tvOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; @@ -6495,6 +7322,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -6520,11 +7348,6 @@ INFOPLIST_FILE = "$(SRCROOT)/Auth/App/iOS/Auth-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; - OTHER_LDFLAGS = ( - "$(inherited)", - "-framework", - FirebaseAuth, - ); PRODUCT_BUNDLE_IDENTIFIER = "com.google.Auth-Example-tvOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; @@ -6830,6 +7653,115 @@ }; name = Release; }; + DE958B7021F7D13E00E6C1C5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "$(SRCROOT)/InstanceID/App/iOS/InstanceID-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseInstanceID.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + DE958B7121F7D13E00E6C1C5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "$(SRCROOT)/InstanceID/App/iOS/InstanceID-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseInstanceID.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + DE958B9221F7D15900E6C1C5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ); + HEADER_SEARCH_PATHS = ( + "\"${SRCROOT}/../\"", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Private\"", + "$(inherited)", + ); + INFOPLIST_FILE = InstanceID/Tests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.google.InstanceID-Tests-iOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InstanceID_Example_iOS.app/InstanceID_Example_iOS"; + }; + name = Debug; + }; + DE958B9321F7D15900E6C1C5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1", + ); + HEADER_SEARCH_PATHS = ( + "\"${SRCROOT}/../\"", + "\"${PODS_ROOT}/Headers/Public\"", + "\"${PODS_ROOT}/Headers/Private\"", + "$(inherited)", + ); + INFOPLIST_FILE = InstanceID/Tests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "com.google.InstanceID-Tests-iOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InstanceID_Example_iOS.app/InstanceID_Example_iOS"; + }; + name = Release; + }; DEAAD39C1FBA11280053BF48 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -7404,6 +8336,42 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 511DD2812225C4D20094D78D /* Build configuration list for PBXNativeTarget "Messaging_Example_tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 511DD2822225C4D20094D78D /* Debug */, + 511DD2832225C4D20094D78D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 511DD28F2225C5A80094D78D /* Build configuration list for PBXNativeTarget "Messaging_Tests_tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 511DD2902225C5A80094D78D /* Debug */, + 511DD2912225C5A80094D78D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 518854E72230652B00CA4141 /* Build configuration list for PBXNativeTarget "InstanceID_Example_tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 518854E52230652B00CA4141 /* Debug */, + 518854E62230652B00CA4141 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 518854F3223066BF00CA4141 /* Build configuration list for PBXNativeTarget "InstanceID_Tests_tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 518854F4223066BF00CA4141 /* Debug */, + 518854F5223066BF00CA4141 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 6003F585195388D10070C39A /* Build configuration list for PBXProject "Firebase" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -7584,7 +8552,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - DE26D2741F705C35004AE1D3 /* Build configuration list for PBXNativeTarget "Auth_EarlGreyTests" */ = { + DE26D2741F705C35004AE1D3 /* Build configuration list for PBXNativeTarget "Auth_E2eTests" */ = { isa = XCConfigurationList; buildConfigurations = ( DE26D2751F705C35004AE1D3 /* Debug */, @@ -7593,24 +8561,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - DE26D28C1F705EC7004AE1D3 /* Build configuration list for PBXNativeTarget "Auth_SwiftSample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DE26D28D1F705EC7004AE1D3 /* Debug */, - DE26D28E1F705EC7004AE1D3 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - DE26D2981F70668F004AE1D3 /* Build configuration list for PBXAggregateTarget "Auth_AllTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DE26D2991F70668F004AE1D3 /* Debug */, - DE26D29A1F70668F004AE1D3 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; DE33738A1E73773400881891 /* Build configuration list for PBXAggregateTarget "AllUnitTests_iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -7701,6 +8651,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + DE958B6F21F7D13E00E6C1C5 /* Build configuration list for PBXNativeTarget "InstanceID_Example_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DE958B7021F7D13E00E6C1C5 /* Debug */, + DE958B7121F7D13E00E6C1C5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DE958B9121F7D15900E6C1C5 /* Build configuration list for PBXNativeTarget "InstanceID_Tests_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DE958B9221F7D15900E6C1C5 /* Debug */, + DE958B9321F7D15900E6C1C5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; DEAAD3A01FBA11280053BF48 /* Build configuration list for PBXNativeTarget "Core_Example_tvOS" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/AllUnitTests_iOS.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/AllUnitTests_iOS.xcscheme index 52d38c8a9b4..663be66e294 100644 --- a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/AllUnitTests_iOS.xcscheme +++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/AllUnitTests_iOS.xcscheme @@ -118,6 +118,20 @@ ReferencedContainer = "container:Firebase.xcodeproj"> + + + + + + + + + + + + + + + + + + + + + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + + + + diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_AllTests.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_E2eTests.xcscheme similarity index 64% rename from Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_AllTests.xcscheme rename to Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_E2eTests.xcscheme index ea753c2bd90..94206220138 100644 --- a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_AllTests.xcscheme +++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_E2eTests.xcscheme @@ -14,9 +14,9 @@ buildForAnalyzing = "YES"> @@ -26,36 +26,15 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - - - - - @@ -63,9 +42,9 @@ @@ -76,7 +55,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" @@ -86,9 +64,9 @@ @@ -104,9 +82,9 @@ diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_Sample.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_Sample.xcscheme index a0ada521f71..79b6c5d1c25 100644 --- a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_Sample.xcscheme +++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_Sample.xcscheme @@ -33,8 +33,8 @@ diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/InstanceID_Example_iOS.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/InstanceID_Example_iOS.xcscheme new file mode 100644 index 00000000000..9ee4fa942f0 --- /dev/null +++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/InstanceID_Example_iOS.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_SwiftSample.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/InstanceID_Example_tvOS.xcscheme similarity index 78% rename from Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_SwiftSample.xcscheme rename to Example/Firebase.xcodeproj/xcshareddata/xcschemes/InstanceID_Example_tvOS.xcscheme index aaf8399daab..fb58fce53ea 100644 --- a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_SwiftSample.xcscheme +++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/InstanceID_Example_tvOS.xcscheme @@ -1,6 +1,6 @@ @@ -26,16 +26,15 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" shouldUseLaunchSchemeArgsEnv = "YES"> @@ -46,7 +45,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" @@ -57,9 +55,9 @@ runnableDebuggingMode = "0"> @@ -76,9 +74,9 @@ runnableDebuggingMode = "0"> diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/InstanceID_Tests_iOS.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/InstanceID_Tests_iOS.xcscheme new file mode 100644 index 00000000000..1c996cdb5e4 --- /dev/null +++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/InstanceID_Tests_iOS.xcscheme @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/InstanceID_Tests_tvOS.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/InstanceID_Tests_tvOS.xcscheme new file mode 100644 index 00000000000..e5e2ff19c2d --- /dev/null +++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/InstanceID_Tests_tvOS.xcscheme @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Messaging_Example_tvOS.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Messaging_Example_tvOS.xcscheme new file mode 100644 index 00000000000..35451cdfc77 --- /dev/null +++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Messaging_Example_tvOS.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Messaging_Tests_tvOS.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Messaging_Tests_tvOS.xcscheme new file mode 100644 index 00000000000..53db7cf3148 --- /dev/null +++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Messaging_Tests_tvOS.xcscheme @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/InstanceID/App/iOS/Base.lproj/LaunchScreen.storyboard b/Example/InstanceID/App/iOS/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000000..6620ff8711a --- /dev/null +++ b/Example/InstanceID/App/iOS/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/InstanceID/App/iOS/Base.lproj/Main.storyboard b/Example/InstanceID/App/iOS/Base.lproj/Main.storyboard new file mode 100644 index 00000000000..d164a237282 --- /dev/null +++ b/Example/InstanceID/App/iOS/Base.lproj/Main.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/InstanceID/App/iOS/FIRAppDelegate.h b/Example/InstanceID/App/iOS/FIRAppDelegate.h new file mode 100644 index 00000000000..6c35f0863dd --- /dev/null +++ b/Example/InstanceID/App/iOS/FIRAppDelegate.h @@ -0,0 +1,23 @@ +/* + * Copyright 2019 Google + * + * 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 UIKit; + +@interface FIRAppDelegate : UIResponder + +@property(strong, nonatomic) UIWindow *window; + +@end diff --git a/Example/InstanceID/App/iOS/FIRAppDelegate.m b/Example/InstanceID/App/iOS/FIRAppDelegate.m new file mode 100644 index 00000000000..14b1dc4d85e --- /dev/null +++ b/Example/InstanceID/App/iOS/FIRAppDelegate.m @@ -0,0 +1,55 @@ +// Copyright 2019 Google +// +// 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 "FIRAppDelegate.h" + +@implementation FIRAppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application { + // Sent when the application is about to move from active to inactive state. This can occur for + // certain types of temporary interruptions (such as an incoming phone call or SMS message) or + // when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame + // rates. Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + // Use this method to release shared resources, save user data, invalidate timers, and store + // enough application state information to restore your application to its current state in case + // it is terminated later. + // If your application supports background execution, this method is called instead of + // applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + // Called as part of the transition from the background to the inactive state; here you can undo + // many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If + // the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application { + // Called when the application is about to terminate. Save data if appropriate. See also + // applicationDidEnterBackground:. +} + +@end diff --git a/Example/InstanceID/App/iOS/FIRViewController.h b/Example/InstanceID/App/iOS/FIRViewController.h new file mode 100644 index 00000000000..c944ca8134e --- /dev/null +++ b/Example/InstanceID/App/iOS/FIRViewController.h @@ -0,0 +1,21 @@ +/* + * Copyright 2019 Google + * + * 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 UIKit; + +@interface FIRViewController : UIViewController + +@end diff --git a/Example/InstanceID/App/iOS/FIRViewController.m b/Example/InstanceID/App/iOS/FIRViewController.m new file mode 100644 index 00000000000..02f0334fbe1 --- /dev/null +++ b/Example/InstanceID/App/iOS/FIRViewController.m @@ -0,0 +1,33 @@ +// Copyright 2019 Google +// +// 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 "FIRViewController.h" + +@interface FIRViewController () + +@end + +@implementation FIRViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +@end diff --git a/Example/InstanceID/App/iOS/InstanceID-Info.plist b/Example/InstanceID/App/iOS/InstanceID-Info.plist new file mode 100644 index 00000000000..fc26896d71d --- /dev/null +++ b/Example/InstanceID/App/iOS/InstanceID-Info.plist @@ -0,0 +1,54 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Example/InstanceID/App/iOS/main.m b/Example/InstanceID/App/iOS/main.m new file mode 100644 index 00000000000..243d254bc3f --- /dev/null +++ b/Example/InstanceID/App/iOS/main.m @@ -0,0 +1,22 @@ +// Copyright 2019 Google +// +// 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 UIKit; +#import "FIRAppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([FIRAppDelegate class])); + } +} diff --git a/Example/InstanceID/App/tvOS/AppDelegate.h b/Example/InstanceID/App/tvOS/AppDelegate.h new file mode 100644 index 00000000000..3e55b05f9a2 --- /dev/null +++ b/Example/InstanceID/App/tvOS/AppDelegate.h @@ -0,0 +1,21 @@ +// Copyright 2019 Google +// +// 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 + +@interface AppDelegate : UIResponder + +@property(strong, nonatomic) UIWindow *window; + +@end diff --git a/Example/InstanceID/App/tvOS/AppDelegate.m b/Example/InstanceID/App/tvOS/AppDelegate.m new file mode 100644 index 00000000000..29f44a3394e --- /dev/null +++ b/Example/InstanceID/App/tvOS/AppDelegate.m @@ -0,0 +1,59 @@ +// Copyright 2019 Google +// +// 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 "AppDelegate.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application { + // Sent when the application is about to move from active to inactive state. This can occur for + // certain types of temporary interruptions (such as an incoming phone call or SMS message) or + // when the user quits the application and it begins the transition to the background state. Use + // this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. + // Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + // Use this method to release shared resources, save user data, invalidate timers, and store + // enough application state information to restore your application to its current state in case + // it is terminated later. If your application supports background execution, this method is + // called instead of applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + // Called as part of the transition from the background to the active state; here you can undo + // many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If + // the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application { + // Called when the application is about to terminate. Save data if appropriate. See also + // applicationDidEnterBackground:. +} + +@end diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..48ecb4fa43e --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { + "idiom" : "tv" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json new file mode 100644 index 00000000000..d29f024ed5c --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..48ecb4fa43e --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { + "idiom" : "tv" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..48ecb4fa43e --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { + "idiom" : "tv" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..16a370df014 --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json new file mode 100644 index 00000000000..d29f024ed5c --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..16a370df014 --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..16a370df014 --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json new file mode 100644 index 00000000000..db288f368f1 --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json @@ -0,0 +1,32 @@ +{ + "assets" : [ + { + "size" : "1280x768", + "idiom" : "tv", + "filename" : "App Icon - App Store.imagestack", + "role" : "primary-app-icon" + }, + { + "size" : "400x240", + "idiom" : "tv", + "filename" : "App Icon.imagestack", + "role" : "primary-app-icon" + }, + { + "size" : "2320x720", + "idiom" : "tv", + "filename" : "Top Shelf Image Wide.imageset", + "role" : "top-shelf-image-wide" + }, + { + "size" : "1920x720", + "idiom" : "tv", + "filename" : "Top Shelf Image.imageset", + "role" : "top-shelf-image" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json new file mode 100644 index 00000000000..7dc95020229 --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + }, + { + "idiom" : "tv-marketing", + "scale" : "1x" + }, + { + "idiom" : "tv-marketing", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json new file mode 100644 index 00000000000..7dc95020229 --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + }, + { + "idiom" : "tv-marketing", + "scale" : "1x" + }, + { + "idiom" : "tv-marketing", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Assets.xcassets/Launch Image.launchimage/Contents.json b/Example/InstanceID/App/tvOS/Assets.xcassets/Launch Image.launchimage/Contents.json new file mode 100644 index 00000000000..d746a609003 --- /dev/null +++ b/Example/InstanceID/App/tvOS/Assets.xcassets/Launch Image.launchimage/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "orientation" : "landscape", + "idiom" : "tv", + "extent" : "full-screen", + "minimum-system-version" : "11.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "tv", + "extent" : "full-screen", + "minimum-system-version" : "9.0", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/InstanceID/App/tvOS/Base.lproj/Main.storyboard b/Example/InstanceID/App/tvOS/Base.lproj/Main.storyboard new file mode 100644 index 00000000000..a5c40f19689 --- /dev/null +++ b/Example/InstanceID/App/tvOS/Base.lproj/Main.storyboard @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/InstanceID/App/tvOS/Info.plist b/Example/InstanceID/App/tvOS/Info.plist new file mode 100644 index 00000000000..02942a34f3e --- /dev/null +++ b/Example/InstanceID/App/tvOS/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UIUserInterfaceStyle + Automatic + + diff --git a/Example/InstanceID/App/tvOS/ViewController.h b/Example/InstanceID/App/tvOS/ViewController.h new file mode 100644 index 00000000000..9e02fefb3bd --- /dev/null +++ b/Example/InstanceID/App/tvOS/ViewController.h @@ -0,0 +1,19 @@ +// Copyright 2019 Google +// +// 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 + +@interface ViewController : UIViewController + +@end diff --git a/Example/InstanceID/App/tvOS/ViewController.m b/Example/InstanceID/App/tvOS/ViewController.m new file mode 100644 index 00000000000..022e80dbfa8 --- /dev/null +++ b/Example/InstanceID/App/tvOS/ViewController.m @@ -0,0 +1,28 @@ +// Copyright 2019 Google +// +// 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 "ViewController.h" + +@interface ViewController () + +@end + +@implementation ViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. +} + +@end diff --git a/Example/InstanceID/App/tvOS/main.m b/Example/InstanceID/App/tvOS/main.m new file mode 100644 index 00000000000..1100a936f02 --- /dev/null +++ b/Example/InstanceID/App/tvOS/main.m @@ -0,0 +1,22 @@ +// Copyright 2019 Google +// +// 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 "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/Example/InstanceID/Tests/FIRInstanceIDAPNSInfoTest.m b/Example/InstanceID/Tests/FIRInstanceIDAPNSInfoTest.m new file mode 100644 index 00000000000..435d1bfe6bc --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDAPNSInfoTest.m @@ -0,0 +1,98 @@ +/* + * Copyright 2019 Google + * + * 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 "Firebase/InstanceID/FIRInstanceIDAPNSInfo.h" + +#import + +#import "Firebase/InstanceID/FIRInstanceIDConstants.h" + +@interface FIRInstanceIDAPNSInfoTest : XCTestCase + +@end + +@implementation FIRInstanceIDAPNSInfoTest + +- (void)testAPNSInfoCreationWithValidDictionary { + NSDictionary *validDictionary = @{ + kFIRInstanceIDTokenOptionsAPNSKey : [@"tokenData" dataUsingEncoding:NSUTF8StringEncoding], + kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(YES) + }; + FIRInstanceIDAPNSInfo *info = + [[FIRInstanceIDAPNSInfo alloc] initWithTokenOptionsDictionary:validDictionary]; + XCTAssertNotNil(info); + XCTAssertEqualObjects(info.deviceToken, validDictionary[kFIRInstanceIDTokenOptionsAPNSKey]); + XCTAssertEqual(info.sandbox, + [validDictionary[kFIRInstanceIDTokenOptionsAPNSIsSandboxKey] boolValue]); +} + +- (void)testAPNSInfoCreationWithInvalidDictionary { + NSDictionary *validDictionary = @{}; + FIRInstanceIDAPNSInfo *info = + [[FIRInstanceIDAPNSInfo alloc] initWithTokenOptionsDictionary:validDictionary]; + XCTAssertNil(info); +} + +- (void)testAPNSInfoCreationWithInvalidTokenFormat { + // Token data stored as NSString instead of NSData + NSDictionary *badDictionary = @{ + kFIRInstanceIDTokenOptionsAPNSKey : @"tokenDataAsString", + kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(YES) + }; + FIRInstanceIDAPNSInfo *info = + [[FIRInstanceIDAPNSInfo alloc] initWithTokenOptionsDictionary:badDictionary]; + XCTAssertNil(info); +} + +- (void)testAPNSInfoCreationWithInvalidSandboxFormat { + // Sandbox key stored as NSString instead of NSNumber (bool) + NSDictionary *validDictionary = @{ + kFIRInstanceIDTokenOptionsAPNSKey : [@"tokenData" dataUsingEncoding:NSUTF8StringEncoding], + kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @"sandboxValueAsString" + }; + FIRInstanceIDAPNSInfo *info = + [[FIRInstanceIDAPNSInfo alloc] initWithTokenOptionsDictionary:validDictionary]; + XCTAssertNil(info); +} + +- (void)testAPNSInfoCreationFromInvalidArchive { + NSData *badData = [@"badData" dataUsingEncoding:NSUTF8StringEncoding]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + FIRInstanceIDAPNSInfo *info = [NSKeyedUnarchiver unarchiveObjectWithData:badData]; +#pragma clang diagnostic pop + XCTAssertNil(info); +} + +// Test that archiving a FIRInstanceIDAPNSInfo object and restoring it from the archive +// yields the same values for all the fields. +- (void)testAPNSInfoEncodingAndDecoding { + NSDictionary *validDictionary = @{ + kFIRInstanceIDTokenOptionsAPNSKey : [@"tokenData" dataUsingEncoding:NSUTF8StringEncoding], + kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @"sandboxValueAsString" + }; + FIRInstanceIDAPNSInfo *info = + [[FIRInstanceIDAPNSInfo alloc] initWithTokenOptionsDictionary:validDictionary]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + NSData *archive = [NSKeyedArchiver archivedDataWithRootObject:info]; + FIRInstanceIDAPNSInfo *restoredInfo = [NSKeyedUnarchiver unarchiveObjectWithData:archive]; +#pragma clang diagnostic pop + XCTAssertEqualObjects(info.deviceToken, restoredInfo.deviceToken); + XCTAssertEqual(info.sandbox, restoredInfo.sandbox); +} + +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDAuthKeyChainTest.m b/Example/InstanceID/Tests/FIRInstanceIDAuthKeyChainTest.m new file mode 100644 index 00000000000..97f575a02d8 --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDAuthKeyChainTest.m @@ -0,0 +1,414 @@ +/* + * Copyright 2019 Google + * + * 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 +#import "Firebase/InstanceID/FIRInstanceIDAuthKeyChain.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenInfo.h" + +static NSString *const kFIRInstanceIDTestKeychainId = @"com.google.iid-tests"; + +static NSString *const kFakeCheckinPlistName = @"com.google.test.IIDStoreTestCheckin"; + +static NSString *const kAuthorizedEntity = @"test-audience"; +static NSString *const kScope = @"test-scope"; +static NSString *const kAuthID = @"test-auth-id"; +static NSString *const kSecret = @"test-secret"; +static NSString *const kToken1 = + @"dOr37DpYQ9M:APA91bE5aQ2expDEmoSNDDrZqS6drAz2V-GHJHEsa-qVdlHXVSlWpUsK-Ta6Oe1QsVSLovL7_" + @"rbm8GNnP7XPfwjtDQrjxYS1BdtxHdVVnQKuxlF3Z0QOwL380l1e1Fz91PX5b77XKj0FIyqzX1z0uJc0-pM6YcaPGg"; +static NSString *const kToken2 = @"c8oEXUYIl3s:APA91bHtJMs_dZ2lXYXIcwsC47abYIuWhEJ_CshY2PJRjVuI_" + @"H659iYUwfmNNghnZVkCmeUdKDSrK8xqVb0PVHxyAW391Ynp2NchMB87kJWb3BS0z" + @"ud6Ej_xDES_oc353eFRvt0E6NXefDmrUCpBY8y89_1eVFFfiA"; +static NSString *const kFirebaseAppID = @"abcdefg:ios:QrjxYS1BdtxHdVVnQKuxlF3Z0QO"; + +static NSString *const kBundleID1 = @"com.google.fcm.dev"; +static NSString *const kBundleID2 = @"com.google.abtesting.dev"; + +@interface FIRInstanceIDAuthKeychain (ExposedForTest) + +@property(nonatomic, copy) + NSMutableDictionary *> *> + *cachedKeychainData; +- (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account; + +@end + +@interface FIRInstanceIDAuthKeyChainTest : XCTestCase + +@end + +@implementation FIRInstanceIDAuthKeyChainTest + +- (void)setUp { + [super setUp]; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testKeyChainNoCorruptionWithUniqueAccount { + XCTestExpectation *noCurruptionExpectation = + [self expectationWithDescription:@"No corruption between different accounts."]; + // Create a keychain with a service and a unique account + NSString *service = [NSString stringWithFormat:@"%@:%@", kAuthorizedEntity, kScope]; + NSString *account1 = kBundleID1; + NSData *tokenInfoData1 = [self tokenDataWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + token:kToken1]; + FIRInstanceIDAuthKeychain *keychain = + [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTestKeychainId]; + __weak FIRInstanceIDAuthKeychain *weakKeychain = keychain; + [keychain setData:tokenInfoData1 + forService:service + accessibility:NULL + account:account1 + handler:^(NSError *error) { + XCTAssertNil(error); + // Create another keychain with the same service but different account. + NSString *account2 = kBundleID2; + NSData *tokenInfoData2 = [self tokenDataWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + token:kToken2]; + [weakKeychain + setData:tokenInfoData2 + forService:service + accessibility:NULL + account:account2 + handler:^(NSError *error) { + XCTAssertNil(error); + // Now query the token and compare, they should not corrupt + // each other. + NSData *data1 = [weakKeychain dataForService:service account:account1]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + FIRInstanceIDTokenInfo *tokenInfo1 = + [NSKeyedUnarchiver unarchiveObjectWithData:data1]; + XCTAssertEqualObjects(kToken1, tokenInfo1.token); + + NSData *data2 = [weakKeychain dataForService:service account:account2]; + FIRInstanceIDTokenInfo *tokenInfo2 = + [NSKeyedUnarchiver unarchiveObjectWithData:data2]; +#pragma clang diagnostic pop + XCTAssertEqualObjects(kToken2, tokenInfo2.token); + // Also check the cache data. + XCTAssertEqual(weakKeychain.cachedKeychainData.count, 1); + XCTAssertEqual(weakKeychain.cachedKeychainData[service].count, 2); + XCTAssertEqualObjects( + weakKeychain.cachedKeychainData[service][account1].firstObject, + tokenInfoData1); + XCTAssertEqualObjects( + weakKeychain.cachedKeychainData[service][account2].firstObject, + tokenInfoData2); + + // Check wildcard query + NSArray *results = [weakKeychain itemsMatchingService:service + account:@"*"]; + XCTAssertEqual(results.count, 2); + + // Clean up keychain at the end + [weakKeychain removeItemsMatchingService:service + account:@"*" + handler:^(NSError *_Nonnull error) { + XCTAssertNil(error); + [noCurruptionExpectation fulfill]; + }]; + }]; + }]; + [self waitForExpectationsWithTimeout:1.0 handler:NULL]; +} + +- (void)testKeyChainNoCorruptionWithUniqueService { + XCTestExpectation *noCurruptionExpectation = + [self expectationWithDescription:@"No corruption between different services."]; + // Create a keychain with a service and a unique account + NSString *service1 = [NSString stringWithFormat:@"%@:%@", kAuthorizedEntity, kScope]; + NSString *account = kBundleID1; + NSData *tokenData = [self tokenDataWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + token:kToken1]; + FIRInstanceIDAuthKeychain *keychain = + [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTestKeychainId]; + __weak FIRInstanceIDAuthKeychain *weakKeychain = keychain; + [keychain setData:tokenData + forService:service1 + accessibility:NULL + account:account + handler:^(NSError *error) { + XCTAssertNil(error); + // Store a checkin info using the same keychain account, but different service. + NSString *service2 = @"com.google.iid.checkin"; + FIRInstanceIDCheckinPreferences *preferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kAuthID + secretToken:kSecret]; + NSString *checkinKeychainContent = [preferences checkinKeychainContent]; + NSData *checkinData = [checkinKeychainContent dataUsingEncoding:NSUTF8StringEncoding]; + + [weakKeychain + setData:checkinData + forService:service2 + accessibility:NULL + account:account + handler:^(NSError *error) { + XCTAssertNil(error); + // Now query the token and compare, they should not corrupt + // each other. + NSData *data1 = [weakKeychain dataForService:service1 account:account]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + FIRInstanceIDTokenInfo *tokenInfo1 = + [NSKeyedUnarchiver unarchiveObjectWithData:data1]; +#pragma clang diagnostic pop + XCTAssertEqualObjects(kToken1, tokenInfo1.token); + + NSData *data2 = [weakKeychain dataForService:service2 account:account]; + NSString *checkinKeychainContent = + [[NSString alloc] initWithData:data2 encoding:NSUTF8StringEncoding]; + FIRInstanceIDCheckinPreferences *checkinPreferences = + [FIRInstanceIDCheckinPreferences + preferencesFromKeychainContents:checkinKeychainContent]; + XCTAssertEqualObjects(checkinPreferences.secretToken, kSecret); + XCTAssertEqualObjects(checkinPreferences.deviceID, kAuthID); + + NSArray *results = [weakKeychain itemsMatchingService:@"*" + account:account]; + XCTAssertEqual(results.count, 2); + // Also check the cache data. + XCTAssertEqual(weakKeychain.cachedKeychainData.count, 2); + XCTAssertEqualObjects( + weakKeychain.cachedKeychainData[service1][account].firstObject, + tokenData); + XCTAssertEqualObjects( + weakKeychain.cachedKeychainData[service2][account].firstObject, + checkinData); + + // Clean up keychain at the end + [weakKeychain removeItemsMatchingService:@"*" + account:@"*" + handler:^(NSError *_Nonnull error) { + XCTAssertNil(error); + [noCurruptionExpectation fulfill]; + }]; + }]; + }]; + [self waitForExpectationsWithTimeout:1.0 handler:NULL]; +} + +- (void)testQueryCachedKeychainItems { + XCTestExpectation *addItemToKeychainExpectation = + [self expectationWithDescription:@"Test added item should be cached properly"]; + // A wildcard query should return empty data when there's nothing in keychain + FIRInstanceIDAuthKeychain *keychain = + [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTestKeychainId]; + id keychainMock = OCMPartialMock(keychain); + + NSArray *result = [keychain itemsMatchingService:@"*" account:@"*"]; + XCTAssertEqual(result.count, 0); + + // Create a keychain item + NSString *service = [NSString stringWithFormat:@"%@:%@", kAuthorizedEntity, kScope]; + NSString *account = kBundleID1; + NSData *tokenData = [self tokenDataWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + token:kToken1]; + __weak FIRInstanceIDAuthKeychain *weakKeychain = keychain; + __weak id weakKeychainMock = keychainMock; + [keychain setData:tokenData + forService:service + accessibility:NULL + account:account + handler:^(NSError *error) { + XCTAssertNil(error); + + // Now if we clean the cache + [weakKeychain.cachedKeychainData removeAllObjects]; + // Then query the item should fetch from keychain. + NSData *data = [weakKeychain dataForService:service account:account]; + XCTAssertEqualObjects(data, tokenData); + // Verify we fetch from keychain by calling to get the query + OCMVerify([weakKeychainMock keychainQueryForService:service account:account]); + // Cache should now have the query item + XCTAssertEqualObjects(weakKeychain.cachedKeychainData[service][account].firstObject, + tokenData); + // Wildcard query should simply return the results without cache it + data = [weakKeychain dataForService:@"*" account:account]; + XCTAssertEqualObjects(data, tokenData); + // Cache should not have wildcard query entry + XCTAssertNil(weakKeychain.cachedKeychainData[@"*"]); + + // Assume keychain has empty service entry + [weakKeychain.cachedKeychainData setObject:[@{} mutableCopy] forKey:service]; + // Query the item + data = [weakKeychain dataForService:service account:account]; + XCTAssertEqualObjects(data, tokenData); + // Cache should have the query item. + XCTAssertEqualObjects(weakKeychain.cachedKeychainData[service][account].firstObject, + tokenData); + + // Clean up keychain at the end + [weakKeychain removeItemsMatchingService:@"*" + account:@"*" + handler:^(NSError *_Nonnull error) { + XCTAssertNil(error); + [addItemToKeychainExpectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:1.0 handler:NULL]; +} + +- (void)testCachedKeychainOverwrite { + XCTestExpectation *overwriteCachedKeychainExpectation = + [self expectationWithDescription:@"Test the cached keychain item is overwrite properly"]; + + FIRInstanceIDAuthKeychain *keychain = + [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTestKeychainId]; + + // Set the cache a different data under the same service but different account + NSData *data = [[NSData alloc] init]; + NSString *service = [NSString stringWithFormat:@"%@:%@", kAuthorizedEntity, kScope]; + + [keychain.cachedKeychainData setObject:[@{kBundleID2 : data} mutableCopy] forKey:service]; + + // Create a keychain item + NSString *account = kBundleID1; + NSData *tokenData = [self tokenDataWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + token:kToken1]; + __weak FIRInstanceIDAuthKeychain *weakKeychain = keychain; + [keychain setData:tokenData + forService:service + accessibility:NULL + account:account + handler:^(NSError *error) { + XCTAssertNil(error); + + // Query the item should fetch from keychain because no entry under the same + // service and account. + NSData *data = [weakKeychain dataForService:service account:account]; + XCTAssertEqualObjects(data, tokenData); + + // Cache should now have the query item + XCTAssertEqualObjects(weakKeychain.cachedKeychainData[service][account].firstObject, + tokenData); + + // Clean up keychain at the end + [weakKeychain removeItemsMatchingService:@"*" + account:@"*" + handler:^(NSError *_Nonnull error) { + XCTAssertNil(error); + [overwriteCachedKeychainExpectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:1.0 handler:NULL]; +} + +- (void)testSetKeychainItemShouldDeleteOldEntry { + XCTestExpectation *overwriteCachedKeychainExpectation = [self + expectationWithDescription:@"Test keychain entry should be deleted before adding a new one"]; + + FIRInstanceIDAuthKeychain *keychain = + [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTestKeychainId]; + + // Assume keychain had a old entry under the same service and account. + // Now if we set the cache a different data under the same service + NSData *oldData = [[NSData alloc] init]; + NSString *service = [NSString stringWithFormat:@"%@:%@", kAuthorizedEntity, kScope]; + NSString *account = kBundleID1; + [keychain.cachedKeychainData setObject:[@{account : oldData} mutableCopy] forKey:service]; + // add a new keychain item + NSData *tokenData = [self tokenDataWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + token:kToken1]; + __weak FIRInstanceIDAuthKeychain *weakKeychain = keychain; + [keychain setData:tokenData + forService:service + accessibility:NULL + account:account + handler:^(NSError *error) { + XCTAssertNil(error); + + // Cache should now have the updated item + XCTAssertEqualObjects(weakKeychain.cachedKeychainData[service][account].firstObject, + tokenData); + + // Clean up keychain at the end + [weakKeychain removeItemsMatchingService:@"*" + account:@"*" + handler:^(NSError *_Nonnull error) { + XCTAssertNil(error); + [overwriteCachedKeychainExpectation fulfill]; + }]; + }]; + [self waitForExpectationsWithTimeout:1.0 handler:NULL]; +} + +- (void)testInvalidQuery { + XCTestExpectation *invalidKeychainQueryExpectation = + [self expectationWithDescription:@"Test invalid keychain query"]; + + FIRInstanceIDAuthKeychain *keychain = + [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTestKeychainId]; + + NSData *data = [[NSData alloc] init]; + [keychain setData:data + forService:@"*" + accessibility:NULL + account:@"*" + handler:^(NSError *error) { + XCTAssertNotNil(error); + [invalidKeychainQueryExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1.0 handler:NULL]; +} + +- (void)testQueryAndAddEntry { + FIRInstanceIDAuthKeychain *keychain = + [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTestKeychainId]; + + // Set the cache a different data under the same service but different account + NSData *data = [[NSData alloc] init]; + NSString *service = [NSString stringWithFormat:@"%@:%@", kAuthorizedEntity, kScope]; + NSString *account1 = kBundleID1; + + [keychain.cachedKeychainData setObject:[@{account1 : data} mutableCopy] forKey:service]; + // Now account2 doesn't exist in cache + NSString *account2 = kBundleID2; + XCTAssertNil(keychain.cachedKeychainData[service][account2]); + // Query account2 + XCTAssertNil([keychain dataForService:service account:account2]); + // Service and account2 should exist in cache. + XCTAssertNotNil(keychain.cachedKeychainData[service][account2]); +} + +#pragma mark - helper function +- (NSData *)tokenDataWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + token:(NSString *)token { + FIRInstanceIDTokenInfo *tokenInfo = + [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:authorizedEntity + scope:scope + token:token + appVersion:@"1.0" + firebaseAppID:kFirebaseAppID]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [NSKeyedArchiver archivedDataWithRootObject:tokenInfo]; +#pragma clang diagnostic pop +} +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDAuthServiceTest.m b/Example/InstanceID/Tests/FIRInstanceIDAuthServiceTest.m new file mode 100644 index 00000000000..aa6ed05374e --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDAuthServiceTest.m @@ -0,0 +1,401 @@ +/* + * Copyright 2019 Google + * + * 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 +#import "Firebase/InstanceID/FIRInstanceIDAuthService.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinService.h" +#import "Firebase/InstanceID/FIRInstanceIDStore.h" +#import "Firebase/InstanceID/NSError+FIRInstanceID.h" + +static NSString *const kDeviceAuthId = @"device-id"; +static NSString *const kSecretToken = @"secret-token"; +static NSString *const kDigest = @"com.google.digest"; +static NSString *const kVersionInfo = @"1.0"; + +@interface FIRInstanceIDCheckinService () +@property(nonatomic, readwrite, strong) FIRInstanceIDCheckinPreferences *checkinPreferences; +@end + +@interface FIRInstanceIDAuthService () +@property(atomic, readwrite, assign) int64_t lastCheckinTimestampSeconds; +@property(atomic, readwrite, assign) int64_t nextScheduledCheckinIntervalSeconds; +@property(atomic, readwrite, assign) int checkinRetryCount; +@property(nonatomic, readonly, strong) + NSMutableArray *checkinHandlers; +@end + +@interface FIRInstanceIDAuthServiceTest : XCTestCase + +@property(nonatomic, readwrite, strong) FIRInstanceIDAuthService *authService; +@property(nonatomic, readwrite, strong) FIRInstanceIDCheckinService *checkinService; +@property(nonatomic, readwrite, strong) id mockCheckinService; +@property(nonatomic, readwrite, strong) id mockStore; +@property(nonatomic, readwrite, copy) FIRInstanceIDDeviceCheckinCompletion checkinCompletion; + +@end + +@implementation FIRInstanceIDAuthServiceTest + +- (void)setUp { + [super setUp]; + + _mockStore = OCMClassMock([FIRInstanceIDStore class]); + _checkinService = [[FIRInstanceIDCheckinService alloc] init]; + _mockCheckinService = OCMPartialMock(_checkinService); + _authService = [[FIRInstanceIDAuthService alloc] initWithCheckinService:_mockCheckinService + store:_mockStore]; + // The tests here are to focus on checkin interval not locale change, so always set locale as + // non-changed. + [[NSUserDefaults standardUserDefaults] setObject:FIRInstanceIDCurrentLocale() + forKey:kFIRInstanceIDUserDefaultsKeyLocale]; +} + +- (void)tearDown { + _checkinCompletion = nil; + [super tearDown]; +} + +/** + * Test scheduling a checkin which completes successfully. Once the checkin is complete + * we should have the valid checkin preferences in memory. + */ +- (void)testScheduleCheckin_initialSuccess { + XCTestExpectation *checkinExpectation = + [self expectationWithDescription:@"Did call checkin service"]; + + FIRInstanceIDCheckinPreferences *checkinPreferences = [self validCheckinPreferences]; + [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) { + self.checkinCompletion(checkinPreferences, nil); + }] checkinWithExistingCheckin:[OCMArg any] + completion:[OCMArg checkWithBlock:^BOOL(id obj) { + [checkinExpectation fulfill]; + self.checkinCompletion = obj; + return obj != nil; + }]]; + + // Always return YES for whether we succeeded in persisting the checkin + [[self.mockStore stub] saveCheckinPreferences:[OCMArg any] + handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]]; + + [self.authService scheduleCheckin:YES]; + + XCTAssertTrue([self.authService hasValidCheckinInfo]); + XCTAssertEqual([self.authService checkinRetryCount], 1); + [self waitForExpectationsWithTimeout:2.0 handler:NULL]; +} + +/** + * Test scheduling a checkin which completes successfully, but fails to save, due to Keychain + * errors. + */ +- (void)testScheduleCheckin_successButFailureInSaving { + XCTestExpectation *checkinFailureExpectation = + [self expectationWithDescription:@"Did receive error after checkin"]; + + FIRInstanceIDCheckinPreferences *checkinPreferences = [self validCheckinPreferences]; + [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) { + self.checkinCompletion(checkinPreferences, nil); + }] checkinWithExistingCheckin:[OCMArg any] + completion:[OCMArg checkWithBlock:^BOOL(id obj) { + self.checkinCompletion = obj; + return obj != nil; + }]]; + + // Always return NO for whether we succeeded in persisting the checkin, to simulate Keychain error + [[self.mockStore stub] saveCheckinPreferences:[OCMArg any] + handler:[OCMArg invokeBlockWithArgs:[OCMArg any], nil]]; + + [self.authService + fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *checkin, NSError *error) { + [checkinFailureExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2.0 handler:NULL]; + XCTAssertFalse([self.authService hasValidCheckinInfo]); +} + +/** + * Test scheduling multiple checkins to complete immediately. Each successive checkin should + * be triggered immediately. + */ +- (void)testMultipleScheduleCheckin_immediately { + XCTestExpectation *checkinExpectation = + [self expectationWithDescription:@"Did call checkin service"]; + __block int checkinHandlerInvocationCount = 0; + + FIRInstanceIDCheckinPreferences *checkinPreferences = [self validCheckinPreferences]; + [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) { + checkinHandlerInvocationCount++; + // Mock successful Checkin after delay. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + [checkinExpectation fulfill]; + self.checkinCompletion(checkinPreferences, nil); + }); + }] checkinWithExistingCheckin:[OCMArg any] + completion:[OCMArg checkWithBlock:^BOOL(id obj) { + self.checkinCompletion = obj; + return obj != nil; + }]]; + + // Always return YES for whether we succeeded in persisting the checkin + [[self.mockStore stub] saveCheckinPreferences:[OCMArg any] + handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]]; + [self.authService scheduleCheckin:YES]; + + // Schedule an immediate checkin again. + // This should just return because the previous checkin isn't over yet. + [self.authService scheduleCheckin:YES]; + + [self waitForExpectationsWithTimeout:5.0 handler:NULL]; + XCTAssertTrue([self.authService hasValidCheckinInfo]); + XCTAssertEqual([self.authService checkinRetryCount], 2); + + // Checkin handler should only be invoked once since the second checkin request should + // return immediately. + XCTAssertEqual(checkinHandlerInvocationCount, 1); +} + +/** + * Test multiple checkins scheduled. The second checkin should be scheduled after some + * delay before the first checkin has returned. Since the latter checkin is not immediate + * we should not run it since the first checkin is already scheduled to be executed later. + */ +- (void)testMultipleScheduleCheckin_notImmediately { + XCTestExpectation *checkinExpectation = + [self expectationWithDescription:@"Did call checkin service"]; + + FIRInstanceIDCheckinPreferences *checkinPreferences = [self validCheckinPreferences]; + [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) { + // Mock successful Checkin after delay. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + [checkinExpectation fulfill]; + self.checkinCompletion(checkinPreferences, nil); + }); + }] checkinWithExistingCheckin:[OCMArg any] + completion:[OCMArg checkWithBlock:^BOOL(id obj) { + self.checkinCompletion = obj; + return obj != nil; + }]]; + + // Always return YES for whether we succeeded in persisting the checkin + [[self.mockStore stub] saveCheckinPreferences:[OCMArg any] + handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]]; + + [self.authService scheduleCheckin:YES]; + + // Schedule another checkin after some delay while the first checkin has not yet returned + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + [self.authService scheduleCheckin:NO]; + }); + + [self waitForExpectationsWithTimeout:5.0 handler:NULL]; + XCTAssertTrue([self.authService hasValidCheckinInfo]); + XCTAssertEqual([self.authService checkinRetryCount], 1); +} + +/** + * Test initial checkin failure which schedules another checkin which should succeed. + */ +- (void)testInitialCheckinFailure_retrySuccess { + XCTestExpectation *checkinExpectation = + [self expectationWithDescription:@"Did call checkin service"]; + __block int checkinHandlerInvocationCount = 0; + + [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) { + checkinHandlerInvocationCount++; + + if (checkinHandlerInvocationCount == 1) { + // Mock failure on first try + NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeTimeout]; + self.checkinCompletion(nil, error); + } else if (checkinHandlerInvocationCount == 2) { + // Mock success on second try + [checkinExpectation fulfill]; + self.checkinCompletion([self validCheckinPreferences], nil); + } else { + // We should not retry for a third time again. + XCTFail(@"Invoking checkin handler invalid number of times."); + } + }] checkinWithExistingCheckin:[OCMArg any] + completion:[OCMArg checkWithBlock:^BOOL(id obj) { + self.checkinCompletion = obj; + return obj != nil; + }]]; + + // Always return YES for whether we succeeded in persisting the checkin + [[self.mockStore stub] saveCheckinPreferences:[OCMArg any] + handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]]; + + [self.authService scheduleCheckin:YES]; + // Schedule another checkin after some delay while the first checkin has not yet returned + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + [self.authService scheduleCheckin:YES]; + XCTAssertTrue([self.authService hasValidCheckinInfo]); + XCTAssertEqual([self.authService checkinRetryCount], 2); + XCTAssertEqual(checkinHandlerInvocationCount, 2); + }); + + [self waitForExpectationsWithTimeout:5.0 handler:NULL]; +} + +/** + * Test initial checkin failure which schedules another checkin which should succeed. If + * a new checkin request comes after that we should not schedule a checkin as we have + * already have valid checkin credentials. + */ +- (void)testInitialCheckinFailure_multipleRetrySuccess { + XCTestExpectation *checkinExpectation = + [self expectationWithDescription:@"Did call checkin service"]; + __block int checkinHandlerInvocationCount = 0; + + [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) { + checkinHandlerInvocationCount++; + + if (checkinHandlerInvocationCount <= 2) { + // Mock failure on first try + NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeTimeout]; + self.checkinCompletion(nil, error); + } else if (checkinHandlerInvocationCount == 3) { + // Mock success on second try + [checkinExpectation fulfill]; + self.checkinCompletion([self validCheckinPreferences], nil); + } else { + // We should not retry for a third time again. + XCTFail(@"Invoking checkin handler invalid number of times."); + } + }] checkinWithExistingCheckin:[OCMArg any] + completion:[OCMArg checkWithBlock:^BOOL(id obj) { + self.checkinCompletion = obj; + return obj != nil; + }]]; + + // Always return YES for whether we succeeded in persisting the checkin + [[self.mockStore stub] saveCheckinPreferences:[OCMArg any] + handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]]; + + [self.authService scheduleCheckin:YES]; + + [self waitForExpectationsWithTimeout:10.0 handler:NULL]; + XCTAssertTrue([self.authService hasValidCheckinInfo]); + XCTAssertEqual([self.authService checkinRetryCount], 3); +} + +/** + * Performing multiple checkin requests should result in multiple handlers being + * called back, but with only a single actual checkin fetch. + */ +- (void)testMultipleCheckinHandlersWithSuccessfulCheckin { + XCTestExpectation *allHandlersCalledExpectation = + [self expectationWithDescription:@"All checkin handlers were called"]; + __block NSInteger checkinHandlerCallbackCount = 0; + __block NSInteger checkinServiceInvocationCount = 0; + + // Always return a successful checkin, and count the number of times CheckinService is called + [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) { + checkinServiceInvocationCount++; + self.checkinCompletion([self validCheckinPreferences], nil); + }] checkinWithExistingCheckin:[OCMArg any] + completion:[OCMArg checkWithBlock:^BOOL(id obj) { + self.checkinCompletion = obj; + return obj != nil; + }]]; + + // Always return YES for whether we succeeded in persisting the checkin + [[self.mockStore stub] saveCheckinPreferences:[OCMArg any] + handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]]; + + NSInteger numHandlers = 10; + for (NSInteger i = 0; i < numHandlers; i++) { + [self.authService + fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *checkin, NSError *error) { + checkinHandlerCallbackCount++; + if (checkinHandlerCallbackCount == numHandlers) { + [allHandlersCalledExpectation fulfill]; + } + }]; + } + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + XCTAssertEqual(checkinServiceInvocationCount, 1); + XCTAssertEqual(checkinHandlerCallbackCount, numHandlers); +} + +/** + * Performing a scheduled checkin *and* simultaneous checkin request should result in + * the number of pending checkin handlers to be 2 (one for the scheduled checkin, one for + * the direct fetch). + */ +- (void)testScheduledAndImmediateCheckinsWithMultipleHandler { + XCTestExpectation *fetchHandlerCalledExpectation = + [self expectationWithDescription:@"Direct checkin handler was called"]; + __block NSInteger checkinServiceInvocationCount = 0; + + // Always return a successful checkin, and count the number of times CheckinService is called + [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) { + checkinServiceInvocationCount++; + // Give the checkin service some time to complete the request + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + self.checkinCompletion([self validCheckinPreferences], nil); + }); + }] checkinWithExistingCheckin:[OCMArg any] + completion:[OCMArg checkWithBlock:^BOOL(id obj) { + self.checkinCompletion = obj; + return obj != nil; + }]]; + + // Always return YES for whether we succeeded in persisting the checkin + [[self.mockStore stub] saveCheckinPreferences:[OCMArg any] + handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]]; + + // Start a scheduled (though immediate) checkin + [self.authService scheduleCheckin:YES]; + + // Request a direct checkin fetch + [self.authService + fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *checkin, NSError *error) { + [fetchHandlerCalledExpectation fulfill]; + }]; + // At this point we should have checkinHandlers, one for scheduled, one for the direct fetch + XCTAssertEqual(self.authService.checkinHandlers.count, 2); + + [self waitForExpectationsWithTimeout:0.5 handler:nil]; + // Make sure only one checkin fetch was performed + XCTAssertEqual(checkinServiceInvocationCount, 1); +} + +#pragma mark - Helper Methods + +- (FIRInstanceIDCheckinPreferences *)validCheckinPreferences { + NSDictionary *gservicesData = @{ + kFIRInstanceIDVersionInfoStringKey : kVersionInfo, + kFIRInstanceIDLastCheckinTimeKey : @(FIRInstanceIDCurrentTimestampInMilliseconds()) + }; + FIRInstanceIDCheckinPreferences *checkinPreferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceAuthId + secretToken:kSecretToken]; + [checkinPreferences updateWithCheckinPlistContents:gservicesData]; + return checkinPreferences; +} + +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDBackupExcludedPlistTest.m b/Example/InstanceID/Tests/FIRInstanceIDBackupExcludedPlistTest.m new file mode 100644 index 00000000000..dba4746b58b --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDBackupExcludedPlistTest.m @@ -0,0 +1,151 @@ +/* + * Copyright 2019 Google + * + * 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 +#import "Firebase/InstanceID/FIRInstanceIDAuthKeyChain.h" +#import "Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.h" +#import "Firebase/InstanceID/FIRInstanceIDStore.h" + +static NSString *const kSubDirectoryName = @"FirebaseInstanceIDBackupPlistTest"; +static NSString *const kTestPlistFileName = @"com.google.test.IIDBackupExcludedPlist"; + +@interface FIRInstanceIDBackupExcludedPlist () +- (BOOL)moveToApplicationSupportSubDirectory:(NSString *)subDirectoryName; +@end + +@interface FIRInstanceIDBackupExcludedPlistTest : XCTestCase + +@property(nonatomic, readwrite, strong) FIRInstanceIDBackupExcludedPlist *plist; + +@end + +@implementation FIRInstanceIDBackupExcludedPlistTest + +- (void)setUp { + [super setUp]; + [FIRInstanceIDStore createSubDirectory:kSubDirectoryName]; + self.plist = [[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:kTestPlistFileName + subDirectory:kSubDirectoryName]; +} + +- (void)tearDown { + [self.plist deleteFile:nil]; + [FIRInstanceIDStore removeSubDirectory:kSubDirectoryName error:nil]; + [super tearDown]; +} + +- (void)testWriteToPlistInDocumentsFolder { + XCTAssertNil([self.plist contentAsDictionary]); + NSDictionary *plistContents = @{@"hello" : @"world", @"id" : @123}; + [self.plist writeDictionary:plistContents error:nil]; + XCTAssertEqualObjects(plistContents, [self.plist contentAsDictionary]); +} + +- (void)testDeleteFileInDocumentsFolder { + NSDictionary *plistContents = @{@"hello" : @"world", @"id" : @123}; + [self.plist writeDictionary:plistContents error:nil]; + XCTAssertEqualObjects(plistContents, [self.plist contentAsDictionary]); + + // Delete file + XCTAssertTrue([self.plist doesFileExist]); + XCTAssertTrue([self.plist deleteFile:nil]); + XCTAssertFalse([self.plist doesFileExist]); +} + +- (void)testWriteToPlistInApplicationSupportFolder { + XCTAssertNil([self.plist contentAsDictionary]); + + NSDictionary *plistContents = @{@"hello" : @"world", @"id" : @123}; + [self.plist writeDictionary:plistContents error:nil]; + + XCTAssertTrue([self.plist doesFileExist]); + XCTAssertEqualObjects(plistContents, [self.plist contentAsDictionary]); + + XCTAssertTrue([self doesPlistFileExist]); +} + +- (void)testMovePlistToApplicationSupportDirectorySuccess { + NSDictionary *plistContents = @{@"hello" : @"world", @"id" : @123}; + [self.plist writeDictionary:plistContents error:nil]; + [self.plist moveToApplicationSupportSubDirectory:kSubDirectoryName]; + XCTAssertTrue([self doesPlistFileExist]); + XCTAssertFalse([self isPlistInDocumentsDirectory]); + + NSDictionary *newPlistContents = @{@"world" : @"hello"}; + [self.plist writeDictionary:newPlistContents error:nil]; + XCTAssertEqualObjects(newPlistContents, [self.plist contentAsDictionary]); +} + +- (void)testMovePlistToApplicationSupportDirectoryFailure { + // This is to test moving data from deprecated document folder to application folder + // which should only apply to iOS. +#if TARGET_OS_IOS + // Delete the subdirectory + [FIRInstanceIDStore removeSubDirectory:kSubDirectoryName error:nil]; + + // Create a new plistl This would try to move or write to the ApplicationSupport directory + // but since the subdirectory is not there anymore it will fail and rather write to the + // Documents folder. + self.plist = [[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:kTestPlistFileName + subDirectory:kSubDirectoryName]; + + NSDictionary *plistContents = @{@"hello" : @"world", @"id" : @123}; + [self.plist writeDictionary:plistContents error:nil]; + + XCTAssertFalse([self doesPlistFileExist]); + XCTAssertTrue([self isPlistInDocumentsDirectory]); + + NSDictionary *newPlistContents = @{@"world" : @"hello"}; + [self.plist writeDictionary:newPlistContents error:nil]; + + XCTAssertEqualObjects(newPlistContents, [self.plist contentAsDictionary]); + + // The new file should still be written to the Documents folder. + XCTAssertFalse([self doesPlistFileExist]); + XCTAssertTrue([self isPlistInDocumentsDirectory]); +#endif +} + +#pragma mark - Private Helpers + +- (BOOL)doesPlistFileExist { +#if TARGET_OS_TV + NSArray *directoryPaths = + NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); +#else + NSArray *directoryPaths = + NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); +#endif + NSString *dirPath = directoryPaths.lastObject; + NSArray *components = + @[ dirPath, kSubDirectoryName, [NSString stringWithFormat:@"%@.plist", kTestPlistFileName] ]; + NSString *plistPath = [NSString pathWithComponents:components]; + return [[NSFileManager defaultManager] fileExistsAtPath:plistPath]; +} + +- (BOOL)isPlistInDocumentsDirectory { + NSArray *directoryPaths = + NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsSupportDirPath = directoryPaths.lastObject; + NSArray *components = + @[ documentsSupportDirPath, [NSString stringWithFormat:@"%@.plist", kTestPlistFileName] ]; + NSString *plistPath = [NSString pathWithComponents:components]; + return [[NSFileManager defaultManager] fileExistsAtPath:plistPath]; +} + +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDCheckinPreferencesTest.m b/Example/InstanceID/Tests/FIRInstanceIDCheckinPreferencesTest.m new file mode 100644 index 00000000000..efe3e2f7b70 --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDCheckinPreferencesTest.m @@ -0,0 +1,88 @@ +/* + * Copyright 2019 Google + * + * 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 "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences_Private.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinService.h" + +static NSString *const kDeviceAuthId = @"1234"; +static NSString *const kSecretToken = @"567890"; + +@interface FIRInstanceIDCheckinPreferencesTest : XCTestCase + +@end + +@implementation FIRInstanceIDCheckinPreferencesTest + +- (void)setUp { + [super setUp]; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testInvalidCheckinInfo { + FIRInstanceIDCheckinPreferences *preferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:@"" secretToken:@""]; + XCTAssertFalse([preferences hasValidCheckinInfo]); +} + +- (void)testCheckinPreferencesReset { + FIRInstanceIDCheckinPreferences *checkin = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceAuthId + secretToken:kSecretToken]; + [checkin reset]; + XCTAssertNil(checkin.deviceID); + XCTAssertNil(checkin.secretToken); + XCTAssertFalse([checkin hasValidCheckinInfo]); +} + +- (void)testInvalidCheckinInfoDueToLocaleChanged { + // Set to a different locale than the current locale. + [[NSUserDefaults standardUserDefaults] setObject:@"zh-Hant" + forKey:kFIRInstanceIDUserDefaultsKeyLocale]; + FIRInstanceIDCheckinPreferences *checkin = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceAuthId + secretToken:kSecretToken]; + XCTAssertFalse([checkin hasValidCheckinInfo], + @"Should consider checkin info invalid as locale has changed."); + // set back the original locale + [[NSUserDefaults standardUserDefaults] setObject:FIRInstanceIDCurrentLocale() + forKey:kFIRInstanceIDUserDefaultsKeyLocale]; +} + +- (void)testCheckinPreferenceRefreshTokenWeekly { + FIRInstanceIDCheckinPreferences *checkin = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceAuthId + secretToken:kSecretToken]; + int64_t now = FIRInstanceIDCurrentTimestampInMilliseconds(); + [checkin updateWithCheckinPlistContents:@{kFIRInstanceIDLastCheckinTimeKey : @(now)}]; + + XCTAssertTrue([checkin hasValidCheckinInfo]); + + // Set last checkin time long time ago. + now = FIRInstanceIDCurrentTimestampInMilliseconds(); + [checkin updateWithCheckinPlistContents:@{ + kFIRInstanceIDLastCheckinTimeKey : @(now - kFIRInstanceIDDefaultCheckinInterval * 1000 - 1) + }]; + + XCTAssertFalse([checkin hasValidCheckinInfo]); +} + +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDCheckinServiceTest.m b/Example/InstanceID/Tests/FIRInstanceIDCheckinServiceTest.m new file mode 100644 index 00000000000..115ed856ffb --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDCheckinServiceTest.m @@ -0,0 +1,210 @@ +/* + * Copyright 2019 Google + * + * 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 +#import +#import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinService.h" +#import "Firebase/InstanceID/FIRInstanceIDUtilities.h" +#import "Firebase/InstanceID/NSError+FIRInstanceID.h" + +static NSString *const kDeviceAuthId = @"1234"; +static NSString *const kSecretToken = @"567890"; +static NSString *const kDigest = @"com.google.digest"; +static NSString *const kVersionInfo = @"1.0"; + +@interface FIRInstanceIDCheckinServiceTest : XCTestCase + +@property(nonatomic, readwrite, strong) FIRInstanceIDCheckinService *checkinService; + +@end + +@implementation FIRInstanceIDCheckinServiceTest + +- (void)setUp { + [super setUp]; + self.checkinService = [[FIRInstanceIDCheckinService alloc] init]; +} + +- (void)tearDown { + self.checkinService = nil; + [super tearDown]; +} + +- (void)testCheckinWithSuccessfulCompletion { + FIRInstanceIDCheckinPreferences *existingCheckin = [self stubCheckinCacheWithValidData]; + + [FIRInstanceIDCheckinService setCheckinTestBlock:[self successfulCheckinCompletionHandler]]; + + XCTestExpectation *checkinCompletionExpectation = + [self expectationWithDescription:@"Checkin Completion"]; + + [self.checkinService + checkinWithExistingCheckin:existingCheckin + completion:^(FIRInstanceIDCheckinPreferences *checkinPreferences, + NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(checkinPreferences.deviceID, kDeviceAuthId); + XCTAssertEqualObjects(checkinPreferences.versionInfo, kVersionInfo); + // For accuracy purposes it's better to compare seconds since the test + // should never run for more than 1 second. + NSInteger expectedTimestampInSeconds = + FIRInstanceIDCurrentTimestampInSeconds(); + NSInteger actualTimestampInSeconds = + checkinPreferences.lastCheckinTimestampMillis / 1000.0; + XCTAssertEqual(expectedTimestampInSeconds, actualTimestampInSeconds); + XCTAssertTrue([checkinPreferences hasValidCheckinInfo]); + [checkinCompletionExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testFailedCheckinService { + [FIRInstanceIDCheckinService setCheckinTestBlock:[self failCheckinCompletionHandler]]; + + XCTestExpectation *checkinCompletionExpectation = + [self expectationWithDescription:@"Checkin Completion"]; + + [self.checkinService + checkinWithExistingCheckin:nil + completion:^(FIRInstanceIDCheckinPreferences *preferences, NSError *error) { + XCTAssertNotNil(error); + XCTAssertNil(preferences.deviceID); + XCTAssertNil(preferences.secretToken); + XCTAssertFalse([preferences hasValidCheckinInfo]); + [checkinCompletionExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 + handler:^(NSError *error) { + if (error) { + XCTFail(@"Checkin Timeout Error: %@", error); + } + }]; +} + +- (void)testCheckinServiceAddsFirebaseUserAgentToHTTPHeader { + NSString *expectedFirebaseUserAgent = [FIRApp firebaseUserAgent]; + + FIRInstanceIDURLRequestTestBlock successHandler = [self successfulCheckinCompletionHandler]; + + [FIRInstanceIDCheckinService + setCheckinTestBlock:^(NSURLRequest *request, + FIRInstanceIDURLRequestTestResponseBlock response) { + NSString *requestFirebaseUserAgentValue = + request.allHTTPHeaderFields[kFIRInstanceIDFirebaseUserAgentKey]; + XCTAssertEqualObjects(requestFirebaseUserAgentValue, expectedFirebaseUserAgent); + successHandler(request, response); + }]; + + XCTestExpectation *checkinCompletionExpectation = + [self expectationWithDescription:@"Checkin Completion"]; + + [self.checkinService + checkinWithExistingCheckin:nil + completion:^(FIRInstanceIDCheckinPreferences *preferences, NSError *error) { + [checkinCompletionExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 + handler:^(NSError *error) { + if (error) { + XCTFail(@"Checkin Timeout Error: %@", error); + } + }]; +} + +- (void)testCheckinServiceFailsWithErrorAfterStopFetching { + [self.checkinService stopFetching]; + + XCTestExpectation *checkinCompletionExpectation = + [self expectationWithDescription:@"Checkin Completion"]; + + [self.checkinService + checkinWithExistingCheckin:nil + completion:^(FIRInstanceIDCheckinPreferences *preferences, NSError *error) { + [checkinCompletionExpectation fulfill]; + XCTAssertNil(preferences); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, kFIRInstanceIDErrorCodeRegistrarFailedToCheckIn); + }]; + + [self waitForExpectationsWithTimeout:5 + handler:^(NSError *error) { + if (error) { + XCTFail(@"Checkin Timeout Error: %@", error); + } + }]; +} + +#pragma mark - Stub + +- (FIRInstanceIDCheckinPreferences *)stubCheckinCacheWithValidData { + NSDictionary *gservicesData = @{ + @"FIRInstanceIDVersionInfo" : kVersionInfo, + @"FIRInstanceIDLastCheckinTimestampKey" : @(FIRInstanceIDCurrentTimestampInMilliseconds()) + }; + FIRInstanceIDCheckinPreferences *checkinPreferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceAuthId + secretToken:kSecretToken]; + [checkinPreferences updateWithCheckinPlistContents:gservicesData]; + return checkinPreferences; +} + +#pragma mark - Swizzle + +- (FIRInstanceIDURLRequestTestBlock)successfulCheckinCompletionHandler { + return ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock testResponse) { + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL + statusCode:200 + HTTPVersion:@"HTTP/1.1" + headerFields:nil]; + + NSMutableDictionary *dataResponse = [NSMutableDictionary dictionary]; + dataResponse[@"android_id"] = @([kDeviceAuthId longLongValue]); + dataResponse[@"security_token"] = @([kSecretToken longLongValue]); + dataResponse[@"time_msec"] = @(FIRInstanceIDCurrentTimestampInMilliseconds()); + dataResponse[@"version_info"] = kVersionInfo; + dataResponse[@"digest"] = kDigest; + NSData *data = [NSJSONSerialization dataWithJSONObject:dataResponse + options:NSJSONWritingPrettyPrinted + error:nil]; + testResponse(data, response, nil); + }; +} + +- (FIRInstanceIDURLRequestTestBlock)failCheckinCompletionHandler { + return ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock testResponse) { + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL + statusCode:200 + HTTPVersion:@"HTTP/1.1" + headerFields:nil]; + + NSError *error = + [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidRequest]; + + testResponse(nil, response, error); + }; +} + +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDCheckinStoreTest.m b/Example/InstanceID/Tests/FIRInstanceIDCheckinStoreTest.m new file mode 100644 index 00000000000..28a4dc9a291 --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDCheckinStoreTest.m @@ -0,0 +1,226 @@ +/* + * Copyright 2019 Google + * + * 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 + +#import "FIRInstanceIDFakeKeychain.h" +#import "Firebase/InstanceID/FIRInstanceIDAuthKeyChain.h" +#import "Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinService.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinStore.h" +#import "Firebase/InstanceID/FIRInstanceIDStore.h" +#import "Firebase/InstanceID/FIRInstanceIDUtilities.h" +#import "Firebase/InstanceID/FIRInstanceIDVersionUtilities.h" + +static const NSTimeInterval kExpectationTimeout = 12; + +@interface FIRInstanceIDCheckinStore () +- (NSString *)bundleIdentifierForKeychainAccount; +@end + +// Testing constants +static NSString *const kFakeCheckinPlistName = @"com.google.test.IIDStoreTestCheckin"; +static NSString *const kSubDirectoryName = @"FirebaseInstanceIDCheckinTest"; + +static NSString *const kAuthorizedEntity = @"test-audience"; +static NSString *const kAuthID = @"test-auth-id"; +static NSString *const kDigest = @"test-digest"; +static NSString *const kScope = @"test-scope"; +static NSString *const kSecret = @"test-secret"; +static NSString *const kToken = @"test-token"; + +static int64_t const kLastCheckinTimestamp = 123456; + +@interface FIRInstanceIDCheckinStoreTest : XCTestCase + +@end + +@implementation FIRInstanceIDCheckinStoreTest + +- (void)setUp { + [super setUp]; + [FIRInstanceIDStore createSubDirectory:kSubDirectoryName]; +} + +- (void)tearDown { + NSString *path = [self pathForCheckinPlist]; + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + NSError *error; + [[NSFileManager defaultManager] removeItemAtPath:path error:&error]; + } + [FIRInstanceIDStore removeSubDirectory:kSubDirectoryName error:nil]; + [super tearDown]; +} + +/** + * Keychain read failure should lead to checkin preferences with invalid credentials. + */ +- (void)testInvalidCheckinPreferencesOnKeychainFail { + XCTestExpectation *checkinInvalidExpectation = [self + expectationWithDescription:@"Checkin preference should be invalid after keychain failure"]; + FIRInstanceIDBackupExcludedPlist *checkinPlist = + [[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:kFakeCheckinPlistName + subDirectory:kSubDirectoryName]; + + FIRInstanceIDFakeKeychain *fakeKeychain = [[FIRInstanceIDFakeKeychain alloc] init]; + + FIRInstanceIDCheckinStore *checkinStore = + [[FIRInstanceIDCheckinStore alloc] initWithCheckinPlist:checkinPlist keychain:fakeKeychain]; + __block FIRInstanceIDCheckinPreferences *preferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kAuthID secretToken:kSecret]; + [preferences updateWithCheckinPlistContents:[[self class] newCheckinPlistPreferences]]; + [checkinStore saveCheckinPreferences:preferences + handler:^(NSError *error) { + XCTAssertNil(error); + fakeKeychain.cannotReadFromKeychain = YES; + preferences = [checkinStore cachedCheckinPreferences]; + + XCTAssertNil(preferences.deviceID); + XCTAssertNil(preferences.secretToken); + XCTAssertFalse([preferences hasValidCheckinInfo]); + + [checkinInvalidExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; +} + +/** + * CheckinStore should not be able to save the checkin preferences if the write to the + * Keychain fails. + */ +- (void)testCheckinSaveFailsOnKeychainWriteFailure { + XCTestExpectation *checkinSaveFailsExpectation = + [self expectationWithDescription:@"Checkin save should fail after keychain write failure"]; + FIRInstanceIDBackupExcludedPlist *checkinPlist = + [[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:kFakeCheckinPlistName + subDirectory:kSubDirectoryName]; + + FIRInstanceIDFakeKeychain *fakeKeychain = [[FIRInstanceIDFakeKeychain alloc] init]; + fakeKeychain.cannotWriteToKeychain = YES; + + FIRInstanceIDCheckinStore *checkinStore = + [[FIRInstanceIDCheckinStore alloc] initWithCheckinPlist:checkinPlist keychain:fakeKeychain]; + + __block FIRInstanceIDCheckinPreferences *preferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kAuthID secretToken:kSecret]; + [preferences updateWithCheckinPlistContents:[[self class] newCheckinPlistPreferences]]; + [checkinStore saveCheckinPreferences:preferences + handler:^(NSError *error) { + XCTAssertNotNil(error); + + preferences = [checkinStore cachedCheckinPreferences]; + XCTAssertNil(preferences.deviceID); + XCTAssertNil(preferences.secretToken); + XCTAssertFalse([preferences hasValidCheckinInfo]); + [checkinSaveFailsExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; +} + +// Write fake checkin data to legacy location, then test if migration worked. +- (void)testCheckinMigrationMovesToNewLocationInKeychain { + XCTestExpectation *checkinMigrationExpectation = + [self expectationWithDescription:@"checkin migration should move to the new location"]; + // Create checkin store class. + FIRInstanceIDBackupExcludedPlist *checkinPlist = + [[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:kFakeCheckinPlistName + subDirectory:kSubDirectoryName]; + + FIRInstanceIDFakeKeychain *fakeKeychain = [[FIRInstanceIDFakeKeychain alloc] init]; + FIRInstanceIDFakeKeychain *weakKeychain = fakeKeychain; + + // Create fake checkin preferences object. + FIRInstanceIDCheckinPreferences *preferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kAuthID secretToken:kSecret]; + [preferences updateWithCheckinPlistContents:[[self class] newCheckinPlistPreferences]]; + + // Write checkin into legacy location in Fake keychain. + NSString *checkinKeychainContent = [preferences checkinKeychainContent]; + NSData *data = [checkinKeychainContent dataUsingEncoding:NSUTF8StringEncoding]; + [fakeKeychain setData:data + forService:kFIRInstanceIDLegacyCheckinKeychainService + accessibility:nil + account:kFIRInstanceIDLegacyCheckinKeychainAccount + handler:^(NSError *error) { + XCTAssertNil(error); + // Check that we saved it correctly to the legacy location. + NSData *dataInLegacyLocation = + [weakKeychain dataForService:kFIRInstanceIDLegacyCheckinKeychainService + account:kFIRInstanceIDLegacyCheckinKeychainAccount]; + XCTAssertNotNil(dataInLegacyLocation); + + FIRInstanceIDCheckinStore *checkinStore = + [[FIRInstanceIDCheckinStore alloc] initWithCheckinPlist:checkinPlist + keychain:weakKeychain]; + // Perform migration. + [checkinStore migrateCheckinItemIfNeeded]; + + // Ensure the item is no longer in the old location. + dataInLegacyLocation = + [weakKeychain dataForService:kFIRInstanceIDLegacyCheckinKeychainService + account:kFIRInstanceIDLegacyCheckinKeychainAccount]; + XCTAssertNil(dataInLegacyLocation); + // Check that it exists in the new location. + NSData *dataInMigratedLocation = + [weakKeychain dataForService:kFIRInstanceIDCheckinKeychainService + account:checkinStore.bundleIdentifierForKeychainAccount]; + XCTAssertNotNil(dataInMigratedLocation); + // Ensure that the data is the same as what we originally saved. + XCTAssertEqualObjects(dataInMigratedLocation, data); + + [checkinMigrationExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil]; +} + +#pragma mark - Private Helpers + +- (BOOL)savePreferencesToPlist:(NSDictionary *)preferences { + NSString *path = [self pathForCheckinPlist]; + return [preferences writeToFile:path atomically:YES]; +} + +- (NSString *)pathForCheckinPlist { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *plistNameWithExtension = [NSString stringWithFormat:@"%@.plist", kFakeCheckinPlistName]; + return [paths[0] stringByAppendingPathComponent:plistNameWithExtension]; +} + ++ (NSDictionary *)checkinPreferences { + return @{ + kFIRInstanceIDDeviceAuthIdKey : kAuthID, + kFIRInstanceIDSecretTokenKey : kSecret, + kFIRInstanceIDDigestStringKey : kDigest, + kFIRInstanceIDGServicesDictionaryKey : @{}, + kFIRInstanceIDLastCheckinTimeKey : @(kLastCheckinTimestamp), + }; +} + ++ (NSDictionary *)newCheckinPlistPreferences { + NSMutableDictionary *oldPreferences = [[self checkinPreferences] mutableCopy]; + [oldPreferences removeObjectForKey:kFIRInstanceIDDeviceAuthIdKey]; + [oldPreferences removeObjectForKey:kFIRInstanceIDSecretTokenKey]; + oldPreferences[kFIRInstanceIDLastCheckinTimeKey] = + @(FIRInstanceIDCurrentTimestampInMilliseconds() - 1000); + return [oldPreferences copy]; +} + +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDFakeKeychain.h b/Example/InstanceID/Tests/FIRInstanceIDFakeKeychain.h new file mode 100644 index 00000000000..0f88b32d90f --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDFakeKeychain.h @@ -0,0 +1,28 @@ +/* + * Copyright 2019 Google + * + * 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 "Firebase/InstanceID/FIRInstanceIDAuthKeyChain.h" + +@interface FIRInstanceIDFakeKeychain : FIRInstanceIDAuthKeychain + +// Flags to simulate problems when reading from or writing to Keychain. +// By default you can always read/write to the Keychain. +@property(nonatomic, readwrite, assign) BOOL cannotReadFromKeychain; +@property(nonatomic, readwrite, assign) BOOL cannotWriteToKeychain; + +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDFakeKeychain.m b/Example/InstanceID/Tests/FIRInstanceIDFakeKeychain.m new file mode 100644 index 00000000000..939566b17c1 --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDFakeKeychain.m @@ -0,0 +1,112 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDFakeKeychain.h" + +static NSString *const kFakeKeychainErrorDomain = @"com.google.iid"; + +@interface FIRInstanceIDFakeKeychain () + +@property(nonatomic, readwrite, strong) NSMutableDictionary *data; + +@end + +@implementation FIRInstanceIDFakeKeychain + +- (instancetype)init { + self = [super init]; + if (self) { + _data = [NSMutableDictionary dictionary]; + } + return self; +} + +- (NSArray *)itemsMatchingService:(NSString *)service account:(NSString *)account { + if (self.cannotReadFromKeychain) { + return @[]; + } + NSMutableArray *results = [NSMutableArray array]; + BOOL accountIsWildcard = [account isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier]; + BOOL serviceIsWildcard = [service isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier]; + for (NSString *accountKey in [self.data allKeys]) { + if (!accountIsWildcard && ![accountKey isEqualToString:account]) { + continue; + } + NSDictionary *services = self.data[accountKey]; + for (NSString *serviceKey in [services allKeys]) { + if (!serviceIsWildcard && ![serviceKey isEqualToString:service]) { + continue; + } + NSData *item = self.data[accountKey][serviceKey]; + [results addObject:item]; + } + } + return results; +} + +- (NSData *)dataForService:(NSString *)service account:(NSString *)account { + if (self.cannotReadFromKeychain) { + return nil; + } + return self.data[account][service]; +} + +- (void)removeItemsMatchingService:(NSString *)service + account:(NSString *)account + handler:(void (^)(NSError *error))handler { + if (self.cannotWriteToKeychain) { + if (handler) { + handler([NSError errorWithDomain:kFakeKeychainErrorDomain code:1001 userInfo:nil]); + } + return; + } + if ([account isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier]) { + // Remove all account keys. + [self.data removeAllObjects]; + } else { + if ([service isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier]) { + // Remove all service keys for this account key. + [self.data[account] removeAllObjects]; + } else { + [self.data[account] removeObjectForKey:service]; + } + } + if (handler) { + handler(nil); + } +} + +- (void)setData:(NSData *)data + forService:(NSString *)service + accessibility:(nullable CFTypeRef)accessibility + account:(NSString *)account + handler:(void (^)(NSError *error))handler { + if (self.cannotWriteToKeychain) { + if (handler) { + handler([NSError errorWithDomain:kFakeKeychainErrorDomain code:1001 userInfo:nil]); + } + return; + } + if (!self.data[account]) { + self.data[account] = [NSMutableDictionary dictionary]; + } + self.data[account][service] = data; + if (handler) { + handler(nil); + } +} + +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDKeyPairMigrationTest.m b/Example/InstanceID/Tests/FIRInstanceIDKeyPairMigrationTest.m new file mode 100644 index 00000000000..14539877386 --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDKeyPairMigrationTest.m @@ -0,0 +1,137 @@ +/* + * Copyright 2019 Google + * + * 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 "Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.h" +#import "Firebase/InstanceID/FIRInstanceIDConstants.h" +#import "Firebase/InstanceID/FIRInstanceIDKeyPair.h" +#import "Firebase/InstanceID/FIRInstanceIDKeychain.h" + +#import +#import "Firebase/InstanceID/FIRInstanceIDKeyPair.h" +#import "Firebase/InstanceID/FIRInstanceIDKeyPairStore.h" +#import "Firebase/InstanceID/FIRInstanceIDKeyPairUtilities.h" +#import "Firebase/InstanceID/FIRInstanceIDUtilities.h" + +@interface FIRInstanceIDKeyPairStore (ExposedForTest) + +@property(nonatomic, readwrite, strong) FIRInstanceIDBackupExcludedPlist *plist; +@property(nonatomic, readwrite, strong) FIRInstanceIDKeyPair *keyPair; +BOOL FIRInstanceIDHasMigratedKeyPair(NSString *legacyPublicKeyTag, NSString *newPublicKeyTag); +NSString *FIRInstanceIDLegacyPublicTagWithSubtype(NSString *subtype); +NSString *FIRInstanceIDLegacyPrivateTagWithSubtype(NSString *subtype); +NSString *FIRInstanceIDPublicTagWithSubtype(NSString *subtype); +NSString *FIRInstanceIDPrivateTagWithSubtype(NSString *subtype); ++ (FIRInstanceIDKeyPair *)keyPairForPrivateKeyTag:(NSString *)privateKeyTag + publicKeyTag:(NSString *)publicKeyTag + error:(NSError *__autoreleasing *)error; ++ (void)deleteKeyPairWithPrivateTag:(NSString *)privateTag + publicTag:(NSString *)publicTag + handler:(void (^)(NSError *))handler; +- (void)migrateKeyPairCacheIfNeededWithHandler:(void (^)(NSError *error))handler; ++ (NSString *)keyStoreFileName; +@end + +// Need to separate the tests from FIRInstanceIDKeyPairStoreTest for separate keychain operations +@interface FIRInstanceIDKeyPairMigrationTest : XCTestCase + +@property(nonatomic, readwrite, strong) FIRInstanceIDKeyPairStore *keyPairStore; + +@end + +@implementation FIRInstanceIDKeyPairMigrationTest + +- (void)setUp { + [super setUp]; + id mockStoreClass = OCMClassMock([FIRInstanceIDKeyPairStore class]); + [[[mockStoreClass stub] andReturn:@"com.google.iid-keypairmanager-test"] keyStoreFileName]; + _keyPairStore = [[FIRInstanceIDKeyPairStore alloc] init]; +} + +- (void)tearDown { + [super tearDown]; + NSError *error = nil; + [self.keyPairStore removeKeyPairCreationTimePlistWithError:&error]; +} + +- (void)testMigrationDataIfLegtacyKeyPairsNotExist { + NSString *legacyPublicKeyTag = + FIRInstanceIDLegacyPublicTagWithSubtype(kFIRInstanceIDKeyPairSubType); + + NSString *publicKeyTag = FIRInstanceIDPublicTagWithSubtype(kFIRInstanceIDKeyPairSubType); + XCTAssertFalse(FIRInstanceIDHasMigratedKeyPair(legacyPublicKeyTag, publicKeyTag)); + + NSString *legacyPrivateKeyTag = + FIRInstanceIDLegacyPrivateTagWithSubtype(kFIRInstanceIDKeyPairSubType); + NSError *error; + FIRInstanceIDKeyPair *keyPair = + [FIRInstanceIDKeyPairStore keyPairForPrivateKeyTag:legacyPrivateKeyTag + publicKeyTag:legacyPublicKeyTag + error:&error]; + XCTAssertFalse([keyPair isValid]); +} + +- (void)testMigrationIfLegacyKeyPairsExist { + XCTestExpectation *migrationCompleteExpectation = + [self expectationWithDescription:@"migration should be done"]; + // create legacy key pairs + NSString *legacyPublicKeyTag = + FIRInstanceIDLegacyPublicTagWithSubtype(kFIRInstanceIDKeyPairSubType); + NSString *legacyPrivateKeyTag = + FIRInstanceIDLegacyPrivateTagWithSubtype(kFIRInstanceIDKeyPairSubType); + FIRInstanceIDKeyPair *keyPair = + [[FIRInstanceIDKeychain sharedInstance] generateKeyPairWithPrivateTag:legacyPrivateKeyTag + publicTag:legacyPublicKeyTag]; + XCTAssertTrue([keyPair isValid]); + + NSError *error; + NSString *publicKeyTag = FIRInstanceIDPublicTagWithSubtype(kFIRInstanceIDKeyPairSubType); + NSString *privateKeyTag = FIRInstanceIDPrivateTagWithSubtype(kFIRInstanceIDKeyPairSubType); + + XCTAssertFalse(FIRInstanceIDHasMigratedKeyPair(legacyPublicKeyTag, publicKeyTag)); + + FIRInstanceIDKeyPair *keyPair1 = + [FIRInstanceIDKeyPairStore keyPairForPrivateKeyTag:legacyPrivateKeyTag + publicKeyTag:legacyPublicKeyTag + error:&error]; + XCTAssertTrue([keyPair1 isValid]); + + [self.keyPairStore migrateKeyPairCacheIfNeededWithHandler:^(NSError *error) { + XCTAssertNil(error); + XCTAssertTrue(FIRInstanceIDHasMigratedKeyPair(legacyPublicKeyTag, publicKeyTag)); + + FIRInstanceIDKeyPair *keyPair2 = + [FIRInstanceIDKeyPairStore keyPairForPrivateKeyTag:privateKeyTag + publicKeyTag:publicKeyTag + error:&error]; + + XCTAssertTrue([keyPair2 isValid]); + XCTAssertEqualObjects(keyPair.publicKeyData, keyPair2.publicKeyData); + XCTAssertEqualObjects(keyPair.privateKeyData, keyPair2.privateKeyData); + + // Clear the legacy data after tests + [FIRInstanceIDKeyPairStore deleteKeyPairWithPrivateTag:legacyPrivateKeyTag + publicTag:legacyPublicKeyTag + handler:^(NSError *error) { + XCTAssertNil(error); + [migrationCompleteExpectation fulfill]; + }]; + }]; + + [self waitForExpectationsWithTimeout:1000 handler:nil]; +} +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDKeyPairStoreTest.m b/Example/InstanceID/Tests/FIRInstanceIDKeyPairStoreTest.m new file mode 100644 index 00000000000..ad5f6913875 --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDKeyPairStoreTest.m @@ -0,0 +1,239 @@ +/* + * Copyright 2019 Google + * + * 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 "Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.h" +#import "Firebase/InstanceID/FIRInstanceIDConstants.h" +#import "Firebase/InstanceID/FIRInstanceIDKeyPair.h" +#import "Firebase/InstanceID/FIRInstanceIDKeychain.h" +#import "Firebase/InstanceID/FIRInstanceIDStore.h" + +#import +#import "Firebase/InstanceID/FIRInstanceIDKeyPair.h" +#import "Firebase/InstanceID/FIRInstanceIDKeyPairStore.h" +#import "Firebase/InstanceID/FIRInstanceIDKeyPairUtilities.h" +#import "Firebase/InstanceID/FIRInstanceIDUtilities.h" + +@interface FIRInstanceIDKeyPairStore (ExposedForTest) + +@property(nonatomic, readwrite, strong) FIRInstanceIDBackupExcludedPlist *plist; +@property(nonatomic, readwrite, strong) FIRInstanceIDKeyPair *keyPair; ++ (NSString *)appIDKeyWithSubtype:(NSString *)subtype; ++ (NSString *)creationTimeKeyWithSubtype:(NSString *)subtype; +- (FIRInstanceIDKeyPair *)generateAndSaveKeyWithSubtype:(NSString *)subtype + creationTime:(int64_t)creationTime + error:(NSError **)error; +- (FIRInstanceIDKeyPair *)validCachedKeyPairWithSubtype:(NSString *)subtype error:(NSError **)error; ++ (NSString *)keyStoreFileName; +- (void)migrateKeyPairCacheIfNeededWithHandler:(void (^)(NSError *error))handler; +@end + +@interface FIRInstanceIDKeyPairStoreTest : XCTestCase + +@property(nonatomic, readwrite, strong) FIRInstanceIDKeyPairStore *keyPairStore; + +@end + +@implementation FIRInstanceIDKeyPairStoreTest + +- (void)setUp { + [super setUp]; + id mockStoreClass = OCMClassMock([FIRInstanceIDKeyPairStore class]); + [[[mockStoreClass stub] andReturn:@"com.google.iid-keypairmanager-test"] keyStoreFileName]; + // Should make sure the standard directory is created. + if (![FIRInstanceIDStore hasSubDirectory:kFIRInstanceIDSubDirectoryName]) { + [FIRInstanceIDStore createSubDirectory:kFIRInstanceIDSubDirectoryName]; + } + _keyPairStore = [[FIRInstanceIDKeyPairStore alloc] init]; +} + +- (void)tearDown { + [super tearDown]; + NSError *error = nil; + [self.keyPairStore removeKeyPairCreationTimePlistWithError:&error]; + [self.keyPairStore deleteSavedKeyPairWithSubtype:kFIRInstanceIDKeyPairSubType handler:nil]; +} + +/** + * The app identity generated should be 11 chars and start with k, l, m, n. It should + * not have "=" as suffix since we do not allow wrapping. + */ +- (void)testIdentity { + NSError *error; + FIRInstanceIDKeyPair *keyPair = [self.keyPairStore loadKeyPairWithError:&error]; + NSString *iid = FIRInstanceIDAppIdentity(keyPair); + XCTAssertEqual(11, iid.length); + XCTAssertFalse([iid hasSuffix:@"="]); +} + +/** + * All identities should be cleared if the associated keypair plist file is missing. + * This indicates that the app is either a fresh install, or was removed and reinstalled. + * + * If/when iOS changes the behavior of the Keychain to also invalidate items when an app is + * installed, this check will no longer be required (both the plist file and the keychain items + * would be missing). + */ +- (void)testIdentityIsInvalidatedWithMissingPlist { + // Mock that the plist doesn't exist, and call the invalidation check. It should + // trigger the identities to be deleted. + id plistMock = OCMPartialMock(self.keyPairStore.plist); + [[[plistMock stub] andReturnValue:OCMOCK_VALUE(NO)] doesFileExist]; + // Mock the keypair store, to check if key pair deletes are requested + id storeMock = OCMPartialMock(self.keyPairStore); + // Now trigger a possible invalidation. + [self.keyPairStore invalidateKeyPairsIfNeeded]; + // Verify that delete was called + OCMVerify([storeMock deleteSavedKeyPairWithSubtype:[OCMArg any] handler:[OCMArg any]]); +} + +- (void)testMigrationWhenPlistExist { + // Mock that the plist doesn't exist, and call the invalidation check. It should + // trigger the identities to be deleted. + id plistMock = OCMPartialMock(self.keyPairStore.plist); + [[[plistMock stub] andReturnValue:OCMOCK_VALUE(YES)] doesFileExist]; + // Mock the keypair store, to check if key pair deletes are requested + id storeMock = OCMPartialMock(self.keyPairStore); + // Now trigger a possible invalidation. + [self.keyPairStore invalidateKeyPairsIfNeeded]; + // Verify that delete was called + OCMVerify([storeMock migrateKeyPairCacheIfNeededWithHandler:nil]); +} + +/** + * The app identity should change when deleted and regenerated. + */ +- (void)testResetIdentity { + XCTestExpectation *identityResetExpectation = + [self expectationWithDescription:@"Identity should be reset"]; + NSError *error; + FIRInstanceIDKeyPair *keyPair = [self.keyPairStore loadKeyPairWithError:&error]; + XCTAssertNil(error); + NSString *iid1 = FIRInstanceIDAppIdentity(keyPair); + + [self.keyPairStore + deleteSavedKeyPairWithSubtype:kFIRInstanceIDKeyPairSubType + handler:^(NSError *error) { + XCTAssertNil(error); + [self.keyPairStore removeKeyPairCreationTimePlistWithError:&error]; + XCTAssertNil(error); + + // regenerate instance-id + FIRInstanceIDKeyPair *keyPair = + [self.keyPairStore loadKeyPairWithError:&error]; + XCTAssertNil(error); + NSString *iid2 = FIRInstanceIDAppIdentity(keyPair); + + XCTAssertNotEqualObjects(iid1, iid2); + [identityResetExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +/** + * We should always cache a valid keypair. + */ +- (void)testCachedKeyPair { + NSError *error; + FIRInstanceIDKeyPair *keyPair = [self.keyPairStore loadKeyPairWithError:&error]; + XCTAssertNil(error); + NSString *iid1 = FIRInstanceIDAppIdentity(keyPair); + + // sleep for some time + [NSThread sleepForTimeInterval:2.0]; + + keyPair = [self.keyPairStore loadKeyPairWithError:&error]; + XCTAssertNil(error); + NSString *iid2 = FIRInstanceIDAppIdentity(keyPair); + + XCTAssertTrue([self.keyPairStore hasCachedKeyPairs]); + XCTAssertEqualObjects(iid1, iid2); +} + +- (void)testAppIdentity { + NSError *error; + NSString *iid1 = [self.keyPairStore appIdentityWithError:&error]; + // sleep for some time + [NSThread sleepForTimeInterval:2.0]; + + NSString *iid2 = [self.keyPairStore appIdentityWithError:&error]; + + XCTAssertEqualObjects(iid1, iid2); +} + +/** + * Test KeyPair cache. After generating a new keyPair requesting it from the cache + * should be successfull and return the same keyPair. + */ +- (void)testKeyPairCache { + NSError *error; + + FIRInstanceIDKeyPair *keyPair1 = + [self.keyPairStore generateAndSaveKeyWithSubtype:kFIRInstanceIDKeyPairSubType + creationTime:FIRInstanceIDCurrentTimestampInSeconds() + error:&error]; + XCTAssertNotNil(keyPair1); + + NSString *iid1 = FIRInstanceIDAppIdentity(keyPair1); + + [NSThread sleepForTimeInterval:2.0]; + + FIRInstanceIDKeyPair *keyPair2 = + [self.keyPairStore validCachedKeyPairWithSubtype:kFIRInstanceIDKeyPairSubType error:&error]; + XCTAssertNil(error); + NSString *iid2 = FIRInstanceIDAppIdentity(keyPair2); + + XCTAssertEqualObjects(iid1, iid2); +} +/** + * Test that if the Keychain preferences does not store any KeyPair, trying to + * load one from the cache should return nil. + */ +- (void)testInvalidKeyPair { + NSError *error; + FIRInstanceIDKeyPair *keyPair = + [self.keyPairStore validCachedKeyPairWithSubtype:kFIRInstanceIDKeyPairSubType error:&error]; + XCTAssertFalse([keyPair isValid]); +} + +/** + * Test deleting the keyPair from Keychain preferences. + */ +- (void)testDeleteKeyPair { + XCTestExpectation *deleteKeyPairExpectation = + [self expectationWithDescription:@"Keypair should be deleted"]; + NSError *error; + [self.keyPairStore generateAndSaveKeyWithSubtype:kFIRInstanceIDKeyPairSubType + creationTime:FIRInstanceIDCurrentTimestampInSeconds() + error:&error]; + + XCTAssertNil(error); + + [self.keyPairStore + deleteSavedKeyPairWithSubtype:kFIRInstanceIDKeyPairSubType + handler:^(NSError *error) { + XCTAssertNil(error); + FIRInstanceIDKeyPair *keyPair2 = [self.keyPairStore + validCachedKeyPairWithSubtype:kFIRInstanceIDKeyPairSubType + error:&error]; + XCTAssertNotNil(error); + XCTAssertNil(keyPair2); + [deleteKeyPairExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDKeyPairTest.m b/Example/InstanceID/Tests/FIRInstanceIDKeyPairTest.m new file mode 100644 index 00000000000..fb5415a2def --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDKeyPairTest.m @@ -0,0 +1,72 @@ +/* + * Copyright 2019 Google + * + * 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 "Firebase/InstanceID/FIRInstanceIDKeyPair.h" +#import "Firebase/InstanceID/FIRInstanceIDKeyPairStore.h" +#import "Firebase/InstanceID/FIRInstanceIDKeyPairUtilities.h" +#import "Firebase/InstanceID/FIRInstanceIDKeychain.h" + +static NSString *kKeyPairPrivateTag = @"iid-keypair-test-priv"; +static NSString *kKeyPairPublicTag = @"iid-keypair-test-public"; + +@interface FIRInstanceIDKeyPairStore (ExposedForTest) ++ (void)deleteKeyPairWithPrivateTag:(NSString *)privateTag + publicTag:(NSString *)publicTag + handler:(void (^)(NSError *))handler; +@end + +@interface FIRInstanceIDKeyPairTest : XCTestCase +@end + +@implementation FIRInstanceIDKeyPairTest + +- (void)testInvalidKeychain { + FIRInstanceIDKeyPair *keypair = [[FIRInstanceIDKeyPair alloc] initWithPrivateKey:nil + publicKey:nil + publicKeyData:nil + privateKeyData:nil]; + XCTAssertNotNil(keypair); + XCTAssertFalse([keypair isValid]); + SecKeyRef publicKeyRef = [keypair publicKey]; + XCTAssertTrue(publicKeyRef == NULL); + SecKeyRef privateKeyRef = [keypair privateKey]; + XCTAssertTrue(privateKeyRef == NULL); + XCTAssertNil(keypair.publicKeyData); + XCTAssertNil(keypair.privateKeyData); + XCTAssertNil(FIRInstanceIDAppIdentity(keypair)); +} + +- (void)testValidKeychain { + FIRInstanceIDKeyPair *keypair = + [[FIRInstanceIDKeychain sharedInstance] generateKeyPairWithPrivateTag:kKeyPairPrivateTag + publicTag:kKeyPairPublicTag]; + XCTAssertNotNil(keypair); + XCTAssertTrue([keypair isValid]); + SecKeyRef publicKeyRef = [keypair publicKey]; + XCTAssertFalse(publicKeyRef == NULL); + SecKeyRef privateKeyRef = [keypair privateKey]; + XCTAssertFalse(privateKeyRef == NULL); + XCTAssertNotNil(keypair.publicKeyData); + XCTAssertNotNil(keypair.privateKeyData); + XCTAssertNotNil(FIRInstanceIDAppIdentity(keypair)); + + [FIRInstanceIDKeyPairStore deleteKeyPairWithPrivateTag:kKeyPairPrivateTag + publicTag:kKeyPairPublicTag + handler:nil]; +} +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDResultTest.m b/Example/InstanceID/Tests/FIRInstanceIDResultTest.m new file mode 100644 index 00000000000..64e7ff84e07 --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDResultTest.m @@ -0,0 +1,132 @@ +/* + * Copyright 2019 Google + * + * 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 +#import "Firebase/InstanceID/FIRInstanceID+Testing.h" +#import "Firebase/InstanceID/NSError+FIRInstanceID.h" + +static NSString *const kFakeIID = @"fE1e1PZJFSQ"; +static NSString *const kFakeToken = + @"fE1e1PZJFSQ:APA91bFAOjp1ahBWn9rTlbjArwBEm_" + @"yUTTzK6dhIvLqzqqCSabaa4TQVM0pGTmF6r7tmMHPe6VYiGMHuCwJFgj5v97xl78sUNMLwuPPhoci8z_" + @"QGlCrTbxCFGzEUfvA3fGpGgIVQU2W6"; + +@interface FIRInstanceID (ExposedForTest) +- (NSString *)cachedTokenIfAvailable; +- (void)defaultTokenWithHandler:(FIRInstanceIDTokenHandler)handler; +@end + +@interface FIRInstanceIDResultTest : XCTestCase { + FIRInstanceID *_instanceID; + id _mockInstanceID; +} + +@end + +@implementation FIRInstanceIDResultTest + +- (void)setUp { + [super setUp]; + _instanceID = [[FIRInstanceID alloc] initPrivately]; + [_instanceID start]; + _mockInstanceID = OCMPartialMock(_instanceID); +} + +- (void)tearDown { + [_mockInstanceID stopMocking]; + [super tearDown]; +} + +- (void)testResultWithFailedIID { + // mocking getting iid failed with error. + OCMStub([_mockInstanceID + getIDWithHandler:([OCMArg invokeBlockWithArgs:[NSNull null], + [NSError errorWithFIRInstanceIDErrorCode:100], + nil])]); + + [_instanceID + instanceIDWithHandler:^(FIRInstanceIDResult *_Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, 100); + }]; +} + +- (void)testResultWithCacheToken { + // mocking getting iid succeed and a cache token exists. + OCMStub([_mockInstanceID + getIDWithHandler:([OCMArg invokeBlockWithArgs:kFakeIID, [NSNull null], nil])]); + OCMStub([_mockInstanceID cachedTokenIfAvailable]).andReturn(kFakeToken); + [_instanceID + instanceIDWithHandler:^(FIRInstanceIDResult *_Nullable result, NSError *_Nullable error) { + XCTAssertNotNil(result); + XCTAssertNil(error); + XCTAssertEqualObjects(result.instanceID, kFakeIID); + XCTAssertEqualObjects(result.token, kFakeToken); + }]; +} + +- (void)testResultWithNewToken { + // mocking getting iid succeed and a new token is generated. + OCMStub([_mockInstanceID + getIDWithHandler:([OCMArg invokeBlockWithArgs:kFakeIID, [NSNull null], nil])]); + OCMStub([_mockInstanceID cachedTokenIfAvailable]).andReturn(nil); + OCMStub([_mockInstanceID + defaultTokenWithHandler:([OCMArg invokeBlockWithArgs:kFakeToken, [NSNull null], nil])]); + [_instanceID + instanceIDWithHandler:^(FIRInstanceIDResult *_Nullable result, NSError *_Nullable error) { + XCTAssertNotNil(result); + XCTAssertNil(error); + XCTAssertEqualObjects(result.instanceID, kFakeIID); + XCTAssertEqualObjects(result.token, kFakeToken); + }]; +} + +- (void)testResultWithFailedFetchingToken { + // mock getting iid succeed and token fails + OCMStub([_mockInstanceID + getIDWithHandler:([OCMArg invokeBlockWithArgs:kFakeIID, [NSNull null], nil])]); + OCMStub([_mockInstanceID cachedTokenIfAvailable]).andReturn(nil); + OCMStub([_mockInstanceID + defaultTokenWithHandler:([OCMArg + invokeBlockWithArgs:[NSNull null], + [NSError errorWithFIRInstanceIDErrorCode:200], + nil])]); + + [_instanceID + instanceIDWithHandler:^(FIRInstanceIDResult *_Nullable result, NSError *_Nullable error) { + XCTAssertNil(result); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, 200); + }]; +} + +- (void)testResultCanBeCoplied { + // mocking getting iid succeed and a cache token exists. + OCMStub([_mockInstanceID + getIDWithHandler:([OCMArg invokeBlockWithArgs:kFakeIID, [NSNull null], nil])]); + OCMStub([_mockInstanceID cachedTokenIfAvailable]).andReturn(kFakeToken); + [_instanceID + instanceIDWithHandler:^(FIRInstanceIDResult *_Nullable result, NSError *_Nullable error) { + FIRInstanceIDResult *resultCopy = [result copy]; + XCTAssertEqualObjects(resultCopy.instanceID, kFakeIID); + XCTAssertEqualObjects(resultCopy.token, kFakeToken); + }]; +} + +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDStoreTest.m b/Example/InstanceID/Tests/FIRInstanceIDStoreTest.m new file mode 100644 index 00000000000..efa5edc2f39 --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDStoreTest.m @@ -0,0 +1,263 @@ +/* + * Copyright 2019 Google + * + * 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 +#import "FIRInstanceIDFakeKeychain.h" +#import "Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinService.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinStore.h" +#import "Firebase/InstanceID/FIRInstanceIDStore.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenInfo.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenStore.h" +#import "Firebase/InstanceID/FIRInstanceIDUtilities.h" + +static NSString *const kSubDirectoryName = @"FirebaseInstanceIDStoreTest"; + +static NSString *const kAuthorizedEntity = @"test-audience"; +static NSString *const kScope = @"test-scope"; +static NSString *const kToken = @"test-token"; +static NSString *const kKey = @"test-key"; +static NSString *const kTimeSuffix = @"-time"; +static NSString *const kAuthID = @"test-auth-id"; +static NSString *const kSecret = @"test-secret"; + +// This should stay in sync with the same constant name in FIRInstanceIDStore. +// We don't want to make a new method in FIRInstanceIDStore to avoid adding +// binary bloat. +static NSString *const kFIRInstanceIDAPNSTokenKey = @"APNSTuple"; + +@interface FIRInstanceIDStore () + +- (NSString *)tokenWithKey:(NSString *)key; +- (void)cacheToken:(NSString *)token withKey:(NSString *)key; + +// APNS ++ (NSString *)legacyAPNSTokenCacheKeyForServerType:(BOOL)isSandbox; ++ (NSData *)dataWithHexString:(NSString *)hex; + +- (void)resetCredentialsIfNeeded; +- (BOOL)hasSavedLibraryVersion; +- (BOOL)hasCheckinPlist; + +@end + +@interface FIRInstanceIDStoreTest : XCTestCase + +@property(nonatomic) FIRInstanceIDStore *instanceIDStore; +@property(nonatomic) FIRInstanceIDBackupExcludedPlist *checkinPlist; +@property(nonatomic) FIRInstanceIDCheckinStore *checkinStore; +@property(nonatomic) FIRInstanceIDTokenStore *tokenStore; +@property(nonatomic) id mockCheckinStore; +@property(nonatomic) id mockTokenStore; +@property(nonatomic) id mockInstanceIDStore; + +@end + +@implementation FIRInstanceIDStoreTest + +- (void)setUp { + [super setUp]; + [FIRInstanceIDStore createSubDirectory:kSubDirectoryName]; + + NSString *checkinPlistName = @"com.google.test.IIDStoreTestCheckin"; + self.checkinPlist = [[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:checkinPlistName + subDirectory:kSubDirectoryName]; + + // checkin store + FIRInstanceIDFakeKeychain *fakeKeychain = [[FIRInstanceIDFakeKeychain alloc] init]; + _checkinStore = [[FIRInstanceIDCheckinStore alloc] initWithCheckinPlist:self.checkinPlist + keychain:fakeKeychain]; + _mockCheckinStore = OCMPartialMock(_checkinStore); + // token store + FIRInstanceIDFakeKeychain *fakeTokenKeychain = [[FIRInstanceIDFakeKeychain alloc] init]; + _tokenStore = [[FIRInstanceIDTokenStore alloc] initWithKeychain:fakeTokenKeychain]; + _mockTokenStore = OCMPartialMock(_tokenStore); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + _instanceIDStore = [[FIRInstanceIDStore alloc] initWithCheckinStore:_mockCheckinStore + tokenStore:_mockTokenStore + delegate:nil]; +#pragma clang diagnostic pop + _mockInstanceIDStore = OCMPartialMock(_instanceIDStore); +} + +- (void)tearDown { + [self.instanceIDStore removeAllCachedTokensWithHandler:nil]; + [self.instanceIDStore removeCheckinPreferencesWithHandler:nil]; + [FIRInstanceIDStore removeSubDirectory:kSubDirectoryName error:nil]; + [_mockCheckinStore stopMocking]; + [_mockTokenStore stopMocking]; + [_mockInstanceIDStore stopMocking]; + [super tearDown]; +} + +/** + * Tests that an InstanceID token can be stored in the FIRInstanceIDStore for + * an authorizedEntity and scope. + */ +- (void)testSaveToken { + XCTestExpectation *tokenExpectation = [self expectationWithDescription:@"token is saved"]; + FIRInstanceIDTokenInfo *tokenInfo = + [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + token:kToken + appVersion:@"1.0" + firebaseAppID:@"firebaseAppID"]; + [self.instanceIDStore saveTokenInfo:tokenInfo + handler:^(NSError *error) { + XCTAssertNil(error); + FIRInstanceIDTokenInfo *retrievedTokenInfo = [self.instanceIDStore + tokenInfoWithAuthorizedEntity:kAuthorizedEntity + scope:kScope]; + XCTAssertEqualObjects(retrievedTokenInfo.token, kToken); + [tokenExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +/** + * Tests that a token can be removed from from FIRInstanceIDStore's cache when specifying + * its authorizedEntity and scope. + */ +- (void)testRemoveCachedToken { + XCTestExpectation *tokenExpectation = [self expectationWithDescription:@"token is removed"]; + FIRInstanceIDTokenInfo *tokenInfo = + [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + token:kToken + appVersion:@"1.0" + firebaseAppID:@"firebaseAppID"]; + [self.instanceIDStore + saveTokenInfo:tokenInfo + handler:^(NSError *error) { + XCTAssertNotNil([self.instanceIDStore tokenInfoWithAuthorizedEntity:kAuthorizedEntity + scope:kScope]); + + [self.instanceIDStore removeCachedTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope]; + XCTAssertNil([self.instanceIDStore tokenInfoWithAuthorizedEntity:kAuthorizedEntity + scope:kScope]); + [tokenExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +/** + * Tests that a checkin authentication ID can be stored in the FIRInstanceIDStore. + */ +- (void)testSaveCheckinAuthID { + XCTestExpectation *checkinExpectation = [self expectationWithDescription:@"checkin is saved"]; + NSDictionary *plistContent = @{ + kFIRInstanceIDDigestStringKey : @"digest-xyz", + kFIRInstanceIDLastCheckinTimeKey : @(FIRInstanceIDCurrentTimestampInMilliseconds()) + }; + FIRInstanceIDCheckinPreferences *preferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kAuthID secretToken:kSecret]; + [preferences updateWithCheckinPlistContents:plistContent]; + [self.instanceIDStore + saveCheckinPreferences:preferences + handler:^(NSError *_Nonnull error) { + XCTAssertNil(error); + FIRInstanceIDCheckinPreferences *cachedPreferences = + [self.instanceIDStore cachedCheckinPreferences]; + + XCTAssertEqualObjects(cachedPreferences.deviceID, kAuthID); + XCTAssertEqualObjects(cachedPreferences.secretToken, kSecret); + [checkinExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +/** + * Tests that a checkin authentication ID can be removed from FIRInstanceIDStore's cache. + */ +- (void)testRemoveCheckinPreferences { + XCTestExpectation *checkinExpectation = [self expectationWithDescription:@"checkin is removed"]; + NSDictionary *plistContent = @{ + kFIRInstanceIDDigestStringKey : @"digest-xyz", + kFIRInstanceIDLastCheckinTimeKey : @(FIRInstanceIDCurrentTimestampInMilliseconds()) + }; + FIRInstanceIDCheckinPreferences *preferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kAuthID secretToken:kSecret]; + [preferences updateWithCheckinPlistContents:plistContent]; + + [self.instanceIDStore + saveCheckinPreferences:preferences + handler:^(NSError *error) { + XCTAssertNil(error); + + [self.instanceIDStore + removeCheckinPreferencesWithHandler:^(NSError *_Nullable error) { + XCTAssertNil(error); + + FIRInstanceIDCheckinPreferences *cachedPreferences = + [self.instanceIDStore cachedCheckinPreferences]; + XCTAssertNil(cachedPreferences.deviceID); + XCTAssertNil(cachedPreferences.secretToken); + [checkinExpectation fulfill]; + }]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testResetCredentialsWithFreshInstall { + FIRInstanceIDCheckinPreferences *checkinPreferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kAuthID secretToken:kSecret]; + // Expect checkin is removed if it's a fresh install. + [[_mockCheckinStore expect] + removeCheckinPreferencesWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]]; + // Always setting up stub after expect. + OCMStub([_mockCheckinStore cachedCheckinPreferences]).andReturn(checkinPreferences); + // Plist file doesn't exist, meaning this is a fresh install. + OCMStub([_mockCheckinStore hasCheckinPlist]).andReturn(NO); + + [_mockInstanceIDStore resetCredentialsIfNeeded]; + OCMVerifyAll(_mockCheckinStore); +} + +- (void)testResetCredentialsWithoutFreshInstall { + FIRInstanceIDCheckinPreferences *checkinPreferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kAuthID secretToken:kSecret]; + // Expect migration happens if it's not a fresh install. + [[_mockCheckinStore expect] migrateCheckinItemIfNeeded]; + // Always setting up stub after expect. + OCMStub([_mockCheckinStore cachedCheckinPreferences]).andReturn(checkinPreferences); + // Mock plist exists, meaning this is not a fresh install. + OCMStub([_mockCheckinStore hasCheckinPlist]).andReturn(YES); + + [_mockInstanceIDStore resetCredentialsIfNeeded]; + OCMVerifyAll(_mockCheckinStore); +} + +- (void)testResetCredentialsWithNoCachedCheckin { + _mockCheckinStore = [OCMockObject niceMockForClass:[FIRInstanceIDCheckinStore class]]; + [[_mockCheckinStore reject] + removeCheckinPreferencesWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]]; + // Always setting up stub after expect. + OCMStub([_checkinStore cachedCheckinPreferences]).andReturn(nil); + + [_instanceIDStore resetCredentialsIfNeeded]; + OCMVerifyAll(_mockCheckinStore); +} +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDTest.m b/Example/InstanceID/Tests/FIRInstanceIDTest.m new file mode 100644 index 00000000000..2bc67cf659d --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDTest.m @@ -0,0 +1,1205 @@ +/* + * Copyright 2019 Google + * + * 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 +#import +#import + +#import "Firebase/InstanceID/FIRInstanceID+Testing.h" +#import "Firebase/InstanceID/FIRInstanceIDAuthService.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h" +#import "Firebase/InstanceID/FIRInstanceIDConstants.h" +#import "Firebase/InstanceID/FIRInstanceIDKeyPair.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenInfo.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenManager.h" +#import "Firebase/InstanceID/FIRInstanceIDUtilities.h" +#import "Firebase/InstanceID/NSError+FIRInstanceID.h" + +static NSString *const kFakeIID = @"12345678"; +static NSString *const kFakeAPNSToken = @"this is a fake apns token"; +static NSString *const kAuthorizedEntity = @"test-audience"; +static NSString *const kScope = @"test-scope"; +static NSString *const kToken = @"test-token"; +static FIRInstanceIDTokenInfo *sTokenInfo; +// Faking checkin calls +static NSString *const kDeviceAuthId = @"device-id"; +static NSString *const kSecretToken = @"secret-token"; +static NSString *const kDigest = @"com.google.digest"; +static NSString *const kVersionInfo = @"1.0"; +// FIRApp configuration. +static NSString *const kGCMSenderID = @"correct_gcm_sender_id"; +static NSString *const kGoogleAppID = @"1:123:ios:123abc"; + +@interface FIRInstanceID (ExposedForTest) +- (NSInteger)retryIntervalToFetchDefaultToken; +- (BOOL)isFCMAutoInitEnabled; +- (void)didCompleteConfigure; +- (NSString *)cachedTokenIfAvailable; +- (void)deleteIdentityWithHandler:(FIRInstanceIDDeleteHandler)handler; ++ (FIRInstanceID *)instanceIDForTests; +- (void)defaultTokenWithHandler:(FIRInstanceIDTokenHandler)handler; +@end + +@interface FIRInstanceIDTest : XCTestCase + +@property(nonatomic, readwrite, assign) BOOL hasCheckinInfo; +@property(nonatomic, readwrite, strong) FIRInstanceID *instanceID; +@property(nonatomic, readwrite, strong) id mockInstanceID; +@property(nonatomic, readwrite, strong) id mockTokenManager; +@property(nonatomic, readwrite, strong) id mockKeyPairStore; +@property(nonatomic, readwrite, strong) id mockAuthService; +@property(nonatomic, readwrite, strong) id tokenRefreshNotificationObserver; + +@property(nonatomic, readwrite, copy) FIRInstanceIDTokenHandler newTokenCompletion; +@property(nonatomic, readwrite, copy) FIRInstanceIDDeleteTokenHandler deleteTokenCompletion; + +@end + +@implementation FIRInstanceIDTest + +- (void)setUp { + [super setUp]; + _instanceID = [[FIRInstanceID alloc] initPrivately]; + [_instanceID start]; + if (!sTokenInfo) { + sTokenInfo = [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + token:kToken + appVersion:nil + firebaseAppID:nil]; + sTokenInfo.cacheTime = [NSDate date]; + } + [self mockInstanceIDObjects]; +} + +- (void)tearDown { + [[NSNotificationCenter defaultCenter] removeObserver:self.tokenRefreshNotificationObserver]; + self.instanceID = nil; + self.mockTokenManager = nil; + self.mockInstanceID = nil; + [super tearDown]; +} + +- (void)mockInstanceIDObjects { + // Mock that we have valid checkin info. Individual tests can override this. + self.hasCheckinInfo = YES; + self.mockAuthService = OCMClassMock([FIRInstanceIDAuthService class]); + + [[[self.mockAuthService stub] andDo:^(NSInvocation *invocation) { + [invocation setReturnValue:&self->_hasCheckinInfo]; + }] hasValidCheckinInfo]; + + self.mockTokenManager = OCMClassMock([FIRInstanceIDTokenManager class]); + [[[self.mockTokenManager stub] andReturn:self.mockAuthService] authService]; + + self.mockKeyPairStore = OCMClassMock([FIRInstanceIDKeyPairStore class]); + _instanceID.fcmSenderID = kAuthorizedEntity; + self.mockInstanceID = OCMPartialMock(_instanceID); + [self.mockInstanceID setTokenManager:self.mockTokenManager]; + [self.mockInstanceID setKeyPairStore:self.mockKeyPairStore]; + + id instanceIDClassMock = OCMClassMock([FIRInstanceID class]); + OCMStub(ClassMethod([instanceIDClassMock minIntervalForDefaultTokenRetry])).andReturn(2); + OCMStub(ClassMethod([instanceIDClassMock maxRetryIntervalForDefaultTokenInSeconds])) + .andReturn(10); +} + +/** + * Tests that the FIRInstanceID's sharedInstance class method produces an instance of + * FIRInstanceID with an associated FIRInstanceIDTokenManager. + */ +- (void)testSharedInstance { + // The shared instance should be `nil` before the app is configured. + XCTAssertNil([FIRInstanceID instanceID]); + + // The shared instance relies on the default app being configured. Configure it. + FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID + GCMSenderID:kGCMSenderID]; + [FIRApp configureWithName:kFIRDefaultAppName options:options]; + FIRInstanceID *instanceID = [FIRInstanceID instanceID]; + XCTAssertNotNil(instanceID); + XCTAssertNotNil(instanceID.tokenManager); + + // Ensure a second call returns the same instance as the first. + FIRInstanceID *secondInstanceID = [FIRInstanceID instanceID]; + XCTAssertEqualObjects(instanceID, secondInstanceID); + + // Reset the default app for the next test. + [FIRApp resetApps]; +} + +- (void)testFCMAutoInitEnabled { + XCTAssertFalse([_instanceID isFCMAutoInitEnabled], + @"When FCM is not available, FCM Auto Init Enabled should be NO."); +} + +- (void)testTokenShouldBeRefreshedIfCacheTokenNeedsToBeRefreshed { + [[[self.mockInstanceID stub] andReturn:kToken] cachedTokenIfAvailable]; + [[[self.mockTokenManager stub] andReturnValue:@(YES)] checkForTokenRefreshPolicy]; + [[[self.mockInstanceID stub] andDo:^(NSInvocation *invocation){ + }] tokenWithAuthorizedEntity:[OCMArg any] + scope:[OCMArg any] + options:[OCMArg any] + handler:[OCMArg any]]; + + [self.mockInstanceID didCompleteConfigure]; + OCMVerify([self.mockInstanceID defaultTokenWithHandler:nil]); + XCTAssertEqualObjects([self.mockInstanceID token], kToken); +} + +- (void)testTokenShouldBeRefreshedIfNoCacheTokenButAutoInitAllowed { + [[[self.mockInstanceID stub] andReturn:nil] cachedTokenIfAvailable]; + [[[self.mockInstanceID stub] andReturnValue:@(YES)] isFCMAutoInitEnabled]; + [[[self.mockInstanceID stub] andDo:^(NSInvocation *invocation){ + }] tokenWithAuthorizedEntity:[OCMArg any] + scope:[OCMArg any] + options:[OCMArg any] + handler:[OCMArg any]]; + + [self.mockInstanceID didCompleteConfigure]; + + OCMVerify([self.mockInstanceID defaultTokenWithHandler:nil]); +} + +- (void)testTokenIsDeletedAlongWithIdentity { + [[[self.mockInstanceID stub] andReturnValue:@(YES)] isFCMAutoInitEnabled]; + [[[self.mockInstanceID stub] andDo:^(NSInvocation *invocation){ + }] tokenWithAuthorizedEntity:[OCMArg any] + scope:[OCMArg any] + options:[OCMArg any] + handler:[OCMArg any]]; + + [self.mockInstanceID deleteIdentityWithHandler:^(NSError *_Nullable error) { + XCTAssertNil([self.mockInstanceID token]); + }]; +} + +- (void)testTokenIsFetchedDuringIIDGeneration { + XCTestExpectation *tokenExpectation = [self + expectationWithDescription:@"Token is refreshed when getID is called to avoid IID conflict."]; + NSError *error; + [[[self.mockKeyPairStore stub] andReturn:kFakeIID] appIdentityWithError:[OCMArg setTo:error]]; + + [self.mockInstanceID getIDWithHandler:^(NSString *identity, NSError *error) { + XCTAssertNotNil(identity); + XCTAssertEqual(identity, kFakeIID); + OCMVerify([self.mockInstanceID token]); + [tokenExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:0.1 + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +/** + * Tests that when a new InstanceID token is successfully produced, + * the callback is invoked with a token that is not an empty string and with no error. + */ +- (void)testNewTokenSuccess { + XCTestExpectation *tokenExpectation = + [self expectationWithDescription:@"New token handler invoked."]; + + NSString *APNSKey = kFIRInstanceIDTokenOptionsAPNSKey; + NSString *serverKey = kFIRInstanceIDTokenOptionsAPNSIsSandboxKey; + + [self stubKeyPairStoreToReturnValidKeypair]; + [self mockAuthServiceToAlwaysReturnValidCheckin]; + + NSData *fakeAPNSDeviceToken = [kFakeAPNSToken dataUsingEncoding:NSUTF8StringEncoding]; + BOOL isSandbox = YES; + NSDictionary *tokenOptions = @{ + APNSKey : fakeAPNSDeviceToken, + serverKey : @(isSandbox), + }; + + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + self.newTokenCompletion(kToken, nil); + }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + keyPair:[OCMArg any] + options:[OCMArg checkWithBlock:^BOOL(id obj) { + NSDictionary *options = (NSDictionary *)obj; + XCTAssertTrue([options[APNSKey] isEqual:fakeAPNSDeviceToken]); + XCTAssertTrue([options[serverKey] isEqual:@(isSandbox)]); + return YES; + }] + handler:[OCMArg checkWithBlock:^BOOL(id obj) { + self.newTokenCompletion = obj; + return obj != nil; + }]]; + + [self.instanceID tokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + options:tokenOptions + handler:^(NSString *token, NSError *error) { + XCTAssertNotNil(token); + XCTAssertGreaterThan(token.length, 0); + XCTAssertNil(error); + [tokenExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +/** + * Get Token should fail if we do not have valid checkin info and are unable to + * retreive one. + */ +- (void)testNewTokenCheckinFailure { + self.hasCheckinInfo = NO; + + __block FIRInstanceIDDeviceCheckinCompletion checkinHandler; + [[[self.mockAuthService stub] andDo:^(NSInvocation *invocation) { + if (checkinHandler) { + FIRInstanceIDErrorCode code = kFIRInstanceIDErrorCodeUnknown; + NSError *error = [NSError errorWithFIRInstanceIDErrorCode:code]; + checkinHandler(nil, error); + } + }] fetchCheckinInfoWithHandler:[OCMArg checkWithBlock:^BOOL(id obj) { + return (checkinHandler = obj) != nil; + }]]; + + XCTestExpectation *tokenExpectation = + [self expectationWithDescription:@"New token handler invoked."]; + + NSDictionary *tokenOptions = @{ + kFIRInstanceIDTokenOptionsAPNSKey : [kFakeAPNSToken dataUsingEncoding:NSUTF8StringEncoding], + kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(YES), + }; + + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + self.newTokenCompletion(kToken, nil); + }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + keyPair:[OCMArg any] + options:[OCMArg any] + handler:[OCMArg checkWithBlock:^BOOL(id obj) { + self.newTokenCompletion = obj; + return obj != nil; + }]]; + + [self.instanceID tokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + options:tokenOptions + handler:^(NSString *token, NSError *error) { + XCTAssertNil(token); + XCTAssertNotNil(error); + [tokenExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:60.0 + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +/** + * Get token with no valid checkin should wait for any existing checkin operation to finish. + * If the checkin succeeds within a stipulated amount of time period getting the token should + * also succeed. + */ +- (void)testNewTokenSuccessAfterWaiting { + self.hasCheckinInfo = NO; + + __block FIRInstanceIDDeviceCheckinCompletion checkinHandler; + [[[self.mockAuthService stub] andDo:^(NSInvocation *invocation) { + if (checkinHandler) { + FIRInstanceIDErrorCode code = kFIRInstanceIDErrorCodeUnknown; + NSError *error = [NSError errorWithFIRInstanceIDErrorCode:code]; + checkinHandler(nil, error); + } + }] fetchCheckinInfoWithHandler:[OCMArg checkWithBlock:^BOOL(id obj) { + return (checkinHandler = obj) != nil; + }]]; + + XCTestExpectation *tokenExpectation = + [self expectationWithDescription:@"New token handler invoked."]; + + NSDictionary *tokenOptions = @{ + kFIRInstanceIDTokenOptionsAPNSKey : [kFakeAPNSToken dataUsingEncoding:NSUTF8StringEncoding], + kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(YES), + }; + + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + self.newTokenCompletion(kToken, nil); + }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + keyPair:[OCMArg any] + options:[OCMArg any] + handler:[OCMArg checkWithBlock:^BOOL(id obj) { + self.newTokenCompletion = obj; + return obj != nil; + }]]; + + [self.instanceID tokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + options:tokenOptions + handler:^(NSString *token, NSError *error) { + XCTAssertNil(token); + XCTAssertNotNil(error); + [tokenExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:60.0 + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +/** + * Test that the prod APNS token is correctly prefixed with "prod". + */ +- (void)testAPNSTokenIsPrefixedCorrectlyForServerType { + NSString *APNSKey = kFIRInstanceIDTokenOptionsAPNSKey; + NSString *serverTypeKey = kFIRInstanceIDTokenOptionsAPNSIsSandboxKey; + NSDictionary *prodTokenOptions = @{ + APNSKey : [kFakeAPNSToken dataUsingEncoding:NSUTF8StringEncoding], + serverTypeKey : @(NO), + }; + + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation){ + }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + keyPair:[OCMArg any] + options:[OCMArg checkWithBlock:^BOOL(id obj) { + NSDictionary *options = (NSDictionary *)obj; + XCTAssertTrue([options[APNSKey] hasPrefix:@"p_"]); + XCTAssertFalse([options[serverTypeKey] boolValue]); + return YES; + }] + handler:OCMOCK_ANY]; + + [self.instanceID tokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + options:prodTokenOptions + handler:^(NSString *token, NSError *error){ + }]; +} + +/** + * Tests that when there is a failure in producing a new InstanceID token, + * the callback is invoked with an error and a nil token. + */ +- (void)testNewTokenFailure { + XCTestExpectation *tokenExpectation = + [self expectationWithDescription:@"New token handler invoked."]; + + NSDictionary *tokenOptions = [NSDictionary dictionary]; + + [self mockAuthServiceToAlwaysReturnValidCheckin]; + + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + NSError *someError = [[NSError alloc] initWithDomain:@"InstanceIDUnitTest" code:0 userInfo:nil]; + self.newTokenCompletion(nil, someError); + }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + keyPair:[OCMArg any] + options:tokenOptions + handler:[OCMArg checkWithBlock:^BOOL(id obj) { + self.newTokenCompletion = obj; + return obj != nil; + }]]; + + [self.instanceID tokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + options:tokenOptions + handler:^(NSString *token, NSError *error) { + XCTAssertNil(token); + XCTAssertNotNil(error); + [tokenExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +/** + * Tests that when a token is deleted successfully, the callback is invoked with no error. + */ +- (void)testDeleteTokenSuccess { + XCTestExpectation *deleteExpectation = + [self expectationWithDescription:@"Delete handler invoked."]; + + [self stubKeyPairStoreToReturnValidKeypair]; + + [self mockAuthServiceToAlwaysReturnValidCheckin]; + + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + self.deleteTokenCompletion(nil); +#pragma clang diagnostic pop + }] deleteTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + keyPair:[OCMArg any] + handler:[OCMArg checkWithBlock:^BOOL(id obj) { + self.deleteTokenCompletion = obj; + return obj != nil; + }]]; + + [self.instanceID deleteTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + handler:^(NSError *error) { + XCTAssertNil(error); + [deleteExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +/** + * Tests that when a token deletion fails, the callback is invoked with an error. + */ +- (void)testDeleteTokenFailure { + XCTestExpectation *deleteExpectation = + [self expectationWithDescription:@"Delete handler invoked."]; + + [self mockAuthServiceToAlwaysReturnValidCheckin]; + + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + NSError *someError = [[NSError alloc] initWithDomain:@"InstanceIDUnitTest" code:0 userInfo:nil]; + self.deleteTokenCompletion(someError); + }] deleteTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + keyPair:[OCMArg any] + handler:[OCMArg checkWithBlock:^BOOL(id obj) { + self.deleteTokenCompletion = obj; + return obj != nil; + }]]; + + [self.instanceID deleteTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + handler:^(NSError *error) { + XCTAssertNotNil(error); + [deleteExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +/** + * Tests that not having a senderID will fetch a `nil` default token. + */ +- (void)testDefaultToken_noSenderID { + _instanceID.fcmSenderID = nil; + XCTAssertNil([self.mockInstanceID token]); +} + +/** + * Tests that not having a cached token results in trying to fetch a new default token. + */ +- (void)testDefaultToken_noCachedToken { + [[[self.mockTokenManager stub] andReturn:nil] + cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity + scope:@"*"]; + + OCMExpect([self.mockInstanceID defaultTokenWithHandler:nil]); + NSString *token = [self.mockInstanceID token]; + XCTAssertNil(token); + [self.mockInstanceID stopMocking]; + OCMVerify([self.mockInstanceID defaultTokenWithHandler:nil]); +} + +/** + * Tests that when we have a cached default token, calling `getToken` returns that token + * without hitting the network. + */ +- (void)testDefaultToken_validCachedToken { + [[[self.mockTokenManager stub] andReturn:sTokenInfo] + cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity + scope:@"*"]; + + [[self.mockInstanceID reject] defaultTokenWithHandler:nil]; + NSString *token = [self.mockInstanceID token]; + XCTAssertEqualObjects(token, kToken); +} + +/** + * Test that when we fetch a new default token and cache it successfully we post a + * tokenRefresh notification which allows to fetch the cached token. + */ +- (void)testDefaultTokenFetch_returnValidToken { + XCTestExpectation *defaultTokenExpectation = + [self expectationWithDescription:@"Successfully got default token."]; + + __block FIRInstanceIDTokenInfo *cachedTokenInfo = nil; + + [self stubKeyPairStoreToReturnValidKeypair]; + + [self mockAuthServiceToAlwaysReturnValidCheckin]; + + // Mock Token manager to always succeed the token fetch, and return + // a particular cached value. + + // Return a dynamic cachedToken variable whenever the cached is checked. + // This uses an invocation-based mock because the |cachedToken| pointer + // will change. Normal stubbing will always return the initial pointer, + // which in this case is 0x0 (nil). + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + [invocation setReturnValue:&cachedTokenInfo]; + }] cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity scope:kFIRInstanceIDDefaultTokenScope]; + + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + self.newTokenCompletion(kToken, nil); + }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kFIRInstanceIDDefaultTokenScope + keyPair:[OCMArg any] + options:[OCMArg any] + handler:[OCMArg checkWithBlock:^BOOL(id obj) { + self.newTokenCompletion = obj; + return obj != nil; + }]]; + + __block int notificationPostCount = 0; + __block NSString *notificationToken = nil; + + NSString *notificationName = kFIRInstanceIDTokenRefreshNotification; + self.tokenRefreshNotificationObserver = [[NSNotificationCenter defaultCenter] + addObserverForName:notificationName + object:nil + queue:nil + usingBlock:^(NSNotification *_Nonnull note) { + // Should have saved token to cache + cachedTokenInfo = sTokenInfo; + + notificationPostCount++; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + notificationToken = [[self.instanceID token] copy]; +#pragma clang diagnostic pop + [defaultTokenExpectation fulfill]; + }]; + + NSString *token = [self.mockInstanceID token]; + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self.tokenRefreshNotificationObserver]; + + XCTAssertNil(token); + XCTAssertEqualObjects(notificationToken, kToken); +} + +/** + * Tests that if we fail to fetch the token from the server for the first time we retry again + * later with exponential backoff unless we succeed. + */ +- (void)testDefaultTokenFetch_retryFetchToken { + const int trialsBeforeSuccess = 3; + __block int newTokenFetchCount = 0; + __block int64_t lastFetchTimestampInSeconds; + + XCTestExpectation *defaultTokenExpectation = + [self expectationWithDescription:@"Successfully got default token."]; + + __block FIRInstanceIDTokenInfo *cachedTokenInfo = nil; + + [self stubKeyPairStoreToReturnValidKeypair]; + + [self mockAuthServiceToAlwaysReturnValidCheckin]; + + // Mock Token manager. + // Return a dynamic cachedToken variable whenever the cached is checked. + // This uses an invocation-based mock because the |cachedToken| pointer + // will change. Normal stubbing will always return the initial pointer, + // which in this case is 0x0 (nil). + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + [invocation setReturnValue:&cachedTokenInfo]; + }] cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity scope:kFIRInstanceIDDefaultTokenScope]; + + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + newTokenFetchCount++; + int64_t delaySinceLastFetchInSeconds = + FIRInstanceIDCurrentTimestampInSeconds() - lastFetchTimestampInSeconds; + // Test exponential backoff. + if (newTokenFetchCount > 1) { + XCTAssertLessThanOrEqual(1 << (newTokenFetchCount - 1), delaySinceLastFetchInSeconds); + } + lastFetchTimestampInSeconds = FIRInstanceIDCurrentTimestampInSeconds(); + + if (newTokenFetchCount < trialsBeforeSuccess) { + NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeTimeout]; + self.newTokenCompletion(nil, error); + } else { + self.newTokenCompletion(kToken, nil); + } + }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kFIRInstanceIDDefaultTokenScope + keyPair:[OCMArg any] + options:[OCMArg any] + handler:[OCMArg checkWithBlock:^BOOL(id obj) { + self.newTokenCompletion = obj; + return obj != nil; + }]]; + + __block int notificationPostCount = 0; + __block NSString *notificationToken = nil; + + NSString *notificationName = kFIRInstanceIDTokenRefreshNotification; + self.tokenRefreshNotificationObserver = [[NSNotificationCenter defaultCenter] + addObserverForName:notificationName + object:nil + queue:nil + usingBlock:^(NSNotification *_Nonnull note) { + // Should have saved token to cache + cachedTokenInfo = sTokenInfo; + + notificationPostCount++; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + notificationToken = [[self.instanceID token] copy]; +#pragma clang diagnostic pop + [defaultTokenExpectation fulfill]; + }]; + + NSString *token = [self.mockInstanceID token]; + [self waitForExpectationsWithTimeout:20.0 handler:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self.tokenRefreshNotificationObserver]; + + XCTAssertNil(token); + XCTAssertEqualObjects(notificationToken, kToken); + XCTAssertEqual(notificationPostCount, 1); + XCTAssertEqual(newTokenFetchCount, trialsBeforeSuccess); +} + +/** + * Tests that when we don't have a cached default token multiple invocations to `getToken` + * lead to a single networking call to fetch the token. Also verify that we post one unique + * TokenRefresh notification for multiple invocations. + */ +- (void)testDefaultToken_multipleInvocations { + __block int newTokenFetchCount = 0; + XCTestExpectation *defaultTokenExpectation = + [self expectationWithDescription:@"Successfully got default token."]; + + __block FIRInstanceIDTokenInfo *cachedTokenInfo = nil; + + [self stubKeyPairStoreToReturnValidKeypair]; + + [self mockAuthServiceToAlwaysReturnValidCheckin]; + + // Mock Token manager. + // Return a dynamic cachedToken variable whenever the cached is checked. + // This uses an invocation-based mock because the |cachedToken| pointer + // will change. Normal stubbing will always return the initial pointer, + // which in this case is 0x0 (nil). + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + [invocation setReturnValue:&cachedTokenInfo]; + }] cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity scope:kFIRInstanceIDDefaultTokenScope]; + + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + // Invoke callback after some delay (network delay) + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + self.newTokenCompletion(kToken, nil); + }); + newTokenFetchCount++; + XCTAssertEqual(newTokenFetchCount, 1); + }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kFIRInstanceIDDefaultTokenScope + keyPair:[OCMArg any] + options:[OCMArg any] + handler:[OCMArg checkWithBlock:^BOOL(id obj) { + self.newTokenCompletion = obj; + return obj != nil; + }]]; + + __block int notificationPostCount = 0; + __block NSString *notificationToken = nil; + NSString *notificationName = kFIRInstanceIDTokenRefreshNotification; + self.tokenRefreshNotificationObserver = [[NSNotificationCenter defaultCenter] + addObserverForName:notificationName + object:nil + queue:nil + usingBlock:^(NSNotification *_Nonnull note) { + // Should have saved token to cache + cachedTokenInfo = sTokenInfo; + + notificationPostCount++; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + notificationToken = [[self.instanceID token] copy]; +#pragma clang diagnostic pop + [defaultTokenExpectation fulfill]; + }]; + + NSString *token = [self.mockInstanceID token]; + // Invoke get token again with some delay. Our initial request to getToken hasn't yet + // returned from the server. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + XCTAssertNil([self.mockInstanceID token]); + }); + // Invoke again after further delay. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + XCTAssertNil([self.mockInstanceID token]); + }); + + [self waitForExpectationsWithTimeout:15.0 handler:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self.tokenRefreshNotificationObserver]; + + XCTAssertNil(token); + XCTAssertEqualObjects(notificationToken, kToken); + XCTAssertEqual(notificationPostCount, 1); + XCTAssertEqual(newTokenFetchCount, 1); +} + +- (void)testDefaultToken_maxRetries { + __block int newTokenFetchCount = 0; + XCTestExpectation *defaultTokenExpectation = + [self expectationWithDescription:@"Did retry maximum times to fetch default token."]; + + [self stubKeyPairStoreToReturnValidKeypair]; + + [self mockAuthServiceToAlwaysReturnValidCheckin]; + + // Mock Token manager. + [[[self.mockTokenManager stub] andReturn:nil] + cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity + scope:kFIRInstanceIDDefaultTokenScope]; + + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + newTokenFetchCount++; + NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeNetwork]; + self.newTokenCompletion(nil, error); + if (newTokenFetchCount == [FIRInstanceID maxRetryCountForDefaultToken]) { + [defaultTokenExpectation fulfill]; + } + }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kFIRInstanceIDDefaultTokenScope + keyPair:[OCMArg any] + options:[OCMArg any] + handler:[OCMArg checkWithBlock:^BOOL(id obj) { + self.newTokenCompletion = obj; + return obj != nil; + }]]; + + // Mock Instance ID's retry interval to 0, to vastly speed up this test. + [[[self.mockInstanceID stub] andReturnValue:@(0)] retryIntervalToFetchDefaultToken]; + + // Try to fetch token once. It should set off retries since we mock failure. + NSString *token = [self.mockInstanceID token]; + + [self waitForExpectationsWithTimeout:1.0 handler:nil]; + XCTAssertNil(token); + XCTAssertEqual(newTokenFetchCount, [FIRInstanceID maxRetryCountForDefaultToken]); +} + +- (void)testInstanceIDWithHandler_WhileRequesting_Success { + [self stubKeyPairStoreToReturnValidKeypair]; + [self mockAuthServiceToAlwaysReturnValidCheckin]; + + // Expect `fetchNewTokenWithAuthorizedEntity` to be called once + XCTestExpectation *fetchNewTokenExpectation = + [self expectationWithDescription:@"fetchNewTokenExpectation"]; + __block FIRInstanceIDTokenHandler tokenHandler; + + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + [invocation getArgument:&tokenHandler atIndex:6]; + [fetchNewTokenExpectation fulfill]; + }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kFIRInstanceIDDefaultTokenScope + keyPair:[OCMArg any] + options:[OCMArg any] + handler:[OCMArg any]]; + + // Make 1st call + XCTestExpectation *handlerExpectation1 = [self expectationWithDescription:@"handlerExpectation1"]; + FIRInstanceIDResultHandler handler1 = + ^(FIRInstanceIDResult *_Nullable result, NSError *_Nullable error) { + [handlerExpectation1 fulfill]; + XCTAssertNotNil(result); + XCTAssertEqual(result.token, kToken); + XCTAssertNil(error); + }; + + [self.mockInstanceID instanceIDWithHandler:handler1]; + + // Make 2nd call + XCTestExpectation *handlerExpectation2 = [self expectationWithDescription:@"handlerExpectation1"]; + FIRInstanceIDResultHandler handler2 = + ^(FIRInstanceIDResult *_Nullable result, NSError *_Nullable error) { + [handlerExpectation2 fulfill]; + XCTAssertNotNil(result); + XCTAssertEqual(result.token, kToken); + XCTAssertNil(error); + }; + + [self.mockInstanceID instanceIDWithHandler:handler2]; + + // Wait for `fetchNewTokenWithAuthorizedEntity` to be performed + [self waitForExpectations:@[ fetchNewTokenExpectation ] timeout:1 enforceOrder:false]; + // Finish token fetch request + tokenHandler(kToken, nil); + + // Wait for completion handlers for both calls to be performed + [self waitForExpectationsWithTimeout:1 handler:NULL]; +} + +- (void)testInstanceIDWithHandler_WhileRequesting_RetrySuccess { + [self stubKeyPairStoreToReturnValidKeypair]; + [self mockAuthServiceToAlwaysReturnValidCheckin]; + + // Expect `fetchNewTokenWithAuthorizedEntity` to be called twice + XCTestExpectation *fetchNewTokenExpectation1 = + [self expectationWithDescription:@"fetchNewTokenExpectation1"]; + XCTestExpectation *fetchNewTokenExpectation2 = + [self expectationWithDescription:@"fetchNewTokenExpectation2"]; + NSArray *fetchNewTokenExpectations = @[ fetchNewTokenExpectation1, fetchNewTokenExpectation2 ]; + + __block NSInteger fetchNewTokenCallCount = 0; + __block FIRInstanceIDTokenHandler tokenHandler; + + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + [invocation getArgument:&tokenHandler atIndex:6]; + [fetchNewTokenExpectations[fetchNewTokenCallCount] fulfill]; + fetchNewTokenCallCount += 1; + }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kFIRInstanceIDDefaultTokenScope + keyPair:[OCMArg any] + options:[OCMArg any] + handler:[OCMArg any]]; + + // Mock Instance ID's retry interval to 0, to vastly speed up this test. + [[[self.mockInstanceID stub] andReturnValue:@(0)] retryIntervalToFetchDefaultToken]; + + // Make 1st call + XCTestExpectation *handlerExpectation1 = [self expectationWithDescription:@"handlerExpectation1"]; + FIRInstanceIDResultHandler handler1 = + ^(FIRInstanceIDResult *_Nullable result, NSError *_Nullable error) { + [handlerExpectation1 fulfill]; + XCTAssertNotNil(result); + XCTAssertEqual(result.token, kToken); + XCTAssertNil(error); + }; + + [self.mockInstanceID instanceIDWithHandler:handler1]; + + // Make 2nd call + XCTestExpectation *handlerExpectation2 = [self expectationWithDescription:@"handlerExpectation1"]; + FIRInstanceIDResultHandler handler2 = + ^(FIRInstanceIDResult *_Nullable result, NSError *_Nullable error) { + [handlerExpectation2 fulfill]; + XCTAssertNotNil(result); + XCTAssertEqual(result.token, kToken); + XCTAssertNil(error); + }; + + [self.mockInstanceID instanceIDWithHandler:handler2]; + + // Wait for the 1st `fetchNewTokenWithAuthorizedEntity` to be performed + [self waitForExpectations:@[ fetchNewTokenExpectation1 ] timeout:1 enforceOrder:false]; + // Fail for the 1st time + tokenHandler(nil, [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeUnknown]); + + // Wait for the 2nd token feth + [self waitForExpectations:@[ fetchNewTokenExpectation2 ] timeout:1 enforceOrder:false]; + // Finish with success + tokenHandler(kToken, nil); + + // Wait for completion handlers for both calls to be performed + [self waitForExpectationsWithTimeout:1 handler:NULL]; +} + +- (void)testInstanceIDWithHandler_WhileRequesting_RetryFailure { + [self stubKeyPairStoreToReturnValidKeypair]; + [self mockAuthServiceToAlwaysReturnValidCheckin]; + + // Expect `fetchNewTokenWithAuthorizedEntity` to be called once + NSMutableArray *fetchNewTokenExpectations = [NSMutableArray array]; + for (NSInteger i = 0; i < [[self.instanceID class] maxRetryCountForDefaultToken]; ++i) { + NSString *name = [NSString stringWithFormat:@"fetchNewTokenExpectation-%ld", (long)i]; + [fetchNewTokenExpectations addObject:[self expectationWithDescription:name]]; + } + + __block NSInteger fetchNewTokenCallCount = 0; + __block FIRInstanceIDTokenHandler tokenHandler; + + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + [invocation getArgument:&tokenHandler atIndex:6]; + [fetchNewTokenExpectations[fetchNewTokenCallCount] fulfill]; + fetchNewTokenCallCount += 1; + }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kFIRInstanceIDDefaultTokenScope + keyPair:[OCMArg any] + options:[OCMArg any] + handler:[OCMArg any]]; + + // Mock Instance ID's retry interval to 0, to vastly speed up this test. + [[[self.mockInstanceID stub] andReturnValue:@(0)] retryIntervalToFetchDefaultToken]; + + // Make 1st call + XCTestExpectation *handlerExpectation1 = [self expectationWithDescription:@"handlerExpectation1"]; + FIRInstanceIDResultHandler handler1 = + ^(FIRInstanceIDResult *_Nullable result, NSError *_Nullable error) { + [handlerExpectation1 fulfill]; + XCTAssertNil(result); + XCTAssertNotNil(error); + }; + + [self.mockInstanceID instanceIDWithHandler:handler1]; + + // Make 2nd call + XCTestExpectation *handlerExpectation2 = [self expectationWithDescription:@"handlerExpectation1"]; + FIRInstanceIDResultHandler handler2 = + ^(FIRInstanceIDResult *_Nullable result, NSError *_Nullable error) { + [handlerExpectation2 fulfill]; + XCTAssertNil(result); + XCTAssertNotNil(error); + }; + + [self.mockInstanceID instanceIDWithHandler:handler2]; + + for (NSInteger i = 0; i < [[self.instanceID class] maxRetryCountForDefaultToken]; ++i) { + // Wait for the i `fetchNewTokenWithAuthorizedEntity` to be performed + [self waitForExpectations:@[ fetchNewTokenExpectations[i] ] timeout:1 enforceOrder:false]; + // Fail for the i time + tokenHandler(nil, [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeUnknown]); + } + + // Wait for completion handlers for both calls to be performed + [self waitForExpectationsWithTimeout:1 handler:NULL]; +} + +/** + * Tests a Keychain read failure while we try to fetch a new InstanceID token. If the Keychain + * read fails we won't be able to fetch the public key which is required while fetching a new + * token. In such a case we should return KeyPair failure. + */ +- (void)testNewTokenFetch_keyChainError { + XCTestExpectation *tokenExpectation = + [self expectationWithDescription:@"New token handler invoked."]; + + [self mockAuthServiceToAlwaysReturnValidCheckin]; + + // Simulate keypair fetch/generation failure. + [[[self.mockKeyPairStore stub] andReturn:nil] loadKeyPairWithError:[OCMArg anyObjectRef]]; + + [[self.mockTokenManager reject] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + keyPair:[OCMArg any] + options:[OCMArg any] + handler:[OCMArg any]]; + + [self.instanceID tokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + options:nil + handler:^(NSString *token, NSError *error) { + XCTAssertNil(token); + XCTAssertNotNil(error); + [tokenExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; + OCMVerifyAll(self.mockTokenManager); +} + +/** + * If a token fetch includes in its options an "apns_token" object, but not a "apns_sandbox" key, + * ensure that an "apns_sandbox" key is added to the token options (via automatic detection). + */ +- (void)testTokenFetchAPNSServerTypeIsIncludedIfAPNSTokenProvided { + XCTestExpectation *apnsServerTypeExpectation = + [self expectationWithDescription:@"apns_sandbox key was included in token options"]; + + [self stubKeyPairStoreToReturnValidKeypair]; + [self mockAuthServiceToAlwaysReturnValidCheckin]; + + NSData *apnsToken = [kFakeAPNSToken dataUsingEncoding:NSUTF8StringEncoding]; + // Option is purposefully missing the apns_sandbox key + NSDictionary *tokenOptions = @{kFIRInstanceIDTokenOptionsAPNSKey : apnsToken}; + + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + // Inspect + NSDictionary *options; + [invocation getArgument:&options atIndex:5]; + if (options[kFIRInstanceIDTokenOptionsAPNSIsSandboxKey] != nil) { + [apnsServerTypeExpectation fulfill]; + } + self.newTokenCompletion(kToken, nil); + }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + keyPair:[OCMArg any] + options:[OCMArg any] + handler:[OCMArg checkWithBlock:^BOOL(id obj) { + self.newTokenCompletion = obj; + return obj != nil; + }]]; + + [self.instanceID tokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + options:tokenOptions + handler:^(NSString *token, NSError *error){ + }]; + + [self waitForExpectationsWithTimeout:60.0 + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +/** + * Tests that if a token was fetched, but during the fetch the APNs data was set, that a new + * token is fetched to associate the APNs data, and is not returned from the cache. + */ +- (void)testTokenFetch_ignoresCacheIfAPNSInfoDifferent { + XCTestExpectation *tokenRequestExpectation = + [self expectationWithDescription:@"Token was fetched from the network"]; + + // Initialize a token in the cache *WITHOUT* APNSInfo + // This token is |kToken|, but we will simulate that a fetch will return another token + NSString *oldCachedToken = kToken; + NSString *fetchedToken = @"abcd123_newtoken"; + __block FIRInstanceIDTokenInfo *cachedTokenInfo = + [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:kAuthorizedEntity + scope:kFIRInstanceIDDefaultTokenScope + token:oldCachedToken + appVersion:@"1.0" + firebaseAppID:@"firebaseAppID"]; + + [self stubKeyPairStoreToReturnValidKeypair]; + + [self mockAuthServiceToAlwaysReturnValidCheckin]; + + // During this test use the default scope ("*") to simulate the default token behavior. + + // Return a dynamic cachedToken variable whenever the cached is checked. + // This uses an invocation-based mock because the |cachedToken| pointer + // will change. Normal stubbing will always return the initial pointer, + // which in this case is 0x0 (nil). + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + [invocation setReturnValue:&cachedTokenInfo]; + }] cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity scope:kFIRInstanceIDDefaultTokenScope]; + + // Mock the network request to return |fetchedToken|, so we can clearly see if the token is + // is different than what was cached. + [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) { + [tokenRequestExpectation fulfill]; + self.newTokenCompletion(fetchedToken, nil); + }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kFIRInstanceIDDefaultTokenScope + keyPair:[OCMArg any] + options:[OCMArg any] + handler:[OCMArg checkWithBlock:^BOOL(id obj) { + self.newTokenCompletion = obj; + return obj != nil; + }]]; + + // Begin request + // Token options has APNS data, which is not associated with the cached token + NSDictionary *tokenOptions = @{ + kFIRInstanceIDTokenOptionsAPNSKey : [@"apns" dataUsingEncoding:NSUTF8StringEncoding], + kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(NO) + }; + [self.instanceID + tokenWithAuthorizedEntity:kAuthorizedEntity + scope:kFIRInstanceIDDefaultTokenScope + options:tokenOptions + handler:^(NSString *_Nullable token, NSError *_Nullable error) { + XCTAssertEqualObjects(token, fetchedToken); + }]; + + [self waitForExpectationsWithTimeout:0.5 handler:nil]; +} + +/** + * Tests that if there is a keychain failure while fetching the InstanceID of the token we should + * return nil for the identity. + */ +- (void)testInstanceIDFetch_keyChainError { + XCTestExpectation *tokenExpectation = + [self expectationWithDescription:@"InstanceID fetch handler invoked."]; + + // Simulate keypair fetch/generation failure. + NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidKeyPair]; + [[[self.mockKeyPairStore stub] andReturn:nil] appIdentityWithError:[OCMArg setTo:error]]; + + [self.instanceID getIDWithHandler:^(NSString *_Nullable identity, NSError *_Nullable error) { + XCTAssertNil(identity); + XCTAssertNotNil(error); + [tokenExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testInstanceIDDelete_keyChainError { + XCTestExpectation *tokenExpectation = + [self expectationWithDescription:@"InstanceID deleteID handler invoked."]; + + // Simulate keypair fetch/generation failure. + NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidKeyPair]; + [[[self.mockKeyPairStore stub] andReturn:nil] appIdentityWithError:[OCMArg setTo:error]]; + + [self.instanceID deleteIDWithHandler:^(NSError *_Nullable error) { + XCTAssertNotNil(error); + [tokenExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +#pragma mark - Private Helpers + +- (void)stubKeyPairStoreToReturnValidKeypair { + [[[self.mockKeyPairStore stub] andReturn:[self createValidMockKeypair]] + loadKeyPairWithError:[OCMArg anyObjectRef]]; +} + +- (id)createValidMockKeypair { + id mockKeypair = OCMClassMock([FIRInstanceIDKeyPair class]); + [[[mockKeypair stub] andReturnValue:@YES] isValid]; + return mockKeypair; +} + +- (FIRInstanceIDCheckinPreferences *)validCheckinPreferences { + NSDictionary *gservicesData = @{ + kFIRInstanceIDVersionInfoStringKey : kVersionInfo, + kFIRInstanceIDLastCheckinTimeKey : @(FIRInstanceIDCurrentTimestampInMilliseconds()) + }; + FIRInstanceIDCheckinPreferences *checkinPreferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceAuthId + secretToken:kSecretToken]; + [checkinPreferences updateWithCheckinPlistContents:gservicesData]; + return checkinPreferences; +} + +- (void)mockAuthServiceToAlwaysReturnValidCheckin { + FIRInstanceIDCheckinPreferences *validCheckin = [self validCheckinPreferences]; + __block FIRInstanceIDDeviceCheckinCompletion checkinHandler; + [[[self.mockAuthService stub] andDo:^(NSInvocation *invocation) { + if (checkinHandler) { + checkinHandler(validCheckin, nil); + } + }] fetchCheckinInfoWithHandler:[OCMArg checkWithBlock:^BOOL(id obj) { + return (checkinHandler = obj) != nil; + }]]; +} + +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDTokenInfoTest.m b/Example/InstanceID/Tests/FIRInstanceIDTokenInfoTest.m new file mode 100644 index 00000000000..c097ecb64e9 --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDTokenInfoTest.m @@ -0,0 +1,175 @@ +/* + * Copyright 2019 Google + * + * 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 "Firebase/InstanceID/FIRInstanceIDTokenInfo.h" + +#import + +#import +#import +#import +#import "Firebase/InstanceID/FIRInstanceIDAPNSInfo.h" +#import "Firebase/InstanceID/FIRInstanceIDUtilities.h" + +static NSString *const kAuthorizedEntity = @"authorizedEntity"; +static NSString *const kScope = @"scope"; +static NSString *const kToken = @"validToken"; +static NSString *const kFirebaseAppID = @"firebaseAppID"; +static BOOL const kAPNSSandbox = NO; + +@interface FIRInstanceIDTokenInfoTest : XCTestCase + +@property(nonatomic, strong) NSData *APNSDeviceToken; +@property(nonatomic, strong) FIRInstanceIDTokenInfo *validTokenInfo; +@property(nonatomic, strong) id mockOptions; + +@end + +@implementation FIRInstanceIDTokenInfoTest + +- (void)setUp { + [super setUp]; + + self.APNSDeviceToken = [@"validDeviceToken" dataUsingEncoding:NSUTF8StringEncoding]; + + self.mockOptions = OCMClassMock([FIROptions class]); + OCMStub([self.mockOptions defaultOptionsDictionary]).andReturn(@{ + kFIRGoogleAppID : kFirebaseAppID + }); + + self.validTokenInfo = + [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + token:kToken + appVersion:FIRInstanceIDCurrentAppVersion() + firebaseAppID:FIRInstanceIDFirebaseAppID()]; + self.validTokenInfo.APNSInfo = + [[FIRInstanceIDAPNSInfo alloc] initWithDeviceToken:self.APNSDeviceToken + isSandbox:kAPNSSandbox]; + self.validTokenInfo.cacheTime = [NSDate date]; +} + +- (void)tearDown { + [self.mockOptions stopMocking]; + [super tearDown]; +} + +- (void)testTokenInfoCreationWithInvalidArchive { + NSData *badData = [@"badData" dataUsingEncoding:NSUTF8StringEncoding]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + FIRInstanceIDTokenInfo *info = [NSKeyedUnarchiver unarchiveObjectWithData:badData]; +#pragma clang diagnostic pop + XCTAssertNil(info); +} + +// Test that archiving a FIRInstanceIDTokenInfo object and restoring it from the archive +// yields the same values for all the fields. +- (void)testTokenInfoEncodingAndDecoding { + FIRInstanceIDTokenInfo *info = self.validTokenInfo; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + NSData *archive = [NSKeyedArchiver archivedDataWithRootObject:info]; + FIRInstanceIDTokenInfo *restoredInfo = [NSKeyedUnarchiver unarchiveObjectWithData:archive]; +#pragma clang diagnostic pop + XCTAssertEqualObjects(restoredInfo.authorizedEntity, info.authorizedEntity); + XCTAssertEqualObjects(restoredInfo.scope, info.scope); + XCTAssertEqualObjects(restoredInfo.token, info.token); + XCTAssertEqualObjects(restoredInfo.appVersion, info.appVersion); + XCTAssertEqualObjects(restoredInfo.firebaseAppID, info.firebaseAppID); + XCTAssertEqualObjects(restoredInfo.cacheTime, info.cacheTime); + XCTAssertEqualObjects(restoredInfo.APNSInfo.deviceToken, info.APNSInfo.deviceToken); + XCTAssertEqual(restoredInfo.APNSInfo.sandbox, info.APNSInfo.sandbox); +} + +// Test that archiving a FIRInstanceIDTokenInfo object with missing fields and restoring it +// from the archive yields the same values for all the fields. +- (void)testTokenInfoEncodingAndDecodingWithMissingFields { + // Don't include appVersion, firebaseAppID, APNSInfo and cacheTime + FIRInstanceIDTokenInfo *sparseInfo = + [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + token:kToken + appVersion:nil + firebaseAppID:nil]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + NSData *archive = [NSKeyedArchiver archivedDataWithRootObject:sparseInfo]; + FIRInstanceIDTokenInfo *restoredInfo = [NSKeyedUnarchiver unarchiveObjectWithData:archive]; +#pragma clang diagnostic pop + XCTAssertEqualObjects(restoredInfo.authorizedEntity, sparseInfo.authorizedEntity); + XCTAssertEqualObjects(restoredInfo.scope, sparseInfo.scope); + XCTAssertEqualObjects(restoredInfo.token, sparseInfo.token); + XCTAssertNil(restoredInfo.appVersion); + XCTAssertNil(restoredInfo.firebaseAppID); + XCTAssertNil(restoredInfo.cacheTime); + XCTAssertNil(restoredInfo.APNSInfo); +} + +- (void)testTokenFreshnessWithLocaleChange { + // Default should be fresh because we mock last fetch token time just now. + XCTAssertTrue([self.validTokenInfo isFresh]); + + // Locale change should affect token refreshness. + // Set to a different locale than the current locale. + [[NSUserDefaults standardUserDefaults] setObject:@"zh-Hant" + forKey:kFIRInstanceIDUserDefaultsKeyLocale]; + [[NSUserDefaults standardUserDefaults] synchronize]; + XCTAssertFalse([self.validTokenInfo isFresh]); + // Reset locale + [[NSUserDefaults standardUserDefaults] setObject:FIRInstanceIDCurrentLocale() + forKey:kFIRInstanceIDUserDefaultsKeyLocale]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (void)testTokenFreshnessWithTokenTimestampChange { + XCTAssertTrue([self.validTokenInfo isFresh]); + // Set last fetch token time 7 days ago. + NSTimeInterval lastFetchTokenTimestamp = + FIRInstanceIDCurrentTimestampInSeconds() - 7 * 24 * 60 * 60; + self.validTokenInfo.cacheTime = [NSDate dateWithTimeIntervalSince1970:lastFetchTokenTimestamp]; + XCTAssertFalse([self.validTokenInfo isFresh]); + + // Set last fetch token time more than 7 days ago. + lastFetchTokenTimestamp = FIRInstanceIDCurrentTimestampInSeconds() - 8 * 24 * 60 * 60; + self.validTokenInfo.cacheTime = [NSDate dateWithTimeIntervalSince1970:lastFetchTokenTimestamp]; + XCTAssertFalse([self.validTokenInfo isFresh]); + + // Set last fetch token time nil to mock legacy storage format. Token should be considered not + // fresh. + self.validTokenInfo.cacheTime = nil; + XCTAssertFalse([self.validTokenInfo isFresh]); +} + +- (void)testTokenFreshnessWithFirebaseAppIDChange { + XCTAssertTrue([self.validTokenInfo isFresh]); + // Change Firebase App ID. + [FIROptions defaultOptions].googleAppID = @"newFirebaseAppID:ios:abcdefg"; + XCTAssertFalse([self.validTokenInfo isFresh]); +} + +- (void)testTokenFreshnessWithAppVersionChange { + XCTAssertTrue([self.validTokenInfo isFresh]); + // Change app version. + self.validTokenInfo = + [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + token:kToken + appVersion:@"1.1" + firebaseAppID:FIRInstanceIDFirebaseAppID()]; + XCTAssertFalse([self.validTokenInfo isFresh]); +} +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDTokenManager+Test.h b/Example/InstanceID/Tests/FIRInstanceIDTokenManager+Test.h new file mode 100644 index 00000000000..14c444bd595 --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDTokenManager+Test.h @@ -0,0 +1,44 @@ +/* + * Copyright 2019 Google + * + * 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 "Firebase/InstanceID/FIRInstanceIDTokenManager.h" + +#import "Firebase/InstanceID/FIRInstanceIDStore.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenOperation.h" + +@class FIRInstanceIDBackupExcludedPlist; +@class FIRInstanceIDCheckinPreferences; +@class FIRInstanceIDCheckinStore; +@class FIRInstanceIDTokenDeleteOperation; +@class FIRInstanceIDTokenFetchOperation; +@class FIRInstanceIDTokenStore; + +@interface FIRInstanceIDTokenManager (Test) + +@property(nonatomic, readonly, strong) FIRInstanceIDStore *instanceIDStore; + +/** + * Create a InstanceID store with specific backup excluded plist and checkin store. + * + * @param checkinStore The persistent store used to save checkin preferences. + * @param tokenStore The persistent store used to cache InstanceID tokens. + * + * @return Store to cache tokens and checkin preferences. + */ +- (instancetype)initWithCheckinStore:(FIRInstanceIDCheckinStore *)checkinStore + tokenStore:(FIRInstanceIDTokenStore *)tokenStore; + +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDTokenManager+Test.m b/Example/InstanceID/Tests/FIRInstanceIDTokenManager+Test.m new file mode 100644 index 00000000000..bf367b5be95 --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDTokenManager+Test.m @@ -0,0 +1,50 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDTokenManager+Test.h" + +#import "Firebase/InstanceID/FIRInstanceIDAuthService.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinStore.h" + +@interface FIRInstanceIDTokenManager () + +@property(nonatomic, readwrite, strong) FIRInstanceIDStore *instanceIDStore; +@property(nonatomic, readwrite, strong) FIRInstanceIDAuthService *authService; + +- (void)configureTokenOperations; + +@end + +@implementation FIRInstanceIDTokenManager (Test) + +- (instancetype)initWithCheckinStore:(FIRInstanceIDCheckinStore *)checkinStore + tokenStore:(FIRInstanceIDTokenStore *)tokenStore { + self = [super init]; + if (self) { + self.instanceIDStore = [[FIRInstanceIDStore alloc] initWithCheckinStore:checkinStore + tokenStore:tokenStore + delegate:self]; + self.authService = [[FIRInstanceIDAuthService alloc] initWithStore:self.instanceIDStore]; + [self configureTokenOperations]; + } + return self; +} + +- (void)store:(FIRInstanceIDStore *)store + didDeleteFCMScopedTokensForCheckin:(FIRInstanceIDCheckinPreferences *)checkin { +} + +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDTokenManagerTest.m b/Example/InstanceID/Tests/FIRInstanceIDTokenManagerTest.m new file mode 100644 index 00000000000..96448387d76 --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDTokenManagerTest.m @@ -0,0 +1,512 @@ +/* + * Copyright 2019 Google + * + * 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 +#import "FIRInstanceIDFakeKeychain.h" +#import "FIRInstanceIDTokenManager+Test.h" +#import "Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinStore.h" +#import "Firebase/InstanceID/FIRInstanceIDStore.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenDeleteOperation.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenFetchOperation.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenInfo.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenManager.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenOperation.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenStore.h" + +static NSString *const kSubDirectoryName = @"FirebaseInstanceIDTokenManagerTest"; + +static NSString *const kAuthorizedEntity = @"test-authorized-entity"; +static NSString *const kScope = @"test-scope"; +static NSString *const kToken = @"test-token"; + +// Use a string (which is converted to NSData) as a placeholder for an actual APNs device token. +static NSString *const kNewAPNSTokenString = @"newAPNSData"; + +@interface FIRInstanceIDTokenOperation () + +- (void)performTokenOperation; +- (void)finishWithResult:(FIRInstanceIDTokenOperationResult)result + token:(nullable NSString *)token + error:(nullable NSError *)error; + +@end + +@interface FIRInstanceIDTokenManager (ExposedForTests) + +- (BOOL)checkForTokenRefreshPolicy; +- (void)updateToAPNSDeviceToken:(NSData *)deviceToken isSandbox:(BOOL)isSandbox; +/** + * Create a fetch operation. This method can be stubbed to return a particular operation instance, + * which makes it easier to unit test different behaviors. + */ +- (FIRInstanceIDTokenFetchOperation *) + createFetchOperationWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + options:(NSDictionary *)options + keyPair:(FIRInstanceIDKeyPair *)keyPair; + +/** + * Create a delete operation. This method can be stubbed to return a particular operation instance, + * which makes it easier to unit test different behaviors. + */ +- (FIRInstanceIDTokenDeleteOperation *) + createDeleteOperationWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences + keyPair:(FIRInstanceIDKeyPair *)keyPair + action:(FIRInstanceIDTokenAction)action; +@end + +@interface FIRInstanceIDTokenManagerTest : XCTestCase + +@property(nonatomic, readwrite, strong) FIRInstanceIDTokenManager *tokenManager; +@property(nonatomic, readwrite, strong) id mockTokenManager; + +@property(nonatomic, readwrite, strong) FIRInstanceIDBackupExcludedPlist *checkinPlist; +@property(nonatomic, readwrite, strong) FIRInstanceIDFakeKeychain *fakeKeyChain; +@property(nonatomic, readwrite, strong) FIRInstanceIDTokenStore *tokenStore; + +@property(nonatomic, readwrite, strong) FIRInstanceIDCheckinPreferences *fakeCheckin; + +@end + +@implementation FIRInstanceIDTokenManagerTest + +- (void)setUp { + [super setUp]; + [FIRInstanceIDStore createSubDirectory:kSubDirectoryName]; + + NSString *checkinPlistFilename = @"com.google.test.IIDCheckinTest"; + self.checkinPlist = + [[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:checkinPlistFilename + subDirectory:kSubDirectoryName]; + + // checkin store + FIRInstanceIDFakeKeychain *fakeCheckinKeychain = [[FIRInstanceIDFakeKeychain alloc] init]; + FIRInstanceIDCheckinStore *checkinStore = + [[FIRInstanceIDCheckinStore alloc] initWithCheckinPlist:self.checkinPlist + keychain:fakeCheckinKeychain]; + + // token store + _fakeKeyChain = [[FIRInstanceIDFakeKeychain alloc] init]; + _tokenStore = [[FIRInstanceIDTokenStore alloc] initWithKeychain:_fakeKeyChain]; + + _tokenManager = [[FIRInstanceIDTokenManager alloc] initWithCheckinStore:checkinStore + tokenStore:self.tokenStore]; + _mockTokenManager = OCMPartialMock(_tokenManager); + + _fakeCheckin = [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:@"fakeDeviceID" + secretToken:@"fakeSecretToken"]; +} + +- (void)tearDown { + NSError *error; + if (![self.checkinPlist deleteFile:&error]) { + XCTFail(@"Failed to delete checkin plist %@", error); + } + + self.tokenManager = nil; + [FIRInstanceIDStore removeSubDirectory:kSubDirectoryName error:nil]; + [super tearDown]; +} + +/** + * Tests that when a new InstanceID token is successfully produced, + * the callback is invoked with a token that is not an empty string and with no error. + */ +- (void)testNewTokenSuccess { + XCTestExpectation *tokenExpectation = + [self expectationWithDescription:@"New token handler invoked."]; + + NSDictionary *tokenOptions = [NSDictionary dictionary]; + + // Create a fake operation that always returns success + FIRInstanceIDTokenFetchOperation *operation = + [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + options:tokenOptions + checkinPreferences:self.fakeCheckin + keyPair:[OCMArg any]]; + id mockOperation = OCMPartialMock(operation); + [[[mockOperation stub] andDo:^(NSInvocation *invocation) { + [invocation.target finishWithResult:FIRInstanceIDTokenOperationSucceeded + token:kToken + error:nil]; + }] performTokenOperation]; + + // Return our fake operation when asked for an operation + [[[self.mockTokenManager stub] andReturn:operation] + createFetchOperationWithAuthorizedEntity:[OCMArg any] + scope:[OCMArg any] + options:[OCMArg any] + keyPair:[OCMArg any]]; + + [self.tokenManager fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + keyPair:[OCMArg any] + options:tokenOptions + handler:^(NSString *token, NSError *error) { + // Keep 'operation' alive, so it's not + // prematurely destroyed + XCTAssertNotNil(operation); + XCTAssertNotNil(mockOperation); + XCTAssertNotNil(token); + XCTAssertGreaterThan(token.length, 0); + XCTAssertNil(error); + [tokenExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +/** + * Tests that when a new InstanceID token is fetched from the server but unsuccessfully + * saved on the client we should return an error instead of the fetched token. + */ +- (void)testNewTokenSaveFailure { + XCTestExpectation *tokenExpectation = + [self expectationWithDescription:@"New token handler invoked."]; + + NSDictionary *tokenOptions = [NSDictionary dictionary]; + // Simulate write to keychain failure. + self.fakeKeyChain.cannotWriteToKeychain = YES; + + // Create a fake operation that always returns success + FIRInstanceIDTokenFetchOperation *operation = + [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + options:tokenOptions + checkinPreferences:self.fakeCheckin + keyPair:[OCMArg any]]; + id mockOperation = OCMPartialMock(operation); + [[[mockOperation stub] andDo:^(NSInvocation *invocation) { + [invocation.target finishWithResult:FIRInstanceIDTokenOperationSucceeded + token:kToken + error:nil]; + }] performTokenOperation]; + + // Return our fake operation when asked for an operation + [[[self.mockTokenManager stub] andReturn:operation] + createFetchOperationWithAuthorizedEntity:[OCMArg any] + scope:[OCMArg any] + options:[OCMArg any] + keyPair:[OCMArg any]]; + + [self.tokenManager fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + keyPair:[OCMArg any] + options:tokenOptions + handler:^(NSString *token, NSError *error) { + // Keep 'operation' alive, so it's not prematurely + // destroyed + XCTAssertNotNil(operation); + XCTAssertNotNil(mockOperation); + XCTAssertNil(token); + XCTAssertNotNil(error); + [tokenExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + XCTAssertNil(error.localizedDescription); + }]; +} + +/** + * Tests that when there is a failure in producing a new InstanceID token, + * the callback is invoked with an error and a nil token. + */ +- (void)testNewTokenFailure { + XCTestExpectation *tokenExpectation = + [self expectationWithDescription:@"New token handler invoked."]; + + NSDictionary *tokenOptions = [NSDictionary dictionary]; + + // Create a fake operation that always returns failure + FIRInstanceIDTokenFetchOperation *operation = + [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + options:tokenOptions + checkinPreferences:self.fakeCheckin + keyPair:[OCMArg any]]; + id mockOperation = OCMPartialMock(operation); + [[[mockOperation stub] andDo:^(NSInvocation *invocation) { + NSError *someError = [[NSError alloc] initWithDomain:@"InstanceIDUnitTest" code:0 userInfo:nil]; + [invocation.target finishWithResult:FIRInstanceIDTokenOperationError token:nil error:someError]; + }] performTokenOperation]; + + // Return our fake operation when asked for an operation + [[[self.mockTokenManager stub] andReturn:operation] + createFetchOperationWithAuthorizedEntity:[OCMArg any] + scope:[OCMArg any] + options:[OCMArg any] + keyPair:[OCMArg any]]; + + [self.tokenManager fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + keyPair:[OCMArg any] + options:tokenOptions + handler:^(NSString *token, NSError *error) { + // Keep 'operation' alive, so it's not + // prematurely destroyed + XCTAssertNotNil(operation); + XCTAssertNotNil(mockOperation); + XCTAssertNil(token); + XCTAssertNotNil(error); + [tokenExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + XCTAssertNil(error.localizedDescription); + }]; +} + +/** + * Tests that when a token is deleted successfully, the callback is invoked with no error. + */ +- (void)testDeleteTokenSuccess { + XCTestExpectation *deleteExpectation = + [self expectationWithDescription:@"Delete handler invoked."]; + + // Create a fake operation that always succeeds + FIRInstanceIDTokenDeleteOperation *operation = [[FIRInstanceIDTokenDeleteOperation alloc] + initWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + checkinPreferences:self.fakeCheckin + keyPair:[OCMArg any] + action:FIRInstanceIDTokenActionDeleteToken]; + id mockOperation = OCMPartialMock(operation); + [[[mockOperation stub] andDo:^(NSInvocation *invocation) { + [invocation.target finishWithResult:FIRInstanceIDTokenOperationSucceeded token:nil error:nil]; + }] performTokenOperation]; + + // Return our fake operation when asked for an operation + [[[self.mockTokenManager stub] andReturn:operation] + createDeleteOperationWithAuthorizedEntity:[OCMArg any] + scope:[OCMArg any] + checkinPreferences:[OCMArg any] + keyPair:[OCMArg any] + action:FIRInstanceIDTokenActionDeleteToken]; + + [self.tokenManager deleteTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + keyPair:[OCMArg any] + handler:^(NSError *error) { + // Keep 'operation' alive, so it's not prematurely + // destroyed + XCTAssertNotNil(operation); + XCTAssertNotNil(mockOperation); + XCTAssertNil(error); + [deleteExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + XCTAssertNil(error.localizedDescription); + }]; +} + +/** + * Tests that when a token deletion fails, the callback is invoked with an error. + */ +- (void)testDeleteTokenFailure { + XCTestExpectation *deleteExpectation = + [self expectationWithDescription:@"Delete handler invoked."]; + + // Create a fake operation that always fails + FIRInstanceIDTokenDeleteOperation *operation = [[FIRInstanceIDTokenDeleteOperation alloc] + initWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + checkinPreferences:self.fakeCheckin + keyPair:[OCMArg any] + action:FIRInstanceIDTokenActionDeleteToken]; + id mockOperation = OCMPartialMock(operation); + [[[mockOperation stub] andDo:^(NSInvocation *invocation) { + NSError *someError = [[NSError alloc] initWithDomain:@"InstanceIDUnitTest" code:0 userInfo:nil]; + [invocation.target finishWithResult:FIRInstanceIDTokenOperationError token:nil error:someError]; + }] performTokenOperation]; + + // Return our fake operation when asked for an operation + [[[self.mockTokenManager stub] andReturn:operation] + createDeleteOperationWithAuthorizedEntity:[OCMArg any] + scope:[OCMArg any] + checkinPreferences:[OCMArg any] + keyPair:[OCMArg any] + action:FIRInstanceIDTokenActionDeleteToken]; + + [self.tokenManager deleteTokenWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + keyPair:[OCMArg any] + handler:^(NSError *error) { + // Keep 'operation' alive, so it's not prematurely + // destroyed + XCTAssertNotNil(operation); + XCTAssertNotNil(mockOperation); + XCTAssertNotNil(error); + [deleteExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + XCTAssertNil(error.localizedDescription); + }]; +} + +#pragma mark - Cached Token Invalidation + +- (void)testCachedTokensInvalidatedOnAppVersionChange { + // Write some fake tokens to cache with a old app version "0.9" + NSArray *entities = @[ @"entity1", @"entity2" ]; + for (NSString *entity in entities) { + FIRInstanceIDTokenInfo *info = + [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:entity + scope:kScope + token:@"abcdef" + appVersion:@"0.9" + firebaseAppID:nil]; + [self.tokenStore saveTokenInfo:info handler:nil]; + } + + // Ensure they tokens now exist. + for (NSString *entity in entities) { + FIRInstanceIDTokenInfo *cachedTokenInfo = + [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope]; + XCTAssertNotNil(cachedTokenInfo); + } + + // Trigger a potential reset, the current app version is 1.0 which is newer than + // the one set in tokenInfo. + [self.tokenManager checkForTokenRefreshPolicy]; + + // Ensure that token data is now missing + for (NSString *entity in entities) { + FIRInstanceIDTokenInfo *cachedTokenInfo = + [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope]; + XCTAssertNil(cachedTokenInfo); + } +} + +- (void)testCachedTokensInvalidatedOnAPNSAddition { + // Write some fake tokens to cache, which have no APNs info + NSArray *entities = @[ @"entity1", @"entity2" ]; + for (NSString *entity in entities) { + FIRInstanceIDTokenInfo *info = + [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:entity + scope:kScope + token:@"abcdef" + appVersion:nil + firebaseAppID:nil]; + [self.tokenStore saveTokenInfo:info handler:nil]; + } + + // Ensure the tokens now exist. + for (NSString *entity in entities) { + FIRInstanceIDTokenInfo *cachedTokenInfo = + [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope]; + XCTAssertNotNil(cachedTokenInfo); + } + + // Trigger a potential reset. + [self triggerAPNSTokenChange]; + + // Ensure that token data is now missing + for (NSString *entity in entities) { + FIRInstanceIDTokenInfo *cachedTokenInfo = + [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope]; + XCTAssertNil(cachedTokenInfo); + } +} + +- (void)testCachedTokensInvalidatedOnAPNSChange { + // Write some fake tokens to cache + NSArray *entities = @[ @"entity1", @"entity2" ]; + NSData *oldAPNSData = [@"oldAPNSToken" dataUsingEncoding:NSUTF8StringEncoding]; + for (NSString *entity in entities) { + FIRInstanceIDTokenInfo *info = + [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:entity + scope:kScope + token:@"abcdef" + appVersion:nil + firebaseAppID:nil]; + info.APNSInfo = [[FIRInstanceIDAPNSInfo alloc] initWithDeviceToken:oldAPNSData isSandbox:NO]; + [self.tokenStore saveTokenInfo:info handler:nil]; + } + + // Ensure the tokens now exist. + for (NSString *entity in entities) { + FIRInstanceIDTokenInfo *cachedTokenInfo = + [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope]; + XCTAssertNotNil(cachedTokenInfo); + } + + // Trigger a potential reset. + [self triggerAPNSTokenChange]; + + // Ensure that token data is now missing + for (NSString *entity in entities) { + FIRInstanceIDTokenInfo *cachedTokenInfo = + [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope]; + XCTAssertNil(cachedTokenInfo); + } +} + +- (void)testCachedTokensNotInvalidatedIfAPNSSame { + // Write some fake tokens to cache, with the current APNs token + NSArray *entities = @[ @"entity1", @"entity2" ]; + NSString *apnsDataString = kNewAPNSTokenString; + NSData *currentAPNSData = [apnsDataString dataUsingEncoding:NSUTF8StringEncoding]; + for (NSString *entity in entities) { + FIRInstanceIDTokenInfo *info = + [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:entity + scope:kScope + token:@"abcdef" + appVersion:nil + firebaseAppID:nil]; + info.APNSInfo = [[FIRInstanceIDAPNSInfo alloc] initWithDeviceToken:currentAPNSData + isSandbox:NO]; + [self.tokenStore saveTokenInfo:info handler:nil]; + } + + // Ensure the tokens now exist. + for (NSString *entity in entities) { + FIRInstanceIDTokenInfo *cachedTokenInfo = + [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope]; + XCTAssertNotNil(cachedTokenInfo); + } + + // Trigger a potential reset. + [self triggerAPNSTokenChange]; + + // Ensure that token data is still there + for (NSString *entity in entities) { + FIRInstanceIDTokenInfo *cachedTokenInfo = + [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope]; + XCTAssertNotNil(cachedTokenInfo); + } +} + +- (void)triggerAPNSTokenChange { + // Trigger a potential reset. + NSData *deviceToken = [kNewAPNSTokenString dataUsingEncoding:NSUTF8StringEncoding]; + [self.tokenManager updateTokensToAPNSDeviceToken:deviceToken isSandbox:NO]; +} + +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDTokenOperationsTest.m b/Example/InstanceID/Tests/FIRInstanceIDTokenOperationsTest.m new file mode 100644 index 00000000000..135f8cb2085 --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDTokenOperationsTest.m @@ -0,0 +1,345 @@ +/* + * Copyright 2019 Google + * + * 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 +#import "Firebase/InstanceID/FIRInstanceIDAuthService.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h" +#import "Firebase/InstanceID/FIRInstanceIDCheckinService.h" +#import "Firebase/InstanceID/FIRInstanceIDConstants.h" +#import "Firebase/InstanceID/FIRInstanceIDKeyPair.h" +#import "Firebase/InstanceID/FIRInstanceIDKeyPairStore.h" +#import "Firebase/InstanceID/FIRInstanceIDKeychain.h" +#import "Firebase/InstanceID/FIRInstanceIDStore.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenDeleteOperation.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenFetchOperation.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenOperation+Private.h" +#import "Firebase/InstanceID/FIRInstanceIDTokenOperation.h" +#import "Firebase/InstanceID/NSError+FIRInstanceID.h" +#import "Firebase/InstanceID/Public/FIRInstanceID.h" + +static NSString *kDeviceID = @"fakeDeviceID"; +static NSString *kSecretToken = @"fakeSecretToken"; +static NSString *kDigestString = @"test-digest"; +static NSString *kVersionInfoString = @"version_info-1.0.0"; +static NSString *kAuthorizedEntity = @"sender-1234567"; +static NSString *kScope = @"fcm"; +static NSString *kRegistrationToken = @"token-12345"; + +static NSString *const kPrivateKeyPairTag = @"com.iid.regclient.test.private"; +static NSString *const kPublicKeyPairTag = @"com.iid.regclient.test.public"; + +@interface FIRInstanceIDKeyPairStore (ExposedForTest) ++ (void)deleteKeyPairWithPrivateTag:(NSString *)privateTag + publicTag:(NSString *)publicTag + handler:(void (^)(NSError *))handler; +@end + +@interface FIRInstanceIDTokenOperation (ExposedForTest) +- (void)performTokenOperation; +@end + +@interface FIRInstanceIDTokenOperationsTest : XCTestCase + +@property(strong, readonly, nonatomic) FIRInstanceIDAuthService *authService; +@property(strong, readonly, nonatomic) id mockAuthService; + +@property(strong, readonly, nonatomic) id mockStore; +@property(strong, readonly, nonatomic) FIRInstanceIDCheckinService *checkinService; +@property(strong, readonly, nonatomic) id mockCheckinService; + +@property(strong, readonly, nonatomic) FIRInstanceIDKeyPair *keyPair; + +@property(nonatomic, readwrite, strong) FIRInstanceIDCheckinPreferences *checkinPreferences; + +@end + +@implementation FIRInstanceIDTokenOperationsTest + +- (void)setUp { + [super setUp]; + _mockStore = OCMClassMock([FIRInstanceIDStore class]); + _checkinService = [[FIRInstanceIDCheckinService alloc] init]; + _mockCheckinService = OCMPartialMock(_checkinService); + _authService = [[FIRInstanceIDAuthService alloc] initWithCheckinService:_mockCheckinService + store:_mockStore]; + // Create a temporary keypair in Keychain + _keyPair = + [[FIRInstanceIDKeychain sharedInstance] generateKeyPairWithPrivateTag:kPrivateKeyPairTag + publicTag:kPublicKeyPairTag]; +} + +- (void)tearDown { + [FIRInstanceIDKeyPairStore deleteKeyPairWithPrivateTag:kPrivateKeyPairTag + publicTag:kPublicKeyPairTag + handler:nil]; + [super tearDown]; +} + +- (void)testThatTokenOperationsAuthHeaderStringMatchesCheckin { + int64_t tenHoursAgo = FIRInstanceIDCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000; + FIRInstanceIDCheckinPreferences *checkin = + [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo]; + + NSString *expectedAuthHeader = [FIRInstanceIDTokenOperation HTTPAuthHeaderFromCheckin:checkin]; + XCTestExpectation *authHeaderMatchesCheckinExpectation = + [self expectationWithDescription:@"Auth header string in request matches checkin info"]; + FIRInstanceIDTokenFetchOperation *operation = + [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + options:nil + checkinPreferences:checkin + keyPair:self.keyPair]; + operation.testBlock = + ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock response) { + NSDictionary *headers = request.allHTTPHeaderFields; + NSString *authHeader = headers[@"Authorization"]; + if ([authHeader isEqualToString:expectedAuthHeader]) { + [authHeaderMatchesCheckinExpectation fulfill]; + } + + // Return a response (doesnt matter what the response is) + NSData *responseBody = [self dataForFetchRequest:request returnValidToken:YES]; + NSHTTPURLResponse *responseObject = [[NSHTTPURLResponse alloc] initWithURL:request.URL + statusCode:200 + HTTPVersion:@"HTTP/1.1" + headerFields:nil]; + response(responseBody, responseObject, nil); + }; + + [operation start]; + + [self waitForExpectationsWithTimeout:0.25 + handler:^(NSError *_Nullable error) { + XCTAssertNil(error.localizedDescription); + }]; +} + +- (void)testThatTokenOperationWithoutCheckInFails { + // If asserts are enabled, test for the assert to be thrown, otherwise check for the resulting + // error in the completion handler. + XCTestExpectation *failedExpectation = + [self expectationWithDescription:@"Operation failed without checkin info"]; + + // This will return hasCheckinInfo == NO + FIRInstanceIDCheckinPreferences *emptyCheckinPreferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:@"" secretToken:@""]; + + FIRInstanceIDTokenOperation *operation = + [[FIRInstanceIDTokenOperation alloc] initWithAction:FIRInstanceIDTokenActionFetch + forAuthorizedEntity:kAuthorizedEntity + scope:kScope + options:nil + checkinPreferences:emptyCheckinPreferences + keyPair:self.keyPair]; + [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result, + NSString *_Nullable token, NSError *_Nullable error) { + [failedExpectation fulfill]; + }]; + + @try { + [operation start]; + } @catch (NSException *exception) { + if (exception.name == NSInternalInconsistencyException) { + [failedExpectation fulfill]; + } + } @finally { + } + + [self waitForExpectationsWithTimeout:0.25 + handler:^(NSError *_Nullable error) { + XCTAssertNil(error.localizedDescription); + }]; +} + +- (void)testThatAnAlreadyCancelledOperationFinishesWithoutStarting { + XCTestExpectation *cancelledExpectation = + [self expectationWithDescription:@"Operation finished as cancelled"]; + XCTestExpectation *didNotCallPerform = + [self expectationWithDescription:@"Did not call performTokenOperation"]; + __block BOOL performWasCalled = NO; + + int64_t tenHoursAgo = FIRInstanceIDCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000; + FIRInstanceIDCheckinPreferences *checkinPreferences = + [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo]; + + FIRInstanceIDTokenOperation *operation = + [[FIRInstanceIDTokenOperation alloc] initWithAction:FIRInstanceIDTokenActionFetch + forAuthorizedEntity:kAuthorizedEntity + scope:kScope + options:nil + checkinPreferences:checkinPreferences + keyPair:self.keyPair]; + [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result, + NSString *_Nullable token, NSError *_Nullable error) { + if (result == FIRInstanceIDTokenOperationCancelled) { + [cancelledExpectation fulfill]; + } + + if (!performWasCalled) { + [didNotCallPerform fulfill]; + } + }]; + id mockOperation = OCMPartialMock(operation); + [[[mockOperation stub] andDo:^(NSInvocation *invocation) { + performWasCalled = YES; + }] performTokenOperation]; + + [operation cancel]; + [operation start]; + + [self waitForExpectationsWithTimeout:0.25 + handler:^(NSError *_Nullable error) { + XCTAssertNil(error.localizedDescription); + }]; +} + +- (void)testThatOptionsDictionaryIsIncludedWithFetchRequest { + XCTestExpectation *optionsIncludedExpectation = + [self expectationWithDescription:@"Options keys were included in token URL request"]; + int64_t tenHoursAgo = FIRInstanceIDCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000; + FIRInstanceIDCheckinPreferences *checkinPreferences = + [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo]; + NSData *fakeDeviceToken = [@"fakeAPNSToken" dataUsingEncoding:NSUTF8StringEncoding]; + BOOL isSandbox = NO; + NSString *apnsTupleString = + FIRInstanceIDAPNSTupleStringForTokenAndServerType(fakeDeviceToken, isSandbox); + NSDictionary *options = @{ + kFIRInstanceIDTokenOptionsFirebaseAppIDKey : @"fakeGMPAppID", + kFIRInstanceIDTokenOptionsAPNSKey : fakeDeviceToken, + kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(isSandbox), + }; + + FIRInstanceIDTokenFetchOperation *operation = + [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + options:options + checkinPreferences:checkinPreferences + keyPair:self.keyPair]; + operation.testBlock = + ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock response) { + NSString *query = [[NSString alloc] initWithData:request.HTTPBody + encoding:NSUTF8StringEncoding]; + NSString *gmpAppIDQueryTuple = + [NSString stringWithFormat:@"%@=%@", kFIRInstanceIDTokenOptionsFirebaseAppIDKey, + options[kFIRInstanceIDTokenOptionsFirebaseAppIDKey]]; + NSRange gmpAppIDRange = [query rangeOfString:gmpAppIDQueryTuple]; + + NSString *apnsQueryTuple = [NSString + stringWithFormat:@"%@=%@", kFIRInstanceIDTokenOptionsAPNSKey, apnsTupleString]; + NSRange apnsRange = [query rangeOfString:apnsQueryTuple]; + + if (gmpAppIDRange.location != NSNotFound && apnsRange.location != NSNotFound) { + [optionsIncludedExpectation fulfill]; + } + + // Return a response (doesnt matter what the response is) + NSData *responseBody = [self dataForFetchRequest:request returnValidToken:YES]; + NSHTTPURLResponse *responseObject = [[NSHTTPURLResponse alloc] initWithURL:request.URL + statusCode:200 + HTTPVersion:@"HTTP/1.1" + headerFields:nil]; + response(responseBody, responseObject, nil); + }; + + [operation start]; + + [self waitForExpectationsWithTimeout:0.25 + handler:^(NSError *_Nullable error) { + XCTAssertNil(error.localizedDescription); + }]; +} + +- (void)testServerResetCommand { + XCTestExpectation *shouldResetIdentityExpectation = + [self expectationWithDescription: + @"When server sends down RST error, clients should return reset identity error."]; + int64_t tenHoursAgo = FIRInstanceIDCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000; + FIRInstanceIDCheckinPreferences *checkinPreferences = + [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo]; + + FIRInstanceIDTokenFetchOperation *operation = + [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity + scope:kScope + options:nil + checkinPreferences:checkinPreferences + keyPair:self.keyPair]; + operation.testBlock = + ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock response) { + // Return a response with Error=RST + NSData *responseBody = [self dataForFetchRequest:request returnValidToken:NO]; + NSHTTPURLResponse *responseObject = [[NSHTTPURLResponse alloc] initWithURL:request.URL + statusCode:200 + HTTPVersion:@"HTTP/1.1" + headerFields:nil]; + response(responseBody, responseObject, nil); + }; + + [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result, + NSString *_Nullable token, NSError *_Nullable error) { + XCTAssertEqual(result, FIRInstanceIDTokenOperationError); + XCTAssertNotNil(error); + XCTAssertEqual(error.code, kFIRInstanceIDErrorCodeInvalidIdentity); + + [shouldResetIdentityExpectation fulfill]; + }]; + + [operation start]; + + [self waitForExpectationsWithTimeout:0.25 + handler:^(NSError *_Nullable error) { + XCTAssertNil(error.localizedDescription); + }]; +} + +- (void)testHTTPAuthHeaderGenerationFromCheckin { + FIRInstanceIDCheckinPreferences *checkinPreferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceID secretToken:kSecretToken]; + NSString *expectedHeader = + [NSString stringWithFormat:@"AidLogin %@:%@", checkinPreferences.deviceID, + checkinPreferences.secretToken]; + NSString *generatedHeader = + [FIRInstanceIDTokenOperation HTTPAuthHeaderFromCheckin:checkinPreferences]; + XCTAssertEqualObjects(generatedHeader, expectedHeader); +} + +#pragma mark - Internal Helpers +- (NSData *)dataForFetchRequest:(NSURLRequest *)request returnValidToken:(BOOL)returnValidToken { + NSString *response; + if (returnValidToken) { + response = [NSString stringWithFormat:@"token=%@", kRegistrationToken]; + } else { + response = @"Error=RST"; + } + return [response dataUsingEncoding:NSUTF8StringEncoding]; +} + +- (FIRInstanceIDCheckinPreferences *)setCheckinPreferencesWithLastCheckinTime:(int64_t)time { + FIRInstanceIDCheckinPreferences *checkinPreferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceID secretToken:kSecretToken]; + NSDictionary *checkinPlistContents = @{ + kFIRInstanceIDDigestStringKey : kDigestString, + kFIRInstanceIDVersionInfoStringKey : kVersionInfoString, + kFIRInstanceIDLastCheckinTimeKey : @(time) + }; + [checkinPreferences updateWithCheckinPlistContents:checkinPlistContents]; + // manually initialize the checkin preferences + self.checkinPreferences = checkinPreferences; + return checkinPreferences; +} + +@end diff --git a/Example/InstanceID/Tests/FIRInstanceIDUtilitiesTest.m b/Example/InstanceID/Tests/FIRInstanceIDUtilitiesTest.m new file mode 100644 index 00000000000..8962d22fbe2 --- /dev/null +++ b/Example/InstanceID/Tests/FIRInstanceIDUtilitiesTest.m @@ -0,0 +1,95 @@ +/* + * Copyright 2019 Google + * + * 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 +#import "Firebase/InstanceID/FIRInstanceIDUtilities.h" + +@interface FIRInstanceIDUtilitiesTest : XCTestCase + +@property(nonatomic, strong) id mainBundleMock; + +@end + +@implementation FIRInstanceIDUtilitiesTest + +- (void)setUp { + _mainBundleMock = OCMPartialMock([NSBundle mainBundle]); + [super setUp]; +} + +- (void)tearDown { + [_mainBundleMock stopMocking]; + [super tearDown]; +} + +- (void)testAPNSTupleStringReturnsNilIfDeviceTokenNil { + NSString *tupleString = FIRInstanceIDAPNSTupleStringForTokenAndServerType(nil, NO); + XCTAssertNil(tupleString); +} + +- (void)testAPNSTupleStringReturnsValidData { + NSData *deviceToken = [@"FAKE_DEVICE_TOKEN" dataUsingEncoding:NSUTF8StringEncoding]; + NSString *expectedTokenString = FIRInstanceIDStringForAPNSDeviceToken(deviceToken); + NSString *tupleString = FIRInstanceIDAPNSTupleStringForTokenAndServerType(deviceToken, NO); + NSArray *components = [tupleString componentsSeparatedByString:@"_"]; + XCTAssertTrue(components.count == 2); + XCTAssertEqualObjects(components.firstObject, @"p"); + XCTAssertEqualObjects(components.lastObject, expectedTokenString); +} + +- (void)testAppVersionReturnsExpectedValue { + NSString *expectedVersion = @"1.2.3"; + NSDictionary *fakeInfoDictionary = @{@"CFBundleShortVersionString" : expectedVersion}; + [[[_mainBundleMock stub] andReturn:fakeInfoDictionary] infoDictionary]; + NSString *appVersion = FIRInstanceIDCurrentAppVersion(); + XCTAssertEqualObjects(appVersion, expectedVersion); +} + +- (void)testAppVersionReturnsEmptyStringWhenNotFound { + NSDictionary *fakeInfoDictionary = @{}; + [[[_mainBundleMock stub] andReturn:fakeInfoDictionary] infoDictionary]; + NSString *appVersion = FIRInstanceIDCurrentAppVersion(); + XCTAssertEqualObjects(appVersion, @""); +} + +- (void)testAppIdentifierReturnsExpectedValue { + NSString *expectedIdentifier = @"com.me.myapp"; + [[[_mainBundleMock stub] andReturn:expectedIdentifier] bundleIdentifier]; + NSString *appIdentifier = FIRInstanceIDAppIdentifier(); + XCTAssertEqualObjects(appIdentifier, expectedIdentifier); +} + +- (void)testAppIdentifierReturnsEmptyStringWhenNotFound { + [[[_mainBundleMock stub] andReturn:nil] bundleIdentifier]; + NSString *appIdentifier = FIRInstanceIDAppIdentifier(); + XCTAssertEqualObjects(appIdentifier, @""); +} + +- (void)testLocaleHasChanged { + NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier]; + [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:appDomain]; + XCTAssertTrue(FIRInstanceIDHasLocaleChanged()); + [[NSUserDefaults standardUserDefaults] setObject:FIRInstanceIDCurrentLocale() + forKey:kFIRInstanceIDUserDefaultsKeyLocale]; + XCTAssertFalse(FIRInstanceIDHasLocaleChanged()); + [[NSUserDefaults standardUserDefaults] setObject:@"zh-Hant" + forKey:kFIRInstanceIDUserDefaultsKeyLocale]; + XCTAssertTrue(FIRInstanceIDHasLocaleChanged()); +} + +@end diff --git a/Example/InstanceID/Tests/Info.plist b/Example/InstanceID/Tests/Info.plist new file mode 100644 index 00000000000..6c6c23c43ad --- /dev/null +++ b/Example/InstanceID/Tests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Example/Messaging/App/tvOS/AppDelegate.h b/Example/Messaging/App/tvOS/AppDelegate.h new file mode 100644 index 00000000000..68cb42947e4 --- /dev/null +++ b/Example/Messaging/App/tvOS/AppDelegate.h @@ -0,0 +1,23 @@ +// Copyright 2019 Google +// +// 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 + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + + +@end + diff --git a/Example/Messaging/App/tvOS/AppDelegate.m b/Example/Messaging/App/tvOS/AppDelegate.m new file mode 100644 index 00000000000..ddbddf17640 --- /dev/null +++ b/Example/Messaging/App/tvOS/AppDelegate.m @@ -0,0 +1,57 @@ +// Copyright 2019 Google +// +// 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 "AppDelegate.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + + +- (void)applicationWillResignActive:(UIApplication *)application { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. +} + + +- (void)applicationDidEnterBackground:(UIApplication *)application { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. +} + + +- (void)applicationWillEnterForeground:(UIApplication *)application { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. +} + + +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. +} + + +- (void)applicationWillTerminate:(UIApplication *)application { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. +} + + +@end diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..48ecb4fa43e --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { + "idiom" : "tv" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json new file mode 100644 index 00000000000..d29f024ed5c --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..48ecb4fa43e --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { + "idiom" : "tv" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..48ecb4fa43e --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { + "idiom" : "tv" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..16a370df014 --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json new file mode 100644 index 00000000000..d29f024ed5c --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..16a370df014 --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..16a370df014 --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json new file mode 100644 index 00000000000..db288f368f1 --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json @@ -0,0 +1,32 @@ +{ + "assets" : [ + { + "size" : "1280x768", + "idiom" : "tv", + "filename" : "App Icon - App Store.imagestack", + "role" : "primary-app-icon" + }, + { + "size" : "400x240", + "idiom" : "tv", + "filename" : "App Icon.imagestack", + "role" : "primary-app-icon" + }, + { + "size" : "2320x720", + "idiom" : "tv", + "filename" : "Top Shelf Image Wide.imageset", + "role" : "top-shelf-image-wide" + }, + { + "size" : "1920x720", + "idiom" : "tv", + "filename" : "Top Shelf Image.imageset", + "role" : "top-shelf-image" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json new file mode 100644 index 00000000000..7dc95020229 --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + }, + { + "idiom" : "tv-marketing", + "scale" : "1x" + }, + { + "idiom" : "tv-marketing", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json new file mode 100644 index 00000000000..7dc95020229 --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + }, + { + "idiom" : "tv-marketing", + "scale" : "1x" + }, + { + "idiom" : "tv-marketing", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Assets.xcassets/Launch Image.launchimage/Contents.json b/Example/Messaging/App/tvOS/Assets.xcassets/Launch Image.launchimage/Contents.json new file mode 100644 index 00000000000..d746a609003 --- /dev/null +++ b/Example/Messaging/App/tvOS/Assets.xcassets/Launch Image.launchimage/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "orientation" : "landscape", + "idiom" : "tv", + "extent" : "full-screen", + "minimum-system-version" : "11.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "tv", + "extent" : "full-screen", + "minimum-system-version" : "9.0", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Messaging/App/tvOS/Base.lproj/Main.storyboard b/Example/Messaging/App/tvOS/Base.lproj/Main.storyboard new file mode 100644 index 00000000000..72d5e2239c4 --- /dev/null +++ b/Example/Messaging/App/tvOS/Base.lproj/Main.storyboard @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Messaging/App/tvOS/Info.plist b/Example/Messaging/App/tvOS/Info.plist new file mode 100644 index 00000000000..02942a34f3e --- /dev/null +++ b/Example/Messaging/App/tvOS/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UIUserInterfaceStyle + Automatic + + diff --git a/Example/Messaging/App/tvOS/ViewController.h b/Example/Messaging/App/tvOS/ViewController.h new file mode 100644 index 00000000000..a11ba59dd2b --- /dev/null +++ b/Example/Messaging/App/tvOS/ViewController.h @@ -0,0 +1,21 @@ +// Copyright 2019 Google +// +// 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 + +@interface ViewController : UIViewController + + +@end + diff --git a/Example/Messaging/App/tvOS/ViewController.m b/Example/Messaging/App/tvOS/ViewController.m new file mode 100644 index 00000000000..34b87b726a0 --- /dev/null +++ b/Example/Messaging/App/tvOS/ViewController.m @@ -0,0 +1,35 @@ +// Copyright 2019 Google +// +// 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 "ViewController.h" + +@interface ViewController () + +@end + +@implementation ViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. +} + + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + + +@end diff --git a/Example/Messaging/App/tvOS/main.m b/Example/Messaging/App/tvOS/main.m new file mode 100644 index 00000000000..4871a46ea97 --- /dev/null +++ b/Example/Messaging/App/tvOS/main.m @@ -0,0 +1,22 @@ +// Copyright 2019 Google +// +// 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 "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/Example/Messaging/Tests/FIRInstanceIDWithFCMTest.m b/Example/Messaging/Tests/FIRInstanceIDWithFCMTest.m new file mode 100644 index 00000000000..afaabbcc38c --- /dev/null +++ b/Example/Messaging/Tests/FIRInstanceIDWithFCMTest.m @@ -0,0 +1,81 @@ +/* + * Copyright 2019 Google + * + * 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 +#import +#import +#import "FIRMessaging_Private.h" +#import "FIRMessaging.h" +#import "FIRMessagingTestUtilities.h" + +@interface FIRInstanceID (ExposedForTest) +- (BOOL)isFCMAutoInitEnabled; +- (instancetype)initPrivately; +- (void)start; +@end + +@interface FIRMessaging () ++ (FIRMessaging *)messagingForTests; +@end + +@interface FIRInstanceIDTest : XCTestCase + +@property(nonatomic, readwrite, strong) FIRInstanceID *instanceID; +@property(nonatomic, readwrite, strong) id mockFirebaseApp; + +@end + +@implementation FIRInstanceIDTest + +- (void)setUp { + [super setUp]; + _instanceID = [[FIRInstanceID alloc] initPrivately]; + [_instanceID start]; + _mockFirebaseApp = OCMClassMock([FIRApp class]); + OCMStub([_mockFirebaseApp defaultApp]).andReturn(_mockFirebaseApp); +} + +- (void)tearDown { + self.instanceID = nil; + [_mockFirebaseApp stopMocking]; + [super tearDown]; +} + +- (void)testFCMAutoInitEnabled { + NSString *const kFIRMessagingTestsAutoInit = @"com.messaging.test_autoInit"; + NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:kFIRMessagingTestsAutoInit]; + FIRMessaging *messaging = [FIRMessagingTestUtilities messagingForTestsWithUserDefaults:defaults]; + id classMock = OCMClassMock([FIRMessaging class]); + OCMStub([classMock messaging]).andReturn(messaging); + OCMStub([_mockFirebaseApp isDataCollectionDefaultEnabled]).andReturn(YES); + messaging.autoInitEnabled = YES; + XCTAssertTrue( + [_instanceID isFCMAutoInitEnabled], + @"When FCM is available, FCM Auto Init Enabled should be FCM's autoInitEnable property."); + + messaging.autoInitEnabled = NO; + XCTAssertFalse( + [_instanceID isFCMAutoInitEnabled], + @"When FCM is available, FCM Auto Init Enabled should be FCM's autoInitEnable property."); + + messaging.autoInitEnabled = YES; + XCTAssertTrue([_instanceID isFCMAutoInitEnabled]); + [classMock stopMocking]; +} + +@end diff --git a/Example/Messaging/Tests/FIRMessagingAnalyticsTest.m b/Example/Messaging/Tests/FIRMessagingAnalyticsTest.m index c8f7e26b803..352fd078c17 100644 --- a/Example/Messaging/Tests/FIRMessagingAnalyticsTest.m +++ b/Example/Messaging/Tests/FIRMessagingAnalyticsTest.m @@ -97,6 +97,20 @@ - (NSInteger)maxUserProperties:(nonnull NSString *)origin { - (void)setConditionalUserProperty:(nonnull FIRAConditionalUserProperty *)conditionalUserProperty { } +- (void)checkLastNotificationForOrigin:(nonnull NSString *)origin + queue:(nonnull dispatch_queue_t)queue + callback:(nonnull void (^)(NSString *_Nullable)) + currentLastNotificationProperty { +} + +- (void)registerAnalyticsListener:(nonnull id)listener + withOrigin:(nonnull NSString *)origin { +} + +- (void)unregisterAnalyticsListenerWithOrigin:(nonnull NSString *)origin { +} + + @end @interface FIRMessagingAnalytics (ExposedForTest) diff --git a/Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m b/Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m index 805ad109f28..d8b684f5cec 100644 --- a/Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m +++ b/Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m @@ -71,6 +71,7 @@ - (void)testValidContextManagerMessage { * Context Manager message with future start date should be successfully scheduled. */ - (void)testMessageWithFutureStartTime { +#if TARGET_OS_IOS NSString *messageIdentifier = @"fcm-cm-test1"; NSString *startTimeString = @"2020-01-12 12:00:00"; // way into the future NSDictionary *message = @{ @@ -83,15 +84,18 @@ - (void)testMessageWithFutureStartTime { XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]); XCTAssertEqual(self.scheduledLocalNotifications.count, 1); + UILocalNotification *notification = [self.scheduledLocalNotifications firstObject]; NSDate *date = [self.dateFormatter dateFromString:startTimeString]; XCTAssertEqual([notification.fireDate compare:date], NSOrderedSame); +#endif } /** * Context Manager message with past end date should not be scheduled. */ - (void)testMessageWithPastEndTime { +#if TARGET_OS_IOS NSString *messageIdentifier = @"fcm-cm-test1"; NSString *startTimeString = @"2010-01-12 12:00:00"; // way into the past NSString *endTimeString = @"2011-01-12 12:00:00"; // way into the past @@ -105,6 +109,7 @@ - (void)testMessageWithPastEndTime { XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]); XCTAssertEqual(self.scheduledLocalNotifications.count, 0); +#endif } /** @@ -112,6 +117,7 @@ - (void)testMessageWithPastEndTime { * scheduled. */ - (void)testMessageWithPastStartAndFutureEndTime { +#if TARGET_OS_IOS NSString *messageIdentifier = @"fcm-cm-test1"; NSDate *startDate = [NSDate dateWithTimeIntervalSinceNow:-1000]; // past NSDate *endDate = [NSDate dateWithTimeIntervalSinceNow:1000]; // future @@ -134,12 +140,14 @@ - (void)testMessageWithPastStartAndFutureEndTime { XCTAssertEqual([notification.fireDate compare:startDate], NSOrderedDescending); // schedule notification after end date XCTAssertEqual([notification.fireDate compare:endDate], NSOrderedAscending); +#endif } /** * Test correctly parsing user data in local notifications. */ - (void)testTimedNotificationsUserInfo { +#if TARGET_OS_IOS NSString *messageIdentifierKey = @"message.id"; NSString *messageIdentifier = @"fcm-cm-test1"; NSString *startTimeString = @"2020-01-12 12:00:00"; // way into the future @@ -159,11 +167,14 @@ - (void)testTimedNotificationsUserInfo { UILocalNotification *notification = [self.scheduledLocalNotifications firstObject]; XCTAssertEqualObjects(notification.userInfo[messageIdentifierKey], messageIdentifier); XCTAssertEqualObjects(notification.userInfo[customDataKey], customData); +#endif + } #pragma mark - Private Helpers - (void)mockSchedulingLocalNotifications { +#if TARGET_OS_IOS id mockApplication = OCMPartialMock([UIApplication sharedApplication]); __block UILocalNotification *notificationToSchedule; [[[mockApplication stub] @@ -179,6 +190,7 @@ - (void)mockSchedulingLocalNotifications { } return NO; }]]; +#endif } @end diff --git a/Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m b/Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m index 33797a2f197..b8a2937e606 100644 --- a/Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m +++ b/Example/Messaging/Tests/FIRMessagingLinkHandlingTest.m @@ -21,10 +21,12 @@ #import "FIRMessaging.h" #import "FIRMessagingConstants.h" #import "FIRMessagingTestNotificationUtilities.h" +#import "FIRMessagingTestUtilities.h" + +NSString *const kFIRMessagingTestsLinkHandlingSuiteName = @"com.messaging.test_linkhandling"; @interface FIRMessaging () -+ (FIRMessaging *)messagingForTests; - (NSURL *)linkURLFromMessage:(NSDictionary *)message; @end @@ -39,10 +41,13 @@ @implementation FIRMessagingLinkHandlingTest - (void)setUp { [super setUp]; - _messaging = [FIRMessaging messagingForTests]; + + NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:kFIRMessagingTestsLinkHandlingSuiteName]; + _messaging = [FIRMessagingTestUtilities messagingForTestsWithUserDefaults:defaults]; } - (void)tearDown { + [self.messaging.messagingUserDefaults removePersistentDomainForName:kFIRMessagingTestsLinkHandlingSuiteName]; _messaging = nil; [super tearDown]; } diff --git a/Example/Messaging/Tests/FIRMessagingReceiverTest.m b/Example/Messaging/Tests/FIRMessagingReceiverTest.m index 02818b754b7..95e6dd9c497 100644 --- a/Example/Messaging/Tests/FIRMessagingReceiverTest.m +++ b/Example/Messaging/Tests/FIRMessagingReceiverTest.m @@ -22,10 +22,9 @@ #import "FIRMessaging.h" #import "FIRMessaging_Private.h" +#import "FIRMessagingTestUtilities.h" -@interface FIRMessaging () -+ (FIRMessaging *)messagingForTests; -@end +NSString *const kFIRMessagingTestsReceiverSuiteName = @"com.messaging.test_receiverTest"; @interface FIRMessagingReceiverTest : XCTestCase @property(nonatomic, readonly, strong) FIRMessaging *messaging; @@ -36,9 +35,15 @@ @implementation FIRMessagingReceiverTest - (void)setUp { [super setUp]; - _messaging = [FIRMessaging messagingForTests]; - [[NSUserDefaults standardUserDefaults] - removePersistentDomainForName:[NSBundle mainBundle].bundleIdentifier]; + NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:kFIRMessagingTestsReceiverSuiteName]; + _messaging = [FIRMessagingTestUtilities messagingForTestsWithUserDefaults:defaults]; +} + +- (void)tearDown { + [self.messaging.messagingUserDefaults removePersistentDomainForName:kFIRMessagingTestsReceiverSuiteName]; + _messaging = nil; + + [super tearDown]; } - (void)testUseMessagingDelegate { diff --git a/Example/Messaging/Tests/FIRMessagingRemoteNotificationsProxyTest.m b/Example/Messaging/Tests/FIRMessagingRemoteNotificationsProxyTest.m index 0e972798c12..7a91dd68bd3 100644 --- a/Example/Messaging/Tests/FIRMessagingRemoteNotificationsProxyTest.m +++ b/Example/Messaging/Tests/FIRMessagingRemoteNotificationsProxyTest.m @@ -70,10 +70,12 @@ @interface FakeAppDelegate : NSObject @property(nonatomic) BOOL remoteNotificationWithFetchHandlerWasCalled; @end @implementation FakeAppDelegate +#if TARGET_OS_IOS - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { self.remoteNotificationMethodWasCalled = YES; } +#endif - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { @@ -101,9 +103,11 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center completionHandler { self.willPresentWasCalled = YES; } +#if TARGET_OS_IOS - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler { self.didReceiveResponseWasCalled = YES; } +#endif // TARGET_OS_IOS @end #endif // __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 @@ -182,6 +186,7 @@ - (void)testIncompleteAppDelegateRemoteNotificationWithFetchHandlerMethod { } - (void)testSwizzledAppDelegateRemoteNotificationMethods { +#if TARGET_OS_IOS FakeAppDelegate *appDelegate = [[FakeAppDelegate alloc] init]; [self.mockProxy swizzleAppDelegateMethods:appDelegate]; [appDelegate application:OCMClassMock([UIApplication class]) didReceiveRemoteNotification:@{}]; @@ -198,6 +203,8 @@ - (void)testSwizzledAppDelegateRemoteNotificationMethods { OCMVerify(FCM_swizzle_appDidReceiveRemoteNotificationWithHandler); // Verify our original method was called XCTAssertTrue(appDelegate.remoteNotificationWithFetchHandlerWasCalled); +#endif + } #if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 @@ -260,7 +267,7 @@ - (void)testSwizzledUserNotificationsCenterDelegate { OCMVerify(FCM_swizzle_willPresentNotificationWithHandler); // Verify our original method was called XCTAssertTrue(delegate.willPresentWasCalled); - +#if TARGET_OS_IOS [delegate userNotificationCenter:OCMClassMock([UNUserNotificationCenter class]) didReceiveNotificationResponse:[self generateMockNotificationResponse] withCompletionHandler:^{}]; @@ -268,6 +275,7 @@ - (void)testSwizzledUserNotificationsCenterDelegate { OCMVerify(FCM_swizzle_appDidReceiveRemoteNotificationWithHandler); // Verify our original method was called XCTAssertTrue(delegate.didReceiveResponseWasCalled); +#endif } - (id)generateMockNotification { @@ -283,10 +291,14 @@ - (id)generateMockNotification { - (id)generateMockNotificationResponse { // Stub out: response.[mock notification above] +#if TARGET_OS_IOS id mockNotificationResponse = OCMClassMock([UNNotificationResponse class]); id mockNotification = [self generateMockNotification]; OCMStub([mockNotificationResponse notification]).andReturn(mockNotification); return mockNotificationResponse; +#else + return nil; +#endif } #endif // __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 diff --git a/Example/Messaging/Tests/FIRMessagingServiceTest.m b/Example/Messaging/Tests/FIRMessagingServiceTest.m index bb447472951..65019806c66 100644 --- a/Example/Messaging/Tests/FIRMessagingServiceTest.m +++ b/Example/Messaging/Tests/FIRMessagingServiceTest.m @@ -22,6 +22,7 @@ #import "FIRMessaging.h" #import "FIRMessagingClient.h" #import "FIRMessagingPubSub.h" +#import "FIRMessagingTestUtilities.h" #import "FIRMessagingTopicsCommon.h" #import "InternalHeaders/FIRMessagingInternalUtilities.h" #import "NSError+FIRMessaging.h" @@ -31,8 +32,9 @@ @"yUTTzK6dhIvLqzqqCSabaa4TQVM0pGTmF6r7tmMHPe6VYiGMHuCwJFgj5v97xl78sUNMLwuPPhoci8z_" @"QGlCrTbxCFGzEUfvA3fGpGgIVQU2W6"; +NSString *const kFIRMessagingTestsServiceSuiteName = @"com.messaging.test_serviceTest"; + @interface FIRMessaging () -+ (FIRMessaging *)messagingForTests; @property(nonatomic, readwrite, strong) FIRMessagingClient *client; @property(nonatomic, readwrite, strong) FIRMessagingPubSub *pubsub; @property(nonatomic, readwrite, strong) NSString *defaultFcmToken; @@ -55,7 +57,8 @@ @interface FIRMessagingServiceTest : XCTestCase { @implementation FIRMessagingServiceTest - (void)setUp { - _messaging = [FIRMessaging messagingForTests]; + NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:kFIRMessagingTestsServiceSuiteName]; + _messaging = [FIRMessagingTestUtilities messagingForTestsWithUserDefaults:defaults]; _messaging.defaultFcmToken = kFakeToken; _mockPubSub = OCMPartialMock(_messaging.pubsub); [_mockPubSub setClient:nil]; @@ -63,6 +66,8 @@ - (void)setUp { } - (void)tearDown { + [_messaging.messagingUserDefaults removePersistentDomainForName:kFIRMessagingTestsServiceSuiteName]; + _messaging = nil; [_mockPubSub stopMocking]; [super tearDown]; } diff --git a/Example/Messaging/Tests/FIRMessagingTest.m b/Example/Messaging/Tests/FIRMessagingTest.m index 66357e8b102..3fb0477ff45 100644 --- a/Example/Messaging/Tests/FIRMessagingTest.m +++ b/Example/Messaging/Tests/FIRMessagingTest.m @@ -20,25 +20,22 @@ #import #import +#import #import "FIRMessaging.h" #import "FIRMessaging_Private.h" +#import "FIRMessagingTestUtilities.h" extern NSString *const kFIRMessagingFCMTokenFetchAPNSOption; -@interface FIRInstanceID (ExposedForTest) - -+ (FIRInstanceID *)instanceIDForTests; - -@end +/// The NSUserDefaults domain for testing. +NSString *const kFIRMessagingDefaultsTestDomain = @"com.messaging.tests"; @interface FIRMessaging () -+ (FIRMessaging *)messagingForTests; @property(nonatomic, readwrite, strong) NSString *defaultFcmToken; @property(nonatomic, readwrite, strong) NSData *apnsTokenData; @property(nonatomic, readwrite, strong) FIRInstanceID *instanceID; -@property(nonatomic, readwrite, strong) NSUserDefaults *messagingUserDefaults; // Direct Channel Methods - (void)updateAutomaticClientConnection; @@ -59,8 +56,12 @@ @implementation FIRMessagingTest - (void)setUp { [super setUp]; - _messaging = [FIRMessaging messagingForTests]; - _messaging.instanceID = [FIRInstanceID instanceIDForTests]; + + // Create the messaging instance with all the necessary dependencies. + NSUserDefaults *defaults = + [[NSUserDefaults alloc] initWithSuiteName:kFIRMessagingDefaultsTestDomain]; + _messaging = [FIRMessagingTestUtilities messagingForTestsWithUserDefaults:defaults]; + _mockFirebaseApp = OCMClassMock([FIRApp class]); OCMStub([_mockFirebaseApp defaultApp]).andReturn(_mockFirebaseApp); _mockInstanceID = OCMPartialMock(self.messaging.instanceID); @@ -69,12 +70,14 @@ - (void)setUp { } - (void)tearDown { + [self.messaging.messagingUserDefaults removePersistentDomainForName:kFIRMessagingDefaultsTestDomain]; self.messaging.shouldEstablishDirectChannel = NO; self.messaging.defaultFcmToken = nil; self.messaging.apnsTokenData = nil; [_mockMessaging stopMocking]; [_mockInstanceID stopMocking]; [_mockFirebaseApp stopMocking]; + _messaging = nil; [super tearDown]; } diff --git a/Example/Messaging/Tests/FIRMessagingTestUtilities.h b/Example/Messaging/Tests/FIRMessagingTestUtilities.h new file mode 100644 index 00000000000..1226cd17c02 --- /dev/null +++ b/Example/Messaging/Tests/FIRMessagingTestUtilities.h @@ -0,0 +1,44 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRMessaging.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRMessaging (TestUtilities) +// Surface the user defaults instance to clean up after tests. +@property(nonatomic, strong) NSUserDefaults *messagingUserDefaults; +@end + +@interface FIRMessagingTestUtilities : NSObject + +/** + Creates an instance of FIRMessaging to use with tests, and will instantiate a new instance of + InstanceID. + + Note: This does not create a FIRApp instance and call `configureWithApp:`. If required, it's up to + each test to do so. + + @param userDefaults The user defaults to be used for Messaging. + @return An instance of FIRMessaging with everything initialized. + */ ++ (FIRMessaging *)messagingForTestsWithUserDefaults:(NSUserDefaults *)userDefaults; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/Messaging/Tests/FIRMessagingTestUtilities.m b/Example/Messaging/Tests/FIRMessagingTestUtilities.m new file mode 100644 index 00000000000..e0314180ac5 --- /dev/null +++ b/Example/Messaging/Tests/FIRMessagingTestUtilities.m @@ -0,0 +1,65 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRMessagingTestUtilities.h" + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRInstanceID (ExposedForTest) + +/// Private initializer to avoid singleton usage. +- (FIRInstanceID *)initPrivately; + +/// Starts fetching and configuration of InstanceID. This is necessary after the `initPrivately` +/// call. +- (void)start; + +@end + +@interface FIRMessaging (ExposedForTest) + +/// Surface internal initializer to avoid singleton usage during tests. +- (instancetype)initWithAnalytics:(nullable id)analytics + withInstanceID:(FIRInstanceID *)instanceID + withUserDefaults:(GULUserDefaults *)defaults; + +/// Kicks off required calls for some messaging tests. +- (void)start; + +@end + +@implementation FIRMessagingTestUtilities + ++ (FIRMessaging *)messagingForTestsWithUserDefaults:(GULUserDefaults *)userDefaults { + // Create the messaging instance with all the necessary dependencies. + FIRInstanceID *instanceID = [[FIRInstanceID alloc] initPrivately]; + [instanceID start]; + + // Create the messaging instance and call `start`. + FIRMessaging *messaging = [[FIRMessaging alloc] initWithAnalytics:nil + withInstanceID:instanceID + withUserDefaults:userDefaults]; + [messaging start]; + return messaging; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Example/Podfile b/Example/Podfile index 7bd3b8bd871..f8016c949ab 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -19,7 +19,7 @@ target 'Core_Example_iOS' do # The next line is the forcing function for the Firebase pod. The Firebase # version's subspecs should depend on the component versions in their # corresponding podspec's. - pod 'Firebase/CoreOnly', '5.16.0' + pod 'Firebase/CoreOnly', '5.19.0' target 'Core_Tests_iOS' do inherit! :search_paths @@ -87,6 +87,17 @@ target 'FDLBuilderTestAppObjC' do end end +target 'InstanceID_Example_iOS' do + platform :ios, '8.0' + + pod 'FirebaseInstanceID' , :path => '../' + + target 'InstanceID_Tests_iOS' do + inherit! :search_paths + pod 'OCMock' + end +end + target 'Messaging_Example_iOS' do platform :ios, '8.0' @@ -124,27 +135,19 @@ target 'Auth_Sample' do pod 'FirebaseCore', :path => '../' pod 'FBSDKLoginKit' pod 'GoogleSignIn' - pod 'FirebaseInstanceID' + pod 'FirebaseInstanceID', :path => '../' pod 'GTMSessionFetcher/Core' target 'Auth_ApiTests' do inherit! :search_paths end - target 'Auth_EarlGreyTests' do + target 'Auth_E2eTests' do inherit! :search_paths pod 'EarlGrey' end end -target 'Auth_SwiftSample' do - platform :ios, '8.0' - pod 'FirebaseAuth', :path => '../' - pod 'FirebaseCore', :path => '../' - pod 'GoogleSignIn' - pod 'FirebaseInstanceID' -end - target 'Core_Example_macOS' do platform :osx, '10.10' @@ -246,3 +249,26 @@ target 'Storage_Example_tvOS' do # inherit! :search_paths # end end + +target 'Messaging_Example_tvOS' do + platform :tvos, '10.0' + + pod 'FirebaseMessaging', :path => '../' + + target 'Messaging_Tests_tvOS' do + inherit! :search_paths + pod 'OCMock' + end +end + +target 'InstanceID_Example_tvOS' do + platform :tvos, '10.0' + + pod 'FirebaseInstanceID', :path => '../' + + target 'InstanceID_Tests_tvOS' do + inherit! :search_paths + pod 'OCMock' + end +end + diff --git a/Example/Storage/Tests/Unit/FIRStorageReferenceTests.m b/Example/Storage/Tests/Unit/FIRStorageReferenceTests.m index 767b9f72783..2bec6d59332 100644 --- a/Example/Storage/Tests/Unit/FIRStorageReferenceTests.m +++ b/Example/Storage/Tests/Unit/FIRStorageReferenceTests.m @@ -162,4 +162,29 @@ - (void)testCopy { XCTAssertNotEqual(ref, copiedRef); } +- (void)testReferenceWithNonExistentFileFailsWithCompletion { + NSString *tempFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"temp.data"]; + FIRStorageReference *ref = [self.storage referenceWithPath:tempFilePath]; + + NSURL *dummyFileURL = [NSURL fileURLWithPath:@"some_non_existing-folder/file.data"]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"completionExpectation"]; + + [ref putFile:dummyFileURL + metadata:nil + completion:^(FIRStorageMetadata *_Nullable metadata, NSError *_Nullable error) { + [expectation fulfill]; + XCTAssertNotNil(error); + XCTAssertNil(metadata); + + XCTAssertEqualObjects(error.domain, FIRStorageErrorDomain); + XCTAssertEqual(error.code, FIRStorageErrorCodeUnknown); + NSString *expectedDescription = [NSString + stringWithFormat:@"File at URL: %@ is not reachable.", dummyFileURL.absoluteString]; + XCTAssertEqualObjects(error.localizedDescription, expectedDescription); + }]; + + [self waitForExpectationsWithTimeout:0.5 handler:NULL]; +} + @end diff --git a/Example/default.profraw b/Example/default.profraw new file mode 100644 index 00000000000..5caaf10542c Binary files /dev/null and b/Example/default.profraw differ diff --git a/Example/tvOSSample/tvOSSample/StorageViewController.swift b/Example/tvOSSample/tvOSSample/StorageViewController.swift index 2e91d92ae01..ed76d7641b7 100644 --- a/Example/tvOSSample/tvOSSample/StorageViewController.swift +++ b/Example/tvOSSample/tvOSSample/StorageViewController.swift @@ -42,7 +42,7 @@ class StorageViewController: UIViewController { } } - /// MARK: - Properties + // MARK: - Properties /// The current internal state of the view controller. private var state: UIState = .cleared { diff --git a/Firebase/Auth/CHANGELOG.md b/Firebase/Auth/CHANGELOG.md index ac4e3ef7894..ea901cb5a56 100644 --- a/Firebase/Auth/CHANGELOG.md +++ b/Firebase/Auth/CHANGELOG.md @@ -1,3 +1,15 @@ +# v5.4.1 +- Deprecate Microsoft and Yahoo OAuth Provider ID (#2517) +- Fix an issue where an exception was thrown when linking OAuth credentials. (#2521) +- Fix an issue where a wrong error was thrown when handling error with + FEDERATED_USER_ID_ALREADY_LINKED. (#2522) + +# v5.4.0 +- Add support of Generic IDP (#2405). + +# v5.3.0 +- Use the new registerInternalLibrary API to register with FirebaseCore. (#2137) + # v5.2.0 - Add support of Game Center sign in (#2127). diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.m b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.m index 7a871e24b01..ff7e0042ba0 100644 --- a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.m +++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailAuthProvider.m @@ -20,6 +20,8 @@ // FIREmailPasswordAuthProviderID is defined in FIRAuthProvider.m. +NS_ASSUME_NONNULL_BEGIN + @implementation FIREmailAuthProvider - (instancetype)init { @@ -37,3 +39,5 @@ + (FIRAuthCredential *)credentialWithEmail:(NSString *)email link:(NSString *)li } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.m b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.m index 28bc43b80dd..84c1461f153 100644 --- a/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.m +++ b/Firebase/Auth/Source/AuthProviders/EmailPassword/FIREmailPasswordAuthCredential.m @@ -20,6 +20,8 @@ #import "FIRAuthExceptionUtils.h" #import "FIRVerifyAssertionRequest.h" +NS_ASSUME_NONNULL_BEGIN + @interface FIREmailPasswordAuthCredential () - (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE; @@ -84,3 +86,5 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.m b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.m index d48033785e8..4f10860e6bb 100644 --- a/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.m +++ b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthCredential.m @@ -20,6 +20,8 @@ #import "FIRAuthExceptionUtils.h" #import "FIRVerifyAssertionRequest.h" +NS_ASSUME_NONNULL_BEGIN + @interface FIRFacebookAuthCredential () - (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE; @@ -65,3 +67,5 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.m b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.m index d2759aef317..d85083093af 100644 --- a/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.m +++ b/Firebase/Auth/Source/AuthProviders/Facebook/FIRFacebookAuthProvider.m @@ -21,6 +21,8 @@ // FIRFacebookAuthProviderID is defined in FIRAuthProvider.m. +NS_ASSUME_NONNULL_BEGIN + @implementation FIRFacebookAuthProvider - (instancetype)init { @@ -34,3 +36,5 @@ + (FIRAuthCredential *)credentialWithAccessToken:(NSString *)accessToken { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthCredential.m b/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthCredential.m index b98aec19cf1..91a4b684e2f 100644 --- a/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthCredential.m +++ b/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthCredential.m @@ -21,6 +21,8 @@ #import "FIRGameCenterAuthProvider.h" #import "FIRVerifyAssertionRequest.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRGameCenterAuthCredential - (nullable instancetype)initWithProvider:(NSString *)provider { @@ -84,3 +86,5 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthProvider.m b/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthProvider.m index 65f79a85965..af8e7e6f74d 100644 --- a/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthProvider.m +++ b/Firebase/Auth/Source/AuthProviders/GameCenter/FIRGameCenterAuthProvider.m @@ -22,6 +22,8 @@ #import "FIRAuthExceptionUtils.h" #import "FIRGameCenterAuthCredential.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRGameCenterAuthProvider - (instancetype)init { @@ -31,7 +33,22 @@ - (instancetype)init { } + (void)getCredentialWithCompletion:(FIRGameCenterCredentialCallback)completion { - __weak GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer]; + /** + Linking GameKit.framework without using it on macOS results in App Store rejection. + Thus we don't link GameKit.framework to our SDK directly. `optionalLocalPlayer` is used for + checking whether the APP that consuming our SDK has linked GameKit.framework. If not, a + `GameKitNotLinkedError` will be raised. + **/ + GKLocalPlayer * _Nullable optionalLocalPlayer = [[NSClassFromString(@"GKLocalPlayer") alloc] init]; + + if (!optionalLocalPlayer) { + if (completion) { + completion(nil, [FIRAuthErrorUtils gameKitNotLinkedError]); + } + return; + } + + __weak GKLocalPlayer *localPlayer = [[optionalLocalPlayer class] localPlayer]; if (!localPlayer.isAuthenticated) { if (completion) { completion(nil, [FIRAuthErrorUtils localPlayerNotAuthenticatedError]); @@ -67,3 +84,5 @@ + (void)getCredentialWithCompletion:(FIRGameCenterCredentialCallback)completion } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.m b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.m index c3737db21f6..f6b536d83a5 100644 --- a/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.m +++ b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthCredential.m @@ -20,6 +20,8 @@ #import "FIRAuthExceptionUtils.h" #import "FIRVerifyAssertionRequest.h" +NS_ASSUME_NONNULL_BEGIN + @interface FIRGitHubAuthCredential () - (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE; @@ -63,3 +65,5 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.m b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.m index 8e0ff767d2e..fa6be6610c5 100644 --- a/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.m +++ b/Firebase/Auth/Source/AuthProviders/GitHub/FIRGitHubAuthProvider.m @@ -21,6 +21,8 @@ // FIRGitHubAuthProviderID is defined in FIRAuthProvider.m. +NS_ASSUME_NONNULL_BEGIN + @implementation FIRGitHubAuthProvider - (instancetype)init { @@ -34,3 +36,5 @@ + (FIRAuthCredential *)credentialWithToken:(NSString *)token { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.m b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.m index fdc9e1a5d22..a4676d9f288 100644 --- a/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.m +++ b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthCredential.m @@ -20,6 +20,8 @@ #import "FIRAuthExceptionUtils.h" #import "FIRVerifyAssertionRequest.h" +NS_ASSUME_NONNULL_BEGIN + @interface FIRGoogleAuthCredential () - (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE; @@ -70,3 +72,5 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.m b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.m index a2f4c79c5cc..93a33d8371e 100644 --- a/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.m +++ b/Firebase/Auth/Source/AuthProviders/Google/FIRGoogleAuthProvider.m @@ -21,6 +21,8 @@ // FIRGoogleAuthProviderID is defined in FIRAuthProvider.m. +NS_ASSUME_NONNULL_BEGIN + @implementation FIRGoogleAuthProvider - (instancetype)init { @@ -35,3 +37,5 @@ + (FIRAuthCredential *)credentialWithIDToken:(NSString *)IDToken } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.m b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.m index eb824020aa5..ebea7be8fa3 100644 --- a/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.m +++ b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.m @@ -16,6 +16,9 @@ #import "FIROAuthCredential.h" +#import "FIRAuthCredential_Internal.h" +#import "FIRAuthExceptionUtils.h" +#import "FIROAuthCredential_Internal.h" #import "FIRVerifyAssertionRequest.h" NS_ASSUME_NONNULL_BEGIN @@ -28,13 +31,32 @@ - (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE; @implementation FIROAuthCredential -- (nullable instancetype)initWithProviderID:(NSString *)providerID - IDToken:(nullable NSString *)IDToken - accessToken:(nullable NSString *)accessToken { +- (nullable instancetype)initWithProvider:(NSString *)provider { + [FIRAuthExceptionUtils raiseMethodNotImplementedExceptionWithReason: + @"Please call the designated initializer."]; + return nil; +} + +- (instancetype)initWithProviderID:(NSString *)providerID + IDToken:(nullable NSString *)IDToken + accessToken:(nullable NSString *)accessToken + pendingToken:(nullable NSString *)pendingToken { self = [super initWithProvider:providerID]; if (self) { _IDToken = IDToken; _accessToken = accessToken; + _pendingToken = pendingToken; + } + return self; +} + +- (instancetype)initWithProviderID:(NSString *)providerID + sessionID:(NSString *)sessionID + OAuthResponseURLString:(NSString *)OAuthResponseURLString { + self = [self initWithProviderID:providerID IDToken:nil accessToken:nil pendingToken:nil]; + if (self) { + _OAuthResponseURLString = OAuthResponseURLString; + _sessionID = sessionID; } return self; } @@ -53,15 +75,20 @@ + (BOOL)supportsSecureCoding { - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { NSString *IDToken = [aDecoder decodeObjectOfClass:[NSString class] forKey:@"IDToken"]; NSString *accessToken = [aDecoder decodeObjectOfClass:[NSString class] forKey:@"accessToken"]; - self = [self initWithProviderID:self.provider IDToken:IDToken accessToken:accessToken]; + NSString *pendingToken = [aDecoder decodeObjectOfClass:[NSString class] forKey:@"pendingToken"]; + self = [self initWithProviderID:self.provider + IDToken:IDToken + accessToken:accessToken + pendingToken:pendingToken]; return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [aCoder encodeObject:self.IDToken forKey:@"IDToken"]; [aCoder encodeObject:self.accessToken forKey:@"accessToken"]; + [aCoder encodeObject:self.pendingToken forKey:@"pendingToken"]; } -NS_ASSUME_NONNULL_END - @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential_Internal.h b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential_Internal.h new file mode 100644 index 00000000000..bcc3248ebba --- /dev/null +++ b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential_Internal.h @@ -0,0 +1,62 @@ +/* + * Copyright 2017 Google + * + * 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 "FIROAuthCredential.h" + +NS_ASSUME_NONNULL_BEGIN + +/** @extension FIROAuthCredential + @brief Internal implementation of FIRAuthCredential for generic credentials. + */ +@interface FIROAuthCredential() + +/** @property OAuthResponseURLString + @brief A string representation of the response URL corresponding to this OAuthCredential. + */ +@property(nonatomic, readonly, nullable) NSString *OAuthResponseURLString; + +/** @property sessionID + @brief The session ID used when completing the headful-lite flow. + */ +@property(nonatomic, readonly, nullable) NSString *sessionID; + +/** @fn initWithProviderId:IDToken:accessToken:pendingToken + @brief Designated initializer. + @param providerID The provider ID associated with the credential being created. + @param IDToken The ID Token associated with the credential being created. + @param accessToken The access token associated with the credential being created. + @param pendingToken The pending token associated with the credential being created. + */ +- (instancetype)initWithProviderID:(NSString *)providerID + IDToken:(nullable NSString *)IDToken + accessToken:(nullable NSString *)accessToken + pendingToken:(nullable NSString *)pendingToken NS_DESIGNATED_INITIALIZER; + +/** @fn initWithProviderId:sessionID:OAuthResponseURLString: + @brief Intitializer which takes a sessionID and an OAuthResponseURLString. + @param providerID The provider ID associated with the credential being created. + @param sessionID The session ID used when completing the headful-lite flow. + @param OAuthResponseURLString The error that occurred if any. + */ +- (instancetype)initWithProviderID:(NSString *)providerID + sessionID:(NSString *)sessionID + OAuthResponseURLString:(NSString *)OAuthResponseURLString; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.m b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.m index 0561703f660..5557c16f53d 100644 --- a/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.m +++ b/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthProvider.m @@ -14,29 +14,346 @@ * limitations under the License. */ +#include #import "FIROAuthProvider.h" +#import "FIRApp.h" +#import "FIRAuthBackend.h" +#import "FIRAuth_Internal.h" +#import "FIRAuthErrorUtils.h" +#import "FIRAuthGlobalWorkQueue.h" +#import "FIRAuthRequestConfiguration.h" +#import "FIRAuthWebUtils.h" +#import "FIROAuthCredential_Internal.h" #import "FIROAuthCredential.h" +#import "FIROptions.h" + +#if TARGET_OS_IOS +#import "FIRAuthURLPresenter.h" +#endif NS_ASSUME_NONNULL_BEGIN -@implementation FIROAuthProvider +/** @typedef FIRHeadfulLiteURLCallBack + @brief The callback invoked at the end of the flow to fetch a headful-lite URL. + @param headfulLiteURL The headful lite URL. + @param error The error that occurred while fetching the headful-lite, if any. + */ +typedef void (^FIRHeadfulLiteURLCallBack)(NSURL *_Nullable headfulLiteURL, + NSError *_Nullable error); + +/** @var kHeadfulLiteURLStringFormat + @brief The format of the URL used to open the headful lite page during sign-in. + */ +NSString *const kHeadfulLiteURLStringFormat = @"https://%@/__/auth/handler?%@"; + +/** @var kauthTypeSignInWithRedirect + @brief The auth type to be specified in the sign-in request with redirect request and response. + */ +static NSString *const kAuthTypeSignInWithRedirect = @"signInWithRedirect"; + +@implementation FIROAuthProvider { + /** @var _auth + @brief The auth instance used for launching the URL presenter. + */ + FIRAuth *_auth; + + /** @var _callbackScheme + @brief The callback URL scheme used for headful-lite sign-in. + */ + NSString *_callbackScheme; +} -+ (FIRAuthCredential *)credentialWithProviderID:(NSString *)providerID ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + IDToken:(NSString *)IDToken + accessToken:(nullable NSString *)accessToken + pendingToken:(nullable NSString *)pendingToken { + return [[FIROAuthCredential alloc] initWithProviderID:providerID + IDToken:IDToken + accessToken:accessToken + pendingToken:pendingToken]; +} + ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID IDToken:(NSString *)IDToken accessToken:(nullable NSString *)accessToken { return [[FIROAuthCredential alloc] initWithProviderID:providerID IDToken:IDToken - accessToken:accessToken]; + accessToken:accessToken + pendingToken:nil]; } + (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID accessToken:(NSString *)accessToken { return [[FIROAuthCredential alloc] initWithProviderID:providerID IDToken:nil - accessToken:accessToken]; + accessToken:accessToken + pendingToken:nil]; } -NS_ASSUME_NONNULL_END ++ (instancetype)providerWithProviderID:(NSString *)providerID { + return [[self alloc]initWithProviderID:providerID auth:[FIRAuth auth]]; +} + ++ (instancetype)providerWithProviderID:(NSString *)providerID auth:(FIRAuth *)auth { + return [[self alloc] initWithProviderID:providerID auth:auth]; +} + +#if TARGET_OS_IOS +- (void)getCredentialWithUIDelegate:(nullable id)UIDelegate + completion:(nullable FIRAuthCredentialCallback)completion { + if (![FIRAuthWebUtils isCallbackSchemeRegisteredForCustomURLScheme:self->_callbackScheme]) { + [NSException raise:NSInternalInconsistencyException + format:@"Please register custom URL scheme '%@' in the app's Info.plist file.", + self->_callbackScheme]; + } + __weak __typeof__(self) weakSelf = self; + __weak FIRAuth *weakAuth = _auth; + __weak NSString *weakProviderID = _providerID; + dispatch_async(FIRAuthGlobalWorkQueue(), ^{ + FIRAuthCredentialCallback callbackOnMainThread = ^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) { + if (completion) { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(credential, error); + }); + } + }; + NSString *eventID = [FIRAuthWebUtils randomStringWithLength:10]; + NSString *sessionID = [FIRAuthWebUtils randomStringWithLength:10]; + __strong __typeof__(self) strongSelf = weakSelf; + [strongSelf getHeadFulLiteURLWithEventID:eventID + sessionID:sessionID + completion:^(NSURL *_Nullable headfulLiteURL, + NSError *_Nullable error) { + if (error) { + callbackOnMainThread(nil, error); + return; + } + FIRAuthURLCallbackMatcher callbackMatcher = ^BOOL(NSURL *_Nullable callbackURL) { + return [FIRAuthWebUtils isExpectedCallbackURL:callbackURL + eventID:eventID + authType:kAuthTypeSignInWithRedirect + callbackScheme:strongSelf->_callbackScheme]; + }; + __strong FIRAuth *strongAuth = weakAuth; + [strongAuth.authURLPresenter presentURL:headfulLiteURL + UIDelegate:UIDelegate + callbackMatcher:callbackMatcher + completion:^(NSURL *_Nullable callbackURL, + NSError *_Nullable error) { + if (error) { + callbackOnMainThread(nil, error); + return; + } + NSString *OAuthResponseURLString = + [strongSelf OAuthResponseForURL:callbackURL error:&error]; + if (error) { + callbackOnMainThread(nil, error); + return; + } + __strong NSString *strongProviderID = weakProviderID; + FIROAuthCredential *credential = + [[FIROAuthCredential alloc] initWithProviderID:strongProviderID + sessionID:sessionID + OAuthResponseURLString:OAuthResponseURLString]; + callbackOnMainThread(credential, nil); + }]; + }]; + }); +} +#endif // TARGET_OS_IOS + +#pragma mark - Internal Methods + +/** @fn initWithProviderID:auth: + @brief returns an instance of @c FIROAuthProvider associated with the provided auth instance. + @param auth The Auth instance to be associated with the OAuthProvider instance. + @return An Instance of @c FIROAuthProvider. + */ +- (nullable instancetype)initWithProviderID:(NSString *)providerID auth:(FIRAuth *)auth { + self = [super init]; + if (self) { + _auth = auth; + _providerID = providerID; + _callbackScheme = [[[_auth.app.options.clientID componentsSeparatedByString:@"."] + reverseObjectEnumerator].allObjects componentsJoinedByString:@"."]; + } + return self; +} + +/** @fn OAuthResponseForURL:error: + @brief Parses the redirected URL and returns a string representation of the OAuth response URL. + @param URL The url to be parsed for an OAuth response URL. + @param error The error that occurred if any. + @return The OAuth response if successful. + */ +- (nullable NSString *)OAuthResponseForURL:(NSURL *)URL error:(NSError *_Nullable *_Nullable)error { + NSDictionary *URLQueryItems = + [FIRAuthWebUtils dictionaryWithHttpArgumentsString:URL.query]; + NSURL *deepLinkURL = [NSURL URLWithString:URLQueryItems[@"deep_link_id"]]; + URLQueryItems = + [FIRAuthWebUtils dictionaryWithHttpArgumentsString:deepLinkURL.query]; + NSString *queryItemLink = URLQueryItems[@"link"]; + if (queryItemLink) { + return queryItemLink; + } + if (!error) { + return nil; + } + NSData *errorData = [URLQueryItems[@"firebaseError"] dataUsingEncoding:NSUTF8StringEncoding]; + NSError *jsonError; + NSDictionary *errorDict = [NSJSONSerialization JSONObjectWithData:errorData + options:0 + error:&jsonError]; + if (jsonError) { + *error = [FIRAuthErrorUtils JSONSerializationErrorWithUnderlyingError:jsonError]; + return nil; + } + *error = [FIRAuthErrorUtils URLResponseErrorWithCode:errorDict[@"code"] + message:errorDict[@"message"]]; + if (!*error) { + NSString *reason; + if(errorDict[@"code"] && errorDict[@"message"]) { + reason = [NSString stringWithFormat:@"[%@] - %@",errorDict[@"code"], errorDict[@"message"]]; + } + *error = [FIRAuthErrorUtils webSignInUserInteractionFailureWithReason:reason]; + } + return nil; +} + +/** @fn getHeadFulLiteURLWithEventID:completion: + @brief Constructs a URL used for opening a headful-lite flow using a given event + ID and session ID. + @param eventID The event ID used for this purpose. + @param sessionID The session ID used when completing the headful lite flow. + @param completion The callback invoked after the URL has been constructed or an error + has been encountered. + */ +- (void)getHeadFulLiteURLWithEventID:(NSString *)eventID + sessionID:(NSString *)sessionID + completion:(FIRHeadfulLiteURLCallBack)completion { + __weak __typeof__(self) weakSelf = self; + [FIRAuthWebUtils fetchAuthDomainWithRequestConfiguration:_auth.requestConfiguration + completion:^(NSString *_Nullable authDomain, + NSError *_Nullable error) { + if (error) { + if (completion) { + completion(nil, error); + } + return; + } + __strong __typeof__(self) strongSelf = weakSelf; + NSString *bundleID = [NSBundle mainBundle].bundleIdentifier; + NSString *clienID = strongSelf->_auth.app.options.clientID; + NSString *apiKey = strongSelf->_auth.requestConfiguration.APIKey; + NSMutableDictionary *urlArguments = [@{ + @"apiKey" : apiKey, + @"authType" : @"signInWithRedirect", + @"ibi" : bundleID ?: @"", + @"clientId" : clienID, + @"sessionId" : [strongSelf hashforString:sessionID], + @"v" : [FIRAuthBackend authUserAgent], + @"eventId" : eventID, + @"providerId" : strongSelf->_providerID, + } mutableCopy]; + if (strongSelf.scopes.count) { + urlArguments[@"scopes"] = [strongSelf.scopes componentsJoinedByString:@","]; + } + if (strongSelf.customParameters.count) { + NSString *customParameters = [strongSelf customParametersStringWithError:&error]; + if (error) { + completion(nil, error); + return; + } + if (customParameters) { + urlArguments[@"customParameters"] = customParameters; + } + } + if (strongSelf->_auth.requestConfiguration.languageCode) { + urlArguments[@"hl"] = strongSelf->_auth.requestConfiguration.languageCode; + } + NSString *argumentsString = [strongSelf httpArgumentsStringForArgsDictionary:urlArguments]; + NSString *URLString = + [NSString stringWithFormat:kHeadfulLiteURLStringFormat, authDomain, argumentsString]; + if (completion) { + NSCharacterSet *set = [NSCharacterSet URLFragmentAllowedCharacterSet]; + completion([NSURL URLWithString: + [URLString stringByAddingPercentEncodingWithAllowedCharacters:set]], nil); + } + }]; +} + +/** @fn customParametersString + @brief Returns a JSON string representation of the custom parameters dictionary corresponding + to the OAuthProvider. + @return The JSON string representation of the custom parameters dictionary corresponding + to the OAuthProvider. + */ +- (nullable NSString *)customParametersStringWithError:(NSError *_Nullable *_Nullable)error { + if (!_customParameters.count) { + return nil; + } + + if (!error) { + return nil; + } + NSError *jsonError; + NSData *customParametersJSONData = + [NSJSONSerialization dataWithJSONObject:_customParameters + options:0 + error:&jsonError]; + if (jsonError) { + *error = [FIRAuthErrorUtils JSONSerializationErrorWithUnderlyingError:jsonError]; + return nil; + } + + NSString *customParamsRawJSON = + [[NSString alloc] initWithData:customParametersJSONData encoding:NSUTF8StringEncoding]; + return customParamsRawJSON; +} + +/** @fn hashforString: + @brief Returns the SHA256 hash representation of a given string object. + @param string The string for which a SHA256 hash is desired. + @return An hexadecimal string representation of the SHA256 hash. + */ +- (NSString *)hashforString:(NSString *)string { + NSData *sessionIDData = [string dataUsingEncoding:NSUTF8StringEncoding]; + NSMutableData *hashOutputData = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH]; + if (CC_SHA256(sessionIDData.bytes, + (CC_LONG)[sessionIDData length], + hashOutputData.mutableBytes)) { + } + return [self hexStringFromData:hashOutputData];; +} + +/** @fn hexStringFromData: + @brief Returns the hexadecimal string representation of an NSData object. + @param data The NSData object for which a hexadecical string is desired. + @return The hexadecimal string representation of the supplied NSData object. + */ +- (NSString *)hexStringFromData:(NSData *)data { + const unsigned char *dataBuffer = (const unsigned char *)[data bytes]; + NSMutableString *string = [[NSMutableString alloc] init]; + for (unsigned int i = 0; i < data.length; i++){ + [string appendFormat:@"%02lx", (unsigned long)dataBuffer[i]]; + } + return [string copy]; +} + +- (NSString *)httpArgumentsStringForArgsDictionary:(NSDictionary *)argsDictionary { + NSMutableArray* arguments = [NSMutableArray arrayWithCapacity:argsDictionary.count]; + NSString* key; + for (key in argsDictionary) { + NSString *description = [argsDictionary[key] description]; + [arguments addObject:[NSString stringWithFormat:@"%@=%@", + [FIRAuthWebUtils stringByUnescapingFromURLArgument:key], + [FIRAuthWebUtils stringByUnescapingFromURLArgument:description]]] ; + } + return [arguments componentsJoinedByString:@"&"]; +} @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m index e2fd9906cd3..c8f1909c9ac 100644 --- a/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m +++ b/Firebase/Auth/Source/AuthProviders/Phone/FIRPhoneAuthProvider.m @@ -111,7 +111,7 @@ - (nullable instancetype)initWithAuth:(FIRAuth *)auth { - (void)verifyPhoneNumber:(NSString *)phoneNumber UIDelegate:(nullable id)UIDelegate completion:(nullable FIRVerificationResultCallback)completion { - if (![self isCallbackSchemeRegistered]) { + if (![FIRAuthWebUtils isCallbackSchemeRegisteredForCustomURLScheme:_callbackScheme]) { [NSException raise:NSInternalInconsistencyException format:@"Please register custom URL scheme '%@' in the app's Info.plist file.", _callbackScheme]; @@ -138,11 +138,7 @@ - (void)verifyPhoneNumber:(NSString *)phoneNumber callBackOnMainThread(nil, error); return; } - NSMutableString *eventID = [[NSMutableString alloc] init]; - for (int i=0; i<10; i++) { - [eventID appendString: - [NSString stringWithFormat:@"%c", 'a' + arc4random_uniform('z' - 'a' + 1)]]; - } + NSString *eventID = [FIRAuthWebUtils randomStringWithLength:10]; [self reCAPTCHAURLWithEventID:eventID completion:^(NSURL *_Nullable reCAPTCHAURL, NSError *_Nullable error) { if (error) { @@ -150,7 +146,10 @@ - (void)verifyPhoneNumber:(NSString *)phoneNumber return; } FIRAuthURLCallbackMatcher callbackMatcher = ^BOOL(NSURL *_Nullable callbackURL) { - return [self isVerifyAppURL:callbackURL eventID:eventID]; + return [FIRAuthWebUtils isExpectedCallbackURL:callbackURL + eventID:eventID + authType:kAuthTypeVerifyApp + callbackScheme:self->_callbackScheme]; }; [self->_auth.authURLPresenter presentURL:reCAPTCHAURL UIDelegate:UIDelegate @@ -205,26 +204,8 @@ + (instancetype)providerWithAuth:(FIRAuth *)auth { #pragma mark - Internal Methods -/** @fn isCallbackSchemeRegistered - @brief Checks whether or not the expected callback scheme has been registered by the app. - @remarks This method is thread-safe. - */ -- (BOOL)isCallbackSchemeRegistered { - NSString *expectedCustomScheme = [_callbackScheme lowercaseString]; - NSArray *urlTypes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleURLTypes"]; - for (NSDictionary *urlType in urlTypes) { - NSArray *urlTypeSchemes = urlType[@"CFBundleURLSchemes"]; - for (NSString *urlTypeScheme in urlTypeSchemes) { - if ([urlTypeScheme.lowercaseString isEqualToString:expectedCustomScheme]) { - return YES; - } - } - } - return NO; -} - /** @fn reCAPTCHATokenForURL:error: - @brief Parses the reCAPTCHA URL and returns. + @brief Parses the reCAPTCHA URL and returns the reCAPTCHA token. @param URL The url to be parsed for a reCAPTCHA token. @param error The error that occurred if any. @return The reCAPTCHA token if successful. @@ -269,46 +250,6 @@ - (NSString *)reCAPTCHATokenForURL:(NSURL *)URL error:(NSError **)error { return nil; } -/** @fn isVerifyAppURL: - @brief Parses a URL into all available query items. - @param URL The url to be checked against the authType string. - @return Whether or not the URL matches authType. - */ -- (BOOL)isVerifyAppURL:(nullable NSURL *)URL eventID:(NSString *)eventID { - if (!URL) { - return NO; - } - NSURLComponents *actualURLComponents = - [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO]; - actualURLComponents.query = nil; - actualURLComponents.fragment = nil; - - NSURLComponents *expectedURLComponents = [NSURLComponents new]; - expectedURLComponents.scheme = _callbackScheme; - expectedURLComponents.host = @"firebaseauth"; - expectedURLComponents.path = @"/link"; - - if (!([[expectedURLComponents URL] isEqual:[actualURLComponents URL]])) { - return NO; - } - actualURLComponents = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO]; - NSArray *queryItems = [actualURLComponents queryItems]; - NSString *deepLinkURL = [FIRAuthWebUtils queryItemValue:@"deep_link_id" from:queryItems]; - if (deepLinkURL == nil) { - return NO; - } - NSURLComponents *deepLinkURLComponents = [NSURLComponents componentsWithString:deepLinkURL]; - NSArray *deepLinkQueryItems = [deepLinkURLComponents queryItems]; - - NSString *deepLinkAuthType = [FIRAuthWebUtils queryItemValue:@"authType" from:deepLinkQueryItems]; - NSString *deepLinkEventID = [FIRAuthWebUtils queryItemValue:@"eventId" from:deepLinkQueryItems]; - if ([deepLinkAuthType isEqualToString:kAuthTypeVerifyApp] && - [deepLinkEventID isEqualToString:eventID]) { - return YES; - } - return NO; -} - /** @fn internalVerifyPhoneNumber:completion: @brief Starts the phone number authentication flow by sending a verifcation code to the specified phone number. @@ -450,11 +391,14 @@ - (void)verifyClientWithCompletion:(FIRVerifyClientCallback)completion { has been encountered. */ - (void)reCAPTCHAURLWithEventID:(NSString *)eventID completion:(FIRReCAPTCHAURLCallBack)completion { - [self fetchAuthDomainWithCompletion:^(NSString *_Nullable authDomain, - NSError *_Nullable error) { + [FIRAuthWebUtils fetchAuthDomainWithRequestConfiguration:_auth.requestConfiguration + completion:^(NSString *_Nullable authDomain, + NSError *_Nullable error) { if (error) { - completion(nil, error); - return; + if (completion) { + completion(nil, error); + return; + } } NSString *bundleID = [NSBundle mainBundle].bundleIdentifier; NSString *clientID = self->_auth.app.options.clientID; @@ -480,40 +424,6 @@ - (void)reCAPTCHAURLWithEventID:(NSString *)eventID completion:(FIRReCAPTCHAURLC }]; } -/** @fn fetchAuthDomainWithCompletion:completion: - @brief Fetches the auth domain associated with the Firebase Project. - @param completion The callback invoked after the auth domain has been constructed or an error - has been encountered. - */ -- (void)fetchAuthDomainWithCompletion:(FIRFetchAuthDomainCallback)completion { - FIRGetProjectConfigRequest *request = - [[FIRGetProjectConfigRequest alloc] initWithRequestConfiguration:_auth.requestConfiguration]; - - [FIRAuthBackend getProjectConfig:request - callback:^(FIRGetProjectConfigResponse *_Nullable response, - NSError *_Nullable error) { - if (error) { - completion(nil, error); - return; - } - NSString *authDomain; - for (NSString *domain in response.authorizedDomains) { - NSInteger index = domain.length - kAuthDomainSuffix.length; - if (index >= 2) { - if ([domain hasSuffix:kAuthDomainSuffix] && domain.length >= kAuthDomainSuffix.length + 2) { - authDomain = domain; - break; - } - } - } - if (!authDomain.length) { - completion(nil, [FIRAuthErrorUtils unexpectedErrorResponseWithDeserializedResponse:response]); - return; - } - completion(authDomain, nil); - }]; -} - @end NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.m b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.m index b7e45773ef6..cb466155850 100644 --- a/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.m +++ b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthCredential.m @@ -20,6 +20,8 @@ #import "FIRAuthExceptionUtils.h" #import "FIRVerifyAssertionRequest.h" +NS_ASSUME_NONNULL_BEGIN + @interface FIRTwitterAuthCredential () - (nullable instancetype)initWithProvider:(NSString *)provider NS_UNAVAILABLE; @@ -67,3 +69,5 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.m b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.m index 5d738cef22a..33771b723ca 100644 --- a/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.m +++ b/Firebase/Auth/Source/AuthProviders/Twitter/FIRTwitterAuthProvider.m @@ -21,6 +21,8 @@ // FIRTwitterAuthProviderID is defined in FIRAuthProvider.m. +NS_ASSUME_NONNULL_BEGIN + @implementation FIRTwitterAuthProvider - (instancetype)init { @@ -34,3 +36,5 @@ + (FIRAuthCredential *)credentialWithToken:(NSString *)token secret:(NSString *) } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuth.m b/Firebase/Auth/Source/FIRAuth.m index a210e8a8e5a..efb3a487dc6 100644 --- a/Firebase/Auth/Source/FIRAuth.m +++ b/Firebase/Auth/Source/FIRAuth.m @@ -53,6 +53,7 @@ #import "FIRGameCenterAuthCredential.h" #import "FIRGetOOBConfirmationCodeRequest.h" #import "FIRGetOOBConfirmationCodeResponse.h" +#import "FIROAuthCredential_Internal.h" #import "FIRResetPasswordRequest.h" #import "FIRResetPasswordResponse.h" #import "FIRSendVerificationCodeRequest.h" @@ -82,6 +83,8 @@ #import "FIRAuthURLPresenter.h" #endif +NS_ASSUME_NONNULL_BEGIN + #pragma mark - Constants #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 @@ -350,7 +353,7 @@ - (instancetype)initWithApp:(FIRApp *)app { return self; } -- (instancetype)initWithAPIKey:(NSString *)APIKey appName:(NSString *)appName { +- (nullable instancetype)initWithAPIKey:(NSString *)APIKey appName:(NSString *)appName { self = [super init]; if (self) { _listenerHandles = [NSMutableArray array]; @@ -438,7 +441,7 @@ - (void)dealloc { #pragma mark - Public API -- (FIRUser *)currentUser { +- (nullable FIRUser *)currentUser { __block FIRUser *result; dispatch_sync(FIRAuthGlobalWorkQueue(), ^{ result = self->_currentUser; @@ -447,7 +450,7 @@ - (FIRUser *)currentUser { } - (void)fetchProvidersForEmail:(NSString *)email - completion:(FIRProviderQueryCallback)completion { + completion:(nullable FIRProviderQueryCallback)completion { dispatch_async(FIRAuthGlobalWorkQueue(), ^{ FIRCreateAuthURIRequest *request = [[FIRCreateAuthURIRequest alloc] initWithIdentifier:email @@ -464,6 +467,29 @@ - (void)fetchProvidersForEmail:(NSString *)email }); } + +- (void)signInWithProvider:(id)provider + UIDelegate:(nullable id)UIDelegate + completion:(nullable FIRAuthDataResultCallback)completion { +#if TARGET_OS_IOS + dispatch_async(FIRAuthGlobalWorkQueue(), ^{ + FIRAuthDataResultCallback decoratedCallback = + [self signInFlowAuthDataResultCallbackByDecoratingCallback:completion]; + [provider getCredentialWithUIDelegate:UIDelegate + completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) { + if (error) { + decoratedCallback(nil, error); + return; + } + [self internalSignInAndRetrieveDataWithCredential:credential + isReauthentication:NO + callback:decoratedCallback]; + }]; + }); +#endif // TARGET_OS_IOS +} + - (void)fetchSignInMethodsForEmail:(nonnull NSString *)email completion:(nullable FIRSignInMethodQueryCallback)completion { dispatch_async(FIRAuthGlobalWorkQueue(), ^{ @@ -484,7 +510,7 @@ - (void)fetchSignInMethodsForEmail:(nonnull NSString *)email - (void)signInWithEmail:(NSString *)email password:(NSString *)password - completion:(FIRAuthDataResultCallback)completion { + completion:(nullable FIRAuthDataResultCallback)completion { dispatch_async(FIRAuthGlobalWorkQueue(), ^{ FIRAuthDataResultCallback decoratedCallback = [self signInFlowAuthDataResultCallbackByDecoratingCallback:completion]; @@ -499,7 +525,7 @@ - (void)signInWithEmail:(NSString *)email - (void)signInWithEmail:(NSString *)email link:(NSString *)link - completion:(FIRAuthDataResultCallback)completion { + completion:(nullable FIRAuthDataResultCallback)completion { dispatch_async(FIRAuthGlobalWorkQueue(), ^{ FIRAuthDataResultCallback decoratedCallback = [self signInFlowAuthDataResultCallbackByDecoratingCallback:completion]; @@ -553,7 +579,7 @@ - (void)signInWithEmail:(NSString *)email - (void)signInAndRetrieveDataWithEmail:(NSString *)email password:(NSString *)password - completion:(FIRAuthDataResultCallback)completion { + completion:(nullable FIRAuthDataResultCallback)completion { dispatch_async(FIRAuthGlobalWorkQueue(), ^{ FIRAuthDataResultCallback decoratedCallback = [self signInFlowAuthDataResultCallbackByDecoratingCallback:completion]; @@ -582,14 +608,14 @@ - (void)internalSignInAndRetrieveDataWithEmail:(NSString *)email callback:completion]; } -/** @fn signInWithGameCenterCredential:callback: +/** @fn signInAndRetrieveDataWithGameCenterCredential:callback: @brief Signs in using a game center credential. @param credential The Game Center Auth Credential used to sign in. @param callback A block which is invoked when the sign in finished (or is cancelled). Invoked asynchronously on the global auth work queue in the future. */ -- (void)signInWithGameCenterCredential:(FIRGameCenterAuthCredential *)credential - callback:(FIRAuthResultCallback)callback { +- (void)signInAndRetrieveDataWithGameCenterCredential:(FIRGameCenterAuthCredential *)credential + callback:(FIRAuthDataResultCallback)callback { FIRSignInWithGameCenterRequest *request = [[FIRSignInWithGameCenterRequest alloc] initWithPlayerID:credential.playerID publicKeyURL:credential.publicKeyURL @@ -601,31 +627,43 @@ - (void)signInWithGameCenterCredential:(FIRGameCenterAuthCredential *)credential [FIRAuthBackend signInWithGameCenter:request callback:^(FIRSignInWithGameCenterResponse *_Nullable response, NSError *_Nullable error) { - if (error) { - if (callback) { - callback(nil, error); - } - return; - } - - [self completeSignInWithAccessToken:response.IDToken - accessTokenExpirationDate:response.approximateExpirationDate - refreshToken:response.refreshToken - anonymous:NO - callback:callback]; - }]; -} - -/** @fn internalSignInWithEmail:link:completion: + if (error) { + if (callback) { + callback(nil, error); + } + return; + } + + [self completeSignInWithAccessToken:response.IDToken + accessTokenExpirationDate:response.approximateExpirationDate + refreshToken:response.refreshToken + anonymous:NO + callback:^(FIRUser *_Nullable user, NSError *_Nullable error) { + FIRAdditionalUserInfo *additionalUserInfo = + [[FIRAdditionalUserInfo alloc] initWithProviderID:FIRGameCenterAuthProviderID + profile:nil + username:nil + isNewUser:response.isNewUser]; + FIRAuthDataResult *result = user ? + [[FIRAuthDataResult alloc] initWithUser:user + additionalUserInfo:additionalUserInfo] : nil; + if (callback) { + callback(result, error); + } + }]; + }]; +} + +/** @fn internalSignInAndRetrieveDataWithEmail:link:completion: @brief Signs in using an email and email sign-in link. @param email The user's email address. @param link The email sign-in link. @param callback A block which is invoked when the sign in finishes (or is cancelled.) Invoked asynchronously on the global auth work queue in the future. */ -- (void)internalSignInWithEmail:(nonnull NSString *)email - link:(nonnull NSString *)link - callback:(nullable FIRAuthResultCallback)callback { +- (void)internalSignInAndRetrieveDataWithEmail:(nonnull NSString *)email + link:(nonnull NSString *)link + callback:(nullable FIRAuthDataResultCallback)callback { if (![self isSignInWithEmailLink:link]) { [FIRAuthExceptionUtils raiseInvalidParameterExceptionWithReason: kInvalidEmailSignInLinkExceptionMessage]; @@ -647,19 +685,39 @@ - (void)internalSignInWithEmail:(nonnull NSString *)email callback:^(FIREmailLinkSignInResponse *_Nullable response, NSError *_Nullable error) { if (error) { - callback(nil, error); + if (callback) { + callback(nil, error); + } return; } [self completeSignInWithAccessToken:response.IDToken accessTokenExpirationDate:response.approximateExpirationDate refreshToken:response.refreshToken anonymous:NO - callback:callback]; + callback:^(FIRUser *_Nullable user, NSError *_Nullable error) { + if (error) { + if (callback) { + callback(nil, error); + } + return; + } + FIRAdditionalUserInfo *additionalUserInfo = + [[FIRAdditionalUserInfo alloc] initWithProviderID:FIREmailAuthProviderID + profile:nil + username:nil + isNewUser:response.isNewUser]; + FIRAuthDataResult *result = user ? + [[FIRAuthDataResult alloc] initWithUser:user + additionalUserInfo:additionalUserInfo] : nil; + if (callback) { + callback(result, error); + } + }]; }]; } - (void)signInWithCredential:(FIRAuthCredential *)credential - completion:(FIRAuthResultCallback)completion { + completion:(nullable FIRAuthResultCallback)completion { dispatch_async(FIRAuthGlobalWorkQueue(), ^{ FIRAuthResultCallback callback = [self signInFlowAuthResultCallbackByDecoratingCallback:completion]; @@ -695,27 +753,31 @@ - (void)internalSignInAndRetrieveDataWithCredential:(FIRAuthCredential *)credent // Special case for email/password credentials FIREmailPasswordAuthCredential *emailPasswordCredential = (FIREmailPasswordAuthCredential *)credential; - FIRAuthResultCallback completeEmailSignIn = ^(FIRUser *user, NSError *error) { - if (callback) { - if (error) { - callback(nil, error); - return; - } - FIRAdditionalUserInfo *additionalUserInfo = - [[FIRAdditionalUserInfo alloc] initWithProviderID:FIREmailAuthProviderID - profile:nil - username:nil - isNewUser:NO]; - FIRAuthDataResult *result = [[FIRAuthDataResult alloc] initWithUser:user - additionalUserInfo:additionalUserInfo]; - callback(result, error); - } - }; + if (emailPasswordCredential.link) { - [self internalSignInWithEmail:emailPasswordCredential.email - link:emailPasswordCredential.link - callback:completeEmailSignIn]; + // Email link sign in + [self internalSignInAndRetrieveDataWithEmail:emailPasswordCredential.email + link:emailPasswordCredential.link + callback:callback]; } else { + // Email password sign in + FIRAuthResultCallback completeEmailSignIn = ^(FIRUser *user, NSError *error) { + if (callback) { + if (error) { + callback(nil, error); + return; + } + FIRAdditionalUserInfo *additionalUserInfo = + [[FIRAdditionalUserInfo alloc] initWithProviderID:FIREmailAuthProviderID + profile:nil + username:nil + isNewUser:NO]; + FIRAuthDataResult *result = [[FIRAuthDataResult alloc] initWithUser:user + additionalUserInfo:additionalUserInfo]; + callback(result, error); + } + }; + [self signInWithEmail:emailPasswordCredential.email password:emailPasswordCredential.password callback:completeEmailSignIn]; @@ -725,16 +787,8 @@ - (void)internalSignInAndRetrieveDataWithCredential:(FIRAuthCredential *)credent if ([credential isKindOfClass:[FIRGameCenterAuthCredential class]]) { // Special case for Game Center credentials. - [self signInWithGameCenterCredential:(FIRGameCenterAuthCredential *)credential - callback:^(FIRUser *_Nullable user, NSError *_Nullable error) { - if (callback) { - FIRAuthDataResult *result; - if (user) { - result = [[FIRAuthDataResult alloc] initWithUser:user additionalUserInfo:nil]; - } - callback(result, error); - } - }]; + [self signInAndRetrieveDataWithGameCenterCredential:(FIRGameCenterAuthCredential *)credential + callback:callback]; return; } @@ -779,6 +833,12 @@ - (void)internalSignInAndRetrieveDataWithCredential:(FIRAuthCredential *)credent requestConfiguration:_requestConfiguration]; request.autoCreate = !isReauthentication; [credential prepareVerifyAssertionRequest:request]; + if ([credential isKindOfClass:[FIROAuthCredential class]]) { + FIROAuthCredential *OAuthCredential = (FIROAuthCredential *)credential; + request.requestURI = OAuthCredential.OAuthResponseURLString; + request.sessionID = OAuthCredential.sessionID; + request.pendingToken = OAuthCredential.pendingToken; + } [FIRAuthBackend verifyAssertion:request callback:^(FIRVerifyAssertionResponse *response, NSError *error) { if (error) { @@ -828,7 +888,8 @@ - (void)signInWithCredential:(FIRAuthCredential *)credential }]; } -- (void)signInAnonymouslyAndRetrieveDataWithCompletion:(FIRAuthDataResultCallback)completion { +- (void)signInAnonymouslyAndRetrieveDataWithCompletion: + (nullable FIRAuthDataResultCallback)completion { dispatch_async(FIRAuthGlobalWorkQueue(), ^{ FIRAuthDataResultCallback decoratedCallback = [self signInFlowAuthDataResultCallbackByDecoratingCallback:completion]; @@ -869,7 +930,7 @@ - (void)signInAnonymouslyAndRetrieveDataWithCompletion:(FIRAuthDataResultCallbac }); } -- (void)signInAnonymouslyWithCompletion:(FIRAuthDataResultCallback)completion { +- (void)signInAnonymouslyWithCompletion:(nullable FIRAuthDataResultCallback)completion { dispatch_async(FIRAuthGlobalWorkQueue(), ^{ FIRAuthDataResultCallback decoratedCallback = [self signInFlowAuthDataResultCallbackByDecoratingCallback:completion]; @@ -961,7 +1022,7 @@ - (void)createUserWithEmail:(NSString *)email - (void)createUserAndRetrieveDataWithEmail:(NSString *)email password:(NSString *)password - completion:(FIRAuthDataResultCallback)completion { + completion:(nullable FIRAuthDataResultCallback)completion { dispatch_async(FIRAuthGlobalWorkQueue(), ^{ FIRAuthDataResultCallback decoratedCallback = [self signInFlowAuthDataResultCallbackByDecoratingCallback:completion]; @@ -1340,18 +1401,18 @@ - (void)setLanguageCode:(nullable NSString *)languageCode { }); } -- (NSString *)additionalFrameworkMarker { +- (nullable NSString *)additionalFrameworkMarker { return self->_requestConfiguration.additionalFrameworkMarker; } -- (void)setAdditionalFrameworkMarker:(NSString *)additionalFrameworkMarker { +- (void)setAdditionalFrameworkMarker:(nullable NSString *)additionalFrameworkMarker { dispatch_sync(FIRAuthGlobalWorkQueue(), ^{ self->_requestConfiguration.additionalFrameworkMarker = [additionalFrameworkMarker copy]; }); } #if TARGET_OS_IOS -- (NSData *)APNSToken { +- (nullable NSData *)APNSToken { __block NSData *result = nil; dispatch_sync(FIRAuthGlobalWorkQueue(), ^{ result = self->_tokenManager.token.data; @@ -1359,7 +1420,7 @@ - (NSData *)APNSToken { return result; } -- (void)setAPNSToken:(NSData *)APNSToken { +- (void)setAPNSToken:(nullable NSData *)APNSToken { [self setAPNSToken:APNSToken type:FIRAuthAPNSTokenTypeUnknown]; } @@ -1762,7 +1823,7 @@ - (FIRAuthDataResultCallback)signInFlowAuthDataResultCallbackByDecoratingCallbac #pragma mark - User-Related Methods -/** @fn updateCurrentUser:savingToDisk: +/** @fn updateCurrentUser:byForce:savingToDisk:error: @brief Update the current user; initializing the user's internal properties correctly, and optionally saving the user to disk. @remarks This method is called during: sign in and sign out events, as well as during class @@ -1772,7 +1833,7 @@ - (FIRAuthDataResultCallback)signInFlowAuthDataResultCallbackByDecoratingCallbac time.) @param saveToDisk Indicates the method should persist the user data to disk. */ -- (BOOL)updateCurrentUser:(FIRUser *)user +- (BOOL)updateCurrentUser:(nullable FIRUser *)user byForce:(BOOL)force savingToDisk:(BOOL)saveToDisk error:(NSError *_Nullable *_Nullable)error { @@ -1951,3 +2012,5 @@ - (nullable NSString *)getUserID { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuthCredential.m b/Firebase/Auth/Source/FIRAuthCredential.m index 63a08c46c89..510d5f906da 100644 --- a/Firebase/Auth/Source/FIRAuthCredential.m +++ b/Firebase/Auth/Source/FIRAuthCredential.m @@ -16,6 +16,8 @@ #import "FIRAuthCredential_Internal.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRAuthCredential - (instancetype)init { @@ -40,3 +42,5 @@ - (void)prepareVerifyAssertionRequest:(FIRVerifyAssertionRequest *)request { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuthDataResult_Internal.h b/Firebase/Auth/Source/FIRAuthDataResult_Internal.h index b90b5fd4a0a..b02bf59de83 100644 --- a/Firebase/Auth/Source/FIRAuthDataResult_Internal.h +++ b/Firebase/Auth/Source/FIRAuthDataResult_Internal.h @@ -16,6 +16,8 @@ #import "FIRAuthDataResult.h" +NS_ASSUME_NONNULL_BEGIN + @interface FIRAuthDataResult () /** @fn initWithUser:additionalUserInfo: @@ -28,3 +30,5 @@ NS_DESIGNATED_INITIALIZER; @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m b/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m index d5cd648e30d..f37dbe4ae5b 100644 --- a/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m +++ b/Firebase/Auth/Source/FIRAuthDefaultUIDelegate.m @@ -17,6 +17,7 @@ #import "FIRAuthDefaultUIDelegate.h" #import +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Firebase/Auth/Source/FIRAuthErrorUtils.h b/Firebase/Auth/Source/FIRAuthErrorUtils.h index a8a937b7d6f..bbf726f20c4 100644 --- a/Firebase/Auth/Source/FIRAuthErrorUtils.h +++ b/Firebase/Auth/Source/FIRAuthErrorUtils.h @@ -16,7 +16,7 @@ #import -@class FIRPhoneAuthCredential; +@class FIRAuthCredential; NS_ASSUME_NONNULL_BEGIN @@ -262,15 +262,16 @@ NS_ASSUME_NONNULL_BEGIN */ + (NSError *)userMismatchError; -/** @fn credentialAlreadyInUseErrorWithMessage: +/** @fn credentialAlreadyInUseErrorWithMessage:email: @brief Constructs an @c NSError with the @c FIRAuthErrorCodeCredentialAlreadyInUse code. @param message Error message from the backend, if any. @param credential Auth credential to be added to the Error User Info dictionary. + @param email Email to be added to the Error User Info dictionary. @return The NSError instance associated with the given FIRAuthError. */ + (NSError *)credentialAlreadyInUseErrorWithMessage:(nullable NSString *)message - credential:(nullable FIRPhoneAuthCredential *)credential; - + credential:(nullable FIRAuthCredential *)credential + email:(nullable NSString *)email; /** @fn operationNotAllowedErrorWithMessage: @brief Constructs an @c NSError with the @c FIRAuthErrorCodeOperationNotAllowed code. @param message Error message from the backend, if any. @@ -451,6 +452,12 @@ NS_ASSUME_NONNULL_BEGIN */ + (NSError *)localPlayerNotAuthenticatedError; +/** @fn gameKitNotLinkedError + @brief Constructs an @c NSError with the @c FIRAuthErrorCodeGameKitNotLinked code. + @return The NSError instance associated with the given FIRAuthError. + */ ++ (NSError *)gameKitNotLinkedError; + /** @fn notificationNotForwardedError @brief Constructs an @c NSError with the @c FIRAuthErrorCodeNotificationNotForwarded code. @return The NSError instance associated with the given FIRAuthError. @@ -493,6 +500,14 @@ NS_ASSUME_NONNULL_BEGIN */ + (NSError *)appVerificationUserInteractionFailureWithReason:(NSString *)reason; +/** @fn webSignInUserInteractionFailureWithReason: + @brief Constructs an @c NSError with the @c + FIRAuthErrorCodeWebSignInUserInteractionFailure code. + @param reason Reason for error, returned via URL response. + @return The NSError instance associated with the given FIRAuthError. + */ ++ (NSError *)webSignInUserInteractionFailureWithReason:(nullable NSString *)reason; + /** @fn URLResponseErrorWithCode:message: @brief Constructs an @c NSError with the code and message provided. @param message Error message from the backend, if any. @@ -507,6 +522,13 @@ NS_ASSUME_NONNULL_BEGIN */ + (NSError *)nullUserErrorWithMessage:(nullable NSString *)message; +/** @fn invalidProviderIDErrorWithMessage: + @brief Constructs an @c NSError with the @c FIRAuthErrorCodeInvalidProviderID code. + @param message Error message from the backend, if any. + @remarks This error indicates that the provider id given for the web operation is invalid. + */ ++ (NSError *)invalidProviderIDErrorWithMessage:(nullable NSString *)message; + /** @fn invalidDynamicLinkDomainErrorWithMessage: @brief Constructs an @c NSError with the code and message provided. @param message Error message from the backend, if any. diff --git a/Firebase/Auth/Source/FIRAuthErrorUtils.m b/Firebase/Auth/Source/FIRAuthErrorUtils.m index 05bd867e053..0300e4a5f9e 100644 --- a/Firebase/Auth/Source/FIRAuthErrorUtils.m +++ b/Firebase/Auth/Source/FIRAuthErrorUtils.m @@ -32,6 +32,11 @@ NSString *const FIRAuthErrorUserInfoEmailKey = @"FIRAuthErrorUserInfoEmailKey"; +NSString *const FIRAuthErrorUserInfoUpdatedCredentialKey = + @"FIRAuthErrorUserInfoUpdatedCredentialKey"; + +NSString *const FIRAuthErrorUserInfoNameKey = @"FIRAuthErrorUserInfoNameKey"; + NSString *const FIRAuthErrorNameKey = @"error_name"; NSString *const FIRAuthUpdatedCredentialKey = @"FIRAuthUpdatedCredentialKey"; @@ -312,11 +317,17 @@ @"The verification ID used to create the phone auth credential is invalid."; /** @var kFIRAuthErrorMessageLocalPlayerNotAuthenticated - @brief Message for @c FIRAuthErrorCodeLocalPlayerNotAuthenticated error code. + @brief Message for @c FIRAuthErrorCodeLocalPlayerNotAuthenticated error code. */ static NSString *const kFIRAuthErrorMessageLocalPlayerNotAuthenticated = @"The local player is not authenticated. Please log the local player in to Game Center."; +/** @var kFIRAuthErrorMessageGameKitNotLinked + @brief Message for @c kFIRAuthErrorMessageGameKitNotLinked error code. + */ +static NSString *const kFIRAuthErrorMessageGameKitNotLinked = + @"The GameKit framework is not linked. Please turn on the Game Center capability."; + /** @var kFIRAuthErrorMessageSessionExpired @brief Message for @c FIRAuthErrorCodeSessionExpired error code. */ @@ -413,6 +424,12 @@ static NSString *const kFIRAuthErrorMessageNullUser = @"A null user object was provided as the " "argument for an operation which requires a non-null user object."; +/** @var kFIRAuthErrorMessageInvalidProviderID + @brief Message for @c FIRAuthErrorCodeInvalidProviderID error code. + */ +static NSString *const kFIRAuthErrorMessageInvalidProviderID = @"The provider ID provided for the " + "attempted web operation is invalid."; + /** @var kFIRAuthErrorMessageInvalidDynamicLinkDomain @brief Message for @c kFIRAuthErrorMessageInvalidDynamicLinkDomain error code. */ @@ -548,14 +565,20 @@ return kFIRAuthErrorMessageWebRequestFailed; case FIRAuthErrorCodeNullUser: return kFIRAuthErrorMessageNullUser; + case FIRAuthErrorCodeInvalidProviderID: + return kFIRAuthErrorMessageInvalidProviderID; case FIRAuthErrorCodeInvalidDynamicLinkDomain: return kFIRAuthErrorMessageInvalidDynamicLinkDomain; case FIRAuthErrorCodeWebInternalError: return kFIRAuthErrorMessageWebInternalError; + case FIRAuthErrorCodeWebSignInUserInteractionFailure: + return kFIRAuthErrorMessageAppVerificationUserInteractionFailure; case FIRAuthErrorCodeMalformedJWT: return kFIRAuthErrorMessageMalformedJWT; case FIRAuthErrorCodeLocalPlayerNotAuthenticated: return kFIRAuthErrorMessageLocalPlayerNotAuthenticated; + case FIRAuthErrorCodeGameKitNotLinked: + return kFIRAuthErrorMessageGameKitNotLinked; } } @@ -675,14 +698,20 @@ return @"ERROR_WEB_NETWORK_REQUEST_FAILED"; case FIRAuthErrorCodeNullUser: return @"ERROR_NULL_USER"; + case FIRAuthErrorCodeInvalidProviderID: + return @"ERROR_INVALID_PROVIDER_ID"; case FIRAuthErrorCodeInvalidDynamicLinkDomain: return @"ERROR_INVALID_DYNAMIC_LINK_DOMAIN"; case FIRAuthErrorCodeWebInternalError: return @"ERROR_WEB_INTERNAL_ERROR"; + case FIRAuthErrorCodeWebSignInUserInteractionFailure: + return @"ERROR_WEB_USER_INTERACTION_FAILURE"; case FIRAuthErrorCodeMalformedJWT: return @"ERROR_MALFORMED_JWT"; case FIRAuthErrorCodeLocalPlayerNotAuthenticated: return @"ERROR_LOCAL_PLAYER_NOT_AUTHENTICATED"; + case FIRAuthErrorCodeGameKitNotLinked: + return @"ERROR_GAME_KIT_NOT_LINKED"; } } @@ -705,7 +734,7 @@ + (NSError *)errorWithCode:(FIRAuthInternalErrorCode)code + (NSError *)errorWithCode:(FIRAuthInternalErrorCode)code underlyingError:(nullable NSError *)underlyingError { - NSDictionary *errorUserInfo = nil; + NSDictionary *errorUserInfo; if (underlyingError) { errorUserInfo = @{ NSUnderlyingErrorKey : underlyingError @@ -724,7 +753,12 @@ + (NSError *)errorWithCode:(FIRAuthInternalErrorCode)code if (!errorUserInfo[NSLocalizedDescriptionKey]) { errorUserInfo[NSLocalizedDescriptionKey] = FIRAuthErrorDescription(errorCode); } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // TODO(wangyue): Remove the deprecated code on next breaking change. errorUserInfo[FIRAuthErrorNameKey] = FIRAuthErrorCodeString(errorCode); +#pragma clang diagnostic pop + errorUserInfo[FIRAuthErrorUserInfoNameKey] = FIRAuthErrorCodeString(errorCode); return [NSError errorWithDomain:FIRAuthErrorDomain code:errorCode userInfo:errorUserInfo]; } else { // This is an internal error. Wrap it in an internal error. @@ -755,16 +789,25 @@ + (NSError *)networkErrorWithUnderlyingError:(NSError *)underlyingError { + (NSError *)unexpectedErrorResponseWithData:(NSData *)data underlyingError:(NSError *)underlyingError { - return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedErrorResponse userInfo:@{ - FIRAuthErrorUserInfoDataKey : data, - NSUnderlyingErrorKey : underlyingError - }]; + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + if (data) { + userInfo[FIRAuthErrorUserInfoDataKey] = data; + } + if (underlyingError) { + userInfo[NSUnderlyingErrorKey] = underlyingError; + } + return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedErrorResponse + userInfo:[userInfo copy]]; } + (NSError *)unexpectedErrorResponseWithDeserializedResponse:(id)deserializedResponse { - return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedErrorResponse userInfo:@{ - FIRAuthErrorUserInfoDeserializedResponseKey : deserializedResponse - }]; + NSDictionary *userInfo; + if (deserializedResponse) { + userInfo = @{ + FIRAuthErrorUserInfoDeserializedResponseKey : deserializedResponse, + }; + } + return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedErrorResponse userInfo:userInfo]; } + (NSError *)malformedJWTErrorWithToken:(NSString *)token @@ -781,40 +824,57 @@ + (NSError *)malformedJWTErrorWithToken:(NSString *)token + (NSError *)unexpectedResponseWithData:(NSData *)data underlyingError:(NSError *)underlyingError { - return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedResponse userInfo:@{ - FIRAuthErrorUserInfoDataKey : data, - NSUnderlyingErrorKey : underlyingError - }]; + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + if (data) { + userInfo[FIRAuthErrorUserInfoDataKey] = data; + } + if (underlyingError) { + userInfo[NSUnderlyingErrorKey] = underlyingError; + } + return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedResponse userInfo:[userInfo copy]]; } + (NSError *)unexpectedResponseWithDeserializedResponse:(id)deserializedResponse { - return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedResponse userInfo:@{ - FIRAuthErrorUserInfoDeserializedResponseKey : deserializedResponse - }]; + NSDictionary *userInfo; + if (deserializedResponse) { + userInfo = @{ + FIRAuthErrorUserInfoDeserializedResponseKey : deserializedResponse, + }; + } + return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedResponse userInfo:userInfo]; } + (NSError *)unexpectedResponseWithDeserializedResponse:(nullable id)deserializedResponse underlyingError:(NSError *)underlyingError { - NSMutableDictionary *userInfo = - [NSMutableDictionary dictionaryWithDictionary:@{ NSUnderlyingErrorKey : underlyingError }]; + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; if (deserializedResponse) { userInfo[FIRAuthErrorUserInfoDeserializedResponseKey] = deserializedResponse; } - return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedResponse userInfo:userInfo]; + if (underlyingError) { + userInfo[NSUnderlyingErrorKey] = underlyingError; + } + return [self errorWithCode:FIRAuthInternalErrorCodeUnexpectedResponse userInfo:[userInfo copy]]; } + (NSError *)RPCResponseDecodingErrorWithDeserializedResponse:(id)deserializedResponse underlyingError:(NSError *)underlyingError { - return [self errorWithCode:FIRAuthInternalErrorCodeRPCResponseDecodingError userInfo:@{ - FIRAuthErrorUserInfoDeserializedResponseKey : deserializedResponse, - NSUnderlyingErrorKey : underlyingError - }]; + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + if (deserializedResponse) { + userInfo[FIRAuthErrorUserInfoDeserializedResponseKey] = deserializedResponse; + } + if (underlyingError) { + userInfo[NSUnderlyingErrorKey] = underlyingError; + } + return [self errorWithCode:FIRAuthInternalErrorCodeRPCResponseDecodingError + userInfo:[userInfo copy]]; } + (NSError *)emailAlreadyInUseErrorWithEmail:(nullable NSString *)email { - NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init]; + NSDictionary *userInfo; if (email.length) { - userInfo[FIRAuthErrorUserInfoEmailKey] = email; + userInfo = @{ + FIRAuthErrorUserInfoEmailKey : email, + }; } return [self errorWithCode:FIRAuthInternalErrorCodeEmailAlreadyInUse userInfo:userInfo]; } @@ -856,8 +916,14 @@ + (NSError *)invalidEmailErrorWithMessage:(nullable NSString *)message { } + (NSError *)accountExistsWithDifferentCredentialErrorWithEmail:(nullable NSString *)email { + NSDictionary *userInfo; + if (email.length) { + userInfo = @{ + FIRAuthErrorUserInfoEmailKey : email, + }; + } return [self errorWithCode:FIRAuthInternalErrorCodeAccountExistsWithDifferentCredential - userInfo:@{ FIRAuthErrorUserInfoEmailKey : email }]; + userInfo:userInfo]; } + (NSError *)providerAlreadyLinkedError { @@ -885,10 +951,23 @@ + (NSError *)userMismatchError { } + (NSError *)credentialAlreadyInUseErrorWithMessage:(nullable NSString *)message - credential:(nullable FIRPhoneAuthCredential *)credential { + credential:(nullable FIRAuthCredential *)credential + email:(nullable NSString *)email { + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; if (credential) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // TODO(wangyue): Remove the deprecated code on next breaking change. + userInfo[FIRAuthUpdatedCredentialKey] = credential; +#pragma clang diagnostic pop + userInfo[FIRAuthErrorUserInfoUpdatedCredentialKey] = credential; + } + if (email.length) { + userInfo[FIRAuthErrorUserInfoEmailKey] = email; + } + if (userInfo.count) { return [self errorWithCode:FIRAuthInternalErrorCodeCredentialAlreadyInUse - userInfo:@{ FIRAuthUpdatedCredentialKey : credential }]; + userInfo:userInfo]; } return [self errorWithCode:FIRAuthInternalErrorCodeCredentialAlreadyInUse message:message]; } @@ -898,9 +977,13 @@ + (NSError *)operationNotAllowedErrorWithMessage:(nullable NSString *)message { } + (NSError *)weakPasswordErrorWithServerResponseReason:(nullable NSString *)reason { - return [self errorWithCode:FIRAuthInternalErrorCodeWeakPassword userInfo:@{ - NSLocalizedFailureReasonErrorKey : reason - }]; + NSDictionary *userInfo; + if (reason.length) { + userInfo = @{ + NSLocalizedFailureReasonErrorKey : reason, + }; + } + return [self errorWithCode:FIRAuthInternalErrorCodeWeakPassword userInfo:userInfo]; } + (NSError *)appNotAuthorizedError { @@ -1000,6 +1083,10 @@ + (NSError *)localPlayerNotAuthenticatedError { return [self errorWithCode:FIRAuthInternalErrorCodeLocalPlayerNotAuthenticated]; } ++ (NSError *)gameKitNotLinkedError { + return [self errorWithCode:FIRAuthInternalErrorCodeGameKitNotLinked]; +} + + (NSError *)notificationNotForwardedError { return [self errorWithCode:FIRAuthInternalErrorCodeNotificationNotForwarded]; } @@ -1021,10 +1108,25 @@ + (NSError *)webContextCancelledErrorWithMessage:(nullable NSString *)message { } + (NSError *)appVerificationUserInteractionFailureWithReason:(NSString *)reason { + NSDictionary *userInfo; + if (reason.length) { + userInfo = @{ + NSLocalizedFailureReasonErrorKey : reason, + }; + } return [self errorWithCode:FIRAuthInternalErrorCodeAppVerificationUserInteractionFailure - userInfo:@{ - NSLocalizedFailureReasonErrorKey : reason - }]; + userInfo:userInfo]; +} + ++ (NSError *)webSignInUserInteractionFailureWithReason:(nullable NSString *)reason { + NSDictionary *userInfo; + if (reason.length) { + userInfo = @{ + NSLocalizedFailureReasonErrorKey : reason, + }; + } + return [self errorWithCode:FIRAuthInternalErrorCodeWebSignInUserInteractionFailure + userInfo:userInfo]; } + (nullable NSError *)URLResponseErrorWithCode:(NSString *)code message:(nullable NSString *)message { @@ -1044,6 +1146,10 @@ + (NSError *)nullUserErrorWithMessage:(nullable NSString *)message { return [self errorWithCode:FIRAuthInternalErrorCodeNullUser message:message]; } ++ (NSError *)invalidProviderIDErrorWithMessage:(nullable NSString *)message { + return [self errorWithCode:FIRAuthInternalErrorCodeInvalidProviderID message:message]; +} + + (NSError *)invalidDynamicLinkDomainErrorWithMessage:(nullable NSString *)message { return [self errorWithCode:FIRAuthInternalErrorCodeInvalidDynamicLinkDomain message:message]; } diff --git a/Firebase/Auth/Source/FIRAuthExceptionUtils.m b/Firebase/Auth/Source/FIRAuthExceptionUtils.m index 0adcd347c1f..3da858f2455 100644 --- a/Firebase/Auth/Source/FIRAuthExceptionUtils.m +++ b/Firebase/Auth/Source/FIRAuthExceptionUtils.m @@ -16,6 +16,8 @@ #import "FIRAuthExceptionUtils.h" +NS_ASSUME_NONNULL_BEGIN + /** @var FIRMethodNotImplementedException @brief The name of the "Method Not Implemented" exception. */ @@ -23,7 +25,7 @@ @implementation FIRAuthExceptionUtils -+ (void)raiseInvalidParameterExceptionWithReason:(NSString *)reason { ++ (void)raiseInvalidParameterExceptionWithReason:(nullable NSString *)reason { [NSException raise:NSInvalidArgumentException format:@"%@", reason]; } @@ -34,3 +36,5 @@ + (void)raiseMethodNotImplementedExceptionWithReason:(nullable NSString *)reason } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuthGlobalWorkQueue.m b/Firebase/Auth/Source/FIRAuthGlobalWorkQueue.m index accac896bc3..6295b8b0d01 100644 --- a/Firebase/Auth/Source/FIRAuthGlobalWorkQueue.m +++ b/Firebase/Auth/Source/FIRAuthGlobalWorkQueue.m @@ -16,6 +16,8 @@ #import "FIRAuthGlobalWorkQueue.h" +NS_ASSUME_NONNULL_BEGIN + dispatch_queue_t FIRAuthGlobalWorkQueue() { static dispatch_once_t once; static dispatch_queue_t queue; @@ -24,3 +26,5 @@ dispatch_queue_t FIRAuthGlobalWorkQueue() { }); return queue; } + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuthInternalErrors.h b/Firebase/Auth/Source/FIRAuthInternalErrors.h index 71a8fd0a5c9..205dc0f3ab0 100644 --- a/Firebase/Auth/Source/FIRAuthInternalErrors.h +++ b/Firebase/Auth/Source/FIRAuthInternalErrors.h @@ -18,6 +18,8 @@ #import "FIRAuthErrors.h" +NS_ASSUME_NONNULL_BEGIN + /** @var FIRAuthPublicErrorCodeFlag @brief Bitmask value indicating the error represents a public error code when this bit is zeroed. Error codes which don't contain this flag will be wrapped in an @c NSError whose @@ -353,6 +355,11 @@ typedef NS_ENUM(NSInteger, FIRAuthInternalErrorCode) { FIRAuthInternalErrorCodeWebInternalError = FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeWebInternalError, + /** Indicates that an internal error occurred within a SFSafariViewController or UIWebview. + */ + FIRAuthInternalErrorCodeWebSignInUserInteractionFailure = + FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeWebSignInUserInteractionFailure, + // The enum values between 17046 and 17051 are reserved and should NOT be used for new error // codes. @@ -375,12 +382,22 @@ typedef NS_ENUM(NSInteger, FIRAuthInternalErrorCode) { FIRAuthInternalErrorCodeLocalPlayerNotAuthenticated = FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeLocalPlayerNotAuthenticated, + /** Indicates that the Game Center local player was not authenticated. + */ + FIRAuthInternalErrorCodeGameKitNotLinked = + FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeGameKitNotLinked, + /** Indicates that a non-null user was expected as an argmument to the operation but a null user was provided. */ FIRAuthInternalErrorCodeNullUser = FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeNullUser, + /** Indicates that the provider id given for the web operation is invalid. + */ + FIRAuthInternalErrorCodeInvalidProviderID = + FIRAuthPublicErrorCodeFlag | FIRAuthErrorCodeInvalidProviderID, + /** Indicates that the Firebase Dynamic Link domain used is either not configured or is unauthorized for the current project. */ @@ -452,3 +469,5 @@ typedef NS_ENUM(NSInteger, FIRAuthInternalErrorCode) { */ FIRAuthInternalErrorCodeRPCResponseDecodingError = 5, }; + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuthKeychain.m b/Firebase/Auth/Source/FIRAuthKeychain.m index 199dea23f60..d196244441e 100644 --- a/Firebase/Auth/Source/FIRAuthKeychain.m +++ b/Firebase/Auth/Source/FIRAuthKeychain.m @@ -38,6 +38,8 @@ */ static NSString *const kAccountPrefix = @"firebase_auth_1_"; +NS_ASSUME_NONNULL_BEGIN + @implementation FIRAuthKeychain { /** @var _service @brief The name of the keychain service. @@ -75,7 +77,7 @@ @implementation FIRAuthKeychain { return self; } -- (NSData *)dataForKey:(NSString *)key error:(NSError **_Nullable)error { +- (nullable NSData *)dataForKey:(NSString *)key error:(NSError **_Nullable)error { if (!key.length) { [NSException raise:NSInvalidArgumentException format:@"%@", @"The key cannot be nil or empty."]; @@ -254,3 +256,5 @@ - (NSDictionary *)legacyGenericPasswordQueryWithKey:(NSString *)key { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuthProvider.m b/Firebase/Auth/Source/FIRAuthProvider.m index 8a08d9b08cd..e382d4e751a 100644 --- a/Firebase/Auth/Source/FIRAuthProvider.m +++ b/Firebase/Auth/Source/FIRAuthProvider.m @@ -42,6 +42,12 @@ // Declared 'extern' in FIRGameCenterAuthProvider.h NSString *const FIRGameCenterAuthProviderID = @"gc.apple.com"; +// Declared 'extern' in FIROAuthProvider.h +NSString *const FIRYahooAuthProviderID = @"yahoo.com"; + +// Declared 'extern' in FIROAuthProvider.h +NSString *const FIRMicrosoftAuthProviderID = @"hotmail.com"; + #pragma mark - sign-in methods constants // Declared 'extern' in FIRGoogleAuthProvider.h diff --git a/Firebase/Auth/Source/FIRAuthSerialTaskQueue.m b/Firebase/Auth/Source/FIRAuthSerialTaskQueue.m index edceeecf99e..78005e00cd0 100644 --- a/Firebase/Auth/Source/FIRAuthSerialTaskQueue.m +++ b/Firebase/Auth/Source/FIRAuthSerialTaskQueue.m @@ -18,6 +18,8 @@ #import "FIRAuthGlobalWorkQueue.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRAuthSerialTaskQueue { /** @var _dispatchQueue @brief The asyncronous dispatch queue into which tasks are enqueued and processed @@ -50,3 +52,5 @@ - (void)enqueueTask:(FIRAuthSerialTask)task { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuthSettings.m b/Firebase/Auth/Source/FIRAuthSettings.m index 575bb3cc51b..8ed5bb60a55 100644 --- a/Firebase/Auth/Source/FIRAuthSettings.m +++ b/Firebase/Auth/Source/FIRAuthSettings.m @@ -16,6 +16,8 @@ #import "FIRAuthSettings.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRAuthSettings - (instancetype)init { @@ -27,3 +29,5 @@ - (instancetype)init { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuthWebUtils.h b/Firebase/Auth/Source/FIRAuthWebUtils.h index dedd1a57e1e..52bf7f4cc7b 100644 --- a/Firebase/Auth/Source/FIRAuthWebUtils.h +++ b/Firebase/Auth/Source/FIRAuthWebUtils.h @@ -45,6 +45,19 @@ typedef void (^FIRFetchAuthDomainCallback)(NSString *_Nullable authDomain, */ + (BOOL)isCallbackSchemeRegisteredForCustomURLScheme:(NSString *)URLScheme; +/** @fn isExpectedCallbackURL:eventID:authType + @brief Parses a URL into all available query items. + @param URL The actual callback URL. + @param eventID The expected event ID. + @param authType The expected auth type. + @param callbackScheme The expected callback custom scheme. + @return Whether or not the actual callback URL matches the expected callback URL. + */ ++ (BOOL)isExpectedCallbackURL:(nullable NSURL *)URL + eventID:(NSString *)eventID + authType:(NSString *)authType + callbackScheme:(NSString *)callbackScheme; + /** @fn fetchAuthDomainWithCompletion:completion: @brief Fetches the auth domain associated with the Firebase Project. @param completion The callback invoked after the auth domain has been constructed or an error @@ -62,6 +75,20 @@ typedef void (^FIRFetchAuthDomainCallback)(NSString *_Nullable authDomain, + (nullable NSString *)queryItemValue:(NSString *)name from:(NSArray *)queryList; +/** @fn dictionaryWithHttpArgumentsString: + @brief Utility function to get a dictionary from a http argument string. + @param argString The http argument string. + @return The resulting dictionary of query arguments. + */ ++ (NSDictionary *)dictionaryWithHttpArgumentsString:(NSString *)argString; + +/** @fn stringByUnescapingFromURLArgument:from: + @brief Utility function to get a string by unescapting URL arguments. + @param argument The argument string. + @return The resulting string after unescaping URL argument. + */ ++ (NSString *)stringByUnescapingFromURLArgument:(NSString *)argument; + @end NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRAuthWebUtils.m b/Firebase/Auth/Source/FIRAuthWebUtils.m index 372e4245e88..423d87e5183 100644 --- a/Firebase/Auth/Source/FIRAuthWebUtils.m +++ b/Firebase/Auth/Source/FIRAuthWebUtils.m @@ -21,6 +21,8 @@ #import "FIRGetProjectConfigRequest.h" #import "FIRGetProjectConfigResponse.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kAuthDomainSuffix @brief The suffix of the auth domain pertiaining to a given Firebase project. */ @@ -51,6 +53,38 @@ + (BOOL)isCallbackSchemeRegisteredForCustomURLScheme:(NSString *)URLScheme { return NO; } ++ (BOOL)isExpectedCallbackURL:(nullable NSURL *)URL + eventID:(NSString *)eventID + authType:(NSString *)authType + callbackScheme:(NSString *)callbackScheme { + if (!URL) { + return NO; + } + NSURLComponents *actualURLComponents = + [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO]; + actualURLComponents.query = nil; + actualURLComponents.fragment = nil; + + NSURLComponents *expectedURLComponents = [[NSURLComponents alloc] init]; + expectedURLComponents.scheme = callbackScheme; + expectedURLComponents.host = @"firebaseauth"; + expectedURLComponents.path = @"/link"; + + if (![expectedURLComponents.URL isEqual:actualURLComponents.URL]) { + return NO; + } + NSDictionary *URLQueryItems = + [self dictionaryWithHttpArgumentsString:URL.query]; + NSURL *deeplinkURL = [NSURL URLWithString:URLQueryItems[@"deep_link_id"]]; + NSDictionary *deeplinkQueryItems = + [self dictionaryWithHttpArgumentsString:deeplinkURL.query]; + if ([deeplinkQueryItems[@"authType"] isEqualToString:authType] && + [deeplinkQueryItems[@"eventId"] isEqualToString:eventID]) { + return YES; + } + return NO; +} + + (void)fetchAuthDomainWithRequestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration completion:(FIRFetchAuthDomainCallback)completion { FIRGetProjectConfigRequest *request = @@ -87,7 +121,6 @@ + (void)fetchAuthDomainWithRequestConfiguration:(FIRAuthRequestConfiguration *)r @param queryList The NSURLQueryItem array. @return The value for the key. */ - + (nullable NSString *)queryItemValue:(NSString *)name from:(NSArray *)queryList { for (NSURLQueryItem *item in queryList) { if ([item.name isEqualToString:name]) { @@ -97,4 +130,44 @@ + (nullable NSString *)queryItemValue:(NSString *)name from:(NSArray @class FIRAuthRequestConfiguration; +@class FIRAuthURLPresenter; #if TARGET_OS_IOS @class FIRAuthAPNSTokenManager; @class FIRAuthAppCredentialManager; @class FIRAuthNotificationManager; -@class FIRAuthURLPresenter; #endif NS_ASSUME_NONNULL_BEGIN @@ -56,13 +56,13 @@ NS_ASSUME_NONNULL_BEGIN */ @property(nonatomic, strong, readonly) FIRAuthNotificationManager *notificationManager; +#endif // TARGET_OS_IOS + /** @property authURLPresenter @brief An object that takes care of presenting URLs via the auth instance. */ @property(nonatomic, strong, readonly) FIRAuthURLPresenter *authURLPresenter; -#endif // TARGET_OS_IOS - /** @fn initWithAPIKey:appName: @brief Designated initializer. @param APIKey The Google Developers Console API key for making requests from your app. diff --git a/Firebase/Auth/Source/FIRSecureTokenService.m b/Firebase/Auth/Source/FIRSecureTokenService.m index 69434ff063e..cf625b4fd1f 100644 --- a/Firebase/Auth/Source/FIRSecureTokenService.m +++ b/Firebase/Auth/Source/FIRSecureTokenService.m @@ -24,6 +24,8 @@ #import "FIRSecureTokenRequest.h" #import "FIRSecureTokenResponse.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kAPIKeyCodingKey @brief The key used to encode the APIKey for NSSecureCoding. */ @@ -204,3 +206,5 @@ - (BOOL)hasValidAccessToken { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/FIRUser.m b/Firebase/Auth/Source/FIRUser.m index 4bfdc1a80a9..9989614437f 100644 --- a/Firebase/Auth/Source/FIRUser.m +++ b/Firebase/Auth/Source/FIRUser.m @@ -1167,8 +1167,8 @@ - (void)unlinkFromProvider:(NSString *)provider FIRSetAccountInfoRequest *setAccountInfoRequest = [[FIRSetAccountInfoRequest alloc] initWithRequestConfiguration:requestConfiguration]; setAccountInfoRequest.accessToken = accessToken; - BOOL isEmailPasswordProvider = [provider isEqualToString:FIREmailAuthProviderID]; - if (isEmailPasswordProvider) { + + if ([provider isEqualToString:FIREmailAuthProviderID]) { if (!self->_hasEmailPasswordCredential) { completeAndCallbackWithError([FIRAuthErrorUtils noSuchProviderError]); return; @@ -1181,6 +1181,7 @@ - (void)unlinkFromProvider:(NSString *)provider } setAccountInfoRequest.deleteProviders = @[ provider ]; } + [FIRAuthBackend setAccountInfo:setAccountInfoRequest callback:^(FIRSetAccountInfoResponse *_Nullable response, NSError *_Nullable error) { @@ -1189,23 +1190,24 @@ - (void)unlinkFromProvider:(NSString *)provider completeAndCallbackWithError(error); return; } - if (isEmailPasswordProvider) { + + // We can't just use the provider info objects in FIRSetAccountInfoResponse because they + // don't have localID and email fields. Remove the specific provider manually. + NSMutableDictionary *mutableProviderData = [self->_providerData mutableCopy]; + [mutableProviderData removeObjectForKey:provider]; + self->_providerData = [mutableProviderData copy]; + + if ([provider isEqualToString:FIREmailAuthProviderID]) { self->_hasEmailPasswordCredential = NO; - } else { - // We can't just use the provider info objects in FIRSetAcccountInfoResponse because they - // don't have localID and email fields. Remove the specific provider manually. - NSMutableDictionary *mutableProviderData = [self->_providerData mutableCopy]; - [mutableProviderData removeObjectForKey:provider]; - self->_providerData = [mutableProviderData copy]; - - #if TARGET_OS_IOS - // After successfully unlinking a phone auth provider, remove the phone number from the - // cached user info. - if ([provider isEqualToString:FIRPhoneAuthProviderID]) { - self->_phoneNumber = nil; - } - #endif } + #if TARGET_OS_IOS + // After successfully unlinking a phone auth provider, remove the phone number from the + // cached user info. + if ([provider isEqualToString:FIRPhoneAuthProviderID]) { + self->_phoneNumber = nil; + } + #endif + if (response.IDToken && response.refreshToken) { FIRSecureTokenService *tokenService = [[FIRSecureTokenService alloc] initWithRequestConfiguration:requestConfiguration diff --git a/Firebase/Auth/Source/FIRUserInfoImpl.m b/Firebase/Auth/Source/FIRUserInfoImpl.m index d172481223d..2e804ab303a 100644 --- a/Firebase/Auth/Source/FIRUserInfoImpl.m +++ b/Firebase/Auth/Source/FIRUserInfoImpl.m @@ -18,6 +18,8 @@ #import "FIRGetAccountInfoResponse.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kProviderIDCodingKey @brief The key used to encode the providerID property for NSSecureCoding. */ @@ -125,3 +127,5 @@ - (void)encodeWithCoder:(NSCoder *)aCoder { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/NSData+FIRBase64.m b/Firebase/Auth/Source/NSData+FIRBase64.m index 173ec9bf227..b53f0532d81 100644 --- a/Firebase/Auth/Source/NSData+FIRBase64.m +++ b/Firebase/Auth/Source/NSData+FIRBase64.m @@ -16,6 +16,8 @@ #import "NSData+FIRBase64.h" +NS_ASSUME_NONNULL_BEGIN + @implementation NSData (FIRBase64) - (NSString *)fir_base64URLEncodedStringWithOptions:(NSDataBase64EncodingOptions)options { @@ -27,3 +29,5 @@ - (NSString *)fir_base64URLEncodedStringWithOptions:(NSDataBase64EncodingOptions } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/Public/FIRAuth.h b/Firebase/Auth/Source/Public/FIRAuth.h index cfc59b1f309..25ee65dac84 100644 --- a/Firebase/Auth/Source/Public/FIRAuth.h +++ b/Firebase/Auth/Source/Public/FIRAuth.h @@ -31,6 +31,8 @@ @class FIRAuthSettings; @class FIRUser; @protocol FIRAuthStateListener; +@protocol FIRAuthUIDelegate; +@protocol FIRFederatedAuthProvider; NS_ASSUME_NONNULL_BEGIN @@ -464,6 +466,54 @@ NS_SWIFT_NAME(Auth) " for Objective-C or signInAndRetrieveData(with:completion:)" " for Swift instead."); +/** @fn signInWithProvider:UIDelegate:completion: + @brief Signs in using the provided auth provider instance. + + @param provider An isntance of an auth provider used to initiate the sign-in flow. + @param UIDelegate Optionally an instance of a class conforming to the FIRAuthUIDelegate + protocol, this is used for presenting the web context. If nil, a default FIRAuthUIDelegate + will be used. + @param completion Optionally; a block which is invoked when the sign in flow finishes, or is + canceled. Invoked asynchronously on the main thread in the future. + + @remarks Possible error codes: +
    +
  • @c FIRAuthErrorCodeOperationNotAllowed - Indicates that email and password + accounts are not enabled. Enable them in the Auth section of the + Firebase console. +
  • +
  • @c FIRAuthErrorCodeUserDisabled - Indicates the user's account is disabled. +
  • +
  • @c FIRAuthErrorCodeWebNetworkRequestFailed - Indicates that a network request within a + SFSafariViewController or UIWebview failed. +
  • +
  • @c FIRAuthErrorCodeWebInternalError - Indicates that an internal error occurred within a + SFSafariViewController or UIWebview. +
  • +
  • @c FIRAuthErrorCodeWebSignInUserInteractionFailure - Indicates a general failure during + a web sign-in flow. +
  • +
  • @c FIRAuthErrorCodeWebContextAlreadyPresented - Indicates that an attempt was made to + present a new web context while one was already being presented. +
  • +
  • @c FIRAuthErrorCodeWebContextCancelled - Indicates that the URL presentation was cancelled prematurely + by the user. +
  • +
  • @c FIRAuthErrorCodeAccountExistsWithDifferentCredential - Indicates the email asserted + by the credential (e.g. the email in a Facebook access token) is already in use by an + existing account, that cannot be authenticated with this sign-in method. Call + fetchProvidersForEmail for this user’s email and then prompt them to sign in with any of + the sign-in providers returned. This error will only be thrown if the "One account per + email address" setting is enabled in the Firebase console, under Auth settings. +
  • +
+ + @remarks See @c FIRAuthErrors for a list of error codes that are common to all API methods. + */ +- (void)signInWithProvider:(id)provider + UIDelegate:(nullable id)UIDelegate + completion:(nullable FIRAuthDataResultCallback)completion; + /** @fn signInAndRetrieveDataWithCredential:completion: @brief Asynchronously signs in to Firebase with the given 3rd-party credentials (e.g. a Facebook login Access Token, a Google ID Token/Access Token pair, etc.) and returns additional diff --git a/Firebase/Auth/Source/Public/FIRAuthErrors.h b/Firebase/Auth/Source/Public/FIRAuthErrors.h index 9d177b662d7..2c90274e5b8 100644 --- a/Firebase/Auth/Source/Public/FIRAuthErrors.h +++ b/Firebase/Auth/Source/Public/FIRAuthErrors.h @@ -16,6 +16,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + /** @class FIRAuthErrors @remarks Error Codes common to all API Methods: @@ -43,23 +45,43 @@ NS_SWIFT_NAME(AuthErrors) extern NSString *const FIRAuthErrorDomain NS_SWIFT_NAME(AuthErrorDomain); /** - @brief The key used to read the updated credential from the userinfo dictionary of the NSError - object returned in the case that the credential being linked in already in use. + @brief Please use `FIRAuthErrorUserInfoUpdatedCredentialKey` for Objective C or + `AuthErrorUserInfoUpdatedCredentialKey` for Swift instead. + */ +extern NSString *const FIRAuthUpdatedCredentialKey + NS_SWIFT_NAME(AuthUpdatedCredentialKey) __attribute__((deprecated)); + +/** + @brief Please use `FIRAuthErrorUserInfoNameKey` for Objective C or + `AuthErrorUserInfoNameKey` for Swift instead. */ -extern NSString *const FIRAuthUpdatedCredentialKey NS_SWIFT_NAME(AuthUpdatedCredentialKey); +extern NSString *const FIRAuthErrorNameKey + NS_SWIFT_NAME(AuthErrorNameKey) __attribute__((deprecated)); /** - @brief The name of the key for the "error_name" string in the NSError userinfo dictionary. + @brief The name of the key for the error short string of an error code. */ -extern NSString *const FIRAuthErrorNameKey NS_SWIFT_NAME(AuthErrorNameKey); +extern NSString *const FIRAuthErrorUserInfoNameKey NS_SWIFT_NAME(AuthErrorUserInfoNameKey); /** - @brief Errors with the code `FIRAuthErrorCodeAccountExistsWithDifferentCredential` may contain - an `NSError.userInfo` dictinary object which contains this key. The value associated with - this key is an NSString of the email address of the account that already exists. + @brief Errors with one of the following three codes: + - `FIRAuthErrorCodeAccountExistsWithDifferentCredential` + - `FIRAuthErrorCodeCredentialAlreadyInUse` + - `FIRAuthErrorCodeEmailAlreadyInUse` + may contain an `NSError.userInfo` dictinary object which contains this key. The value + associated with this key is an NSString of the email address of the account that already + exists. */ extern NSString *const FIRAuthErrorUserInfoEmailKey NS_SWIFT_NAME(AuthErrorUserInfoEmailKey); +/** + @brief The key used to read the updated Auth credential from the userInfo dictionary of the + NSError object returned. This is the updated auth credential the developer should use for + recovery if applicable. + */ +extern NSString *const FIRAuthErrorUserInfoUpdatedCredentialKey + NS_SWIFT_NAME(AuthErrorUserInfoUpdatedCredentialKey); + /** @brief Error codes used by Firebase Auth. */ @@ -299,6 +321,10 @@ typedef NS_ENUM(NSInteger, FIRAuthErrorCode) { */ FIRAuthErrorCodeWebInternalError = 17062, + /** Indicates a general failure during a web sign-in flow. + */ + FIRAuthErrorCodeWebSignInUserInteractionFailure = 17063, + /** Indicates that the local player was not authenticated prior to attempting Game Center signin. */ FIRAuthErrorCodeLocalPlayerNotAuthenticated = 17066, @@ -308,11 +334,20 @@ typedef NS_ENUM(NSInteger, FIRAuthErrorCode) { */ FIRAuthErrorCodeNullUser = 17067, + /** + * Represents the error code for when the given provider id for a web operation is invalid. + */ + FIRAuthErrorCodeInvalidProviderID = 17071, + /** Indicates that the Firebase Dynamic Link domain used is either not configured or is unauthorized for the current project. */ FIRAuthErrorCodeInvalidDynamicLinkDomain = 17074, + /** Indicates that the GameKit framework is not linked prior to attempting Game Center signin. + */ + FIRAuthErrorCodeGameKitNotLinked = 17076, + /** Indicates an error occurred while attempting to access the keychain. */ FIRAuthErrorCodeKeychainError = 17995, @@ -328,3 +363,5 @@ typedef NS_ENUM(NSInteger, FIRAuthErrorCode) { } NS_SWIFT_NAME(AuthErrorCode); @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/Public/FIRAuthSettings.h b/Firebase/Auth/Source/Public/FIRAuthSettings.h index d3fee3ea981..05fc60199c8 100644 --- a/Firebase/Auth/Source/Public/FIRAuthSettings.h +++ b/Firebase/Auth/Source/Public/FIRAuthSettings.h @@ -16,6 +16,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + /** @class FIRAuthSettings @brief Determines settings related to an auth object. */ @@ -28,3 +30,5 @@ appVerificationDisabledForTesting; @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/Public/FIRAuthUIDelegate.h b/Firebase/Auth/Source/Public/FIRAuthUIDelegate.h index 9b32968268d..9df4f6e407e 100644 --- a/Firebase/Auth/Source/Public/FIRAuthUIDelegate.h +++ b/Firebase/Auth/Source/Public/FIRAuthUIDelegate.h @@ -15,7 +15,8 @@ */ #import -#import + +@class UIViewController; NS_ASSUME_NONNULL_BEGIN diff --git a/Firebase/Auth/Source/Public/FIRFederatedAuthProvider.h b/Firebase/Auth/Source/Public/FIRFederatedAuthProvider.h new file mode 100644 index 00000000000..51190e28cd9 --- /dev/null +++ b/Firebase/Auth/Source/Public/FIRFederatedAuthProvider.h @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Google + * + * 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 + +#if TARGET_OS_IOS +#import "FIRAuthUIDelegate.h" +#endif // TARGET_OS_IOS + +@class FIRAuthCredential; + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(FederatedAuthProvider) +@protocol FIRFederatedAuthProvider + +/** @typedef FIRAuthCredentialCallback + @brief The type of block invoked when obtaining an auth credential. + @param credential The credential obtained. + @param error The error that occurred if any. + */ +typedef void(^FIRAuthCredentialCallback)(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error) + NS_SWIFT_NAME(AuthCredentialCallback); + +#if TARGET_OS_IOS +/** @fn getCredentialWithUIDelegate:completion: + @brief Used to obtain an auth credential via a mobile web flow. + @param UIDelegate An optional UI delegate used to presenet the mobile web flow. + @param completion Optionally; a block which is invoked asynchronously on the main thread when + the mobile web flow is completed. + */ +- (void)getCredentialWithUIDelegate:(nullable id)UIDelegate + completion:(nullable FIRAuthCredentialCallback)completion; +#endif // TARGET_OS_IOS + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/Public/FIRGameCenterAuthProvider.h b/Firebase/Auth/Source/Public/FIRGameCenterAuthProvider.h index d49b8f6a505..5e59404adae 100644 --- a/Firebase/Auth/Source/Public/FIRGameCenterAuthProvider.h +++ b/Firebase/Auth/Source/Public/FIRGameCenterAuthProvider.h @@ -34,7 +34,7 @@ NS_SWIFT_NAME(GameCenterAuthSignInMethod); /** @typedef FIRGameCenterCredentialCallback @brief The type of block invoked when the Game Center credential code has finished. @param credential On success, the credential will be provided, nil otherwise. - @param error On error, the error that occured, nil otherwise. + @param error On error, the error that occurred, nil otherwise. */ typedef void (^FIRGameCenterCredentialCallback)(FIRAuthCredential *_Nullable credential, NSError *_Nullable error) diff --git a/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.h b/Firebase/Auth/Source/Public/FIROAuthCredential.h similarity index 66% rename from Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.h rename to Firebase/Auth/Source/Public/FIROAuthCredential.h index be6209efb61..43b1d8111f2 100644 --- a/Firebase/Auth/Source/AuthProviders/OAuth/FIROAuthCredential.h +++ b/Firebase/Auth/Source/Public/FIROAuthCredential.h @@ -16,13 +16,14 @@ #import -#import "FIRAuthCredential_Internal.h" +#import "FIRAuthCredential.h" NS_ASSUME_NONNULL_BEGIN /** @class FIROAuthCredential @brief Internal implementation of FIRAuthCredential for generic credentials. */ +NS_SWIFT_NAME(OAuthCredential) @interface FIROAuthCredential : FIRAuthCredential /** @property IDToken @@ -35,15 +36,15 @@ NS_ASSUME_NONNULL_BEGIN */ @property(nonatomic, readonly, nullable) NSString *accessToken; -/** @fn initWithProviderId:IDToken:accessToken: - @brief Designated initializer. - @param providerID The provider ID associated with the credential being created. - @param IDToken The ID Token associated with the credential being created. - @param accessToken The access token associated with the credential being created. +/** @property pendingToken + @brief The pending token used when completing the headful-lite flow. */ -- (nullable instancetype)initWithProviderID:(NSString *)providerID - IDToken:(nullable NSString*)IDToken - accessToken:(nullable NSString *)accessToken; +@property(nonatomic, readonly, nullable) NSString *pendingToken; + +/** @fn init + @brief This class is not supposed to be instantiated directly. + */ +- (instancetype)init NS_UNAVAILABLE; @end diff --git a/Firebase/Auth/Source/Public/FIROAuthProvider.h b/Firebase/Auth/Source/Public/FIROAuthProvider.h index cc628f8c66b..1ddf1f834df 100644 --- a/Firebase/Auth/Source/Public/FIROAuthProvider.h +++ b/Firebase/Auth/Source/Public/FIROAuthProvider.h @@ -16,15 +16,61 @@ #import -@class FIRAuthCredential; +#import "FIRFederatedAuthProvider.h" + +@class FIRAuth; +@class FIROAuthCredential; NS_ASSUME_NONNULL_BEGIN +/** + @brief A string constant identifying the Microsoft identity provider. + */ +extern NSString *const FIRMicrosoftAuthProviderID NS_SWIFT_NAME(MicrosoftAuthProviderID) + DEPRECATED_MSG_ATTRIBUTE("Please use \"microsoft.com\" instead."); + +/** + @brief A string constant identifying the Yahoo identity provider. + */ +extern NSString *const FIRYahooAuthProviderID NS_SWIFT_NAME(YahooAuthProviderID) + DEPRECATED_MSG_ATTRIBUTE("Please use \"yahoo.com\" instead."); + /** @class FIROAuthProvider @brief A concrete implementation of `FIRAuthProvider` for generic OAuth Providers. */ NS_SWIFT_NAME(OAuthProvider) -@interface FIROAuthProvider : NSObject +@interface FIROAuthProvider : NSObject + +/** @property scopes + @brief Array used to configure the OAuth scopes. + */ +@property(nonatomic, copy, nullable) NSArray *scopes; + +/** @property customParameters + @brief Dictionary used to configure the OAuth custom parameters. + */ +@property(nonatomic, copy, nullable) NSDictionary *customParameters; + +/** @property providerID + @brief The provider ID indicating the specific OAuth provider this OAuthProvider instance + represents. + */ +@property(nonatomic, copy, readonly) NSString *providerID; + +/** @fn providerWithProviderID: + @param providerID The provider ID of the IDP for which this auth provider instance will be + configured. + @return An instance of FIROAuthProvider corresponding to the specified provider ID. + */ ++ (FIROAuthProvider *)providerWithProviderID:(NSString *)providerID; + +/** @fn providerWithProviderID:auth: + @param providerID The provider ID of the IDP for which this auth provider instance will be + configured. + @param auth The auth instance to be associated with the FIROAuthProvider instance. + @return An instance of FIROAuthProvider corresponding to the specified provider ID. + */ ++ (FIROAuthProvider *)providerWithProviderID:(NSString *)providerID auth:(FIRAuth *)auth; /** @fn credentialWithProviderID:IDToken:accessToken: @brief Creates an `FIRAuthCredential` for that OAuth 2 provider identified by providerID, ID @@ -34,12 +80,27 @@ NS_SWIFT_NAME(OAuthProvider) @param IDToken The IDToken associated with the Auth credential being created. @param accessToken The accessstoken associated with the Auth credential be created, if available. + @param pendingToken The pending token used when completing the headful-lite flow. @return A FIRAuthCredential for the specified provider ID, ID token and access token. */ -+ (FIRAuthCredential *)credentialWithProviderID:(NSString *)providerID - IDToken:(NSString *)IDToken - accessToken:(nullable NSString *)accessToken; ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + IDToken:(NSString *)IDToken + accessToken:(nullable NSString *)accessToken + pendingToken:(nullable NSString *)pendingToken; + +/** @fn credentialWithProviderID:IDToken:accessToken: + @brief Creates an `FIRAuthCredential` for that OAuth 2 provider identified by providerID, ID + token and access token. + @param providerID The provider ID associated with the Auth credential being created. + @param IDToken The IDToken associated with the Auth credential being created. + @param accessToken The accessstoken associated with the Auth credential be created, if + available. + @return A FIRAuthCredential for the specified provider ID, ID token and access token. + */ ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + IDToken:(NSString *)IDToken + accessToken:(nullable NSString *)accessToken; /** @fn credentialWithProviderID:accessToken: @brief Creates an `FIRAuthCredential` for that OAuth 2 provider identified by providerID using @@ -49,8 +110,8 @@ NS_SWIFT_NAME(OAuthProvider) @param accessToken The accessstoken associated with the Auth credential be created @return A FIRAuthCredential. */ -+ (FIRAuthCredential *)credentialWithProviderID:(NSString *)providerID - accessToken:(NSString *)accessToken; ++ (FIROAuthCredential *)credentialWithProviderID:(NSString *)providerID + accessToken:(NSString *)accessToken; /** @fn init @brief This class is not meant to be initialized. diff --git a/Firebase/Auth/Source/Public/FIRPhoneAuthProvider.h b/Firebase/Auth/Source/Public/FIRPhoneAuthProvider.h index 587d64ccedb..a4301c16b58 100644 --- a/Firebase/Auth/Source/Public/FIRPhoneAuthProvider.h +++ b/Firebase/Auth/Source/Public/FIRPhoneAuthProvider.h @@ -38,8 +38,8 @@ extern NSString *const _Nonnull FIRPhoneAuthSignInMethod NS_SWIFT_NAME(PhoneAuth @param verificationID On success, the verification ID provided, nil otherwise. @param error On error, the error that occurred, nil otherwise. */ -typedef void (^FIRVerificationResultCallback)(NSString *_Nullable verificationID, - NSError *_Nullable error) +typedef void (^FIRVerificationResultCallback) + (NSString *_Nullable verificationID, NSError *_Nullable error) NS_SWIFT_NAME(VerificationResultCallback); /** @class FIRPhoneAuthProvider diff --git a/Firebase/Auth/Source/Public/FirebaseAuth.h b/Firebase/Auth/Source/Public/FirebaseAuth.h index 15693fe9d09..462d2ecf869 100644 --- a/Firebase/Auth/Source/Public/FirebaseAuth.h +++ b/Firebase/Auth/Source/Public/FirebaseAuth.h @@ -26,9 +26,11 @@ #import "FirebaseAuthVersion.h" #import "FIREmailAuthProvider.h" #import "FIRFacebookAuthProvider.h" +#import "FIRFederatedAuthProvider.h" #import "FIRGameCenterAuthProvider.h" #import "FIRGitHubAuthProvider.h" #import "FIRGoogleAuthProvider.h" +#import "FIROAuthCredential.h" #import "FIROAuthProvider.h" #import "FIRTwitterAuthProvider.h" #import "FIRUser.h" diff --git a/Firebase/Auth/Source/RPCs/FIRAuthBackend.m b/Firebase/Auth/Source/RPCs/FIRAuthBackend.m index d254174ca97..e862b5cd9f7 100644 --- a/Firebase/Auth/Source/RPCs/FIRAuthBackend.m +++ b/Firebase/Auth/Source/RPCs/FIRAuthBackend.m @@ -59,11 +59,14 @@ #import "FIRVerifyPhoneNumberRequest.h" #import "FIRVerifyPhoneNumberResponse.h" +#import "../AuthProviders/OAuth/FIROAuthCredential_Internal.h" #if TARGET_OS_IOS #import "../AuthProviders/Phone/FIRPhoneAuthCredential_Internal.h" #import "FIRPhoneAuthProvider.h" #endif +NS_ASSUME_NONNULL_BEGIN + /** @var kClientVersionHeader @brief HTTP header name for the client version. */ @@ -125,6 +128,12 @@ */ static NSString *const kErrorMessageKey = @"message"; +/** @var kReturnIDPCredentialErrorMessageKey + @brief The key for "errorMessage" value in JSON responses from the server, In case + returnIDPCredential of a verifyAssertion request is set to @YES. + */ +static NSString *const kReturnIDPCredentialErrorMessageKey = @"errorMessage"; + /** @var kUserNotFoundErrorMessage @brief This is the error message returned when the user is not found, which means the user account has been deleted given the token was once valid. @@ -285,9 +294,15 @@ */ static NSString *const kUnauthorizedDomainErrorMessage = @"UNAUTHORIZED_DOMAIN"; +/** @var kInvalidProviderIDErrorMessage + @brief This is the error message the server will respond with if the provider id given for the + web operation is invalid. + */ +static NSString *const kInvalidProviderIDErrorMessage = @"INVALID_PROVIDER_ID"; + /** @var kInvalidDynamicLinkDomainErrorMessage - @brief This is the error message the server will respond with if the dynamic link domain provided - in the request is invalid. + @brief This is the error message the server will respond with if the dynamic link domain + provided in the request is invalid. */ static NSString *const kInvalidDynamicLinkDomainErrorMessage = @"INVALID_DYNAMIC_LINK_DOMAIN"; @@ -370,6 +385,11 @@ */ static NSString *const kCaptchaCheckFailedErrorMessage = @"CAPTCHA_CHECK_FAILED"; +/** @var kInvalidPendingToken + @brief Generic IDP error codes. + */ +static NSString *const kInvalidPendingToken = @"INVALID_PENDING_TOKEN"; + /** @var gBackendImplementation @brief The singleton FIRAuthBackendImplementation instance to use. */ @@ -736,7 +756,8 @@ - (void)verifyPhoneNumber:(FIRVerifyPhoneNumberRequest *)request providerID:FIRPhoneAuthProviderID]; callback(nil, [FIRAuthErrorUtils credentialAlreadyInUseErrorWithMessage:nil - credential:credential]); + credential:credential + email:nil]); return; } callback(response, nil); @@ -800,7 +821,7 @@ - (void)signInWithGameCenter:(FIRSignInWithGameCenterRequest *)request */ - (void)postWithRequest:(id)request response:(id)response - callback:(void (^)(NSError *error))callback { + callback:(void (^)(NSError * _Nullable error))callback { NSError *error; NSData *bodyData; if ([request containsPostBody]) { @@ -911,7 +932,24 @@ - (void)postWithRequest:(id)request underlyingError:error]); return; } - + // In case returnIDPCredential of a verifyAssertion request is set to @YES, the server may + // return a 200 with a response that may contain a server error. + if ([request isKindOfClass:[FIRVerifyAssertionRequest class]]) { + FIRVerifyAssertionRequest *verifyAssertionRequest = (FIRVerifyAssertionRequest *)request; + if (verifyAssertionRequest.returnIDPCredential) { + NSString *errorMessage = dictionary[kReturnIDPCredentialErrorMessageKey]; + if ([errorMessage isKindOfClass:[NSString class]]) { + NSString *errorString = (NSString *)errorMessage; + NSError *clientError = [[self class] clientErrorWithServerErrorMessage:errorString + errorDictionary:@{} + response:response]; + if (clientError) { + callback(clientError); + return; + } + } + } + } // Success! The response object originally passed in can be used by the caller. callback(nil); }]; @@ -980,7 +1018,8 @@ + (nullable NSError *)clientErrorWithServerErrorMessage:(NSString *)serverErrorM return [FIRAuthErrorUtils customTokenMistmatchErrorWithMessage:serverDetailErrorMessage]; } - if ([shortErrorMessage isEqualToString:kInvalidCredentialErrorMessage]) { + if ([shortErrorMessage isEqualToString:kInvalidCredentialErrorMessage] || + [shortErrorMessage isEqualToString:kInvalidPendingToken]) { return [FIRAuthErrorUtils invalidCredentialErrorWithMessage:serverDetailErrorMessage]; } @@ -1024,8 +1063,22 @@ + (nullable NSError *)clientErrorWithServerErrorMessage:(NSString *)serverErrorM } if ([shortErrorMessage isEqualToString:kFederatedUserIDAlreadyLinkedMessage]) { + FIROAuthCredential *credential; + NSString *email; + if ([response isKindOfClass:[FIRVerifyAssertionResponse class]]) { + FIRVerifyAssertionResponse *verifyAssertion = (FIRVerifyAssertionResponse *)response; + if (verifyAssertion.oauthIDToken.length || verifyAssertion.oauthAccessToken.length) { + credential = + [[FIROAuthCredential alloc] initWithProviderID:verifyAssertion.providerID + IDToken:verifyAssertion.oauthIDToken + accessToken:verifyAssertion.oauthAccessToken + pendingToken:verifyAssertion.pendingToken]; + } + email = verifyAssertion.email; + } return [FIRAuthErrorUtils credentialAlreadyInUseErrorWithMessage:serverDetailErrorMessage - credential:nil]; + credential:credential + email:email]; } if ([shortErrorMessage isEqualToString:kWeakPasswordErrorMessagePrefix]) { @@ -1072,6 +1125,10 @@ + (nullable NSError *)clientErrorWithServerErrorMessage:(NSString *)serverErrorM return [FIRAuthErrorUtils invalidContinueURIErrorWithMessage:serverDetailErrorMessage]; } + if ([shortErrorMessage isEqualToString:kInvalidProviderIDErrorMessage]) { + return [FIRAuthErrorUtils invalidProviderIDErrorWithMessage:serverDetailErrorMessage]; + } + if ([shortErrorMessage isEqualToString:kInvalidDynamicLinkDomainErrorMessage]) { return [FIRAuthErrorUtils invalidDynamicLinkDomainErrorWithMessage:serverDetailErrorMessage]; } @@ -1152,3 +1209,5 @@ + (nullable NSError *)clientErrorWithServerErrorMessage:(NSString *)serverErrorM } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.m b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.m index dae46fb0643..de97d4d412d 100644 --- a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.m +++ b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIRequest.m @@ -16,6 +16,8 @@ #import "FIRCreateAuthURIRequest.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kCreateAuthURIEndpoint @brief The "createAuthUri" endpoint. */ @@ -93,3 +95,5 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.m b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.m index 6f2937f504f..474582e7f81 100644 --- a/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.m +++ b/Firebase/Auth/Source/RPCs/FIRCreateAuthURIResponse.m @@ -16,6 +16,8 @@ #import "FIRCreateAuthURIResponse.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRCreateAuthURIResponse - (BOOL)setWithDictionary:(NSDictionary *)dictionary @@ -30,3 +32,5 @@ - (BOOL)setWithDictionary:(NSDictionary *)dictionary } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.m b/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.m index 2222210c1de..701d4463346 100644 --- a/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.m +++ b/Firebase/Auth/Source/RPCs/FIRDeleteAccountRequest.m @@ -16,6 +16,8 @@ #import "FIRDeleteAccountRequest.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kCreateAuthURIEndpoint @brief The "deleteAccount" endpoint. */ @@ -63,3 +65,5 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.h b/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.h index 59226d6899b..cf09f942f47 100644 --- a/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.h +++ b/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.h @@ -18,9 +18,13 @@ #import "FIRAuthRPCResponse.h" +NS_ASSUME_NONNULL_BEGIN + /** @class FIRDeleteAccountResponse @brief Represents the response from the deleteAccount endpoint. @see https://developers.google.com/identity/toolkit/web/reference/relyingparty/deleteAccount */ @interface FIRDeleteAccountResponse : NSObject @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.m b/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.m index ae981753fd7..d75d2eb77af 100644 --- a/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.m +++ b/Firebase/Auth/Source/RPCs/FIRDeleteAccountResponse.m @@ -16,6 +16,8 @@ #import "FIRDeleteAccountResponse.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRDeleteAccountResponse - (BOOL)setWithDictionary:(NSDictionary *)dictionary @@ -24,3 +26,5 @@ - (BOOL)setWithDictionary:(NSDictionary *)dictionary } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIREmailLinkSignInRequest.m b/Firebase/Auth/Source/RPCs/FIREmailLinkSignInRequest.m index 9787e8e57b5..2750f9fe991 100644 --- a/Firebase/Auth/Source/RPCs/FIREmailLinkSignInRequest.m +++ b/Firebase/Auth/Source/RPCs/FIREmailLinkSignInRequest.m @@ -16,6 +16,8 @@ #import "FIREmailLinkSignInRequest.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kEmailLinkSigninEndpoint @brief The "EmailLinkSignin" endpoint. */ @@ -68,3 +70,5 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIREmailLinkSignInResponse.m b/Firebase/Auth/Source/RPCs/FIREmailLinkSignInResponse.m index cd36d41c3b9..f58cab5fb4c 100644 --- a/Firebase/Auth/Source/RPCs/FIREmailLinkSignInResponse.m +++ b/Firebase/Auth/Source/RPCs/FIREmailLinkSignInResponse.m @@ -16,6 +16,8 @@ #import "FIREmailLinkSignInResponse.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIREmailLinkSignInResponse - (BOOL)setWithDictionary:(NSDictionary *)dictionary @@ -30,3 +32,5 @@ - (BOOL)setWithDictionary:(NSDictionary *)dictionary } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.m b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.m index fde79fbfb13..e7079371b81 100644 --- a/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.m +++ b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoRequest.m @@ -16,6 +16,8 @@ #import "FIRGetAccountInfoRequest.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kGetAccountInfoEndpoint @brief The "getAccountInfo" endpoint. */ @@ -46,3 +48,5 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.m b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.m index 19ab64ac713..cb78b78674a 100644 --- a/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.m +++ b/Firebase/Auth/Source/RPCs/FIRGetAccountInfoResponse.m @@ -18,6 +18,8 @@ #import "FIRAuthErrorUtils.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kErrorKey @brief The key for the "error" value in JSON responses from the server. */ @@ -102,3 +104,5 @@ - (BOOL)setWithDictionary:(NSDictionary *)dictionary } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m index 82f1a271fa8..4fa953f06d9 100644 --- a/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m +++ b/Firebase/Auth/Source/RPCs/FIRGetOOBConfirmationCodeRequest.m @@ -21,6 +21,8 @@ #import "FIRAuthErrorUtils.h" #import "FIRAuth_Internal.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kEndpoint @brief The getOobConfirmationCode endpoint name. */ @@ -132,7 +134,7 @@ + (NSString *)requestTypeStringValueForRequestType: } } -+ (FIRGetOOBConfirmationCodeRequest *) ++ (nullable FIRGetOOBConfirmationCodeRequest *) passwordResetRequestWithEmail:(NSString *)email actionCodeSettings:(nullable FIRActionCodeSettings *)actionCodeSettings requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration { @@ -143,7 +145,7 @@ + (NSString *)requestTypeStringValueForRequestType: requestConfiguration:requestConfiguration]; } -+ (FIRGetOOBConfirmationCodeRequest *) ++ (nullable FIRGetOOBConfirmationCodeRequest *) verifyEmailRequestWithAccessToken:(NSString *)accessToken actionCodeSettings:(nullable FIRActionCodeSettings *)actionCodeSettings requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration { @@ -154,7 +156,7 @@ + (NSString *)requestTypeStringValueForRequestType: requestConfiguration:requestConfiguration]; } -+ (FIRGetOOBConfirmationCodeRequest *) ++ (nullable FIRGetOOBConfirmationCodeRequest *) signInWithEmailLinkRequest:(NSString *)email actionCodeSettings:(nullable FIRActionCodeSettings *)actionCodeSettings requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration { @@ -242,3 +244,5 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRGetProjectConfigResponse.h b/Firebase/Auth/Source/RPCs/FIRGetProjectConfigResponse.h index 317ec81c73c..bd27cd2713f 100644 --- a/Firebase/Auth/Source/RPCs/FIRGetProjectConfigResponse.h +++ b/Firebase/Auth/Source/RPCs/FIRGetProjectConfigResponse.h @@ -18,6 +18,8 @@ #import "FIRAuthRPCResponse.h" +NS_ASSUME_NONNULL_BEGIN + /** @class FIRGetProjectConfigResponse @brief Represents the response from the getProjectConfig endpoint. */ @@ -34,3 +36,5 @@ @property(nonatomic, strong, readonly, nullable) NSArray *authorizedDomains; @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRGetProjectConfigResponse.m b/Firebase/Auth/Source/RPCs/FIRGetProjectConfigResponse.m index 259a4fbae03..030edd1979a 100644 --- a/Firebase/Auth/Source/RPCs/FIRGetProjectConfigResponse.m +++ b/Firebase/Auth/Source/RPCs/FIRGetProjectConfigResponse.m @@ -16,6 +16,8 @@ #import "FIRGetProjectConfigResponse.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRGetProjectConfigResponse - (BOOL)setWithDictionary:(NSDictionary *)dictionary @@ -36,3 +38,5 @@ - (BOOL)setWithDictionary:(NSDictionary *)dictionary } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.m b/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.m index 27d6d8cf05d..129503755c6 100644 --- a/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.m +++ b/Firebase/Auth/Source/RPCs/FIRResetPasswordRequest.m @@ -16,6 +16,8 @@ #import "FIRResetPasswordRequest.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kResetPasswordEndpoint @brief The "resetPassword" endpoint. */ @@ -33,9 +35,9 @@ @implementation FIRResetPasswordRequest -- (instancetype)initWithOobCode:(NSString *)oobCode - newPassword:(NSString *)newPassword - requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration { +- (nullable instancetype)initWithOobCode:(NSString *)oobCode + newPassword:(nullable NSString *)newPassword + requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration { self = [super initWithEndpoint:kResetPasswordEndpoint requestConfiguration:requestConfiguration]; if (self) { _oobCode = oobCode; @@ -54,3 +56,5 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.m b/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.m index 6092cfe1142..4f43cc987ba 100644 --- a/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.m +++ b/Firebase/Auth/Source/RPCs/FIRResetPasswordResponse.m @@ -16,6 +16,8 @@ #import "FIRResetPasswordResponse.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRResetPasswordResponse - (BOOL)setWithDictionary:(NSDictionary *)dictionary @@ -27,3 +29,5 @@ - (BOOL)setWithDictionary:(NSDictionary *)dictionary } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.m b/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.m index 786ea6c0f8f..b733a94fa97 100644 --- a/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.m +++ b/Firebase/Auth/Source/RPCs/FIRSecureTokenRequest.m @@ -17,6 +17,8 @@ #import "FIRSecureTokenRequest.h" #import "FIRAuthRequestConfiguration.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kFIRSecureTokenServiceGetTokenURLFormat @brief The format of the secure token service URLs. Requires string format substitution with the client's API Key. @@ -157,3 +159,5 @@ + (void)setHost:(NSString *)host { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.m b/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.m index b97fda50a40..1b1797b9f9c 100644 --- a/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.m +++ b/Firebase/Auth/Source/RPCs/FIRSecureTokenResponse.m @@ -18,6 +18,8 @@ #import "FIRAuthErrorUtils.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kExpiresInKey @brief The key for the number of seconds till the access token expires. */ @@ -68,3 +70,5 @@ - (BOOL)setWithDictionary:(NSDictionary *)dictionary } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.m b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.m index f455d474ecb..ef06d2b6f1f 100644 --- a/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.m +++ b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoRequest.m @@ -20,6 +20,8 @@ #import "FIRAuth_Internal.h" #import "FIRGetAccountInfoResponse.h" +NS_ASSUME_NONNULL_BEGIN + NSString *const FIRSetAccountInfoUserAttributeEmail = @"EMAIL"; NSString *const FIRSetAccountInfoUserAttributeDisplayName = @"DISPLAY_NAME"; @@ -173,3 +175,6 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) } @end + +NS_ASSUME_NONNULL_END + diff --git a/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.m b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.m index ff9c7a6f754..7054a44e02d 100644 --- a/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.m +++ b/Firebase/Auth/Source/RPCs/FIRSetAccountInfoResponse.m @@ -16,6 +16,8 @@ #import "FIRSetAccountInfoResponse.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRSetAccountInfoResponseProviderUserInfo - (instancetype)initWithDictionary:(NSDictionary *)dictionary { @@ -57,3 +59,5 @@ - (BOOL)setWithDictionary:(NSDictionary *)dictionary } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.m b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.m index 52a02154a31..5d50e0a2a1b 100644 --- a/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.m +++ b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserRequest.m @@ -16,6 +16,8 @@ #import "FIRSignUpNewUserRequest.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kSignupNewUserEndpoint @brief The "SingupNewUserEndpoint" endpoint. */ @@ -84,3 +86,5 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.m b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.m index 2071e0772bb..03d0616e054 100644 --- a/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.m +++ b/Firebase/Auth/Source/RPCs/FIRSignUpNewUserResponse.m @@ -16,6 +16,8 @@ #import "FIRSignUpNewUserResponse.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRSignUpNewUserResponse - (BOOL)setWithDictionary:(NSDictionary *)dictionary @@ -28,3 +30,5 @@ - (BOOL)setWithDictionary:(NSDictionary *)dictionary } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.h b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.h index 3136b805a4e..595ee9b32af 100644 --- a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.h +++ b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.h @@ -33,10 +33,10 @@ NS_ASSUME_NONNULL_BEGIN */ @property(nonatomic, copy, nullable) NSString *requestURI; -/** @property pendingIDToken - @brief The Firebase ID Token for the non-trusted IDP pending to be confirmed by the user. +/** @property pendingToken + @brief The Firebase ID Token for the IDP pending to be confirmed by the user. */ -@property(nonatomic, copy, nullable) NSString *pendingIDToken; +@property(nonatomic, copy, nullable) NSString *pendingToken; /** @property accessToken @brief The STS Access Token for the authenticated user, only needed for linking the user. @@ -66,6 +66,16 @@ NS_ASSUME_NONNULL_BEGIN */ @property(nonatomic, copy, nullable) NSString *providerIDToken; +/** @property returnIDPCredential + @brief Whether the response should return the IDP credential directly. + */ +@property(nonatomic, assign) BOOL returnIDPCredential; + +/** @property providerOAuthTokenSecret + @brief A session ID used to map this request to a headful-lite flow. + */ +@property(nonatomic, copy, nullable) NSString *sessionID; + /** @property providerOAuthTokenSecret @brief An OAuth client secret from the IDP. */ diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.m b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.m index 274fd075e9c..3a819d7a8ec 100644 --- a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.m +++ b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionRequest.m @@ -16,6 +16,8 @@ #import "FIRVerifyAssertionRequest.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kVerifyAssertionEndpoint @brief The "verifyAssertion" endpoint. */ @@ -56,10 +58,10 @@ */ static NSString *const kPostBodyKey = @"postBody"; -/** @var kPendingIDTokenKey - @brief The key for the "pendingIdToken" value in the request. +/** @var kPendingTokenKey + @brief The key for the "pendingToken" value in the request. */ -static NSString *const kPendingIDTokenKey = @"pendingIdToken"; +static NSString *const kPendingTokenKey = @"pendingToken"; /** @var kAutoCreateKey @brief The key for the "autoCreate" value in the request. @@ -77,6 +79,16 @@ */ static NSString *const kReturnSecureTokenKey = @"returnSecureToken"; +/** @var kReturnIDPCredentialKey + @brief The key for the "returnIdpCredential" value in the request. + */ +static NSString *const kReturnIDPCredentialKey = @"returnIdpCredential"; + +/** @var kSessionIDKey + @brief The key for the "sessionID" value in the request. + */ +static NSString *const kSessionIDKey = @"sessionId"; + @implementation FIRVerifyAssertionRequest - (nullable instancetype)initWithProviderID:(NSString *)providerID @@ -87,6 +99,7 @@ - (nullable instancetype)initWithProviderID:(NSString *)providerID _providerID = providerID; _returnSecureToken = YES; _autoCreate = YES; + _returnIDPCredential = YES; } return self; } @@ -107,9 +120,9 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) value:_providerAccessToken]]; } - if (!_providerIDToken && !_providerAccessToken) { + if (!_providerIDToken && !_providerAccessToken && !_pendingToken && !_requestURI) { [NSException raise:NSInvalidArgumentException - format:@"Either IDToken or accessToken must be supplied."]; + format:@"One of IDToken, accessToken, pendingToken, or requestURI must be supplied."]; } if (_providerOAuthTokenSecret) { @@ -123,12 +136,12 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) } [components setQueryItems:queryItems]; NSMutableDictionary *body = [@{ - kRequestURIKey : @"http://localhost", // Unused by server, but required + kRequestURIKey : _requestURI ?: @"http://localhost", // Unused by server, but required kPostBodyKey : [components query] } mutableCopy]; - if (_pendingIDToken) { - body[kPendingIDTokenKey] = _pendingIDToken; + if (_pendingToken) { + body[kPendingTokenKey] = _pendingToken; } if (_accessToken) { body[kIDTokenKey] = _accessToken; @@ -137,9 +150,19 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) body[kReturnSecureTokenKey] = @YES; } + if (_returnIDPCredential) { + body[kReturnIDPCredentialKey] = @YES; + } + + if (_sessionID) { + body[kSessionIDKey] = _sessionID; + } + body[kAutoCreateKey] = @(_autoCreate); return body; } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.h b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.h index ce796eee9ee..a75d0a259d4 100644 --- a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.h +++ b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.h @@ -181,6 +181,26 @@ NS_ASSUME_NONNULL_BEGIN */ @property(nonatomic, strong, readonly, nullable) NSString *username; +/** @property oauthIDToken + @brief The ID token for the OpenID OAuth extension. + */ +@property(nonatomic, strong, readonly, nullable) NSString *oauthIDToken; + +/** @property oauthExpirationDate + @brief The approximate expiration date of the oauth access token. + */ +@property(nonatomic, copy, readonly, nullable) NSDate *oauthExpirationDate; + +/** @property oauthAccessToken + @brief The access token for the OpenID OAuth extension. + */ +@property(nonatomic, strong, readonly, nullable) NSString *oauthAccessToken; + +/** @property pendingToken + @brief The pending ID Token string. + */ +@property(nonatomic, copy, nullable) NSString *pendingToken; + @end NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.m b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.m index 5ee39fa52cb..e4792414cc7 100644 --- a/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.m +++ b/Firebase/Auth/Source/RPCs/FIRVerifyAssertionResponse.m @@ -16,6 +16,8 @@ #import "FIRVerifyAssertionResponse.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRVerifyAssertionResponse - (BOOL)setWithDictionary:(NSDictionary *)dictionary @@ -70,7 +72,14 @@ - (BOOL)setWithDictionary:(NSDictionary *)dictionary _verifiedProvider = [[NSArray alloc] initWithArray:verifiedProvider copyItems:YES]; } + _oauthIDToken = [dictionary[@"oauthIdToken"] copy]; + _oauthExpirationDate = [dictionary[@"oauthExpireIn"] isKindOfClass:[NSString class]] ? + [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"oauthExpireIn"] doubleValue]] : nil; + _oauthAccessToken = [dictionary[@"oauthAccessToken"] copy]; + _pendingToken = [dictionary[@"pendingToken"] copy]; return YES; } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.m b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.m index 79e60f4effb..9ad46a02f9b 100644 --- a/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.m +++ b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenRequest.m @@ -16,6 +16,8 @@ #import "FIRVerifyCustomTokenRequest.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kVerifyCustomTokenEndpoint @brief The "verifyPassword" endpoint. */ @@ -55,3 +57,5 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.m b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.m index b6c3818c591..8b673601c45 100644 --- a/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.m +++ b/Firebase/Auth/Source/RPCs/FIRVerifyCustomTokenResponse.m @@ -16,6 +16,8 @@ #import "FIRVerifyCustomTokenResponse.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRVerifyCustomTokenResponse - (BOOL)setWithDictionary:(NSDictionary *)dictionary @@ -29,3 +31,5 @@ - (BOOL)setWithDictionary:(NSDictionary *)dictionary } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.m b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.m index 515a425240b..5849da68f1e 100644 --- a/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.m +++ b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordRequest.m @@ -16,6 +16,8 @@ #import "FIRVerifyPasswordRequest.h" +NS_ASSUME_NONNULL_BEGIN + /** @var kVerifyPasswordEndpoint @brief The "verifyPassword" endpoint. */ @@ -90,3 +92,5 @@ - (nullable id)unencodedHTTPRequestBodyWithError:(NSError *_Nullable *_Nullable) } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.m b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.m index 71b4edd100f..b42a37100c1 100644 --- a/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.m +++ b/Firebase/Auth/Source/RPCs/FIRVerifyPasswordResponse.m @@ -16,6 +16,8 @@ #import "FIRVerifyPasswordResponse.h" +NS_ASSUME_NONNULL_BEGIN + @implementation FIRVerifyPasswordResponse - (BOOL)setWithDictionary:(NSDictionary *)dictionary @@ -32,3 +34,5 @@ - (BOOL)setWithDictionary:(NSDictionary *)dictionary } @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Core/CHANGELOG.md b/Firebase/Core/CHANGELOG.md index 775c7de2905..05eb0cd3a03 100644 --- a/Firebase/Core/CHANGELOG.md +++ b/Firebase/Core/CHANGELOG.md @@ -1,4 +1,8 @@ -# Unreleased +# 2019-03-19 -- v5.4.0 -- M45 +- [changed] Allow Bundle IDs that have a valid prefix to enable richer extension support. (#2515) +- [changed] Deprecated `FIRAnalyticsConfiguration` API in favor of new methods on the Analytics SDK. + Please call the new APIs directly: Enable/disable Analytics with `Analytics.setAnalyticsCollectionEnabled(_)` + and modify the session timeout interval with `Analytics.setSessionTimeoutInterval(_)`. # 2019-01-22 -- v5.2.0 -- M41 - [changed] Added a registerInternalLibrary API. Now other Firebase libraries register with FirebaseCore diff --git a/Firebase/Core/FIRAnalyticsConfiguration.m b/Firebase/Core/FIRAnalyticsConfiguration.m index 33aa1687f52..e584839efca 100644 --- a/Firebase/Core/FIRAnalyticsConfiguration.m +++ b/Firebase/Core/FIRAnalyticsConfiguration.m @@ -16,7 +16,10 @@ #import "Private/FIRAnalyticsConfiguration+Internal.h" +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" @implementation FIRAnalyticsConfiguration +#pragma clang diagnostic pop + (FIRAnalyticsConfiguration *)sharedInstance { static FIRAnalyticsConfiguration *sharedInstance = nil; diff --git a/Firebase/Core/FIRApp.m b/Firebase/Core/FIRApp.m index 799db26f5c8..01324cea98e 100644 --- a/Firebase/Core/FIRApp.m +++ b/Firebase/Core/FIRApp.m @@ -161,8 +161,8 @@ + (void)configureWithName:(NSString *)name options:(FIROptions *)options { if (!((character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9') || character == '_' || character == '-')) { [NSException raise:kFirebaseCoreErrorDomain - format:@"App name should only contain Letters, " - @"Numbers, Underscores, and Dashes."]; + format:@"App name can only contain alphanumeric (A-Z,a-z,0-9), " + @"hyphen (-), and underscore (_) characters"]; } } @@ -320,6 +320,7 @@ - (BOOL)configureCore { if ([firAnalyticsClass respondsToSelector:startWithConfigurationSelector]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [firAnalyticsClass performSelector:startWithConfigurationSelector withObject:[FIRConfiguration sharedInstance].analyticsConfiguration withObject:_options]; @@ -360,9 +361,12 @@ - (void)setDataCollectionDefaultEnabled:(BOOL)dataCollectionDefaultEnabled { } // The Analytics flag has not been explicitly set, so update with the value being set. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [[FIRAnalyticsConfiguration sharedInstance] setAnalyticsCollectionEnabled:dataCollectionDefaultEnabled persistSetting:NO]; +#pragma clang diagnostic pop } - (BOOL)isDataCollectionDefaultEnabled { @@ -525,8 +529,8 @@ - (void)checkExpectedBundleID { NSString *expectedBundleID = [self expectedBundleID]; // The checking is only done when the bundle ID is provided in the serviceInfo dictionary for // backward compatibility. - if (expectedBundleID != nil && ![FIRBundleUtil hasBundleIdentifier:expectedBundleID - inBundles:bundles]) { + if (expectedBundleID != nil && ![FIRBundleUtil hasBundleIdentifierPrefix:expectedBundleID + inBundles:bundles]) { FIRLogError(kFIRLoggerCore, @"I-COR000008", @"The project's Bundle ID is inconsistent with " @"either the Bundle ID in '%@.%@', or the Bundle ID in the options if you are " @@ -571,33 +575,32 @@ + (BOOL)validateAppID:(NSString *)appID { return NO; } - // All app IDs must start with at least ":". - NSString *const versionPattern = @"^\\d+:"; - NSRegularExpression *versionRegex = - [NSRegularExpression regularExpressionWithPattern:versionPattern options:0 error:NULL]; - if (!versionRegex) { + NSScanner *stringScanner = [NSScanner scannerWithString:appID]; + stringScanner.charactersToBeSkipped = nil; + + NSString *appIDVersion; + if (![stringScanner scanCharactersFromSet:[NSCharacterSet decimalDigitCharacterSet] + intoString:&appIDVersion]) { return NO; } - NSRange appIDRange = NSMakeRange(0, appID.length); - NSArray *versionMatches = [versionRegex matchesInString:appID options:0 range:appIDRange]; - if (versionMatches.count != 1) { + if (![stringScanner scanString:@":" intoString:NULL]) { + // appIDVersion must be separated by ":" return NO; } - NSRange versionRange = [(NSTextCheckingResult *)versionMatches.firstObject range]; - NSString *appIDVersion = [appID substringWithRange:versionRange]; - NSArray *knownVersions = @[ @"1:" ]; + NSArray *knownVersions = @[ @"1" ]; if (![knownVersions containsObject:appIDVersion]) { // Permit unknown yet properly formatted app ID versions. + FIRLogInfo(kFIRLoggerCore, @"I-COR000010", @"Unknown GOOGLE_APP_ID version: %@", appIDVersion); return YES; } - if (![FIRApp validateAppIDFormat:appID withVersion:appIDVersion]) { + if (![self validateAppIDFormat:appID withVersion:appIDVersion]) { return NO; } - if (![FIRApp validateAppIDFingerprint:appID withVersion:appIDVersion]) { + if (![self validateAppIDFingerprint:appID withVersion:appIDVersion]) { return NO; } @@ -627,33 +630,76 @@ + (BOOL)validateAppIDFormat:(NSString *)appID withVersion:(NSString *)version { return NO; } - if (![version hasSuffix:@":"]) { + NSScanner *stringScanner = [NSScanner scannerWithString:appID]; + stringScanner.charactersToBeSkipped = nil; + + // Skip version part + // '**::ios:' + if (![stringScanner scanString:version intoString:NULL]) { + // The version part is missing or mismatched + return NO; + } + + // Validate version part (see part between '*' symbols below) + // '*:*:ios:' + if (![stringScanner scanString:@":" intoString:NULL]) { + // appIDVersion must be separated by ":" + return NO; + } + + // Validate version part (see part between '*' symbols below) + // ':**:ios:'. + NSInteger projectNumber = NSNotFound; + if (![stringScanner scanInteger:&projectNumber]) { + // NO project number found. + return NO; + } + + // Validate version part (see part between '*' symbols below) + // ':*:*ios:'. + if (![stringScanner scanString:@":" intoString:NULL]) { + // The project number must be separated by ":" + return NO; + } + + // Validate version part (see part between '*' symbols below) + // '::*ios*:'. + NSString *platform; + if (![stringScanner scanUpToString:@":" intoString:&platform]) { + return NO; + } + + if (![platform isEqualToString:@"ios"]) { + // The platform must be @"ios" return NO; } - if (![appID hasPrefix:version]) { + // Validate version part (see part between '*' symbols below) + // '::ios*:*'. + if (![stringScanner scanString:@":" intoString:NULL]) { + // The platform must be separated by ":" return NO; } - NSString *const pattern = @"^\\d+:ios:[a-f0-9]+$"; - NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern - options:0 - error:NULL]; - if (!regex) { + // Validate version part (see part between '*' symbols below) + // '::ios:**'. + unsigned long long fingerprint = NSNotFound; + if (![stringScanner scanHexLongLong:&fingerprint]) { + // Fingerprint part is missing return NO; } - NSRange localRange = NSMakeRange(version.length, appID.length - version.length); - NSUInteger numberOfMatches = [regex numberOfMatchesInString:appID options:0 range:localRange]; - if (numberOfMatches != 1) { + if (!stringScanner.isAtEnd) { + // There are not allowed characters in the fingerprint part return NO; } + return YES; } /** * Validates that the fingerprint of the app ID string is what is expected based on the supplied - * version. The version must end in ":". + * version. * * Note that the v1 hash algorithm is not permitted on the client and cannot be fully validated. * @@ -663,18 +709,6 @@ + (BOOL)validateAppIDFormat:(NSString *)appID withVersion:(NSString *)version { * otherwise. */ + (BOOL)validateAppIDFingerprint:(NSString *)appID withVersion:(NSString *)version { - if (!appID.length || !version.length) { - return NO; - } - - if (![version hasSuffix:@":"]) { - return NO; - } - - if (![appID hasPrefix:version]) { - return NO; - } - // Extract the supplied fingerprint from the supplied app ID. // This assumes the app ID format is the same for all known versions below. If the app ID format // changes in future versions, the tokenizing of the app ID format will need to take into account @@ -695,7 +729,7 @@ + (BOOL)validateAppIDFingerprint:(NSString *)appID withVersion:(NSString *)versi return NO; } - if ([version isEqual:@"1:"]) { + if ([version isEqual:@"1"]) { // The v1 hash algorithm is not permitted on the client so the actual hash cannot be validated. return YES; } diff --git a/Firebase/Core/FIRBundleUtil.m b/Firebase/Core/FIRBundleUtil.m index 93ee02e97db..841833a8519 100644 --- a/Firebase/Core/FIRBundleUtil.m +++ b/Firebase/Core/FIRBundleUtil.m @@ -14,6 +14,8 @@ #import "Private/FIRBundleUtil.h" +#import + @implementation FIRBundleUtil + (NSArray *)relevantBundles { @@ -45,13 +47,29 @@ + (NSArray *)relevantURLSchemes { return result; } -+ (BOOL)hasBundleIdentifier:(NSString *)bundleIdentifier inBundles:(NSArray *)bundles { ++ (BOOL)hasBundleIdentifierPrefix:(NSString *)bundleIdentifier inBundles:(NSArray *)bundles { for (NSBundle *bundle in bundles) { - if ([bundle.bundleIdentifier isEqualToString:bundleIdentifier]) { + // This allows app extensions that have the app's bundle as their prefix to pass this test. + NSString *applicationBundleIdentifier = + [GULAppEnvironmentUtil isAppExtension] + ? [self bundleIdentifierByRemovingLastPartFrom:bundleIdentifier] + : bundleIdentifier; + + if ([applicationBundleIdentifier isEqualToString:bundle.bundleIdentifier]) { return YES; } } return NO; } ++ (NSString *)bundleIdentifierByRemovingLastPartFrom:(NSString *)bundleIdentifier { + NSString *bundleIDComponentsSeparator = @"."; + + NSMutableArray *bundleIDComponents = + [[bundleIdentifier componentsSeparatedByString:bundleIDComponentsSeparator] mutableCopy]; + [bundleIDComponents removeLastObject]; + + return [bundleIDComponents componentsJoinedByString:bundleIDComponentsSeparator]; +} + @end diff --git a/Firebase/Core/FIRConfiguration.m b/Firebase/Core/FIRConfiguration.m index cd6486257ef..f8378d77790 100644 --- a/Firebase/Core/FIRConfiguration.m +++ b/Firebase/Core/FIRConfiguration.m @@ -30,7 +30,10 @@ + (instancetype)sharedInstance { - (instancetype)init { self = [super init]; if (self) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" _analyticsConfiguration = [FIRAnalyticsConfiguration sharedInstance]; +#pragma clang diagnostic pop } return self; } diff --git a/Firebase/Core/FIROptions.m b/Firebase/Core/FIROptions.m index 08aba37fcdb..75c7706aee6 100644 --- a/Firebase/Core/FIROptions.m +++ b/Firebase/Core/FIROptions.m @@ -41,7 +41,7 @@ // Library version ID. NSString *const kFIRLibraryVersionID = @"5" // Major version (one or more digits) - @"02" // Minor version (exactly 2 digits) + @"04" // Minor version (exactly 2 digits) @"00" // Build number (exactly 2 digits) @"000"; // Fixed "000" // Plist file name. diff --git a/Firebase/Core/Private/FIRBundleUtil.h b/Firebase/Core/Private/FIRBundleUtil.h index c458a2c4c6d..d9475dd29e0 100644 --- a/Firebase/Core/Private/FIRBundleUtil.h +++ b/Firebase/Core/Private/FIRBundleUtil.h @@ -45,8 +45,9 @@ + (NSArray *)relevantURLSchemes; /** - * Checks if the bundle identifier exists in the given bundles. + * Checks if any of the given bundles have a matching bundle identifier prefix (removing extension + * suffixes). */ -+ (BOOL)hasBundleIdentifier:(NSString *)bundleIdentifier inBundles:(NSArray *)bundles; ++ (BOOL)hasBundleIdentifierPrefix:(NSString *)bundleIdentifier inBundles:(NSArray *)bundles; @end diff --git a/Firebase/Core/Public/FIRAnalyticsConfiguration.h b/Firebase/Core/Public/FIRAnalyticsConfiguration.h index ca1d32c6e20..add4a38ee7b 100644 --- a/Firebase/Core/Public/FIRAnalyticsConfiguration.h +++ b/Firebase/Core/Public/FIRAnalyticsConfiguration.h @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN * This class provides configuration fields for Firebase Analytics. */ NS_SWIFT_NAME(AnalyticsConfiguration) +DEPRECATED_MSG_ATTRIBUTE("Use these methods directly on the `Analytics` class.") @interface FIRAnalyticsConfiguration : NSObject /** @@ -30,10 +31,13 @@ NS_SWIFT_NAME(AnalyticsConfiguration) + (FIRAnalyticsConfiguration *)sharedInstance NS_SWIFT_NAME(shared()); /** + * Deprecated. * Sets the minimum engagement time in seconds required to start a new session. The default value * is 10 seconds. */ -- (void)setMinimumSessionInterval:(NSTimeInterval)minimumSessionInterval; +- (void)setMinimumSessionInterval:(NSTimeInterval)minimumSessionInterval + DEPRECATED_MSG_ATTRIBUTE( + "Sessions are started immediately. More information at https://bit.ly/2FU46av"); /** * Sets the interval of inactivity in seconds that terminates the current session. The default diff --git a/Firebase/Core/Public/FIRConfiguration.h b/Firebase/Core/Public/FIRConfiguration.h index 95bba5e7b39..b88fcaf9f8d 100644 --- a/Firebase/Core/Public/FIRConfiguration.h +++ b/Firebase/Core/Public/FIRConfiguration.h @@ -32,7 +32,9 @@ NS_SWIFT_NAME(FirebaseConfiguration) @property(class, nonatomic, readonly) FIRConfiguration *sharedInstance NS_SWIFT_NAME(shared); /** The configuration class for Firebase Analytics. */ -@property(nonatomic, readwrite) FIRAnalyticsConfiguration *analyticsConfiguration; +@property(nonatomic, readwrite) + FIRAnalyticsConfiguration *analyticsConfiguration DEPRECATED_MSG_ATTRIBUTE( + "Use the methods available here directly on the `Analytics` class."); /** * Sets the logging level for internal Firebase logging. Firebase will only log messages diff --git a/Firebase/Database/CHANGELOG.md b/Firebase/Database/CHANGELOG.md index b7eb71cfc74..cd2526ac4d1 100644 --- a/Firebase/Database/CHANGELOG.md +++ b/Firebase/Database/CHANGELOG.md @@ -1,3 +1,6 @@ +# v5.1.1 +- [fixed] Fixed crash in FSRWebSocket. (#2485) + # v5.0.2 - [fixed] Fixed undefined behavior sanitizer issues. (#1443, #1444) diff --git a/Firebase/Database/third_party/SocketRocket/FSRWebSocket.m b/Firebase/Database/third_party/SocketRocket/FSRWebSocket.m index 4cd481b4e61..d0c1e20dd5d 100644 --- a/Firebase/Database/third_party/SocketRocket/FSRWebSocket.m +++ b/Firebase/Database/third_party/SocketRocket/FSRWebSocket.m @@ -1354,7 +1354,11 @@ - (void)_sendFrameWithOpcode:(FSROpCode)opcode data:(id)data; { [self assertOnWorkQueue]; - NSAssert(data == nil || [data isKindOfClass:[NSData class]] || [data isKindOfClass:[NSString class]], @"Function expects nil, NSString or NSData"); + if (data == nil) { + return; + } + + NSAssert([data isKindOfClass:[NSData class]] || [data isKindOfClass:[NSString class]], @"Function expects nil, NSString or NSData"); size_t payloadLength = [data isKindOfClass:[NSString class]] ? [(NSString *)data lengthOfBytesUsingEncoding:NSUTF8StringEncoding] : [data length]; diff --git a/Firebase/DynamicLinks/CHANGELOG.md b/Firebase/DynamicLinks/CHANGELOG.md index d920f922ae8..1a24eb88195 100644 --- a/Firebase/DynamicLinks/CHANGELOG.md +++ b/Firebase/DynamicLinks/CHANGELOG.md @@ -1,3 +1,9 @@ +# v3.4.2 +- Fixes an issue with certain analytics attribution parameters not being recorded on an app install. (#2462) + +# v3.4.1 +- Return call validation for sysctlbyname. (#2394) + # v3.4.0 - Bug fixes and internal SDK changes. (#2238, #2220) diff --git a/Firebase/DynamicLinks/FIRDynamicLink+Private.h b/Firebase/DynamicLinks/FIRDynamicLink+Private.h index 72bb666bb1a..43b310e8975 100644 --- a/Firebase/DynamicLinks/FIRDynamicLink+Private.h +++ b/Firebase/DynamicLinks/FIRDynamicLink+Private.h @@ -41,11 +41,11 @@ typedef NS_ENUM(NSUInteger, FIRDynamicLinkMatchConfidence) { @property(nonatomic, copy, nullable) NSString *matchMessage; -@property(nonatomic, copy, readonly) NSDictionary *parametersDictionary; +@property(nonatomic, copy, readonly) NSDictionary *parametersDictionary; @property(nonatomic, assign, readwrite) FIRDLMatchType matchType; -- (instancetype)initWithParametersDictionary:(NSDictionary *)parametersDictionary; +- (instancetype)initWithParametersDictionary:(NSDictionary *)parametersDictionary; @end diff --git a/Firebase/DynamicLinks/FIRDynamicLink.m b/Firebase/DynamicLinks/FIRDynamicLink.m index cbe2e23d973..bbe7c4ef5a7 100644 --- a/Firebase/DynamicLinks/FIRDynamicLink.m +++ b/Firebase/DynamicLinks/FIRDynamicLink.m @@ -28,10 +28,12 @@ - (NSString *)description { self.minimumAppVersion ?: @"N/A", self.matchMessage]; } -- (instancetype)initWithParametersDictionary:(NSDictionary *)parameters { +- (instancetype)initWithParametersDictionary:(NSDictionary *)parameters { NSParameterAssert(parameters.count > 0); if (self = [super init]) { + _parametersDictionary = [parameters copy]; + NSString *urlString = parameters[kFIRDLParameterDeepLinkIdentifier]; _url = [NSURL URLWithString:urlString]; _inviteId = parameters[kFIRDLParameterInviteId]; @@ -39,26 +41,62 @@ - (instancetype)initWithParametersDictionary:(NSDictionary *)parameters { _minimumAppVersion = parameters[kFIRDLParameterMinimumAppVersion]; if (parameters[kFIRDLParameterMatchType]) { - _matchType = [[self class] matchTypeWithString:parameters[kFIRDLParameterMatchType]]; + [self setMatchType:[[self class] matchTypeWithString:parameters[kFIRDLParameterMatchType]]]; } else if (_url || _inviteId) { // If matchType not present assume unique match for compatibility with server side behavior // on iOS 8. - _matchType = FIRDLMatchTypeUnique; + [self setMatchType:FIRDLMatchTypeUnique]; } + _matchMessage = parameters[kFIRDLParameterMatchMessage]; } return self; } -- (NSDictionary *)parametersDictionary { - NSMutableDictionary *parametersDictionary = [NSMutableDictionary dictionary]; - parametersDictionary[kFIRDLParameterInviteId] = _inviteId; - parametersDictionary[kFIRDLParameterDeepLinkIdentifier] = [_url absoluteString]; - parametersDictionary[kFIRDLParameterMatchType] = [[self class] stringWithMatchType:_matchType]; - parametersDictionary[kFIRDLParameterWeakMatchEndpoint] = _weakMatchEndpoint; - parametersDictionary[kFIRDLParameterMinimumAppVersion] = _minimumAppVersion; - parametersDictionary[kFIRDLParameterMatchMessage] = _matchMessage; - return parametersDictionary; +#pragma mark - Properties + +- (void)setUrl:(NSURL *)url { + _url = [url copy]; + [self setParametersDictionaryValue:[_url absoluteString] + forKey:kFIRDLParameterDeepLinkIdentifier]; +} + +- (void)setMinimumAppVersion:(NSString *)minimumAppVersion { + _minimumAppVersion = [minimumAppVersion copy]; + [self setParametersDictionaryValue:_minimumAppVersion forKey:kFIRDLParameterMinimumAppVersion]; +} + +- (void)setInviteId:(NSString *)inviteId { + _inviteId = [inviteId copy]; + [self setParametersDictionaryValue:_inviteId forKey:kFIRDLParameterInviteId]; +} + +- (void)setWeakMatchEndpoint:(NSString *)weakMatchEndpoint { + _weakMatchEndpoint = [weakMatchEndpoint copy]; + [self setParametersDictionaryValue:_weakMatchEndpoint forKey:kFIRDLParameterWeakMatchEndpoint]; +} + +- (void)setMatchType:(FIRDLMatchType)matchType { + _matchType = matchType; + [self setParametersDictionaryValue:[[self class] stringWithMatchType:_matchType] + forKey:kFIRDLParameterMatchType]; +} + +- (void)setMatchMessage:(NSString *)matchMessage { + _matchMessage = [matchMessage copy]; + [self setParametersDictionaryValue:_matchMessage forKey:kFIRDLParameterMatchMessage]; +} + +- (void)setParametersDictionaryValue:(id)value forKey:(NSString *)key { + NSMutableDictionary *parametersDictionary = + [self.parametersDictionary mutableCopy]; + if (value == nil) { + [parametersDictionary removeObjectForKey:key]; + } else { + parametersDictionary[key] = value; + } + + _parametersDictionary = [parametersDictionary copy]; } - (FIRDynamicLinkMatchConfidence)matchConfidence { diff --git a/Firebase/DynamicLinks/Utilities/FDLUtilities.m b/Firebase/DynamicLinks/Utilities/FDLUtilities.m index 32f6ae25018..4201de0a2e1 100644 --- a/Firebase/DynamicLinks/Utilities/FDLUtilities.m +++ b/Firebase/DynamicLinks/Utilities/FDLUtilities.m @@ -169,11 +169,16 @@ BOOL FIRDLOSVersionSupported(NSString *_Nullable systemVersion, NSString *minSup static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ size_t size; - sysctlbyname("hw.machine", NULL, &size, NULL, 0); - char *machine = calloc(1, size); - sysctlbyname("hw.machine", machine, &size, NULL, 0); - machineString = [NSString stringWithCString:machine encoding:NSUTF8StringEncoding]; - free(machine); + + // compute string size + if (sysctlbyname("hw.machine", NULL, &size, NULL, 0) == 0) { + // get device name + char *machine = calloc(1, size); + if (sysctlbyname("hw.machine", machine, &size, NULL, 0) == 0) { + machineString = [NSString stringWithCString:machine encoding:NSUTF8StringEncoding]; + } + free(machine); + } }); return machineString; } @@ -206,12 +211,11 @@ BOOL FIRDLIsURLForWhiteListedCustomDomain(NSURL *_Nullable URL) { options:NSCaseInsensitiveSearch | NSAnchoredSearch] .location) == 0) { // The (short) URL needs to be longer than the domainURIPrefix, it's first character after - // the domainURIPrefix needs to be '/' or '?' and should be followed by at-least one more + // the domainURIPrefix needs to be '/' and should be followed by at-least one more // character. if (urlStr.length > domainURIPrefixStr.length + 1 && - ([urlStr characterAtIndex:domainURIPrefixStr.length] == '/' || - [urlStr characterAtIndex:domainURIPrefixStr.length] == '?')) { - // Check if there are any more '/' after the first '/' or '?' trailing the + ([urlStr characterAtIndex:domainURIPrefixStr.length] == '/')) { + // Check if there are any more '/' after the first '/'trailing the // domainURIPrefix. This does not apply to unique match links copied from the clipboard. // The clipboard links will have '?link=' after the domainURIPrefix. NSString *urlWithoutDomainURIPrefix = @@ -244,17 +248,17 @@ BOOL FIRDLCanParseUniversalLinkURL(NSURL *_Nullable URL) { } BOOL FIRDLMatchesShortLinkFormat(NSURL *URL) { - // Short Durable Link URLs always have a path, except for certain custom domain URLs e.g. - // 'https://google.com?link=abcd' will not have a path component. - // FIRDLIsURLForWhiteListedCustomDomain implicitely checks for path component in custom domain - // URLs. - BOOL hasPath = URL.path.length > 0 || FIRDLIsURLForWhiteListedCustomDomain(URL); + // Short Durable Link URLs always have a path. + BOOL hasPath = URL.path.length > 0; + BOOL matchesRegularExpression = + ([URL.path rangeOfString:@"/[^/]+" options:NSRegularExpressionSearch].location != NSNotFound); // Must be able to parse (also checks if the URL conforms to *.app.goo.gl/* or goo.gl/app/*) - BOOL canParse = FIRDLCanParseUniversalLinkURL(URL); + BOOL canParse = FIRDLCanParseUniversalLinkURL(URL) | FIRDLIsURLForWhiteListedCustomDomain(URL); + ; // Path cannot be prefixed with /link/dismiss BOOL isDismiss = [[URL.path lowercaseString] hasPrefix:@"/link/dismiss"]; - return hasPath && !isDismiss && canParse; + return hasPath && matchesRegularExpression && !isDismiss && canParse; } NSString *FIRDLMatchTypeStringFromServerString(NSString *_Nullable serverMatchTypeString) { diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLogger.h b/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLogger.h new file mode 100644 index 00000000000..0599159c696 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLogger.h @@ -0,0 +1,61 @@ +/* + * Copyright 2018 Google + * + * 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 "FIRIAMClientInfoFetcher.h" +#import "FIRIAMTimeFetcher.h" + +NS_ASSUME_NONNULL_BEGIN + +/// Values for different fiam activity types. +typedef NS_ENUM(NSInteger, FIRIAMAnalyticsLogEventType) { + + FIRIAMAnalyticsLogEventUnknown = -1, + + FIRIAMAnalyticsEventMessageImpression = 0, + FIRIAMAnalyticsEventActionURLFollow = 1, + FIRIAMAnalyticsEventMessageDismissAuto = 2, + FIRIAMAnalyticsEventMessageDismissClick = 3, + FIRIAMAnalyticsEventMessageDismissSwipe = 4, + + // category: errors happened + FIRIAMAnalyticsEventImageFetchError = 11, + FIRIAMAnalyticsEventImageFormatUnsupported = 12, + + FIRIAMAnalyticsEventFetchAPINetworkError = 13, + FIRIAMAnalyticsEventFetchAPIClientError = 14, // server returns 4xx status code + FIRIAMAnalyticsEventFetchAPIServerError = 15, // server returns 5xx status code + + // Events for test messages + FIRIAMAnalyticsEventTestMessageImpression = 16, + FIRIAMAnalyticsEventTestMessageClick = 17, +}; + +// a protocol for collecting Analytics log records. It's implementation will decide +// what to do with that analytics log record +@protocol FIRIAMAnalyticsEventLogger +/** + * Adds an analytics log record. + * @param eventTimeInMs the timestamp in ms for when the event happened. + * if it's nil, the implementation will use the current system for this info. + */ +- (void)logAnalyticsEventForType:(FIRIAMAnalyticsLogEventType)eventType + forCampaignID:(NSString *)campaignID + withCampaignName:(NSString *)campaignName + eventTimeInMs:(nullable NSNumber *)eventTimeInMs + completion:(void (^)(BOOL success))completion; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.h b/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.h new file mode 100644 index 00000000000..41e289aaf75 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.h @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Google + * + * 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 "FIRIAMAnalyticsEventLogger.h" + +@class FIRIAMClearcutLogger; +@protocol FIRIAMTimeFetcher; +@protocol FIRAnalyticsInterop; + +NS_ASSUME_NONNULL_BEGIN +/** + * Implementation of protocol FIRIAMAnalyticsEventLogger by doing two things + * 1 Firing Firebase Analytics Events for impressions and clicks and dismisses + * 2 Making clearcut logging for all other types of analytics events + */ +@interface FIRIAMAnalyticsEventLoggerImpl : NSObject +- (instancetype)init NS_UNAVAILABLE; + +/** + * + * @param userDefaults needed for tracking upload timing info persistently.If nil, using + * NSUserDefaults standardUserDefaults. It's defined as a parameter to help with + * unit testing mocking + */ +- (instancetype)initWithClearcutLogger:(FIRIAMClearcutLogger *)ctLogger + usingTimeFetcher:(id)timeFetcher + usingUserDefaults:(nullable NSUserDefaults *)userDefaults + analytics:(nullable id)analytics; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.m b/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.m new file mode 100644 index 00000000000..9e99a370308 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.m @@ -0,0 +1,170 @@ +/* + * Copyright 2018 Google + * + * 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 "FIRIAMAnalyticsEventLoggerImpl.h" + +#import +#import +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMClearcutLogger.h" + +typedef void (^FIRAUserPropertiesCallback)(NSDictionary *userProperties); + +@interface FIRIAMAnalyticsEventLoggerImpl () +@property(readonly, nonatomic) FIRIAMClearcutLogger *clearCutLogger; +@property(readonly, nonatomic) id timeFetcher; +@property(nonatomic, readonly) NSUserDefaults *userDefaults; +@end + +// in these kFAXX constants, FA represents FirebaseAnalytics +static NSString *const kFIREventOriginFIAM = @"fiam"; +; +static NSString *const kFAEventNameForImpression = @"firebase_in_app_message_impression"; +static NSString *const kFAEventNameForAction = @"firebase_in_app_message_action"; +static NSString *const kFAEventNameForDismiss = @"firebase_in_app_message_dismiss"; + +// In order to support tracking conversions from clicking a fiam event, we need to set +// an analytics user property with the fiam message's campaign id. +// This is the user property as kFIRUserPropertyLastNotification defined for FCM. +// Unlike FCM, FIAM would only allow the user property to exist up to certain expiration time +// after which, we stop attributing any further conversions to that fiam message click. +// So we include kFAUserPropertyPrefixForFIAM as the prefix for the entry written by fiam SDK +// to avoid removing entries written by FCM SDK +static NSString *const kFAUserPropertyForLastNotification = @"_ln"; +static NSString *const kFAUserPropertyPrefixForFIAM = @"fiam:"; + +// This user defaults key is for the entry to tell when we should remove the private user +// property from a prior action url click to stop conversion attribution for a campaign +static NSString *const kFIAMUserDefaualtsKeyForRemoveUserPropertyTimeInSeconds = + @"firebase-iam-conversion-tracking-expires-in-seconds"; + +@implementation FIRIAMAnalyticsEventLoggerImpl { + id _analytics; +} + +- (instancetype)initWithClearcutLogger:(FIRIAMClearcutLogger *)ctLogger + usingTimeFetcher:(id)timeFetcher + usingUserDefaults:(nullable NSUserDefaults *)userDefaults + analytics:(nullable id)analytics { + if (self = [super init]) { + _clearCutLogger = ctLogger; + _timeFetcher = timeFetcher; + _analytics = analytics; + _userDefaults = userDefaults ? userDefaults : [NSUserDefaults standardUserDefaults]; + + if (!_analytics) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM280002", + @"Firebase In App Messaging was not configured with FirebaseAnalytics."); + } + } + return self; +} + +- (NSDictionary *)constructFAEventParamsWithCampaignID:(NSString *)campaignID + campaignName:(NSString *)campaignName { + // event parameter names are aligned with definitions in event_names_util.cc + return @{ + @"_nmn" : campaignName ?: @"unknown", + @"_nmid" : campaignID ?: @"unknown", + @"_ndt" : @([self.timeFetcher currentTimestampInSeconds]) + }; +} + +- (void)logFAEventsForMessageImpressionWithcampaignID:(NSString *)campaignID + campaignName:(NSString *)campaignName { + if (_analytics) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280001", + @"Log campaign impression Firebase Analytics event for campaign ID %@", campaignID); + + NSDictionary *params = [self constructFAEventParamsWithCampaignID:campaignID + campaignName:campaignName]; + [_analytics logEventWithOrigin:kFIREventOriginFIAM + name:kFAEventNameForImpression + parameters:params]; + } +} + +- (BOOL)setAnalyticsUserPropertyForKey:(NSString *)key withValue:(NSString *)value { + if (!_analytics || !key || !value) { + return NO; + } + [_analytics setUserPropertyWithOrigin:kFIREventOriginFIAM name:key value:value]; + return YES; +} + +- (void)logFAEventsForMessageActionWithCampaignID:(NSString *)campaignID + campaignName:(NSString *)campaignName { + if (_analytics) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280004", + @"Log action click Firebase Analytics event for campaign ID %@", campaignID); + + NSDictionary *params = [self constructFAEventParamsWithCampaignID:campaignID + campaignName:campaignName]; + + [_analytics logEventWithOrigin:kFIREventOriginFIAM + name:kFAEventNameForAction + parameters:params]; + } + + // set a special user property so that conversion events can be queried based on that + // for reporting purpose + NSString *conversionTrackingUserPropertyValue = + [NSString stringWithFormat:@"%@%@", kFAUserPropertyPrefixForFIAM, campaignID]; + + if ([self setAnalyticsUserPropertyForKey:kFAUserPropertyForLastNotification + withValue:conversionTrackingUserPropertyValue]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280009", + @"User property for conversion tracking was set for campaign %@", campaignID); + } +} + +- (void)logFAEventsForMessageDismissWithcampaignID:(NSString *)campaignID + campaignName:(NSString *)campaignName { + if (_analytics) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280007", + @"Log message dismiss Firebase Analytics event for campaign ID %@", campaignID); + + NSDictionary *params = [self constructFAEventParamsWithCampaignID:campaignID + campaignName:campaignName]; + [_analytics logEventWithOrigin:kFIREventOriginFIAM + name:kFAEventNameForDismiss + parameters:params]; + } +} + +- (void)logAnalyticsEventForType:(FIRIAMAnalyticsLogEventType)eventType + forCampaignID:(NSString *)campaignID + withCampaignName:(NSString *)campaignName + eventTimeInMs:(nullable NSNumber *)eventTimeInMs + completion:(void (^)(BOOL success))completion { + // log Firebase Analytics event first + if (eventType == FIRIAMAnalyticsEventMessageImpression) { + [self logFAEventsForMessageImpressionWithcampaignID:campaignID campaignName:campaignName]; + } else if (eventType == FIRIAMAnalyticsEventActionURLFollow) { + [self logFAEventsForMessageActionWithCampaignID:campaignID campaignName:campaignName]; + } else if (eventType == FIRIAMAnalyticsEventMessageDismissAuto || + eventType == FIRIAMAnalyticsEventMessageDismissClick) { + [self logFAEventsForMessageDismissWithcampaignID:campaignID campaignName:campaignName]; + } + + // and do clearcut logging as well + [self.clearCutLogger logAnalyticsEventForType:eventType + forCampaignID:campaignID + withCampaignName:campaignName + eventTimeInMs:eventTimeInMs + completion:completion]; +} +@end diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.h b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.h new file mode 100644 index 00000000000..7cacd37eaa3 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.h @@ -0,0 +1,51 @@ +/* + * Copyright 2018 Google + * + * 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 FIRIAMClearcutLogRecord; +@protocol FIRIAMTimeFetcher; + +NS_ASSUME_NONNULL_BEGIN +// class for sending requests to clearcut over its http API +@interface FIRIAMClearcutHttpRequestSender : NSObject + +/** + * Create an FIRIAMClearcutHttpRequestSender instance with specified clearcut server. + * + * @param serverHost API server host. + * @param osMajorVersion detected iOS major version of the current device + */ +- (instancetype)initWithClearcutHost:(NSString *)serverHost + usingTimeFetcher:(id)timeFetcher + withOSMajorVersion:(NSString *)osMajorVersion; + +/** + * Sends a batch of FIRIAMClearcutLogRecord records to clearcut server. + * @param logs an array of log records to be sent. + * @param completion is the handler to triggered upon completion. 'success' is a bool + * to indicate if the sending is successful. 'shouldRetryLogs' indicates if these + * logs need to be retried later on. On success case, waitTimeInMills is the value + * returned from clearcut server to indicate the minimal wait time before another + * send request can be attempted. + */ + +- (void)sendClearcutHttpRequestForLogs:(NSArray *)logs + withCompletion:(void (^)(BOOL success, + BOOL shouldRetryLogs, + int64_t waitTimeInMills))completion; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.m b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.m new file mode 100644 index 00000000000..ad292c721f0 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.m @@ -0,0 +1,202 @@ +/* + * Copyright 2018 Google + * + * 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 "FIRCore+InAppMessaging.h" +#import "FIRIAMClearcutHttpRequestSender.h" +#import "FIRIAMClearcutLogStorage.h" +#import "FIRIAMClientInfoFetcher.h" +#import "FIRIAMTimeFetcher.h" + +@interface FIRIAMClearcutHttpRequestSender () +@property(readonly, copy, nonatomic) NSString *serverHostName; + +@property(readwrite, nonatomic) id timeFetcher; +@property(readonly, copy, nonatomic) NSString *osMajorVersion; +@end + +@implementation FIRIAMClearcutHttpRequestSender + +- (instancetype)initWithClearcutHost:(NSString *)serverHost + usingTimeFetcher:(id)timeFetcher + withOSMajorVersion:(NSString *)osMajorVersion { + if (self = [super init]) { + _serverHostName = [serverHost copy]; + _timeFetcher = timeFetcher; + _osMajorVersion = [osMajorVersion copy]; + } + return self; +} + +- (void)updateRequestBodyWithClearcutEnvelopeFields:(NSMutableDictionary *)bodyDict { + bodyDict[@"client_info"] = @{ + @"client_type" : @15, // 15 is the enum value for IOS_FIREBASE client + @"ios_client_info" : @{@"os_major_version" : self.osMajorVersion ?: @""} + }; + bodyDict[@"log_source"] = @"FIREBASE_INAPPMESSAGING"; + + NSTimeInterval nowInMs = [self.timeFetcher currentTimestampInSeconds] * 1000; + bodyDict[@"request_time_ms"] = @((long)nowInMs); +} + +- (NSArray *)constructLogEventsArrayLogRecords: + (NSArray *)logRecords { + NSMutableArray *logEvents = [[NSMutableArray alloc] init]; + for (id next in logRecords) { + FIRIAMClearcutLogRecord *logRecord = (FIRIAMClearcutLogRecord *)next; + [logEvents addObject:@{ + @"event_time_ms" : @((long)logRecord.eventTimestampInSeconds * 1000), + @"source_extension_json" : logRecord.eventExtensionJsonString ?: @"" + }]; + } + + return [logEvents copy]; +} + +// @return nil if error happened in constructing the body +- (NSDictionary *)constructRequestBodyWithRetryRecords: + (NSArray *)logRecords { + NSMutableDictionary *body = [[NSMutableDictionary alloc] init]; + [self updateRequestBodyWithClearcutEnvelopeFields:body]; + body[@"log_event"] = [self constructLogEventsArrayLogRecords:logRecords]; + return [body copy]; +} + +// a helper method for dealing with the response received from +// executing NSURLSessionDataTask. Triggers the completion callback accordingly +- (void)handleClearcutAPICallResponseWithData:(NSData *)data + response:(NSURLResponse *)response + error:(NSError *)error + completion: + (nonnull void (^)(BOOL success, + BOOL shouldRetryLogs, + int64_t waitTimeInMills))completion { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250003", + @"Internal error: encountered error in uploading clearcut message" + ":%@", + error); + completion(NO, YES, 0); + return; + } + + if (![response isKindOfClass:[NSHTTPURLResponse class]]) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250008", + @"Received non http response from sending " + "clearcut requests %@", + response); + completion(NO, YES, 0); + return; + } + + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode == 200) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM250004", + @"Sending clearcut logging request was successful"); + + NSError *errorJson = nil; + NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data + options:kNilOptions + error:&errorJson]; + + int64_t waitTimeFromClearcutServer = 0; + if (!errorJson && responseDict[@"next_request_wait_millis"]) { + waitTimeFromClearcutServer = [responseDict[@"next_request_wait_millis"] longLongValue]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM250007", + @"Wait time from clearcut server response is %d seconds", + (int)waitTimeFromClearcutServer / 1000); + } + completion(YES, NO, waitTimeFromClearcutServer); + } else if (httpResponse.statusCode == 400) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250012", + @"Seeing 400 status code in response and we are discarding this log" + @"record"); + // 400 means bad request data and it won't be successful with retries. So + // we give up on these log records + completion(NO, NO, 0); + } else { + // May need to handle 401 errors if we do authentication in the future + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250005", + @"Other http status code seen in clearcut request response %d", + (int)httpResponse.statusCode); + // can be retried + completion(NO, YES, 0); + } +} + +- (void)sendClearcutHttpRequestForLogs:(NSArray *)logs + withCompletion:(nonnull void (^)(BOOL success, + BOOL shouldRetryLogs, + int64_t waitTimeInMills))completion { + NSDictionary *requestBody = [self constructRequestBodyWithRetryRecords:logs]; + + if (!requestBody) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250014", + @"Not able to construct request body for clearcut request, giving up"); + completion(NO, NO, 0); + } else { + // sending the log via a http request + NSURLSession *URLSession = [NSURLSession sharedSession]; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + [request setHTTPMethod:@"POST"]; + [request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + [request addValue:@"application/json" forHTTPHeaderField:@"Accept"]; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM250001", + @"Request body dictionary is %@ for clearcut logging request", requestBody); + + NSError *error; + NSData *requestBodyData = [NSJSONSerialization dataWithJSONObject:requestBody + options:0 + error:&error]; + + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250011", + @"Error in creating request body json for clearcut requests:%@", error); + completion(NO, NO, 0); + return; + } + + NSString *requestURLString = + [NSString stringWithFormat:@"https://%@/log?format=json_proto", self.serverHostName]; + [request setURL:[NSURL URLWithString:requestURLString]]; + [request setHTTPBody:requestBodyData]; + + NSURLSessionDataTask *clearCutLogDataTask = + [URLSession dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + [self handleClearcutAPICallResponseWithData:data + response:response + error:error + completion:completion]; + }]; + + if (clearCutLogDataTask == nil) { + NSString *errorDesc = @"Internal error: NSURLSessionDataTask failed to be created due to " + "possibly incorrect parameters"; + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250005", @"%@", errorDesc); + completion(NO, NO, 0); + } else { + [clearCutLogDataTask resume]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM250002", + @"Making a restful api for sending clearcut logging data with " + "a NSURLSessionDataTask request as %@", + clearCutLogDataTask.currentRequest); + } + } +} +@end diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.h b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.h new file mode 100644 index 00000000000..d24a3177934 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.h @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Google + * + * 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 +@interface FIRIAMClearcutLogRecord : NSObject +@property(nonatomic, copy, readonly) NSString *eventExtensionJsonString; +@property(nonatomic, readonly) NSInteger eventTimestampInSeconds; +- (instancetype)initWithExtensionJsonString:(NSString *)jsonString + eventTimestampInSeconds:(NSInteger)eventTimestampInSeconds; +@end + +@protocol FIRIAMTimeFetcher; + +// A local persistent storage for saving FIRIAMClearcutLogRecord objects +// so that they can be delivered to clearcut server. +// Based on the clearcut log structure, our strategy is to store the json string +// for the source extension since it does not need to be modified upon delivery retries. +// The envelope of the clearcut log will be reconstructed when delivery is +// attempted. + +@interface FIRIAMClearcutLogStorage : NSObject +- (instancetype)initWithExpireAfterInSeconds:(NSInteger)expireInSeconds + withTimeFetcher:(id)timeFetcher; + +// add new records into the storage +- (void)pushRecords:(NSArray *)newRecords; + +// pop all the records that have not expired yet. With this call, these +// records are removed from the book of this local storage object. +// @param upTo the cap on how many records to be popped. +- (NSArray *)popStillValidRecordsForUpTo:(NSInteger)upTo; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.m b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.m new file mode 100644 index 00000000000..e4ee2d272db --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.m @@ -0,0 +1,171 @@ +/* + * Copyright 2018 Google + * + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMClearcutLogStorage.h" +#import "FIRIAMTimeFetcher.h" + +@implementation FIRIAMClearcutLogRecord +static NSString *const kEventTimestampKey = @"event_ts_seconds"; +static NSString *const kEventExtensionJson = @"extension_js"; + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (instancetype)initWithExtensionJsonString:(NSString *)jsonString + eventTimestampInSeconds:(NSInteger)eventTimestampInSeconds { + self = [super init]; + if (self != nil) { + _eventTimestampInSeconds = eventTimestampInSeconds; + _eventExtensionJsonString = jsonString; + } + return self; +} + +- (id)initWithCoder:(NSCoder *)decoder { + self = [super init]; + if (self != nil) { + _eventTimestampInSeconds = [decoder decodeIntegerForKey:kEventTimestampKey]; + _eventExtensionJsonString = [decoder decodeObjectOfClass:[NSString class] + forKey:kEventExtensionJson]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)encoder { + [encoder encodeInteger:self.eventTimestampInSeconds forKey:kEventTimestampKey]; + [encoder encodeObject:self.eventExtensionJsonString forKey:kEventExtensionJson]; +} +@end + +@interface FIRIAMClearcutLogStorage () +@property(nonatomic) NSInteger recordExpiresInSeconds; +@property(nonatomic) NSMutableArray *records; +@property(nonatomic) id timeFetcher; +@end + +// We keep all the records in memory and flush them into files upon receiving +// applicationDidEnterBackground notifications. +@implementation FIRIAMClearcutLogStorage + ++ (NSString *)determineCacheFilePath { + static NSString *logCachePath; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + NSString *libraryDirPath = + NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0]; + logCachePath = + [NSString stringWithFormat:@"%@/firebase-iam-clearcut-retry-records", libraryDirPath]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM230001", + @"Persistent file path for clearcut log records is %@", logCachePath); + }); + return logCachePath; +} + +- (instancetype)initWithExpireAfterInSeconds:(NSInteger)expireInSeconds + withTimeFetcher:(id)timeFetcher { + if (self = [super init]) { + _records = [[NSMutableArray alloc] init]; + _timeFetcher = timeFetcher; + _recordExpiresInSeconds = expireInSeconds; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appWillBecomeInactive) + name:UIApplicationWillResignActiveNotification + object:nil]; + @try { + [self loadFromCachePath:nil]; + } @catch (NSException *exception) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM230004", + @"Non-fatal exception in loading persisted clearcut log records: %@.", + exception); + } + } + return self; +} + +- (void)appWillBecomeInactive { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + [self saveIntoCacheWithPath:nil]; + }); +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)pushRecords:(NSArray *)newRecords { + @synchronized(self) { + [self.records addObjectsFromArray:newRecords]; + } +} + +- (NSArray *)popStillValidRecordsForUpTo:(NSInteger)upTo { + NSMutableArray *resultArray = [[NSMutableArray alloc] init]; + NSInteger nowInSeconds = (NSInteger)[self.timeFetcher currentTimestampInSeconds]; + + NSInteger next = 0; + + @synchronized(self) { + while (resultArray.count < upTo && next < self.records.count) { + FIRIAMClearcutLogRecord *nextRecord = self.records[next++]; + if (nextRecord.eventTimestampInSeconds > nowInSeconds - self.recordExpiresInSeconds) { + // record not expired yet + [resultArray addObject:nextRecord]; + } + } + + [self.records removeObjectsInRange:NSMakeRange(0, next)]; + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM230005", + @"Returning %d clearcut retry records from popStillValidRecords", + (int)resultArray.count); + return resultArray; +} + +- (void)loadFromCachePath:(NSString *)cacheFilePath { + NSString *filePath = cacheFilePath == nil ? [self.class determineCacheFilePath] : cacheFilePath; + + NSTimeInterval start = [self.timeFetcher currentTimestampInSeconds]; + id fetchedClearcutRetryRecords = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath]; + if (fetchedClearcutRetryRecords) { + @synchronized(self) { + self.records = (NSMutableArray *)fetchedClearcutRetryRecords; + } + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM230002", + @"Loaded %d clearcut log records from file in %lf seconds", (int)self.records.count, + (double)[self.timeFetcher currentTimestampInSeconds] - start); + } +} + +- (BOOL)saveIntoCacheWithPath:(NSString *)cacheFilePath { + NSString *filePath = cacheFilePath == nil ? [self.class determineCacheFilePath] : cacheFilePath; + @synchronized(self) { + BOOL saveResult = [NSKeyedArchiver archiveRootObject:self.records toFile:filePath]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM230003", + @"Saving %d clearcut log records into file is %@", (int)self.records.count, + saveResult ? @"successful" : @"failure"); + + return saveResult; + } +} +@end diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.h b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.h new file mode 100644 index 00000000000..d894a00f14f --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.h @@ -0,0 +1,46 @@ +/* + * Copyright 2018 Google + * + * 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 "FIRIAMAnalyticsEventLogger.h" +#import "FIRIAMClientInfoFetcher.h" +#import "FIRIAMTimeFetcher.h" + +@class FIRIAMClearcutUploader; + +NS_ASSUME_NONNULL_BEGIN +// FIRIAMAnalyticsEventLogger implementation using Clearcut. It turns a IAM analytics event +// into the corresponding FIRIAMClearcutLogRecord and then hand it over to +// a FIRIAMClearcutUploader instance for the actual sending and potential failure and retry +// logic +@interface FIRIAMClearcutLogger : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Create an instance which uses NSURLSession to make clearcut api calls. + * + * @param clientInfoFetcher used to fetch iid info for the current app. + * @param timeFetcher time fetcher object + * @param uploader FIRIAMClearcutUploader object for receiving the log record + */ +- (instancetype)initWithFBProjectNumber:(NSString *)fbProjectNumber + fbAppId:(NSString *)fbAppId + clientInfoFetcher:(FIRIAMClientInfoFetcher *)clientInfoFetcher + usingTimeFetcher:(id)timeFetcher + usingUploader:(FIRIAMClearcutUploader *)uploader; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.m b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.m new file mode 100644 index 00000000000..4cb8a134b89 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.m @@ -0,0 +1,203 @@ +/* + * Copyright 2018 Google + * + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMClearcutLogStorage.h" +#import "FIRIAMClearcutLogger.h" +#import "FIRIAMClearcutUploader.h" + +@interface FIRIAMClearcutLogger () + +// these two writable for assisting unit testing need +@property(readwrite, nonatomic) FIRIAMClearcutHttpRequestSender *requestSender; +@property(readwrite, nonatomic) id timeFetcher; + +@property(readonly, nonatomic) FIRIAMClientInfoFetcher *clientInfoFetcher; +@property(readonly, nonatomic) FIRIAMClearcutUploader *ctUploader; + +@property(readonly, copy, nonatomic) NSString *fbProjectNumber; +@property(readonly, copy, nonatomic) NSString *fbAppId; + +@end + +@implementation FIRIAMClearcutLogger { + NSString *_iid; +} +- (instancetype)initWithFBProjectNumber:(NSString *)fbProjectNumber + fbAppId:(NSString *)fbAppId + clientInfoFetcher:(FIRIAMClientInfoFetcher *)clientInfoFetcher + usingTimeFetcher:(id)timeFetcher + usingUploader:(FIRIAMClearcutUploader *)uploader { + if (self = [super init]) { + _fbProjectNumber = fbProjectNumber; + _fbAppId = fbAppId; + _clientInfoFetcher = clientInfoFetcher; + _timeFetcher = timeFetcher; + _ctUploader = uploader; + } + return self; +} + ++ (void)updateSourceExtensionDictWithAnalyticsEventEnumType:(FIRIAMAnalyticsLogEventType)eventType + forDict:(NSMutableDictionary *)dict { + switch (eventType) { + case FIRIAMAnalyticsEventMessageImpression: + dict[@"event_type"] = @"IMPRESSION_EVENT_TYPE"; + break; + case FIRIAMAnalyticsEventActionURLFollow: + dict[@"event_type"] = @"CLICK_EVENT_TYPE"; + break; + case FIRIAMAnalyticsEventMessageDismissAuto: + dict[@"dismiss_type"] = @"AUTO"; + break; + case FIRIAMAnalyticsEventMessageDismissClick: + dict[@"dismiss_type"] = @"CLICK"; + break; + case FIRIAMAnalyticsEventMessageDismissSwipe: + dict[@"dismiss_type"] = @"SWIPE"; + break; + case FIRIAMAnalyticsEventImageFetchError: + dict[@"render_error_reason"] = @"IMAGE_FETCH_ERROR"; + break; + case FIRIAMAnalyticsEventImageFormatUnsupported: + dict[@"render_error_reason"] = @"IMAGE_UNSUPPORTED_FORMAT"; + break; + case FIRIAMAnalyticsEventFetchAPIClientError: + dict[@"fetch_error_reason"] = @"CLIENT_ERROR"; + break; + case FIRIAMAnalyticsEventFetchAPIServerError: + dict[@"fetch_error_reason"] = @"SERVER_ERROR"; + break; + case FIRIAMAnalyticsEventFetchAPINetworkError: + dict[@"fetch_error_reason"] = @"NETWORK_ERROR"; + break; + case FIRIAMAnalyticsEventTestMessageImpression: + dict[@"event_type"] = @"TEST_MESSAGE_IMPRESSION_EVENT_TYPE"; + break; + case FIRIAMAnalyticsEventTestMessageClick: + dict[@"event_type"] = @"TEST_MESSAGE_CLICK_EVENT_TYPE"; + break; + case FIRIAMAnalyticsLogEventUnknown: + break; + } +} + +// constructing CampaignAnalytics proto defined in campaign_analytics.proto and serialize it into +// a string. +// @return nil if error happened +- (NSString *)constructSourceExtensionJsonForClearcutWithEventType: + (FIRIAMAnalyticsLogEventType)eventType + forCampaignID:(NSString *)campaignID + eventTimeInMs:(NSNumber *)eventTimeInMs + instanceID:(NSString *)instanceID { + NSMutableDictionary *campaignAnalyticsDict = [[NSMutableDictionary alloc] init]; + + campaignAnalyticsDict[@"project_number"] = self.fbProjectNumber; + campaignAnalyticsDict[@"campaign_id"] = campaignID; + campaignAnalyticsDict[@"client_app"] = + @{@"google_app_id" : self.fbAppId, @"firebase_instance_id" : instanceID}; + campaignAnalyticsDict[@"client_timestamp_millis"] = eventTimeInMs; + [self.class updateSourceExtensionDictWithAnalyticsEventEnumType:eventType + forDict:campaignAnalyticsDict]; + + campaignAnalyticsDict[@"fiam_sdk_version"] = [self.clientInfoFetcher getIAMSDKVersion]; + + // turn campaignAnalyticsDict into a json string + NSError *error; + NSData *jsonData = [NSJSONSerialization + dataWithJSONObject:campaignAnalyticsDict // Here you can pass array or dictionary + options:0 // Pass 0 if you don't care about the readability of the generated + // string + error:&error]; + + if (jsonData) { + NSString *jsonString; + jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM210006", + @"Source extension json string produced as %@", jsonString); + return jsonString; + } else { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM210007", + @"Error in generating source extension json string: %@", error); + return nil; + } +} + +- (void)logAnalyticsEventForType:(FIRIAMAnalyticsLogEventType)eventType + forCampaignID:(NSString *)campaignID + withEventTimeInMs:(nullable NSNumber *)eventTimeInMs + IID:(NSString *)iid + completion:(void (^)(BOOL success))completion { + NSTimeInterval nowInMs = [self.timeFetcher currentTimestampInSeconds] * 1000; + if (!eventTimeInMs) { + eventTimeInMs = @((long)nowInMs); + } + + NSString *sourceExtensionJsonString = + [self constructSourceExtensionJsonForClearcutWithEventType:eventType + forCampaignID:campaignID + eventTimeInMs:eventTimeInMs + instanceID:iid]; + + FIRIAMClearcutLogRecord *newRecord = [[FIRIAMClearcutLogRecord alloc] + initWithExtensionJsonString:sourceExtensionJsonString + eventTimestampInSeconds:eventTimeInMs.integerValue / 1000]; + [self.ctUploader addNewLogRecord:newRecord]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM210003", + @"One more clearcut log record created and sent to uploader with source extension %@", + sourceExtensionJsonString); + completion(YES); +} + +- (void)logAnalyticsEventForType:(FIRIAMAnalyticsLogEventType)eventType + forCampaignID:(NSString *)campaignID + withCampaignName:(NSString *)campaignName + eventTimeInMs:(nullable NSNumber *)eventTimeInMs + completion:(void (^)(BOOL success))completion { + if (!_iid) { + [self.clientInfoFetcher + fetchFirebaseIIDDataWithProjectNumber:self.fbProjectNumber + withCompletion:^(NSString *_Nullable iid, NSString *_Nullable token, + NSError *_Nullable error) { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM210001", + @"Failed to get iid value for clearcut logging %@", + error); + completion(NO); + } else { + // persist iid through the whole life-cycle + self->_iid = iid; + [self logAnalyticsEventForType:eventType + forCampaignID:campaignID + withEventTimeInMs:eventTimeInMs + IID:iid + completion:completion]; + } + }]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM210004", + @"Using remembered iid for event logging"); + [self logAnalyticsEventForType:eventType + forCampaignID:campaignID + withEventTimeInMs:eventTimeInMs + IID:_iid + completion:completion]; + } +} +@end diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.h b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.h new file mode 100644 index 00000000000..9c0d1139b36 --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.h @@ -0,0 +1,75 @@ +/* + * Copyright 2018 Google + * + * 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 FIRIAMClearcutLogRecord; +@class FIRIAMClearcutHttpRequestSender; +@class FIRIAMClearcutLogStorage; + +@protocol FIRIAMTimeFetcher; + +NS_ASSUME_NONNULL_BEGIN + +// class for defining a number of configs to control clearcut upload behavior +@interface FIRIAMClearcutStrategy : NSObject + +// minimalWaitTimeInMills and maximumWaitTimeInMills defines the bottom and +// upper bound of the wait time before next upload if prior upload attempt was +// successful. Clearcut may return a value to give the wait time guidance in +// the upload response, but we also use these two values for sanity check to avoid +// too crazy behavior if the guidance value from server does not make sense +@property(nonatomic, readonly) NSInteger minimalWaitTimeInMills; +@property(nonatomic, readonly) NSInteger maximumWaitTimeInMills; + +// back off wait time in mills if a prior upload attempt fails +@property(nonatomic, readonly) NSInteger failureBackoffTimeInMills; + +// the maximum number of log records to be sent in one upload attempt +@property(nonatomic, readonly) NSInteger batchSendSize; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMinWaitTimeInMills:(NSInteger)minWaitTimeInMills + maxWaitTimeInMills:(NSInteger)maxWaitTimeInMills + failureBackoffTimeInMills:(NSInteger)failureBackoffTimeInMills + batchSendSize:(NSInteger)batchSendSize; + +- (NSString *)description; +@end + +// A class for accepting new clearcut logs and scheduling the uploading of the logs in batches +// based on defined strategies. +@interface FIRIAMClearcutUploader : NSObject +- (instancetype)init NS_UNAVAILABLE; + +/** + * + * @param userDefaults needed for tracking upload timing info persistently.If nil, using + * NSUserDefaults standardUserDefaults. It's defined as a parameter to help with + * unit testing mocking + */ +- (instancetype)initWithRequestSender:(FIRIAMClearcutHttpRequestSender *)requestSender + timeFetcher:(id)timeFetcher + logStorage:(FIRIAMClearcutLogStorage *)retryStorage + usingStrategy:(FIRIAMClearcutStrategy *)strategy + usingUserDefaults:(nullable NSUserDefaults *)userDefaults; +/** + * This should return very quickly without blocking on and actual log uploading to + * clearcut server, which is done asynchronously + */ +- (void)addNewLogRecord:(FIRIAMClearcutLogRecord *)record; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.m b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.m new file mode 100644 index 00000000000..1e676536f1c --- /dev/null +++ b/Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.m @@ -0,0 +1,233 @@ +/* + * Copyright 2018 Google + * + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMClearcutUploader.h" +#import "FIRIAMTimeFetcher.h" + +#import "FIRIAMClearcutHttpRequestSender.h" +#import "FIRIAMClearcutLogStorage.h" + +// a macro for turning a millisecond value into seconds +#define MILLS_TO_SECONDS(x) (((long)x) / 1000) + +@implementation FIRIAMClearcutStrategy +- (instancetype)initWithMinWaitTimeInMills:(NSInteger)minWaitTimeInMills + maxWaitTimeInMills:(NSInteger)maxWaitTimeInMills + failureBackoffTimeInMills:(NSInteger)failureBackoffTimeInMills + batchSendSize:(NSInteger)batchSendSize { + if (self = [super init]) { + _minimalWaitTimeInMills = minWaitTimeInMills; + _maximumWaitTimeInMills = maxWaitTimeInMills; + _failureBackoffTimeInMills = failureBackoffTimeInMills; + _batchSendSize = batchSendSize; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"min wait time in seconds:%ld;max wait time in seconds:%ld;" + "failure backoff time in seconds:%ld;batch send size:%d", + MILLS_TO_SECONDS(self.minimalWaitTimeInMills), + MILLS_TO_SECONDS(self.maximumWaitTimeInMills), + MILLS_TO_SECONDS(self.failureBackoffTimeInMills), + (int)self.batchSendSize]; +} +@end + +@interface FIRIAMClearcutUploader () { + dispatch_queue_t _queue; + BOOL _nextSendScheduled; +} + +@property(readwrite, nonatomic) FIRIAMClearcutHttpRequestSender *requestSender; +@property(nonatomic, assign) int64_t nextValidSendTimeInMills; + +@property(nonatomic, readonly) id timeFetcher; +@property(nonatomic, readonly) FIRIAMClearcutLogStorage *logStorage; + +@property(nonatomic, readonly) FIRIAMClearcutStrategy *strategy; +@property(nonatomic, readonly) NSUserDefaults *userDefaults; +@end + +static NSString *FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills = + @"firebase-iam-next-clearcut-upload-timestamp-in-mills"; + +/** + * The high level behavior in this implementation is like this + * 1 New records always pushed into FIRIAMClearcutLogStorage first. + * 2 Upload log records in batches. + * 3 If prior upload was successful, next upload would wait for the time parsed out of the + * clearcut response body. + * 4 If prior upload failed, next upload attempt would wait for failureBackoffTimeInMills defined + * in strategy + * 5 When app + */ + +@implementation FIRIAMClearcutUploader + +- (instancetype)initWithRequestSender:(FIRIAMClearcutHttpRequestSender *)requestSender + timeFetcher:(id)timeFetcher + logStorage:(FIRIAMClearcutLogStorage *)logStorage + usingStrategy:(FIRIAMClearcutStrategy *)strategy + usingUserDefaults:(nullable NSUserDefaults *)userDefaults { + if (self = [super init]) { + _nextSendScheduled = NO; + _timeFetcher = timeFetcher; + _requestSender = requestSender; + _logStorage = logStorage; + _strategy = strategy; + _queue = dispatch_queue_create("com.google.firebase.inappmessaging.clearcut_upload", NULL); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + + _userDefaults = userDefaults ? userDefaults : [NSUserDefaults standardUserDefaults]; + // it would be 0 if it does not exist, which is equvilent to saying that + // you can send now + _nextValidSendTimeInMills = (int64_t) + [_userDefaults doubleForKey:FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills]; + + // seed the first send upon SDK start-up + [self scheduleNextSend]; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260001", + @"FIRIAMClearcutUploader created with strategy as %@", self.strategy); + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)appWillEnterForeground:(UIApplication *)application { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260010", + @"App foregrounded, FIRIAMClearcutUploader will seed next send"); + [self scheduleNextSend]; +} + +- (void)addNewLogRecord:(FIRIAMClearcutLogRecord *)record { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260002", + @"New log record sent to clearcut uploader"); + + [self.logStorage pushRecords:@[ record ]]; + [self scheduleNextSend]; +} + +- (void)attemptUploading { + NSArray *availbleLogs = + [self.logStorage popStillValidRecordsForUpTo:self.strategy.batchSendSize]; + + if (availbleLogs.count > 0) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260011", @"Deliver %d clearcut records", + (int)availbleLogs.count); + [self.requestSender + sendClearcutHttpRequestForLogs:availbleLogs + withCompletion:^(BOOL success, BOOL shouldRetryLogs, + int64_t waitTimeInMills) { + if (success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260003", + @"Delivering %d clearcut records was successful", + (int)availbleLogs.count); + // make sure the effective wait time is between two bounds + // defined in strategy + waitTimeInMills = + MAX(self.strategy.minimalWaitTimeInMills, waitTimeInMills); + + waitTimeInMills = + MIN(waitTimeInMills, self.strategy.maximumWaitTimeInMills); + } else { + // failed to deliver + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260004", + @"Failed to attempt the delivery of %d clearcut " + @"records and should-retry for them is %@", + (int)availbleLogs.count, shouldRetryLogs ? @"YES" : @"NO"); + if (shouldRetryLogs) { + /** + * Note that there is a chance that the app crashes before we can + * call pushRecords: on the logStorage below which means we lost + * these log records permanently. This is a trade-off between handling + * duplicate records on server side vs taking the risk of lossing + * data. This implementation picks the latter. + */ + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260007", + @"Push failed log records back to storage"); + [self.logStorage pushRecords:availbleLogs]; + } + + waitTimeInMills = (int64_t)self.strategy.failureBackoffTimeInMills; + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260005", + @"Wait for at least %ld seconds before next upload attempt", + MILLS_TO_SECONDS(waitTimeInMills)); + + self.nextValidSendTimeInMills = + (int64_t)[self.timeFetcher currentTimestampInSeconds] * 1000 + + waitTimeInMills; + + // persisted so that it can be recovered next time the app runs + [self.userDefaults + setDouble:(double)self.nextValidSendTimeInMills + forKey: + FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills]; + + @synchronized(self) { + self->_nextSendScheduled = NO; + } + [self scheduleNextSend]; + }]; + + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260007", @"No clearcut records to be uploaded"); + @synchronized(self) { + _nextSendScheduled = NO; + } + } +} + +- (void)scheduleNextSend { + @synchronized(self) { + if (_nextSendScheduled) { + // already scheduled next send, don't do it again + return; + } else { + _nextSendScheduled = YES; + } + } + + int64_t delayTimeInMills = + self.nextValidSendTimeInMills - (int64_t)[self.timeFetcher currentTimestampInSeconds] * 1000; + + if (delayTimeInMills <= 0) { + delayTimeInMills = 0; // no need to delay since we can send now + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260006", + @"Next upload attempt scheduled in %d seconds", (int)delayTimeInMills / 1000); + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayTimeInMills * (int64_t)NSEC_PER_MSEC), + _queue, ^{ + [self attemptUploading]; + }); +} + +@end diff --git a/Firebase/InAppMessaging/CHANGELOG.md b/Firebase/InAppMessaging/CHANGELOG.md new file mode 100644 index 00000000000..c4b18ce979b --- /dev/null +++ b/Firebase/InAppMessaging/CHANGELOG.md @@ -0,0 +1,9 @@ +# 2019-03-05 -- v0.13.0 +- Added a feature allowing developers to programmatically register a delegate for updates on in-app engagement (impression, click, display errors). + +# 2018-09-25 -- v0.12.0 +- Separated UI functionality into a new open source SDK called FirebaseInAppMessagingDisplay. +- Respect fetch between wait time returned from API responses. + +# 2018-08-15 -- v0.11.0 +- First Beta Release. diff --git a/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.h b/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.h new file mode 100644 index 00000000000..7fea6e6f38c --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.h @@ -0,0 +1,41 @@ +/* + * Copyright 2017 Google + * + * 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 FIRIAMMessageDefinition; +@protocol FIRIAMTimeFetcher; + +NS_ASSUME_NONNULL_BEGIN + +// Class responsible for parsing the json response data from the restful API endpoint +// for serving eligible messages for the current SDK clients. +@interface FIRIAMFetchResponseParser : NSObject + +// Turn the API response into a number of FIRIAMMessageDefinition objects. If any of them is invalid +// it would be ignored and not represented in the response array. +// @param discardCount if not nil, it would contain, on return, the number of invalid messages +// detected uring parsing. +// @param fetchWaitTime would be non nil if fetch wait time data is found in the api response. +- (NSArray *)parseAPIResponseDictionary:(NSDictionary *)responseDict + discardedMsgCount:(NSInteger *)discardCount + fetchWaitTimeInSeconds: + (NSNumber *_Nullable *_Nonnull)fetchWaitTime; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithTimeFetcher:(id)timeFetcher; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.m b/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.m new file mode 100644 index 00000000000..771c4eb51c2 --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.m @@ -0,0 +1,313 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRCore+InAppMessaging.h" +#import "FIRIAMDisplayTriggerDefinition.h" +#import "FIRIAMFetchResponseParser.h" +#import "FIRIAMMessageContentData.h" +#import "FIRIAMMessageContentDataWithImageURL.h" +#import "FIRIAMMessageDefinition.h" +#import "FIRIAMTimeFetcher.h" +#import "UIColor+FIRIAMHexString.h" + +@interface FIRIAMFetchResponseParser () +@property(nonatomic) id timeFetcher; +@end + +@implementation FIRIAMFetchResponseParser + +- (instancetype)initWithTimeFetcher:(id)timeFetcher { + if (self = [super init]) { + _timeFetcher = timeFetcher; + } + return self; +} + +- (NSArray *)parseAPIResponseDictionary:(NSDictionary *)responseDict + discardedMsgCount:(NSInteger *)discardCount + fetchWaitTimeInSeconds:(NSNumber **)fetchWaitTime { + if (fetchWaitTime != nil) { + *fetchWaitTime = nil; // It would be set to non nil value if it's detected in responseDict + if ([responseDict[@"expirationEpochTimestampMillis"] isKindOfClass:NSString.class]) { + NSTimeInterval nextFetchTimeInResponse = + [responseDict[@"expirationEpochTimestampMillis"] doubleValue] / 1000; + NSTimeInterval fetchWaitTimeInSeconds = + nextFetchTimeInResponse - [self.timeFetcher currentTimestampInSeconds]; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900005", + @"Detected next fetch epoch time in API response as %f seconds and wait for %f " + "seconds before next fetch.", + nextFetchTimeInResponse, fetchWaitTimeInSeconds); + + if (fetchWaitTimeInSeconds > 0.01) { + *fetchWaitTime = @(fetchWaitTimeInSeconds); + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900018", + @"Fetch wait time calculated from server response is negative. Discard it."); + } + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900014", + @"No fetch epoch time detected in API response."); + } + } + + NSArray *messageArray = responseDict[@"messages"]; + NSInteger discarded = 0; + + NSMutableArray *definitions = [[NSMutableArray alloc] init]; + for (NSDictionary *nextMsg in messageArray) { + FIRIAMMessageDefinition *nextDefinition = + [self convertToMessageDefinitionWithMessageDict:nextMsg]; + if (nextDefinition) { + [definitions addObject:nextDefinition]; + } else { + FIRLogInfo(kFIRLoggerInAppMessaging, @"I-IAM900001", + @"No definition generated for message node %@", nextMsg); + discarded++; + } + } + FIRLogDebug( + kFIRLoggerInAppMessaging, @"I-IAM900002", + @"%lu message definitions were parsed out successfully and %lu messages are discarded", + (unsigned long)definitions.count, (unsigned long)discarded); + + if (discardCount) { + *discardCount = discarded; + } + return [definitions copy]; +} + +// Return nil if no valid triggering condition can be detected +- (NSArray *)parseTriggeringCondition: + (NSArray *)triggerConditions { + if (triggerConditions == nil || triggerConditions.count == 0) { + return nil; + } + + NSMutableArray *triggers = [[NSMutableArray alloc] init]; + + for (NSDictionary *nextTriggerCondition in triggerConditions) { + if (nextTriggerCondition[@"fiamTrigger"]) { + if ([nextTriggerCondition[@"fiamTrigger"] isEqualToString:@"ON_FOREGROUND"]) { + [triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc] initForAppForegroundTrigger]]; + } + } else if ([nextTriggerCondition[@"event"] isKindOfClass:[NSDictionary class]]) { + NSDictionary *triggeringEvent = (NSDictionary *)nextTriggerCondition[@"event"]; + if (triggeringEvent[@"name"]) { + [triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc] + initWithFirebaseAnalyticEvent:triggeringEvent[@"name"]]]; + } + } + } + + return [triggers copy]; +} + +// For one element in the restful API response's messages array, convert into +// a FIRIAMMessageDefinition object. If the conversion fails, a nil is returned. +- (FIRIAMMessageDefinition *)convertToMessageDefinitionWithMessageDict:(NSDictionary *)messageNode { + @try { + BOOL isTestMessage = NO; + + id isTestCampaignNode = messageNode[@"isTestCampaign"]; + if ([isTestCampaignNode isKindOfClass:[NSNumber class]]) { + isTestMessage = [isTestCampaignNode boolValue]; + } + + id vanillaPayloadNode = messageNode[@"vanillaPayload"]; + if (![vanillaPayloadNode isKindOfClass:[NSDictionary class]]) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900012", + @"vanillaPayload does not exist or does not represent a dictionary in " + "message node %@", + messageNode); + return nil; + } + + NSString *messageID = vanillaPayloadNode[@"campaignId"]; + if (!messageID) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900010", + @"messsage id is missing in message node %@", messageNode); + return nil; + } + + NSString *messageName = vanillaPayloadNode[@"campaignName"]; + if (!messageName && !isTestMessage) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900011", + @"campaign name is missing in non-test message node %@", messageNode); + return nil; + } + + NSTimeInterval startTimeInSeconds = 0; + NSTimeInterval endTimeInSeconds = 0; + if (!isTestMessage) { + // Parsing start/end times out of non-test messages. They are strings in the + // json response. + id startTimeNode = vanillaPayloadNode[@"campaignStartTimeMillis"]; + if ([startTimeNode isKindOfClass:[NSString class]]) { + startTimeInSeconds = [startTimeNode doubleValue] / 1000.0; + } + + id endTimeNode = vanillaPayloadNode[@"campaignEndTimeMillis"]; + if ([endTimeNode isKindOfClass:[NSString class]]) { + endTimeInSeconds = [endTimeNode doubleValue] / 1000.0; + } + } + + id contentNode = messageNode[@"content"]; + if (![contentNode isKindOfClass:[NSDictionary class]]) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900013", + @"content node does not exist or does not represent a dictionary in " + "message node %@", + messageNode); + return nil; + } + + NSDictionary *content = (NSDictionary *)contentNode; + FIRIAMRenderingMode mode; + UIColor *viewCardBackgroundColor, *btnBgColor, *btnTxtColor, *titleTextColor; + viewCardBackgroundColor = btnBgColor = btnTxtColor = titleTextColor = nil; + + NSString *title, *body, *imageURLStr, *actionURLStr, *actionButtonText; + title = body = imageURLStr = actionButtonText = actionURLStr = nil; + + if ([content[@"banner"] isKindOfClass:[NSDictionary class]]) { + NSDictionary *bannerNode = (NSDictionary *)contentNode[@"banner"]; + mode = FIRIAMRenderAsBannerView; + + title = bannerNode[@"title"][@"text"]; + titleTextColor = [UIColor firiam_colorWithHexString:bannerNode[@"title"][@"hexColor"]]; + + body = bannerNode[@"body"][@"text"]; + + imageURLStr = bannerNode[@"imageUrl"]; + actionURLStr = bannerNode[@"action"][@"actionUrl"]; + viewCardBackgroundColor = + [UIColor firiam_colorWithHexString:bannerNode[@"backgroundHexColor"]]; + + } else if ([content[@"modal"] isKindOfClass:[NSDictionary class]]) { + mode = FIRIAMRenderAsModalView; + + NSDictionary *modalNode = (NSDictionary *)contentNode[@"modal"]; + title = modalNode[@"title"][@"text"]; + titleTextColor = [UIColor firiam_colorWithHexString:modalNode[@"title"][@"hexColor"]]; + + body = modalNode[@"body"][@"text"]; + + imageURLStr = modalNode[@"imageUrl"]; + actionButtonText = modalNode[@"actionButton"][@"text"][@"text"]; + btnBgColor = + [UIColor firiam_colorWithHexString:modalNode[@"actionButton"][@"buttonHexColor"]]; + + actionURLStr = modalNode[@"action"][@"actionUrl"]; + viewCardBackgroundColor = + [UIColor firiam_colorWithHexString:modalNode[@"backgroundHexColor"]]; + } else if ([content[@"imageOnly"] isKindOfClass:[NSDictionary class]]) { + mode = FIRIAMRenderAsImageOnlyView; + NSDictionary *imageOnlyNode = (NSDictionary *)contentNode[@"imageOnly"]; + + imageURLStr = imageOnlyNode[@"imageUrl"]; + + if (!imageURLStr) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900007", + @"Image url is missing for image-only message %@", messageNode); + return nil; + } + actionURLStr = imageOnlyNode[@"action"][@"actionUrl"]; + } else { + // Unknown message type + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900003", + @"Unknown message type in message node %@", messageNode); + return nil; + } + + if (title == nil && mode != FIRIAMRenderAsImageOnlyView) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900004", + @"Title text is missing in message node %@", messageNode); + return nil; + } + + NSURL *imageURL = (imageURLStr.length == 0) ? nil : [NSURL URLWithString:imageURLStr]; + NSURL *actionURL = (actionURLStr.length == 0) ? nil : [NSURL URLWithString:actionURLStr]; + FIRIAMRenderingEffectSetting *renderEffect = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderEffect.viewMode = mode; + + if (viewCardBackgroundColor) { + renderEffect.displayBGColor = viewCardBackgroundColor; + } + + if (btnBgColor) { + renderEffect.btnBGColor = btnBgColor; + } + + if (btnTxtColor) { + renderEffect.btnTextColor = btnTxtColor; + } + + if (titleTextColor) { + renderEffect.textColor = titleTextColor; + } + + NSArray *triggersDefinition = + [self parseTriggeringCondition:messageNode[@"triggeringConditions"]]; + + if (isTestMessage) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900008", + @"A test message with id %@ was parsed successfully.", messageID); + renderEffect.isTestMessage = YES; + } else { + // Triggering definitions should always be present for a non-test message. + if (!triggersDefinition || triggersDefinition.count == 0) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900009", + @"No valid triggering condition is detected in message definition" + " with id %@", + messageID); + return nil; + } + } + + FIRIAMMessageContentDataWithImageURL *msgData = + [[FIRIAMMessageContentDataWithImageURL alloc] initWithMessageTitle:title + messageBody:body + actionButtonText:actionButtonText + actionURL:actionURL + imageURL:imageURL + usingURLSession:nil]; + + FIRIAMMessageRenderData *renderData = + [[FIRIAMMessageRenderData alloc] initWithMessageID:messageID + messageName:messageName + contentData:msgData + renderingEffect:renderEffect]; + + if (isTestMessage) { + return [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:renderData]; + } else { + return [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData + startTime:startTimeInSeconds + endTime:endTimeInSeconds + triggerDefinition:triggersDefinition]; + } + } @catch (NSException *e) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900006", + @"Error in parsing message node %@ " + "with error %@", + messageNode, e); + return nil; + } +} +@end diff --git a/Firebase/InAppMessaging/Data/FIRIAMMessageContentData.h b/Firebase/InAppMessaging/Data/FIRIAMMessageContentData.h new file mode 100644 index 00000000000..663096e192a --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMMessageContentData.h @@ -0,0 +1,42 @@ +/* + * Copyright 2017 Google + * + * 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 protocol models the message content (non-ui related) data for an in-app message. + */ +@protocol FIRIAMMessageContentData +@property(nonatomic, readonly, nonnull) NSString *titleText; +@property(nonatomic, readonly, nonnull) NSString *bodyText; +@property(nonatomic, readonly, nullable) NSString *actionButtonText; +@property(nonatomic, readonly, nullable) NSURL *actionURL; +@property(nonatomic, readonly, nullable) NSURL *imageURL; + +// Load image data and report the result in the callback block. +// Expect these cases in the callback block +// If error happens, error parameter will be non-nil. +// If no error happens and imageData parameter is nil, it indicates the case that there +// is no image assoicated with the message. +// If error is nil and imageData is not nil, then the image data is loaded successfully +- (void)loadImageDataWithBlock:(void (^)(NSData *_Nullable imageData, + NSError *_Nullable error))block; + +// convert to a description string of the content +- (NSString *)description; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.h b/Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.h new file mode 100644 index 00000000000..eecff0986e5 --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.h @@ -0,0 +1,47 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMMessageContentData.h" + +NS_ASSUME_NONNULL_BEGIN +/** + * An implementation for protocol FIRIAMMessageContentData. This class takes a image url + * and fetch it over the network to retrieve the image data. + */ +@interface FIRIAMMessageContentDataWithImageURL : NSObject +/** + * Create an instance which uses NSURLSession to do the image data fetching. + * + * @param title Message title text. + * @param body Message body text. + * @param actionButtonText Text for action button. + * @param actionURL url string for action. + * @param imageURL the url to the image. It can be nil to indicate the non-image in-app + * message case. + * @param URLSession can be nil in which case the class would create NSURLSession + * internally to perform the network request. Having it here so that + * it's easier for doing mocking with unit testing. + */ +- (instancetype)initWithMessageTitle:(NSString *)title + messageBody:(NSString *)body + actionButtonText:(nullable NSString *)actionButtonText + actionURL:(nullable NSURL *)actionURL + imageURL:(nullable NSURL *)imageURL + usingURLSession:(nullable NSURLSession *)URLSession; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.m b/Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.m new file mode 100644 index 00000000000..82a09f87b9f --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.m @@ -0,0 +1,138 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMMessageContentData.h" +#import "FIRIAMMessageContentDataWithImageURL.h" +#import "FIRIAMSDKRuntimeErrorCodes.h" + +static NSInteger const SuccessHTTPStatusCode = 200; + +@interface FIRIAMMessageContentDataWithImageURL () +@property(nonatomic, readwrite, nonnull, copy) NSString *titleText; +@property(nonatomic, readwrite, nonnull, copy) NSString *bodyText; +@property(nonatomic, copy, nullable) NSString *actionButtonText; +@property(nonatomic, copy, nullable) NSURL *actionURL; +@property(nonatomic, nullable, copy) NSURL *imageURL; +@property(readonly) NSURLSession *URLSession; +@end + +@implementation FIRIAMMessageContentDataWithImageURL +- (instancetype)initWithMessageTitle:(NSString *)title + messageBody:(NSString *)body + actionButtonText:(nullable NSString *)actionButtonText + actionURL:(nullable NSURL *)actionURL + imageURL:(nullable NSURL *)imageURL + usingURLSession:(nullable NSURLSession *)URLSession { + if (self = [super init]) { + _titleText = title; + _bodyText = body; + _imageURL = imageURL; + _actionButtonText = actionButtonText; + _actionURL = actionURL; + + if (imageURL) { + _URLSession = URLSession ? URLSession : [NSURLSession sharedSession]; + } + } + return self; +} + +#pragma protocol FIRIAMMessageContentData + +- (NSString *)description { + return [NSString stringWithFormat:@"Message content: title '%@'," + "body '%@', imageURL '%@', action URL '%@'", + self.titleText, self.bodyText, self.imageURL, self.actionURL]; +} + +- (NSString *)getTitleText { + return _titleText; +} + +- (NSString *)getBodyText { + return _bodyText; +} + +- (nullable NSString *)getActionButtonText { + return _actionButtonText; +} + +- (void)loadImageDataWithBlock:(void (^)(NSData *_Nullable imageData, + NSError *_Nullable error))block { + if (!block) { + // no need for any further action if block is nil + return; + } + + if (!_imageURL) { + // no image data since image url is nil + block(nil, nil); + } else { + NSURLRequest *imageDataRequest = [NSURLRequest requestWithURL:_imageURL]; + NSURLSessionDataTask *task = [_URLSession + dataTaskWithRequest:imageDataRequest + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000003", + @"Error in fetching image: %@", error); + block(nil, error); + } else { + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode == SuccessHTTPStatusCode) { + if (httpResponse.MIMEType == nil || ![httpResponse.MIMEType hasPrefix:@"image"]) { + NSString *errorDesc = + [NSString stringWithFormat:@"None image MIME type %@" + " detected for url %@", + httpResponse.MIMEType, self.imageURL]; + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000004", @"%@", errorDesc); + + NSError *error = + [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain + code:FIRIAMSDKRuntimeErrorNonImageMimetypeFromImageURL + userInfo:@{NSLocalizedDescriptionKey : errorDesc}]; + block(nil, error); + } else { + block(data, nil); + } + } else { + NSString *errorDesc = + [NSString stringWithFormat:@"Failed HTTP request to crawl image %@: " + "HTTP status code as %ld", + self->_imageURL, (long)httpResponse.statusCode]; + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000001", @"%@", errorDesc); + NSError *error = + [NSError errorWithDomain:NSURLErrorDomain + code:httpResponse.statusCode + userInfo:@{NSLocalizedDescriptionKey : errorDesc}]; + block(nil, error); + } + } else { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000002", + @"Internal error: got a non http response from fetching image for " + @"image url as %@", + self->_imageURL); + } + } + }]; + [task resume]; + } +} +@end diff --git a/Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.h b/Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.h new file mode 100644 index 00000000000..ecce5246c55 --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.h @@ -0,0 +1,60 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMMessageRenderData.h" + +@class FIRIAMDisplayTriggerDefinition; + +NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMMessageDefinition : NSObject +@property(nonatomic, nonnull, readonly) FIRIAMMessageRenderData *renderData; + +// metadata data that does not affect the rendering content/effect directly +@property(nonatomic, readonly) NSTimeInterval startTime; +@property(nonatomic, readonly) NSTimeInterval endTime; + +// a fiam message can have multiple triggers and any of them on its own can cause +// the message to be rendered +@property(nonatomic, readonly) NSArray *renderTriggers; + +/// A flag for client-side testing messages +@property(nonatomic, readonly) BOOL isTestMessage; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Create a regular message definition. + */ +- (instancetype)initWithRenderData:(FIRIAMMessageRenderData *)renderData + startTime:(NSTimeInterval)startTime + endTime:(NSTimeInterval)endTime + triggerDefinition:(NSArray *)renderTriggers; + +/** + * Create a test message definition. + */ +- (instancetype)initTestMessageWithRenderData:(FIRIAMMessageRenderData *)renderData; + +- (BOOL)messageHasExpired; +- (BOOL)messageHasStarted; + +// should this message be rendered when the app gets foregrounded? +- (BOOL)messageRenderedOnAppForegroundEvent; +// should this message be rendered when a given analytics event is fired? +- (BOOL)messageRenderedOnAnalyticsEvent:(NSString *)eventName; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.m b/Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.m new file mode 100644 index 00000000000..fa083fbe448 --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.m @@ -0,0 +1,85 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMMessageDefinition.h" +#import "FIRIAMDisplayTriggerDefinition.h" + +@implementation FIRIAMMessageRenderData + +- (instancetype)initWithMessageID:(NSString *)messageID + messageName:(NSString *)messageName + contentData:(id)contentData + renderingEffect:(FIRIAMRenderingEffectSetting *)renderEffect { + if (self = [super init]) { + _contentData = contentData; + _renderingEffectSettings = renderEffect; + _messageID = [messageID copy]; + _name = [messageName copy]; + } + return self; +} +@end + +@implementation FIRIAMMessageDefinition +- (instancetype)initWithRenderData:(FIRIAMMessageRenderData *)renderData + startTime:(NSTimeInterval)startTime + endTime:(NSTimeInterval)endTime + triggerDefinition:(NSArray *)renderTriggers { + if (self = [super init]) { + _renderData = renderData; + _renderTriggers = renderTriggers; + _startTime = startTime; + _endTime = endTime; + _isTestMessage = NO; + } + return self; +} + +- (instancetype)initTestMessageWithRenderData:(FIRIAMMessageRenderData *)renderData { + if (self = [super init]) { + _renderData = renderData; + _isTestMessage = YES; + } + return self; +} + +- (BOOL)messageHasExpired { + return self.endTime < [[NSDate date] timeIntervalSince1970]; +} + +- (BOOL)messageRenderedOnAppForegroundEvent { + for (FIRIAMDisplayTriggerDefinition *nextTrigger in self.renderTriggers) { + if (nextTrigger.triggerType == FIRIAMRenderTriggerOnAppForeground) { + return YES; + } + } + return NO; +} + +- (BOOL)messageRenderedOnAnalyticsEvent:(NSString *)eventName { + for (FIRIAMDisplayTriggerDefinition *nextTrigger in self.renderTriggers) { + if (nextTrigger.triggerType == FIRIAMRenderTriggerOnFirebaseAnalyticsEvent && + [nextTrigger.firebaseEventName isEqualToString:eventName]) { + return YES; + } + } + return NO; +} + +- (BOOL)messageHasStarted { + return self.startTime < [[NSDate date] timeIntervalSince1970]; +} +@end diff --git a/Firebase/InAppMessaging/Data/FIRIAMMessageRenderData.h b/Firebase/InAppMessaging/Data/FIRIAMMessageRenderData.h new file mode 100644 index 00000000000..b414657dcd4 --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMMessageRenderData.h @@ -0,0 +1,36 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMRenderingEffectSetting.h" + +@protocol FIRIAMMessageContentData; +NS_ASSUME_NONNULL_BEGIN +// This wraps the data that's needed for render the message's content in UI. It also contains +// certain meta data that's needed in responding to user's action +@interface FIRIAMMessageRenderData : NSObject +@property(nonatomic, nonnull, readonly) id contentData; +@property(nonatomic, nonnull, readonly) FIRIAMRenderingEffectSetting *renderingEffectSettings; +@property(nonatomic, nonnull, copy, readonly) NSString *messageID; +@property(nonatomic, nonnull, copy, readonly) NSString *name; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMessageID:(NSString *)messageID + messageName:(NSString *)messageName + contentData:(id)contentData + renderingEffect:(FIRIAMRenderingEffectSetting *)renderEffect; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.h b/Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.h new file mode 100644 index 00000000000..dfc648ca344 --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.h @@ -0,0 +1,56 @@ +/* + * Copyright 2017 Google + * + * 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 + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, FIRIAMRenderingMode) { + FIRIAMRenderAsBannerView, + FIRIAMRenderAsModalView, + FIRIAMRenderAsImageOnlyView +}; + +/** + * A class for modeling rendering effect settings for in-app messaging + */ +@interface FIRIAMRenderingEffectSetting : NSObject + +@property(nonatomic) FIRIAMRenderingMode viewMode; + +// background color for the display area, including both the text's background and +// padding's background +@property(nonatomic, copy) UIColor *displayBGColor; + +// text color, covering both the title and body texts +@property(nonatomic, copy) UIColor *textColor; + +// text color for action button +@property(nonatomic, copy) UIColor *btnTextColor; + +// background color for action button +@property(nonatomic, copy) UIColor *btnBGColor; + +// duration of the banner view before triggering auto-dismiss +@property(nonatomic) CGFloat autoDimissBannerAfterNSeconds; + +// A flag to control rendering the message as a client-side testing message +@property(nonatomic) BOOL isTestMessage; + ++ (instancetype)getDefaultRenderingEffectSetting; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.m b/Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.m new file mode 100644 index 00000000000..6fd7fb43f7e --- /dev/null +++ b/Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.m @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMRenderingEffectSetting.h" + +@implementation FIRIAMRenderingEffectSetting + ++ (instancetype)getDefaultRenderingEffectSetting { + FIRIAMRenderingEffectSetting *setting = [[FIRIAMRenderingEffectSetting alloc] init]; + + setting.btnBGColor = [UIColor colorWithRed:0.3 green:0.55 blue:0.28 alpha:1.0]; + setting.displayBGColor = [UIColor whiteColor]; + setting.textColor = [UIColor blackColor]; + setting.btnTextColor = [UIColor whiteColor]; + setting.autoDimissBannerAfterNSeconds = 12; + setting.isTestMessage = NO; + return setting; +} +@end diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.h b/Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.h similarity index 54% rename from Firestore/Source/Local/FSTMemoryMutationQueue.h rename to Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.h index 3a7808d0ebb..cba59391c45 100644 --- a/Firestore/Source/Local/FSTMemoryMutationQueue.h +++ b/Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.h @@ -16,26 +16,19 @@ #import -#import "Firestore/Source/Local/FSTMemoryPersistence.h" -#import "Firestore/Source/Local/FSTMutationQueue.h" +typedef NS_ENUM(NSInteger, FIRIAMRenderTrigger) { + FIRIAMRenderTriggerOnAppForeground, + FIRIAMRenderTriggerOnFirebaseAnalyticsEvent +}; NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMDisplayTriggerDefinition : NSObject +@property(nonatomic, readonly) FIRIAMRenderTrigger triggerType; -@class FSTLocalSerializer; - -@interface FSTMemoryMutationQueue : NSObject - -- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence NS_DESIGNATED_INITIALIZER; - -- (instancetype)init NS_UNAVAILABLE; - -/** - * Checks to see if there are any references to a document with the given key. - */ -- (BOOL)containsKey:(const firebase::firestore::model::DocumentKey &)key; - -- (size_t)byteSizeWithSerializer:(FSTLocalSerializer *)serializer; +// applicable only when triggerType == FIRIAMRenderTriggerOnFirebaseAnalyticsEvent +@property(nonatomic, copy, nullable, readonly) NSString *firebaseEventName; +- (instancetype)initForAppForegroundTrigger; +- (instancetype)initWithFirebaseAnalyticEvent:(NSString *)title; @end - NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.m b/Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.m new file mode 100644 index 00000000000..3613de24696 --- /dev/null +++ b/Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.m @@ -0,0 +1,33 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMDisplayTriggerDefinition.h" + +@implementation FIRIAMDisplayTriggerDefinition +- (instancetype)initForAppForegroundTrigger { + if (self = [super init]) { + _triggerType = FIRIAMRenderTriggerOnAppForeground; + } + return self; +} +- (instancetype)initWithFirebaseAnalyticEvent:(NSString *)title { + if (self = [super init]) { + _triggerType = FIRIAMRenderTriggerOnFirebaseAnalyticsEvent; + _firebaseEventName = title; + } + return self; +} +@end diff --git a/Firebase/InAppMessaging/FIRCore+InAppMessaging.h b/Firebase/InAppMessaging/FIRCore+InAppMessaging.h new file mode 100644 index 00000000000..e38593b26a5 --- /dev/null +++ b/Firebase/InAppMessaging/FIRCore+InAppMessaging.h @@ -0,0 +1,36 @@ +/* + * Copyright 2017 Google + * + * 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 + +// This file contains declarations that should go into FirebaseCore when +// Firebase InAppMessaging is merged into master. Keep them separate now to help +// with build from development folder and avoid merge conflicts. + +// this should eventually be in FIRLogger.h +extern FIRLoggerService kFIRLoggerInAppMessaging; + +// this should eventually be in FIRError.h +extern NSString *const kFirebaseInAppMessagingErrorDomain; + +// this should eventually be in FIRError.h FIRAppInternal.h:46: +extern NSString *const kFIRServiceInAppMessaging; + +// InAppMessaging doesn't provide any functionality to other components, +// so it provides a private, empty protocol that it conforms to and use it for registration. + +@protocol FIRInAppMessagingInstanceProvider +@end diff --git a/Firebase/InAppMessaging/FIRCore+InAppMessaging.m b/Firebase/InAppMessaging/FIRCore+InAppMessaging.m new file mode 100644 index 00000000000..219c48bedf4 --- /dev/null +++ b/Firebase/InAppMessaging/FIRCore+InAppMessaging.m @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRCore+InAppMessaging.h" + +NSString *const kFIRServiceInAppMessaging = @"InAppMessaging"; +NSString *const kFirebaseInAppMessagingErrorDomain = @"com.firebase.inappmessaging"; +FIRLoggerService kFIRLoggerInAppMessaging = @"[Firebase/InAppMessaging]"; diff --git a/Firebase/InAppMessaging/FIRInAppMessaging.m b/Firebase/InAppMessaging/FIRInAppMessaging.m new file mode 100644 index 00000000000..a0773e78d36 --- /dev/null +++ b/Firebase/InAppMessaging/FIRInAppMessaging.m @@ -0,0 +1,144 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRInAppMessaging.h" + +#import + +#import +#import +#import +#import +#import + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMDisplayExecutor.h" +#import "FIRIAMRuntimeManager.h" +#import "FIRInAppMessaging+Bootstrap.h" +#import "FIRInAppMessagingPrivate.h" + +static BOOL _autoBootstrapOnFIRAppInit = YES; + +@implementation FIRInAppMessaging { + BOOL _messageDisplaySuppressed; +} + +// Call this to present the SDK being auto bootstrapped with other Firebase SDKs. It needs +// to be triggered before [FIRApp configure] is executed. This should only be needed for +// testing app that wants to use custom fiam SDK settings. ++ (void)disableAutoBootstrapWithFIRApp { + _autoBootstrapOnFIRAppInit = NO; +} + +// extract macro value into a C string +#define STR_FROM_MACRO(x) #x +#define STR(x) STR_FROM_MACRO(x) + ++ (void)load { + [FIRApp + registerInternalLibrary:(Class)self + withName:@"fire-fiam" + withVersion:[NSString stringWithUTF8String:STR(FIRInAppMessaging_LIB_VERSION)]]; +} + ++ (nonnull NSArray *)componentsToRegister { + FIRDependency *analyticsDep = [FIRDependency dependencyWithProtocol:@protocol(FIRAnalyticsInterop) + isRequired:YES]; + FIRComponentCreationBlock creationBlock = + ^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) { + // Ensure it's cached so it returns the same instance every time fiam is called. + *isCacheable = YES; + id analytics = FIR_COMPONENT(FIRAnalyticsInterop, container); + return [[FIRInAppMessaging alloc] initWithAnalytics:analytics]; + }; + FIRComponent *fiamProvider = + [FIRComponent componentWithProtocol:@protocol(FIRInAppMessagingInstanceProvider) + instantiationTiming:FIRInstantiationTimingLazy + dependencies:@[ analyticsDep ] + creationBlock:creationBlock]; + + return @[ fiamProvider ]; +} + ++ (void)configureWithApp:(FIRApp *)app { + if (!app.isDefaultApp) { + // Only configure for the default FIRApp. + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170000", + @"Firebase InAppMessaging only works with the default app."); + return; + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170001", + @"Got notification for kFIRAppReadyToConfigureSDKNotification"); + if (_autoBootstrapOnFIRAppInit) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170002", + @"Auto bootstrap Firebase in-app messaging SDK"); + [self bootstrapIAMFromFIRApp:app]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170003", + @"No auto bootstrap Firebase in-app messaging SDK"); + } +} + +- (instancetype)initWithAnalytics:(id)analytics { + if (self = [super init]) { + _messageDisplaySuppressed = NO; + _analytics = analytics; + } + return self; +} + ++ (FIRInAppMessaging *)inAppMessaging { + FIRApp *defaultApp = [FIRApp defaultApp]; // Missing configure will be logged here. + id inAppMessaging = + FIR_COMPONENT(FIRInAppMessagingInstanceProvider, defaultApp.container); + return (FIRInAppMessaging *)inAppMessaging; +} + +- (BOOL)messageDisplaySuppressed { + return _messageDisplaySuppressed; +} + +- (void)setMessageDisplaySuppressed:(BOOL)suppressed { + _messageDisplaySuppressed = suppressed; + [[FIRIAMRuntimeManager getSDKRuntimeInstance] setShouldSuppressMessageDisplay:suppressed]; +} + +- (BOOL)automaticDataCollectionEnabled { + return [FIRIAMRuntimeManager getSDKRuntimeInstance].automaticDataCollectionEnabled; +} + +- (void)setAutomaticDataCollectionEnabled:(BOOL)automaticDataCollectionEnabled { + [FIRIAMRuntimeManager getSDKRuntimeInstance].automaticDataCollectionEnabled = + automaticDataCollectionEnabled; +} + +- (void)setMessageDisplayComponent:(id)messageDisplayComponent { + _messageDisplayComponent = messageDisplayComponent; + + if (messageDisplayComponent == nil) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM290002", @"messageDisplayComponent set to nil."); + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM290001", + @"Setting a non-nil message display component"); + } + + // Forward the setting to the display executor. + [FIRIAMRuntimeManager getSDKRuntimeInstance].displayExecutor.messageDisplayComponent = + messageDisplayComponent; +} + +@end diff --git a/Firebase/InAppMessaging/FIRInAppMessagingPrivate.h b/Firebase/InAppMessaging/FIRInAppMessagingPrivate.h new file mode 100644 index 00000000000..87c0a3cd7b7 --- /dev/null +++ b/Firebase/InAppMessaging/FIRInAppMessagingPrivate.h @@ -0,0 +1,26 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRCore+InAppMessaging.h" +#import "FIRInAppMessaging.h" + +@protocol FIRInAppMessagingInstanceProvider; +@protocol FIRLibrary; + +@interface FIRInAppMessaging () +@property(nonatomic, readwrite, strong) id _Nullable analytics; +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.h b/Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.h new file mode 100644 index 00000000000..82fb06f41ae --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.h @@ -0,0 +1,89 @@ +/* + * Copyright 2017 Google + * + * 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 + +/// Values for different fiam activity types. +typedef NS_ENUM(NSInteger, FIRIAMActivityType) { + FIRIAMActivityTypeFetchMessage = 0, + FIRIAMActivityTypeRenderMessage = 1, + FIRIAMActivityTypeDismissMessage = 2, + + // Triggered checks + FIRIAMActivityTypeCheckForOnOpenMessage = 3, + FIRIAMActivityTypeCheckForAnalyticsEventMessage = 4, + FIRIAMActivityTypeCheckForFetch = 5, +}; + +NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMActivityRecord : NSObject +@property(nonatomic, nonnull, readonly) NSDate *timestamp; +@property(nonatomic, readonly) FIRIAMActivityType activityType; +@property(nonatomic, readonly) BOOL success; +@property(nonatomic, copy, nonnull, readonly) NSString *detail; + +- (instancetype)init NS_UNAVAILABLE; +// Current timestamp would be fetched if parameter 'timestamp' is passed in as null +- (instancetype)initWithActivityType:(FIRIAMActivityType)type + isSuccessful:(BOOL)isSuccessful + withDetail:(NSString *)detail + timestamp:(nullable NSDate *)timestamp; + +- (NSString *)displayStringForActivityType; +@end + +/** + * This is the class for tracking fiam flow related activity logs. Its content can later on be + * retrieved for debugging/reporting purpose. + */ +@interface FIRIAMActivityLogger : NSObject + +// If it's NO, activity logs of certain types won't get recorded by Logger. Consult +// isMandatoryType implementation to tell what are the types belong to verbose mode +// Turn it on for debugging cases +@property(nonatomic, readonly) BOOL verboseMode; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Parameter maxBeforeReduce and sizeAfterReduce defines the shrinking behavior when we reach + * the size cap of log storage: when we see the number of log records goes beyond + * maxBeforeReduce, we would trigger a reduction action which would bring the array length to be + * the size as defined by sizeAfterReduce + * + * @param verboseMode see the comments for the verboseMode property + * @param loadFromCache loads from cache to initialize the log list if it's true. Be aware that + * in this case, you should not call this method in main thread since reading the cache file + * can take time. + */ +- (instancetype)initWithMaxCountBeforeReduce:(NSInteger)maxBeforeReduce + withSizeAfterReduce:(NSInteger)sizeAfterReduce + verboseMode:(BOOL)verboseMode + loadFromCache:(BOOL)loadFromCache; + +/** + * Inserting a new record into activity log. + * + * @param newRecord new record to be inserted + */ +- (void)addLogRecord:(FIRIAMActivityRecord *)newRecord; + +/** + * Get a immutable copy of the existing activity log records. + */ +- (NSArray *)readRecords; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.m b/Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.m new file mode 100644 index 00000000000..95c344847af --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.m @@ -0,0 +1,215 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMActivityLogger.h" +@implementation FIRIAMActivityRecord + +static NSString *const kActiveTypeArchiveKey = @"type"; +static NSString *const kIsSuccessArchiveKey = @"is_success"; +static NSString *const kTimeStampArchiveKey = @"timestamp"; +static NSString *const kDetailArchiveKey = @"detail"; + +- (id)initWithCoder:(NSCoder *)decoder { + self = [super init]; + if (self != nil) { + _activityType = [decoder decodeIntegerForKey:kActiveTypeArchiveKey]; + _timestamp = [decoder decodeObjectForKey:kTimeStampArchiveKey]; + _success = [decoder decodeBoolForKey:kIsSuccessArchiveKey]; + _detail = [decoder decodeObjectForKey:kDetailArchiveKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)encoder { + [encoder encodeInteger:self.activityType forKey:kActiveTypeArchiveKey]; + [encoder encodeObject:self.timestamp forKey:kTimeStampArchiveKey]; + [encoder encodeBool:self.success forKey:kIsSuccessArchiveKey]; + [encoder encodeObject:self.detail forKey:kDetailArchiveKey]; +} + +- (instancetype)initWithActivityType:(FIRIAMActivityType)type + isSuccessful:(BOOL)isSuccessful + withDetail:(NSString *)detail + timestamp:(nullable NSDate *)timestamp { + if (self = [super init]) { + _activityType = type; + _success = isSuccessful; + _detail = detail; + _timestamp = timestamp ? timestamp : [[NSDate alloc] init]; + } + return self; +} + +- (NSString *)displayStringForActivityType { + switch (self.activityType) { + case FIRIAMActivityTypeFetchMessage: + return @"Message Fetching"; + case FIRIAMActivityTypeRenderMessage: + return @"Message Rendering"; + case FIRIAMActivityTypeDismissMessage: + return @"Message Dismiss"; + case FIRIAMActivityTypeCheckForOnOpenMessage: + return @"OnOpen Msg Check"; + case FIRIAMActivityTypeCheckForAnalyticsEventMessage: + return @"Analytic Msg Check"; + case FIRIAMActivityTypeCheckForFetch: + return @"Fetch Check"; + } +} +@end + +@interface FIRIAMActivityLogger () +@property(nonatomic) BOOL isDirty; + +// always insert at the head of this array so that they are always in anti-chronological order +@property(nonatomic, nonnull) NSMutableArray *activityRecords; + +// When we see the number of log records goes beyond maxRecordCountBeforeReduce, we would trigger +// a reduction action which would bring the array length to be the size as defined by +// newSizeAfterReduce +@property(nonatomic, readonly) NSInteger maxRecordCountBeforeReduce; +@property(nonatomic, readonly) NSInteger newSizeAfterReduce; + +@end + +@implementation FIRIAMActivityLogger +- (instancetype)initWithMaxCountBeforeReduce:(NSInteger)maxBeforeReduce + withSizeAfterReduce:(NSInteger)sizeAfterReduce + verboseMode:(BOOL)verboseMode + loadFromCache:(BOOL)loadFromCache { + if (self = [super init]) { + _maxRecordCountBeforeReduce = maxBeforeReduce; + _newSizeAfterReduce = sizeAfterReduce; + _activityRecords = [[NSMutableArray alloc] init]; + _verboseMode = verboseMode; + _isDirty = NO; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appWillBecomeInactive) + name:UIApplicationWillResignActiveNotification + object:nil]; + + if (loadFromCache) { + @try { + [self loadFromCachePath:nil]; + } @catch (NSException *exception) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM310003", + @"Non-fatal exception in loading persisted activity log records: %@.", + exception); + } + } + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + ++ (NSString *)determineCacheFilePath { + static NSString *logCachePath; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + NSString *cacheDirPath = + NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0]; + logCachePath = [NSString stringWithFormat:@"%@/firebase-iam-activity-log-cache", cacheDirPath]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM310001", + @"Persistent file path for activity log data is %@", logCachePath); + }); + return logCachePath; +} + +- (void)loadFromCachePath:(NSString *)cacheFilePath { + NSString *filePath = cacheFilePath == nil ? [self.class determineCacheFilePath] : cacheFilePath; + + id fetchedActivityRecords = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath]; + + if (fetchedActivityRecords) { + @synchronized(self) { + self.activityRecords = (NSMutableArray *)fetchedActivityRecords; + self.isDirty = NO; + } + } +} + +- (BOOL)saveIntoCacheWithPath:(NSString *)cacheFilePath { + NSString *filePath = cacheFilePath == nil ? [self.class determineCacheFilePath] : cacheFilePath; + @synchronized(self) { + BOOL result = [NSKeyedArchiver archiveRootObject:self.activityRecords toFile:filePath]; + if (result) { + self.isDirty = NO; + } + return result; + } +} + +- (void)appWillBecomeInactive { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM310004", + @"App will become inactive, save" + " activity logs"); + + if (self.isDirty) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + if ([self saveIntoCacheWithPath:nil]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM310002", + @"Persisting activity log data is was successful"); + } else { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM310005", + @"Persisting activity log data has failed"); + } + }); + } +} + +// Helper function to determine if a given activity type should be recorded under +// non verbose type. ++ (BOOL)isMandatoryType:(FIRIAMActivityType)type { + switch (type) { + case FIRIAMActivityTypeFetchMessage: + case FIRIAMActivityTypeRenderMessage: + case FIRIAMActivityTypeDismissMessage: + return YES; + default: + return NO; + } +} + +- (void)addLogRecord:(FIRIAMActivityRecord *)newRecord { + if (self.verboseMode || [FIRIAMActivityLogger isMandatoryType:newRecord.activityType]) { + @synchronized(self) { + [self.activityRecords insertObject:newRecord atIndex:0]; + + if (self.activityRecords.count >= self.maxRecordCountBeforeReduce) { + NSRange removeRange; + removeRange.location = self.newSizeAfterReduce; + removeRange.length = self.maxRecordCountBeforeReduce - self.newSizeAfterReduce; + [self.activityRecords removeObjectsInRange:removeRange]; + } + self.isDirty = YES; + } + } +} + +- (NSArray *)readRecords { + return [self.activityRecords copy]; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.h b/Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.h new file mode 100644 index 00000000000..1c9a11045a4 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.h @@ -0,0 +1,75 @@ +/* + * Copyright 2017 Google + * + * 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 +@interface FIRIAMImpressionRecord : NSObject +@property(nonatomic, readonly, copy) NSString *messageID; +@property(nonatomic, readonly) long impressionTimeInSeconds; + +- (NSString *)description; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMessageID:(NSString *)messageID + impressionTimeInSeconds:(long)impressionTime NS_DESIGNATED_INITIALIZER; +@end + +// this protocol defines the interface for classes that can be used to track info regarding +// display & fetch of iam messages. The info tracked here can be used to decide if it's due for +// next display and/or fetch of iam messages. +@protocol FIRIAMBookKeeper +@property(nonatomic, readonly) double lastDisplayTime; +@property(nonatomic, readonly) double lastFetchTime; +@property(nonatomic, readonly) NSTimeInterval nextFetchWaitTime; + +// only call this when it's considered to be a valid impression (for example, meeting the minimum +// display time requirement). +- (void)recordNewImpressionForMessage:(NSString *)messageID + withStartTimestampInSeconds:(double)timestamp; + +- (void)recordNewFetchWithFetchCount:(NSInteger)fetchedMsgCount + withTimestampInSeconds:(double)fetchTimestamp + nextFetchWaitTime:(nullable NSNumber *)nextFetchWaitTime; + +// When we fetch the eligible message list from the sdk server, it can contain messages that are +// already impressed for those that are defined to be displayed repeatedly (messages with custom +// display frequency). We need then clean up the impression records for these messages so that +// they can be displayed again on client side. +- (void)clearImpressionsWithMessageList:(NSArray *)messageList; +// fetch the impression list +- (NSArray *)getImpressions; + +// For certain clients, they only need to get the list of the message ids in existing impression +// records. This is a helper method for that. +- (NSArray *)getMessageIDsFromImpressions; +@end + +// implementation of FIRIAMBookKeeper protocol by storing data within iOS UserDefaults. +// TODO: switch to something else if there is risks for the data being unintentionally deleted by +// the app +@interface FIRIAMBookKeeperViaUserDefaults : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithUserDefaults:(NSUserDefaults *)userDefaults NS_DESIGNATED_INITIALIZER; + +// for testing, don't use them for production purpose +- (void)cleanupImpressions; +- (void)cleanupFetchRecords; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.m b/Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.m new file mode 100644 index 00000000000..a7019d7bd80 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.m @@ -0,0 +1,260 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRCore+InAppMessaging.h" +#import "FIRIAMBookKeeper.h" + +NSString *const FIRIAM_UserDefaultsKeyForImpressions = @"firebase-iam-message-impressions"; +NSString *const FIRIAM_UserDefaultsKeyForLastImpressionTimestamp = + @"firebase-iam-last-impression-timestamp"; +NSString *FIRIAM_UserDefaultsKeyForLastFetchTimestamp = @"firebase-iam-last-fetch-timestamp"; + +// The two keys used to map FIRIAMImpressionRecord object to a NSDictionary object for +// persistence. +NSString *const FIRIAM_ImpressionDictKeyForID = @"message_id"; +NSString *const FIRIAM_ImpressionDictKeyForTimestamp = @"impression_time"; + +static NSString *const kUserDefaultsKeyForFetchWaitTime = @"firebase-iam-fetch-wait-time"; + +// 24 hours +static NSTimeInterval kDefaultFetchWaitTimeInSeconds = 24 * 60 * 60; + +// 3 days +static NSTimeInterval kMaxFetchWaitTimeInSeconds = 3 * 24 * 60 * 60; + +@interface FIRIAMBookKeeperViaUserDefaults () +@property(nonatomic) double lastDisplayTime; +@property(nonatomic) double lastFetchTime; +@property(nonatomic) double nextFetchWaitTime; +@property(nonatomic, nonnull) NSUserDefaults *defaults; +@end + +@interface FIRIAMImpressionRecord () +- (instancetype)initWithStorageDictionary:(NSDictionary *)dict; +@end + +@implementation FIRIAMImpressionRecord + +- (instancetype)initWithMessageID:(NSString *)messageID + impressionTimeInSeconds:(long)impressionTime { + if (self = [super init]) { + _messageID = messageID; + _impressionTimeInSeconds = impressionTime; + } + return self; +} + +- (instancetype)initWithStorageDictionary:(NSDictionary *)dict { + id timestamp = dict[FIRIAM_ImpressionDictKeyForTimestamp]; + id messageID = dict[FIRIAM_ImpressionDictKeyForID]; + + if (![timestamp isKindOfClass:[NSNumber class]] || ![messageID isKindOfClass:[NSString class]]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270003", + @"Incorrect data in the dictionary object for creating a FIRIAMImpressionRecord" + " object"); + return nil; + } else { + return [self initWithMessageID:messageID + impressionTimeInSeconds:((NSNumber *)timestamp).longValue]; + } +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ impressed at %ld in seconds", self.messageID, + self.impressionTimeInSeconds]; +} +@end + +@implementation FIRIAMBookKeeperViaUserDefaults + +- (instancetype)initWithUserDefaults:(NSUserDefaults *)userDefaults { + if (self = [super init]) { + _defaults = userDefaults; + + // ok if it returns 0 due to the entry being absent + _lastDisplayTime = [_defaults doubleForKey:FIRIAM_UserDefaultsKeyForLastImpressionTimestamp]; + _lastFetchTime = [_defaults doubleForKey:FIRIAM_UserDefaultsKeyForLastFetchTimestamp]; + + id fetchWaitTimeEntry = [_defaults objectForKey:kUserDefaultsKeyForFetchWaitTime]; + + if (![fetchWaitTimeEntry isKindOfClass:NSNumber.class]) { + // This corresponds to the case there is no wait time entry is set in user defaults yet + _nextFetchWaitTime = kDefaultFetchWaitTimeInSeconds; + } else { + _nextFetchWaitTime = ((NSNumber *)fetchWaitTimeEntry).doubleValue; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270009", + @"Next fetch wait time loaded from user defaults is %lf", _nextFetchWaitTime); + } + } + return self; +} + +// A helper function for reading and verifying the stored array data for impressions +// in UserDefaults. It returns nil if it does not exist or fail to pass the data type +// checking. +- (NSArray *)fetchImpressionArrayFromStorage { + id impressionsData = [self.defaults objectForKey:FIRIAM_UserDefaultsKeyForImpressions]; + + if (impressionsData && ![impressionsData isKindOfClass:[NSArray class]]) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM270007", + @"Found non-array data from impression userdefaults storage with key %@", + FIRIAM_UserDefaultsKeyForImpressions); + return nil; + } + return (NSArray *)impressionsData; +} + +- (void)recordNewImpressionForMessage:(NSString *)messageID + withStartTimestampInSeconds:(double)timestamp { + @synchronized(self) { + NSArray *oldImpressions = [self fetchImpressionArrayFromStorage]; + // oldImpressions could be nil at the first time + NSMutableArray *newImpressions = + oldImpressions ? [oldImpressions mutableCopy] : [[NSMutableArray alloc] init]; + + // Two cases + // If a prior impression exists for that messageID, update its impression timestamp + // If a prior impression for that messageID does not exist, add a new entry for the + // messageID. + + NSDictionary *newImpressionEntry = @{ + FIRIAM_ImpressionDictKeyForID : messageID, + FIRIAM_ImpressionDictKeyForTimestamp : [NSNumber numberWithDouble:timestamp] + }; + + BOOL oldImpressionRecordFound = NO; + + for (int i = 0; i < newImpressions.count; i++) { + if ([newImpressions[i] isKindOfClass:[NSDictionary class]]) { + NSDictionary *currentItem = (NSDictionary *)newImpressions[i]; + if ([messageID isEqualToString:currentItem[FIRIAM_ImpressionDictKeyForID]]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270001", + @"Updating timestamp of existing impression record to be %f for " + "message %@", + timestamp, messageID); + + [newImpressions replaceObjectAtIndex:i withObject:newImpressionEntry]; + oldImpressionRecordFound = YES; + break; + } + } + } + + if (!oldImpressionRecordFound) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270002", + @"Insert the first impression record for message %@ with timestamp in seconds " + "as %f", + messageID, timestamp); + [newImpressions addObject:newImpressionEntry]; + } + + [self.defaults setObject:newImpressions forKey:FIRIAM_UserDefaultsKeyForImpressions]; + [self.defaults setDouble:timestamp forKey:FIRIAM_UserDefaultsKeyForLastImpressionTimestamp]; + self.lastDisplayTime = timestamp; + } +} + +- (void)clearImpressionsWithMessageList:(NSArray *)messageList { + @synchronized(self) { + NSArray *existingImpressions = [self fetchImpressionArrayFromStorage]; + + NSSet *messageIDSet = [NSSet setWithArray:messageList]; + NSPredicate *notInMessageListPredicate = + [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { + if (![evaluatedObject isKindOfClass:[NSDictionary class]]) { + return NO; // unexpected item. Throw it away + } + NSDictionary *impression = (NSDictionary *)evaluatedObject; + return impression[FIRIAM_ImpressionDictKeyForID] && + ![messageIDSet containsObject:impression[FIRIAM_ImpressionDictKeyForID]]; + }]; + + NSArray *updatedImpressions = + [existingImpressions filteredArrayUsingPredicate:notInMessageListPredicate]; + + if (existingImpressions.count != updatedImpressions.count) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270004", + @"Updating the impression records after purging %d items based on the " + "server fetch response", + (int)(existingImpressions.count - updatedImpressions.count)); + [self.defaults setObject:updatedImpressions forKey:FIRIAM_UserDefaultsKeyForImpressions]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270005", + @"No impression records update due to no change after applying the server " + "message list"); + } + } +} + +- (NSArray *)getImpressions { + NSArray *impressionsFromStorage = [self fetchImpressionArrayFromStorage]; + + NSMutableArray *resultArray = [[NSMutableArray alloc] init]; + + for (NSDictionary *next in impressionsFromStorage) { + FIRIAMImpressionRecord *nextImpression = + [[FIRIAMImpressionRecord alloc] initWithStorageDictionary:next]; + [resultArray addObject:nextImpression]; + } + + return resultArray; +} + +- (NSArray *)getMessageIDsFromImpressions { + NSArray *impressionsFromStorage = [self fetchImpressionArrayFromStorage]; + + NSMutableArray *resultArray = [[NSMutableArray alloc] init]; + + for (NSDictionary *next in impressionsFromStorage) { + [resultArray addObject:next[FIRIAM_ImpressionDictKeyForID]]; + } + + return resultArray; +} + +- (void)recordNewFetchWithFetchCount:(NSInteger)fetchedMsgCount + withTimestampInSeconds:(double)fetchTimestamp + nextFetchWaitTime:(nullable NSNumber *)nextFetchWaitTime; +{ + [self.defaults setDouble:fetchTimestamp forKey:FIRIAM_UserDefaultsKeyForLastFetchTimestamp]; + self.lastFetchTime = fetchTimestamp; + + if (nextFetchWaitTime) { + if (nextFetchWaitTime.doubleValue > kMaxFetchWaitTimeInSeconds) { + FIRLogInfo(kFIRLoggerInAppMessaging, @"I-IAM270006", + @"next fetch wait time %lf is too large. Ignore it.", + nextFetchWaitTime.doubleValue); + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270008", + @"Setting next fetch wait time as %lf from fetch response.", + nextFetchWaitTime.doubleValue); + self.nextFetchWaitTime = nextFetchWaitTime.doubleValue; + [self.defaults setObject:nextFetchWaitTime forKey:kUserDefaultsKeyForFetchWaitTime]; + } + } +} + +- (void)cleanupImpressions { + [self.defaults setObject:@[] forKey:FIRIAM_UserDefaultsKeyForImpressions]; +} + +- (void)cleanupFetchRecords { + [self.defaults setDouble:0 forKey:FIRIAM_UserDefaultsKeyForLastFetchTimestamp]; + self.lastFetchTime = 0; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.h b/Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.h new file mode 100644 index 00000000000..09b1e6a2ff3 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.h @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Google + * + * 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 + +// A class for wrapping the interactions for retrieving client side info to be used in request +// parameter for interacting with Firebase iam servers. + +NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMClientInfoFetcher : NSObject +// Fetch the up-to-date Firebase instance id and token data. Since it involves a server interaction, +// completion callback is provided for receiving the result. +- (void)fetchFirebaseIIDDataWithProjectNumber:(NSString *)projectNumber + withCompletion:(void (^)(NSString *_Nullable iid, + NSString *_Nullable token, + NSError *_Nullable error))completion; + +// Following are synchronous methods for fetching data +- (nullable NSString *)getDeviceLanguageCode; +- (nullable NSString *)getAppVersion; +- (nullable NSString *)getOSVersion; +- (nullable NSString *)getOSMajorVersion; +- (nullable NSString *)getTimezone; +- (NSString *)getIAMSDKVersion; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.m b/Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.m new file mode 100644 index 00000000000..b7809770524 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.m @@ -0,0 +1,120 @@ +/* + * Copyright 2017 Google + * + * 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 +#import + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMClientInfoFetcher.h" + +// declaratons for FIRInstanceID SDK +@implementation FIRIAMClientInfoFetcher +- (void)fetchFirebaseIIDDataWithProjectNumber:(NSString *)projectNumber + withCompletion:(void (^)(NSString *_Nullable iid, + NSString *_Nullable token, + NSError *_Nullable error))completion { + FIRInstanceID *iid = [FIRInstanceID instanceID]; + + // tokenWithAuthorizedEntity would only communicate with server on periodical cycles. + // For other times, it's going to fetch from local cache, so it's not causing any performance + // concern in the fetch flow. + [iid tokenWithAuthorizedEntity:projectNumber + scope:@"fiam" + options:nil + handler:^(NSString *_Nullable token, NSError *_Nullable error) { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM190001", + @"Error in fetching iid token: %@", + error.localizedDescription); + completion(nil, nil, error); + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM190002", + @"Successfully generated iid token"); + // now we can go ahead to fetch the id + [iid getIDWithHandler:^(NSString *_Nullable identity, + NSError *_Nullable error) { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM190004", + @"Error in fetching iid value: %@", + error.localizedDescription); + } else { + FIRLogDebug( + kFIRLoggerInAppMessaging, @"I-IAM190005", + @"Successfully in fetching both iid value as %@ and iid token" + " as %@", + identity, token); + completion(identity, token, nil); + } + }]; + } + }]; +} + +- (nullable NSString *)getDeviceLanguageCode { + // No caching since it's requested at pretty low frequency and we get the benefit of seeing + // updated info the setting has changed + NSArray *preferredLanguages = [NSLocale preferredLanguages]; + return preferredLanguages.firstObject; +} + +- (nullable NSString *)getAppVersion { + // Since this won't change, read it once in the whole life-cycle of the app and cache its value + static NSString *appVersion = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + }); + return appVersion; +} + +- (nullable NSString *)getOSVersion { + // Since this won't change, read it once in the whole life-cycle of the app and cache its value + static NSString *OSVersion = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSOperatingSystemVersion systemVersion = [NSProcessInfo processInfo].operatingSystemVersion; + OSVersion = [NSString stringWithFormat:@"%ld.%ld.%ld", (long)systemVersion.majorVersion, + (long)systemVersion.minorVersion, + (long)systemVersion.patchVersion]; + }); + return OSVersion; +} + +- (nullable NSString *)getOSMajorVersion { + NSArray *versionItems = [[self getOSVersion] componentsSeparatedByString:@"."]; + + if (versionItems.count > 0) { + return (NSString *)versionItems[0]; + } else { + return nil; + } +} + +- (nullable NSString *)getTimezone { + // No caching to deal with potential changes. + return [NSTimeZone localTimeZone].name; +} + +// extract macro value into a C string +#define STR_FROM_MACRO(x) #x +#define STR(x) STR_FROM_MACRO(x) + +- (NSString *)getIAMSDKVersion { + // FIRInAppMessaging_LIB_VERSION macro comes from pod definition + return [NSString stringWithUTF8String:STR(FIRInAppMessaging_LIB_VERSION)]; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.h b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.h new file mode 100644 index 00000000000..5d379911fe2 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.h @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMDisplayCheckTriggerFlow.h" + +// An implementation of FIRIAMDisplayCheckTriggerFlow by triggering the display check when +// a Firebase Analytics event is fired. +@interface FIRIAMDisplayCheckOnAnalyticEventsFlow : FIRIAMDisplayCheckTriggerFlow +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.m b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.m new file mode 100644 index 00000000000..e83ba0a62cd --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.m @@ -0,0 +1,66 @@ +/* + * Copyright 2017 Google + * + * 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 +#import + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMDisplayCheckOnAnalyticEventsFlow.h" +#import "FIRIAMDisplayExecutor.h" +#import "FIRInAppMessagingPrivate.h" + +@interface FIRIAMDisplayCheckOnAnalyticEventsFlow () +@end + +@implementation FIRIAMDisplayCheckOnAnalyticEventsFlow { + dispatch_queue_t eventListenerQueue; +} + +- (void)start { + @synchronized(self) { + if (eventListenerQueue == nil) { + eventListenerQueue = + dispatch_queue_create("com.google.firebase.inappmessage.firevent_listener", NULL); + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM140002", + @"Start observing Firebase Analytics events for rendering messages."); + + [[FIRInAppMessaging inAppMessaging].analytics registerAnalyticsListener:self + withOrigin:@"fiam"]; + } +} + +- (void)messageTriggered:(NSString *)name parameters:(NSDictionary *)parameters { + // Dispatch to a serial queue eventListenerQueue to avoid the complications that two + // concurrent Firebase Analytics events triggering the + // checkAndDisplayNextContextualMessageForAnalyticsEvent flow concurrently. + dispatch_async(self->eventListenerQueue, ^{ + [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:name]; + }); +} + +- (void)stop { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM140003", + @"Stop observing Firebase Analytics events for display check."); + + @synchronized(self) { + [[FIRInAppMessaging inAppMessaging].analytics unregisterAnalyticsListenerWithOrigin:@"fiam"]; + } +} + +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.h b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.h new file mode 100644 index 00000000000..f69ee97c794 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.h @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMDisplayCheckTriggerFlow.h" + +// an implementation of FIRIAMDisplayExecutor by triggering the display when app is foregrounded +@interface FIRIAMDisplayCheckOnAppForegroundFlow : FIRIAMDisplayCheckTriggerFlow +- (void)start; +- (void)stop; +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m new file mode 100644 index 00000000000..11d880ae15a --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m @@ -0,0 +1,55 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRCore+InAppMessaging.h" +#import "FIRIAMDisplayCheckOnAppForegroundFlow.h" +#import "FIRIAMDisplayExecutor.h" + +@implementation FIRIAMDisplayCheckOnAppForegroundFlow + +- (void)start { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM500002", + @"Start observing app foreground notifications for rendering messages."); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; +} + +- (void)appWillEnterForeground:(UIApplication *)application { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM500001", + @"App foregrounded, wake up to check in-app messaging."); + + // Show the message with 0.5 second delay so that the app's UI is more stable. + // When messages are displayed, the UI operation will be dispatched back to main UI thread. + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 500 * (int64_t)NSEC_PER_MSEC), + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + }); +} + +- (void)stop { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM500004", + @"Stop observing app foreground notifications."); + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h new file mode 100644 index 00000000000..834561dd4d7 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h @@ -0,0 +1,22 @@ +/* + * Copyright 2018 Google + * + * 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 "FIRIAMDisplayCheckTriggerFlow.h" + +@interface FIRIAMDisplayCheckOnFetchDoneNotificationFlow : FIRIAMDisplayCheckTriggerFlow +- (void)start; +- (void)stop; +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.m b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.m new file mode 100644 index 00000000000..227900e1cf1 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.m @@ -0,0 +1,62 @@ +/* + * Copyright 2018 Google + * + * 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 "FIRCore+InAppMessaging.h" + +#import "FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h" +#import "FIRIAMDisplayExecutor.h" + +extern NSString *const kFIRIAMFetchIsDoneNotification; + +@implementation FIRIAMDisplayCheckOnFetchDoneNotificationFlow + +- (void)start { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240001", + @"Start observing fetch done notifications for rendering messages."); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(fetchIsDone) + name:kFIRIAMFetchIsDoneNotification + object:nil]; +} + +- (void)checkAndRenderMessage { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + }); +} + +- (void)fetchIsDone { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240002", + @"Fetch is done. Start message rendering flow."); + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 500 * (int64_t)NSEC_PER_MSEC), + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + [self checkAndRenderMessage]; + }); +} + +- (void)stop { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240003", + @"Stop observing fetch is done notifications."); + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.h b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.h new file mode 100644 index 00000000000..ed05c784b7d --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.h @@ -0,0 +1,37 @@ +/* + * Copyright 2017 Google + * + * 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 FIRIAMDisplayExecutor; +NS_ASSUME_NONNULL_BEGIN + +// Parent class for modeling different flows in which we would trigger the check to see if there +// is appropriate in-app messaging to be rendered. Notice that the flow only triggers the check +// and whether it turns out to have any eligible message to be displayed depending on if certain +// conditions are met +@interface FIRIAMDisplayCheckTriggerFlow : NSObject + +// Accessed by subclasses, not intended by other clients +@property(nonatomic, nonnull, readonly) FIRIAMDisplayExecutor *displayExecutor; +- (instancetype)initWithDisplayFlow:(FIRIAMDisplayExecutor *)displayExecutor; + +// subclasses should implement the follow two methods to start/stop their concrete +// display check flow +- (void)start; +- (void)stop; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.m b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.m new file mode 100644 index 00000000000..e95e786ef2e --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.m @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMDisplayCheckTriggerFlow.h" + +@implementation FIRIAMDisplayCheckTriggerFlow +- (instancetype)initWithDisplayFlow:(FIRIAMDisplayExecutor *)displayExecutor { + if (self = [super init]) { + _displayExecutor = displayExecutor; + } + return self; +} + +// Providing fake implementations to avoid xcode complain about incomplete implementation. +- (void)start { +} +- (void)stop { +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.h b/Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.h new file mode 100644 index 00000000000..602ea57787f --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.h @@ -0,0 +1,63 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMActionURLFollower.h" +#import "FIRIAMActivityLogger.h" +#import "FIRIAMBookKeeper.h" +#import "FIRIAMClearcutLogger.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMTimeFetcher.h" +#import "FIRInAppMessaging.h" +#import "FIRInAppMessagingRendering.h" + +NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMDisplaySetting : NSObject +@property(nonatomic) NSTimeInterval displayMinIntervalInMinutes; +@end + +// The class for checking if there are appropriate messages to be displayed and if so, render it. +// There are other flows that would determine the timing for the checking and then use this class +// instance for the actual check/display. +// +// In addition to fetch eligible message from message cache, this class also ensures certain +// conditions are satisfied for the rendering +// 1 No current in-app message is being displayed +// 2 For non-contextual messages, the display interval in display setting is met. +@interface FIRIAMDisplayExecutor : NSObject + +- (instancetype)initWithInAppMessaging:(FIRInAppMessaging *)inAppMessaging + setting:(FIRIAMDisplaySetting *)setting + messageCache:(FIRIAMMessageClientCache *)cache + timeFetcher:(id)timeFetcher + bookKeeper:(id)displayBookKeeper + actionURLFollower:(FIRIAMActionURLFollower *)actionURLFollower + activityLogger:(FIRIAMActivityLogger *)activityLogger + analyticsEventLogger:(id)analyticsEventLogger; + +// Check and display next in-app message eligible for app open trigger +- (void)checkAndDisplayNextAppForegroundMessage; +// Check and display next in-app message eligible for analytics event trigger with given event name. +- (void)checkAndDisplayNextContextualMessageForAnalyticsEvent:(NSString *)eventName; + +// a boolean flag that can be used to suppress/resume displaying messages. +@property(nonatomic) BOOL suppressMessageDisplay; + +// This is the display component used by display executor for actual message rendering. +@property(nonatomic) id messageDisplayComponent; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.m b/Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.m new file mode 100644 index 00000000000..ed5cc2868e6 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.m @@ -0,0 +1,553 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRCore+InAppMessaging.h" +#import "FIRIAMActivityLogger.h" +#import "FIRIAMDisplayExecutor.h" +#import "FIRIAMMessageContentData.h" +#import "FIRIAMMessageDefinition.h" +#import "FIRIAMSDKRuntimeErrorCodes.h" +#import "FIRInAppMessaging.h" + +@implementation FIRIAMDisplaySetting +@end + +@interface FIRIAMDisplayExecutor () +@property(nonatomic) id timeFetcher; + +// YES if a message is being rendered at this time +@property(nonatomic) BOOL isMsgBeingDisplayed; +@property(nonatomic) NSTimeInterval lastDisplayTime; +@property(nonatomic, nonnull, readonly) FIRInAppMessaging *inAppMessaging; +@property(nonatomic, nonnull, readonly) FIRIAMDisplaySetting *setting; +@property(nonatomic, nonnull, readonly) FIRIAMMessageClientCache *messageCache; +@property(nonatomic, nonnull, readonly) id displayBookKeeper; +@property(nonatomic) BOOL impressionRecorded; +@property(nonatomic, nonnull, readonly) id analyticsEventLogger; +@property(nonatomic, nonnull, readonly) FIRIAMActionURLFollower *actionURLFollower; +@end + +@implementation FIRIAMDisplayExecutor { + FIRIAMMessageDefinition *_currentMsgBeingDisplayed; +} + +#pragma mark - FIRInAppMessagingDisplayDelegate methods +- (void)messageClicked:(FIRInAppMessagingDisplayMessage *)inAppMessage { + // Call through to app-side delegate. + __weak id appSideDelegate = self.inAppMessaging.delegate; + if ([appSideDelegate respondsToSelector:@selector(messageClicked:)]) { + [appSideDelegate messageClicked:inAppMessage]; + } + + self.isMsgBeingDisplayed = NO; + if (!_currentMsgBeingDisplayed.renderData.messageID) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400030", + @"messageClicked called but " + "there is no current message ID."); + return; + } + + if (_currentMsgBeingDisplayed.isTestMessage) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400031", + @"A test message clicked. Do test event impression/click analytics logging"); + + [self.analyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression + forCampaignID:_currentMsgBeingDisplayed.renderData.messageID + withCampaignName:_currentMsgBeingDisplayed.renderData.name + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400036", + @"Logging analytics event for url following %@", + success ? @"succeeded" : @"failed"); + }]; + + [self.analyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageClick + forCampaignID:_currentMsgBeingDisplayed.renderData.messageID + withCampaignName:_currentMsgBeingDisplayed.renderData.name + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400039", + @"Logging analytics event for url following %@", + success ? @"succeeded" : @"failed"); + }]; + } else { + // Logging the impression + [self recordValidImpression:_currentMsgBeingDisplayed.renderData.messageID + withMessageName:_currentMsgBeingDisplayed.renderData.name]; + + [self.analyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow + forCampaignID:_currentMsgBeingDisplayed.renderData.messageID + withCampaignName:_currentMsgBeingDisplayed.renderData.name + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400032", + @"Logging analytics event for url following %@", + success ? @"succeeded" : @"failed"); + }]; + } + + NSURL *actionURL = _currentMsgBeingDisplayed.renderData.contentData.actionURL; + + if (!actionURL) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400033", + @"messageClicked called but " + "there is no action url specified in the message data."); + // it's equivalent to closing the message with no further action + return; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400037", @"Following action url %@", + actionURL.absoluteString); + @try { + [self.actionURLFollower + followActionURL:actionURL + withCompletionBlock:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400034", + @"Seeing %@ from following action URL", success ? @"success" : @"error"); + }]; + } @catch (NSException *e) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400035", + @"Exception encountered in following " + "action url (%@): %@ ", + actionURL, e.description); + @throw; + } + } +} + +- (void)messageDismissed:(FIRInAppMessagingDisplayMessage *)inAppMessage + dismissType:(FIRInAppMessagingDismissType)dismissType { + // Call through to app-side delegate. + __weak id appSideDelegate = self.inAppMessaging.delegate; + if ([appSideDelegate respondsToSelector:@selector(messageDismissed:dismissType:)]) { + [appSideDelegate messageDismissed:inAppMessage dismissType:dismissType]; + } + + self.isMsgBeingDisplayed = NO; + if (!_currentMsgBeingDisplayed.renderData.messageID) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400014", + @"messageDismissedWithType called but " + "there is no current message ID."); + return; + } + + if (_currentMsgBeingDisplayed.isTestMessage) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400020", + @"A test message dismissed. Record the impression event."); + [self.analyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression + forCampaignID:_currentMsgBeingDisplayed.renderData.messageID + withCampaignName:_currentMsgBeingDisplayed.renderData.name + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400038", + @"Logging analytics event for url following %@", + success ? @"succeeded" : @"failed"); + }]; + + return; + } + + // Logging the impression + [self recordValidImpression:_currentMsgBeingDisplayed.renderData.messageID + withMessageName:_currentMsgBeingDisplayed.renderData.name]; + + FIRIAMAnalyticsLogEventType logEventType = dismissType == FIRInAppMessagingDismissTypeAuto + ? FIRIAMAnalyticsEventMessageDismissAuto + : FIRIAMAnalyticsEventMessageDismissClick; + + [self.analyticsEventLogger + logAnalyticsEventForType:logEventType + forCampaignID:_currentMsgBeingDisplayed.renderData.messageID + withCampaignName:_currentMsgBeingDisplayed.renderData.name + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400004", + @"Logging analytics event for message dismiss %@", + success ? @"succeeded" : @"failed"); + }]; +} + +- (void)impressionDetectedForMessage:(FIRInAppMessagingDisplayMessage *)inAppMessage { + __weak id appSideDelegate = self.inAppMessaging.delegate; + if ([appSideDelegate respondsToSelector:@selector(impressionDetectedForMessage:)]) { + [appSideDelegate impressionDetectedForMessage:inAppMessage]; + } + + if (!_currentMsgBeingDisplayed.renderData.messageID) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400022", + @"impressionDetected called but " + "there is no current message ID."); + return; + } + + if (!_currentMsgBeingDisplayed.isTestMessage) { + // Displayed long enough to be a valid impression. + [self recordValidImpression:_currentMsgBeingDisplayed.renderData.messageID + withMessageName:_currentMsgBeingDisplayed.renderData.name]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400011", + @"A test message. Record the test message impression event."); + return; + } +} + +- (void)displayErrorForMessage:(FIRInAppMessagingDisplayMessage *)inAppMessage + error:(NSError *)error { + __weak id appSideDelegate = self.inAppMessaging.delegate; + if ([appSideDelegate respondsToSelector:@selector(displayErrorForMessage:error:)]) { + [appSideDelegate displayErrorForMessage:inAppMessage error:error]; + } + + self.isMsgBeingDisplayed = NO; + + if (!_currentMsgBeingDisplayed.renderData.messageID) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400017", + @"displayErrorEncountered called but " + "there is no current message ID."); + return; + } + + NSString *messageID = _currentMsgBeingDisplayed.renderData.messageID; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400009", + @"Display ran into error for message %@: %@", messageID, error); + + if (_currentMsgBeingDisplayed.isTestMessage) { + [self displayMessageLoadError:error]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400012", + @"A test message. No analytics tracking " + "from image data loading failure"); + return; + } + + // we remove the message from the client side cache so that it won't be retried until next time + // it's fetched again from server. + [self.messageCache removeMessageWithId:messageID]; + NSString *messageName = _currentMsgBeingDisplayed.renderData.name; + + if ([error.domain isEqualToString:NSURLErrorDomain]) { + [self.analyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventImageFetchError + forCampaignID:messageID + withCampaignName:messageName + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400010", + @"Logging analytics event for image fetch error %@", + success ? @"succeeded" : @"failed"); + }]; + } else if (error.code == FIRIAMSDKRuntimeErrorNonImageMimetypeFromImageURL) { + [self.analyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventImageFormatUnsupported + forCampaignID:messageID + withCampaignName:messageName + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400013", + @"Logging analytics event for image format error %@", + success ? @"succeeded" : @"failed"); + }]; + } +} + +- (void)recordValidImpression:(NSString *)messageID withMessageName:(NSString *)messageName { + if (!self.impressionRecorded) { + [self.displayBookKeeper recordNewImpressionForMessage:messageID + withStartTimestampInSeconds:self.lastDisplayTime]; + self.impressionRecorded = YES; + [self.messageCache removeMessageWithId:messageID]; + // Log an impression analytics event as well. + [self.analyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression + forCampaignID:messageID + withCampaignName:messageName + eventTimeInMs:nil + completion:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400007", + @"Logging analytics event for impression %@", + success ? @"succeeded" : @"failed"); + }]; + } +} + +- (void)displayMessageLoadError:(NSError *)error { + NSString *errorMsg = error.userInfo[NSLocalizedDescriptionKey] + ? error.userInfo[NSLocalizedDescriptionKey] + : @"Message loading failed"; + UIAlertController *alert = [UIAlertController + alertControllerWithTitle:@"Firebase InAppMessaging fail to load a test message" + message:errorMsg + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action){ + }]; + + [alert addAction:defaultAction]; + + [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert + animated:YES + completion:nil]; +} + +- (instancetype)initWithInAppMessaging:(FIRInAppMessaging *)inAppMessaging + setting:(FIRIAMDisplaySetting *)setting + messageCache:(FIRIAMMessageClientCache *)cache + timeFetcher:(id)timeFetcher + bookKeeper:(id)displayBookKeeper + actionURLFollower:(FIRIAMActionURLFollower *)actionURLFollower + activityLogger:(FIRIAMActivityLogger *)activityLogger + analyticsEventLogger:(id)analyticsEventLogger { + if (self = [super init]) { + _inAppMessaging = inAppMessaging; + _timeFetcher = timeFetcher; + _lastDisplayTime = displayBookKeeper.lastDisplayTime; + _setting = setting; + _messageCache = cache; + _displayBookKeeper = displayBookKeeper; + _isMsgBeingDisplayed = NO; + _analyticsEventLogger = analyticsEventLogger; + _actionURLFollower = actionURLFollower; + _suppressMessageDisplay = NO; // always allow message display on startup + } + return self; +} + +- (void)checkAndDisplayNextContextualMessageForAnalyticsEvent:(NSString *)eventName { + // synchronizing on self so that we won't potentially enter the render flow from two + // threads: example like showing analytics triggered message and a regular app open + // triggered message + @synchronized(self) { + if (self.suppressMessageDisplay) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400015", + @"Message display is being suppressed. No contextual message rendering."); + return; + } + + if (!self.messageDisplayComponent) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400026", + @"Message display component is not present yet. No display should happen."); + return; + } + + if (self.isMsgBeingDisplayed) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400008", + @"An in-app message display is in progress, do not check analytics event " + "based message for now."); + + return; + } + + // Pop up next analytics event based message to be displayed + FIRIAMMessageDefinition *nextAnalyticsBasedMessage = + [self.messageCache nextOnFirebaseAnalyticEventDisplayMsg:eventName]; + + if (nextAnalyticsBasedMessage) { + [self displayForMessage:nextAnalyticsBasedMessage + triggerType:FIRInAppMessagingDisplayTriggerTypeOnAnalyticsEvent]; + } + } +} + +- (FIRInAppMessagingBannerDisplay *) + bannerMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition + imageData:(FIRInAppMessagingImageData *)imageData + triggerType:(FIRInAppMessagingDisplayTriggerType)triggerType { + NSString *title = definition.renderData.contentData.titleText; + NSString *body = definition.renderData.contentData.bodyText; + + FIRInAppMessagingBannerDisplay *bannerMessage = [[FIRInAppMessagingBannerDisplay alloc] + initWithMessageID:definition.renderData.messageID + campaignName:definition.renderData.name + renderAsTestMessage:definition.isTestMessage + triggerType:triggerType + titleText:title + bodyText:body + textColor:definition.renderData.renderingEffectSettings.textColor + backgroundColor:definition.renderData.renderingEffectSettings.displayBGColor + imageData:imageData + actionURL:definition.renderData.contentData.actionURL]; + + return bannerMessage; +} + +- (FIRInAppMessagingImageOnlyDisplay *) + imageOnlyMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition + imageData:(FIRInAppMessagingImageData *)imageData + triggerType:(FIRInAppMessagingDisplayTriggerType)triggerType { + FIRInAppMessagingImageOnlyDisplay *imageOnlyMessage = [[FIRInAppMessagingImageOnlyDisplay alloc] + initWithMessageID:definition.renderData.messageID + campaignName:definition.renderData.name + renderAsTestMessage:definition.isTestMessage + triggerType:triggerType + imageData:imageData + actionURL:definition.renderData.contentData.actionURL]; + + return imageOnlyMessage; +} + +- (FIRInAppMessagingModalDisplay *) + modalViewMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition + imageData:(FIRInAppMessagingImageData *)imageData + triggerType:(FIRInAppMessagingDisplayTriggerType)triggerType { + // For easier reference in this method. + FIRIAMMessageRenderData *renderData = definition.renderData; + + NSString *title = renderData.contentData.titleText; + NSString *body = renderData.contentData.bodyText; + + FIRInAppMessagingActionButton *actionButton = nil; + + if (definition.renderData.contentData.actionButtonText) { + actionButton = [[FIRInAppMessagingActionButton alloc] + initWithButtonText:renderData.contentData.actionButtonText + buttonTextColor:renderData.renderingEffectSettings.btnTextColor + backgroundColor:renderData.renderingEffectSettings.btnBGColor]; + } + + FIRInAppMessagingModalDisplay *modalViewMessage = [[FIRInAppMessagingModalDisplay alloc] + initWithMessageID:definition.renderData.messageID + campaignName:definition.renderData.name + renderAsTestMessage:definition.isTestMessage + triggerType:triggerType + titleText:title + bodyText:body + textColor:renderData.renderingEffectSettings.textColor + backgroundColor:renderData.renderingEffectSettings.displayBGColor + imageData:imageData + actionButton:actionButton + actionURL:definition.renderData.contentData.actionURL]; + + return modalViewMessage; +} + +- (FIRInAppMessagingDisplayMessage *) + displayMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition + imageData:(FIRInAppMessagingImageData *)imageData + triggerType:(FIRInAppMessagingDisplayTriggerType)triggerType { + switch (definition.renderData.renderingEffectSettings.viewMode) { + case FIRIAMRenderAsBannerView: + return [self bannerMessageWithMessageDefinition:definition + imageData:imageData + triggerType:triggerType]; + case FIRIAMRenderAsModalView: + return [self modalViewMessageWithMessageDefinition:definition + imageData:imageData + triggerType:triggerType]; + case FIRIAMRenderAsImageOnlyView: + return [self imageOnlyMessageWithMessageDefinition:definition + imageData:imageData + triggerType:triggerType]; + default: + return nil; + } +} + +- (void)displayForMessage:(FIRIAMMessageDefinition *)message + triggerType:(FIRInAppMessagingDisplayTriggerType)triggerType { + _currentMsgBeingDisplayed = message; + [message.renderData.contentData + loadImageDataWithBlock:^(NSData *_Nullable imageNSData, NSError *error) { + FIRInAppMessagingImageData *imageData = nil; + + if (error) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400019", + @"Error in loading image data for the message."); + + FIRInAppMessagingDisplayMessage *erroredMessage = + [self displayMessageWithMessageDefinition:message + imageData:imageData + triggerType:triggerType]; + // short-circuit to display error handling + [self displayErrorForMessage:erroredMessage error:error]; + return; + } else if (imageNSData != nil) { + imageData = [[FIRInAppMessagingImageData alloc] + initWithImageURL:message.renderData.contentData.imageURL.absoluteString + imageData:imageNSData]; + } + + self.impressionRecorded = NO; + self.isMsgBeingDisplayed = YES; + + FIRInAppMessagingDisplayMessage *displayMessage = + [self displayMessageWithMessageDefinition:message + imageData:imageData + triggerType:triggerType]; + [self.messageDisplayComponent displayMessage:displayMessage displayDelegate:self]; + }]; +} + +- (BOOL)enoughIntervalFromLastDisplay { + NSTimeInterval intervalFromLastDisplayInSeconds = + [self.timeFetcher currentTimestampInSeconds] - self.lastDisplayTime; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400005", + @"Interval time from last display is %lf seconds", intervalFromLastDisplayInSeconds); + + return intervalFromLastDisplayInSeconds >= self.setting.displayMinIntervalInMinutes * 60.0; +} + +- (void)checkAndDisplayNextAppForegroundMessage { + // synchronizing on self so that we won't potentially enter the render flow from two + // threads: example like showing analytics triggered message and a regular app open + // triggered message concurrently + @synchronized(self) { + if (!self.messageDisplayComponent) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400027", + @"Message display component is not present yet. No display should happen."); + return; + } + + if (self.suppressMessageDisplay) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400016", + @"Message display is being suppressed. No regular message rendering."); + return; + } + + if (self.isMsgBeingDisplayed) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400002", + @"An in-app message display is in progress, do not over-display on top of it."); + return; + } + + if ([self.messageCache hasTestMessage] || [self enoughIntervalFromLastDisplay]) { + // We can display test messages anytime or display regular messages when + // the display time interval has been reached + FIRIAMMessageDefinition *nextForegroundMessage = [self.messageCache nextOnAppOpenDisplayMsg]; + + if (nextForegroundMessage) { + [self displayForMessage:nextForegroundMessage + triggerType:FIRInAppMessagingDisplayTriggerTypeOnAppForeground]; + self.lastDisplayTime = [self.timeFetcher currentTimestampInSeconds]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400001", + @"No appropriate in-app message detected for display."); + } + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400003", + @"Minimal display interval of %lf seconds has not been reached yet.", + self.setting.displayMinIntervalInMinutes * 60.0); + } + } +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.h b/Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.h new file mode 100644 index 00000000000..cd88606bcc7 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.h @@ -0,0 +1,59 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMActivityLogger.h" +#import "FIRIAMBookKeeper.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMSDKModeManager.h" +#import "FIRIAMTimeFetcher.h" + +@protocol FIRIAMAnalyticsEventLogger; + +NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMFetchSetting : NSObject +@property(nonatomic) NSTimeInterval fetchMinIntervalInMinutes; +@end + +typedef void (^FIRIAMFetchMessageCompletionHandler)( + NSArray *_Nullable messages, + NSNumber *_Nullable nextFetchWaitTime, + NSInteger discardedMessageCount, + NSError *_Nullable error); + +@protocol FIRIAMMessageFetcher +- (void)fetchMessagesWithImpressionList:(NSArray *)impressonList + withCompletion:(FIRIAMFetchMessageCompletionHandler)completion; +@end + +// Parent class for supporting different fetching flows. Subclass is supposed to trigger +// checkAndFetch at appropriate moments based on its fetch strategy +@interface FIRIAMFetchFlow : NSObject +- (instancetype)initWithSetting:(FIRIAMFetchSetting *)setting + messageCache:(FIRIAMMessageClientCache *)cache + messageFetcher:(id)messageFetcher + timeFetcher:(id)timeFetcher + bookKeeper:(id)displayBookKeeper + activityLogger:(FIRIAMActivityLogger *)activityLogger + analyticsEventLogger:(id)analyticsEventLogger + FIRIAMSDKModeManager:(FIRIAMSDKModeManager *)sdkModeManager; + +// Triggers a potential fetch of in-app messaging from the source. It would check and respect the +// the fetchMinIntervalInMinutes defined in setting +- (void)checkAndFetch; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.m b/Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.m new file mode 100644 index 00000000000..133b79a85fc --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.m @@ -0,0 +1,253 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRCore+InAppMessaging.h" +#import "FIRIAMClearcutLogger.h" +#import "FIRIAMFetchFlow.h" + +@implementation FIRIAMFetchSetting +@end + +// the notification message to say that the fetch flow is done +NSString *const kFIRIAMFetchIsDoneNotification = @"FIRIAMFetchIsDoneNotification"; + +@interface FIRIAMFetchFlow () +@property(nonatomic) id timeFetcher; +@property(nonatomic) NSTimeInterval lastFetchTime; +@property(nonatomic, nonnull, readonly) FIRIAMFetchSetting *setting; +@property(nonatomic, nonnull, readonly) FIRIAMMessageClientCache *messageCache; +@property(nonatomic) id messageFetcher; +@property(nonatomic, nonnull, readonly) id fetchBookKeeper; +@property(nonatomic, nonnull, readonly) FIRIAMActivityLogger *activityLogger; +@property(nonatomic, nonnull, readonly) id analyticsEventLogger; + +@property(nonatomic, nonnull, readonly) FIRIAMSDKModeManager *sdkModeManager; +@end + +@implementation FIRIAMFetchFlow +- (instancetype)initWithSetting:(FIRIAMFetchSetting *)setting + messageCache:(FIRIAMMessageClientCache *)cache + messageFetcher:(id)messageFetcher + timeFetcher:(id)timeFetcher + bookKeeper:(id)fetchBookKeeper + activityLogger:(FIRIAMActivityLogger *)activityLogger + analyticsEventLogger:(id)analyticsEventLogger + FIRIAMSDKModeManager:(FIRIAMSDKModeManager *)sdkModeManager { + if (self = [super init]) { + _timeFetcher = timeFetcher; + _lastFetchTime = [fetchBookKeeper lastFetchTime]; + _setting = setting; + _messageCache = cache; + _messageFetcher = messageFetcher; + _fetchBookKeeper = fetchBookKeeper; + _activityLogger = activityLogger; + _analyticsEventLogger = analyticsEventLogger; + _sdkModeManager = sdkModeManager; + } + return self; +} + +- (FIRIAMAnalyticsLogEventType)fetchErrorToLogEventType:(NSError *)error { + if ([error.domain isEqual:NSURLErrorDomain]) { + if (error.code == NSURLErrorNotConnectedToInternet) { + return FIRIAMAnalyticsEventFetchAPINetworkError; + } else { + // error.code could be a non 2xx status code + if (error.code > 0) { + if (error.code >= 400 && error.code < 500) { + return FIRIAMAnalyticsEventFetchAPIClientError; + } else { + if (error.code >= 500 && error.code < 600) { + return FIRIAMAnalyticsEventFetchAPIServerError; + } + } + } + } + } + + return FIRIAMAnalyticsLogEventUnknown; +} + +- (void)sendFetchIsDoneNotification { + [[NSNotificationCenter defaultCenter] postNotificationName:kFIRIAMFetchIsDoneNotification + object:self]; +} + +- (void)handleSuccessullyFetchedMessages:(NSArray *)messagesInResponse + withFetchWaitTime:(NSNumber *_Nullable)fetchWaitTime + requestImpressions:(NSArray *)requestImpressions { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700004", @"%lu messages were fetched successfully.", + (unsigned long)messagesInResponse.count); + + for (FIRIAMMessageDefinition *next in messagesInResponse) { + if (next.isTestMessage && self.sdkModeManager.currentMode != FIRIAMSDKModeTesting) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700006", + @"Seeing test message in fetch response. Turn " + "the current instance into a testing instance."); + [self.sdkModeManager becomeTestingInstance]; + } + } + + NSArray *responseMessageIDs = + [messagesInResponse valueForKeyPath:@"renderData.messageID"]; + NSArray *impressionMessageIDs = [requestImpressions valueForKey:@"messageID"]; + + // We are going to clear impression records for those IDs that are in both impressionMessageIDs + // and responseMessageIDs. This is to avoid incorrectly clearing impressions records that come + // in between the sending the request and receiving the response for the fetch operation. + // So we are computing intersection between responseMessageIDs and impressionMessageIDs and use + // that for impression log clearing. + NSMutableSet *idIntersection = [NSMutableSet setWithArray:responseMessageIDs]; + [idIntersection intersectSet:[NSSet setWithArray:impressionMessageIDs]]; + + [self.fetchBookKeeper clearImpressionsWithMessageList:[idIntersection allObjects]]; + [self.messageCache setMessageData:messagesInResponse]; + + [self.sdkModeManager registerOneMoreFetch]; + [self.fetchBookKeeper recordNewFetchWithFetchCount:messagesInResponse.count + withTimestampInSeconds:[self.timeFetcher currentTimestampInSeconds] + nextFetchWaitTime:fetchWaitTime]; +} + +- (void)checkAndFetch { + NSTimeInterval intervalFromLastFetchInSeconds = + [self.timeFetcher currentTimestampInSeconds] - self.fetchBookKeeper.lastFetchTime; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700005", + @"Interval from last time fetch is %lf seconds", intervalFromLastFetchInSeconds); + + BOOL fetchIsAllowedNow = NO; + + if (intervalFromLastFetchInSeconds >= self.fetchBookKeeper.nextFetchWaitTime) { + // it's enough wait time interval from last fetch. + fetchIsAllowedNow = YES; + } else { + FIRIAMSDKMode sdkMode = [self.sdkModeManager currentMode]; + if (sdkMode == FIRIAMSDKModeNewlyInstalled || sdkMode == FIRIAMSDKModeTesting) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700007", + @"OK to fetch due to current SDK mode being %@", + FIRIAMDescriptonStringForSDKMode(sdkMode)); + fetchIsAllowedNow = YES; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700008", + @"Interval from last time fetch is %lf seconds, smaller than fetch wait time %lf", + intervalFromLastFetchInSeconds, self.fetchBookKeeper.nextFetchWaitTime); + } + } + + if (fetchIsAllowedNow) { + // we are allowed to fetch in-app message from time interval wise + + FIRIAMActivityRecord *record = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForFetch + isSuccessful:YES + withDetail:@"OK to do a fetch" + timestamp:nil]; + [self.activityLogger addLogRecord:record]; + + NSArray *impressions = [self.fetchBookKeeper getImpressions]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700001", @"Go ahead to fetch messages"); + + NSTimeInterval fetchStartTime = [[NSDate date] timeIntervalSince1970]; + + [self.messageFetcher + fetchMessagesWithImpressionList:impressions + withCompletion:^(NSArray *_Nullable messages, + NSNumber *_Nullable nextFetchWaitTime, + NSInteger discardedMessageCount, + NSError *_Nullable error) { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM700002", + @"Error happened during message fetching %@", error); + + FIRIAMAnalyticsLogEventType eventType = + [self fetchErrorToLogEventType:error]; + + [self.analyticsEventLogger logAnalyticsEventForType:eventType + forCampaignID:@"all" + withCampaignName:@"all" + eventTimeInMs:nil + completion:^(BOOL success){ + // nothing to do + }]; + + FIRIAMActivityRecord *record = [[FIRIAMActivityRecord alloc] + initWithActivityType:FIRIAMActivityTypeFetchMessage + isSuccessful:NO + withDetail:error.description + timestamp:nil]; + [self.activityLogger addLogRecord:record]; + } else { + double fetchOperationLatencyInMills = + ([[NSDate date] timeIntervalSince1970] - fetchStartTime) * 1000; + NSString *impressionListString = + [impressions componentsJoinedByString:@","]; + NSString *activityLogDetail = @""; + + if (discardedMessageCount > 0) { + activityLogDetail = [NSString + stringWithFormat: + @"%lu messages fetched with impression list as [%@]" + " and %lu messages are discarded due to data being " + "invalid. It took" + " %lf milliseconds", + (unsigned long)messages.count, impressionListString, + (unsigned long)discardedMessageCount, + fetchOperationLatencyInMills]; + } else { + activityLogDetail = [NSString + stringWithFormat: + @"%lu messages fetched with impression list as [%@]. It took" + " %lf milliseconds", + (unsigned long)messages.count, impressionListString, + fetchOperationLatencyInMills]; + } + + FIRIAMActivityRecord *record = [[FIRIAMActivityRecord alloc] + initWithActivityType:FIRIAMActivityTypeFetchMessage + isSuccessful:YES + withDetail:activityLogDetail + timestamp:nil]; + [self.activityLogger addLogRecord:record]; + + // Now handle the fetched messages. + [self handleSuccessullyFetchedMessages:messages + withFetchWaitTime:nextFetchWaitTime + requestImpressions:impressions]; + } + // Send this regardless whether fetch is successful or not. + [self sendFetchIsDoneNotification]; + }]; + + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700003", + @"Only %lf seconds from last fetch time. No action.", + intervalFromLastFetchInSeconds); + // for no fetch case, we still send out the notification so that and display flow can continue + // from here. + [self sendFetchIsDoneNotification]; + FIRIAMActivityRecord *record = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForFetch + isSuccessful:NO + withDetail:@"Abort due to check time interval " + "not reached yet" + timestamp:nil]; + [self.activityLogger addLogRecord:record]; + } +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.h b/Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.h new file mode 100644 index 00000000000..ddbdb684f50 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.h @@ -0,0 +1,22 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMFetchFlow.h" + +// an implementation of FIRIAMDisplayExecutor by triggering the display when app is foregrounded +@interface FIRIAMFetchOnAppForegroundFlow : FIRIAMFetchFlow +- (void)start; +- (void)stop; +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.m b/Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.m new file mode 100644 index 00000000000..3d7831eb442 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.m @@ -0,0 +1,51 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRCore+InAppMessaging.h" +#import "FIRIAMFetchOnAppForegroundFlow.h" +@implementation FIRIAMFetchOnAppForegroundFlow +- (void)start { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM600002", + @"Start observing app foreground notifications for message fetching."); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; +} + +- (void)appWillEnterForeground:(UIApplication *)application { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM600001", + @"App foregrounded, wake up to see if we can fetch in-app messaging."); + // for fetch operation, dispatch it to non main UI thread to avoid blocking. It's ok to dispatch + // to a concurrent global queue instead of serial queue since app open event won't happen at + // fast speed to cause race conditions + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + [self checkAndFetch]; + }); +} + +- (void)stop { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM600003", + @"Stop observing app foreground notifications."); + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.h b/Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.h new file mode 100644 index 00000000000..53c1ed55d2b --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.h @@ -0,0 +1,91 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMBookKeeper.h" +#import "FIRIAMFetchResponseParser.h" +#import "FIRIAMMessageDefinition.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRIAMServerMsgFetchStorage; +@class FIRIAMDisplayCheckOnAnalyticEventsFlow; + +@interface FIRIAMContextualTrigger +@property(nonatomic, copy, readonly) NSString *eventName; +@end + +@interface FIRIAMContextualTriggerListener ++ (void)listenForTriggers:(NSArray *)triggers + withCallback:(void (^)(FIRIAMContextualTrigger *matchedTrigger))callback; +@end + +@protocol FIRIAMCacheDataObserver +- (void)dataChanged; +@end + +// This class serves as an in-memory cache of the messages that would be searched for finding next +// message to be rendered. Its content can be loaded from client persistent storage upon SDK +// initialization and then updated whenever a new fetch is made to server to receive the last +// list. In the case a message has been rendered, it's removed from the cache so that it's not +// considered next time for the message search. +// +// This class is also responsible for setting up and tearing down appropriate analytics event +// listening flow based on whether the current active event list contains any analytics event +// trigger based messages. +// +// This class exists so that we can do message match more efficiently (in-memory search vs search +// in local persistent storage) by using appropriate in-memory data structure. +@interface FIRIAMMessageClientCache : NSObject + +// used to inform the analytics event display check flow about whether it should start/stop +// analytics event listening based on the latest message definitions +// make it weak to avoid retaining cycle +@property(nonatomic, weak, nullable) + FIRIAMDisplayCheckOnAnalyticEventsFlow *analycisEventDislayCheckFlow; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithBookkeeper:(id)bookKeeper + usingResponseParser:(FIRIAMFetchResponseParser *)responseParser; + +// set an observer for watching for data changes in the cache +- (void)setDataObserver:(id)observer; + +// Returns YES if there are any test messages in the cache. +- (BOOL)hasTestMessage; + +// read all the messages as a copy stored in cache +- (NSArray *)allRegularMessages; + +// clients that are to display messages should use nextOnAppOpenDisplayMsg or +// nextOnFirebaseAnalyticEventDisplayMsg to fetch the next eligible message and use +// removeMessageWithId to remove it from cache once the message has been correctly rendered + +// Fetch next eligible messages that are appropriate for display at app open time +- (nullable FIRIAMMessageDefinition *)nextOnAppOpenDisplayMsg; +// Fetch next eligible message that matches the event triggering condition +- (nullable FIRIAMMessageDefinition *)nextOnFirebaseAnalyticEventDisplayMsg:(NSString *)eventName; + +// Call this after a message has been rendered to remove it from the cache. +- (void)removeMessageWithId:(NSString *)messgeId; + +// reset messages data +- (void)setMessageData:(NSArray *)messages; +// load messages from persistent storage +- (void)loadMessageDataFromServerFetchStorage:(FIRIAMServerMsgFetchStorage *)fetchStorage + withCompletion:(void (^)(BOOL success))completion; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.m b/Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.m new file mode 100644 index 00000000000..c6c4933a41f --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.m @@ -0,0 +1,223 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRCore+InAppMessaging.h" +#import "FIRIAMDisplayCheckOnAnalyticEventsFlow.h" +#import "FIRIAMDisplayTriggerDefinition.h" +#import "FIRIAMFetchResponseParser.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMServerMsgFetchStorage.h" + +@interface FIRIAMMessageClientCache () + +// messages not for client-side testing +@property(nonatomic) NSMutableArray *regularMessages; +// messages for client-side testing +@property(nonatomic) NSMutableArray *testMessages; +@property(nonatomic, weak) id observer; +@property(nonatomic) NSMutableSet *firebaseAnalyticEventsToWatch; +@property(nonatomic) id bookKeeper; +@property(readonly, nonatomic) FIRIAMFetchResponseParser *responseParser; + +@end + +// Methods doing read and write operations on messages field is synchronized to avoid +// race conditions like change the array while iterating through it +@implementation FIRIAMMessageClientCache +- (instancetype)initWithBookkeeper:(id)bookKeeper + usingResponseParser:(FIRIAMFetchResponseParser *)responseParser { + if (self = [super init]) { + _bookKeeper = bookKeeper; + _responseParser = responseParser; + } + return self; +} + +- (void)setDataObserver:(id)observer { + self.observer = observer; +} + +// reset messages data +- (void)setMessageData:(NSArray *)messages { + @synchronized(self) { + NSSet *impressionSet = + [NSSet setWithArray:[self.bookKeeper getMessageIDsFromImpressions]]; + + NSMutableArray *regularMessages = [[NSMutableArray alloc] init]; + self.testMessages = [[NSMutableArray alloc] init]; + + // split between test vs non-test messages + for (FIRIAMMessageDefinition *next in messages) { + if (next.isTestMessage) { + [self.testMessages addObject:next]; + } else { + [regularMessages addObject:next]; + } + } + + // while resetting the whole message set, we do prefiltering based on the impressions + // data to get rid of messages we don't care so that the future searches are more efficient + NSPredicate *notImpressedPredicate = + [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { + FIRIAMMessageDefinition *message = (FIRIAMMessageDefinition *)evaluatedObject; + return ![impressionSet containsObject:message.renderData.messageID]; + }]; + + self.regularMessages = + [[regularMessages filteredArrayUsingPredicate:notImpressedPredicate] mutableCopy]; + [self setupAnalyticsEventListening]; + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160001", + @"There are %lu test messages and %lu regular messages and " + "%lu Firebase Analytics events to watch after " + "resetting the message cache", + (unsigned long)self.testMessages.count, (unsigned long)self.regularMessages.count, + (unsigned long)self.firebaseAnalyticEventsToWatch.count); + [self.observer dataChanged]; +} + +// triggered after self.messages are updated so that we can correctly enable/disable listening +// on analytics event based on current fiam message set +- (void)setupAnalyticsEventListening { + self.firebaseAnalyticEventsToWatch = [[NSMutableSet alloc] init]; + for (FIRIAMMessageDefinition *nextMessage in self.regularMessages) { + // if it's event based triggering, add it to the watch set + for (FIRIAMDisplayTriggerDefinition *nextTrigger in nextMessage.renderTriggers) { + if (nextTrigger.triggerType == FIRIAMRenderTriggerOnFirebaseAnalyticsEvent) { + [self.firebaseAnalyticEventsToWatch addObject:nextTrigger.firebaseEventName]; + } + } + } + + if (self.analycisEventDislayCheckFlow) { + if ([self.firebaseAnalyticEventsToWatch count] > 0) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160010", + @"There are analytics event trigger based messages, enable listening"); + [self.analycisEventDislayCheckFlow start]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160011", + @"No analytics event trigger based messages, disable listening"); + [self.analycisEventDislayCheckFlow stop]; + } + } +} + +- (NSArray *)allRegularMessages { + return [self.regularMessages copy]; +} + +- (BOOL)hasTestMessage { + return self.testMessages.count > 0; +} + +- (nullable FIRIAMMessageDefinition *)nextOnAppOpenDisplayMsg { + // search from the start to end in the list (which implies the display priority) for the + // first match (some messages in the cache may not be eligible for the current display + // message fetch + NSSet *impressionSet = + [NSSet setWithArray:[self.bookKeeper getMessageIDsFromImpressions]]; + + @synchronized(self) { + // always first check test message which always have higher prirority + if (self.testMessages.count > 0) { + FIRIAMMessageDefinition *testMessage = self.testMessages[0]; + // always remove test message right away when being fetched for display + [self.testMessages removeObjectAtIndex:0]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160007", + @"Returning a test message for app foreground display"); + return testMessage; + } + + for (FIRIAMMessageDefinition *next in self.regularMessages) { + // message being active and message not impressed yet + if ([next messageHasStarted] && ![next messageHasExpired] && + ![impressionSet containsObject:next.renderData.messageID] && + [next messageRenderedOnAppForegroundEvent]) { + return next; + } + } + } + return nil; +} + +- (nullable FIRIAMMessageDefinition *)nextOnFirebaseAnalyticEventDisplayMsg:(NSString *)eventName { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160005", + @"Inside nextOnFirebaseAnalyticEventDisplay for checking contextual trigger match"); + if (![self.firebaseAnalyticEventsToWatch containsObject:eventName]) { + return nil; + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160006", + @"There could be a potential message match for analytics event %@", eventName); + NSSet *impressionSet = + [NSSet setWithArray:[self.bookKeeper getMessageIDsFromImpressions]]; + @synchronized(self) { + for (FIRIAMMessageDefinition *next in self.regularMessages) { + // message being active and message not impressed yet and the contextual trigger condition + // match + if ([next messageHasStarted] && ![next messageHasExpired] && + ![impressionSet containsObject:next.renderData.messageID] && + [next messageRenderedOnAnalyticsEvent:eventName]) { + return next; + } + } + } + return nil; +} + +- (void)removeMessageWithId:(NSString *)messageID { + FIRIAMMessageDefinition *msgToRemove = nil; + @synchronized(self) { + for (FIRIAMMessageDefinition *next in self.regularMessages) { + if ([next.renderData.messageID isEqualToString:messageID]) { + msgToRemove = next; + break; + } + } + + if (msgToRemove) { + [self.regularMessages removeObject:msgToRemove]; + [self setupAnalyticsEventListening]; + } + } + + // triggers the observer outside synchronization block + if (msgToRemove) { + [self.observer dataChanged]; + } +} + +- (void)loadMessageDataFromServerFetchStorage:(FIRIAMServerMsgFetchStorage *)fetchStorage + withCompletion:(void (^)(BOOL success))completion { + [fetchStorage readResponseDictionary:^(NSDictionary *_Nonnull response, BOOL success) { + if (success) { + NSInteger discardCount; + NSNumber *fetchWaitTime; + NSArray *messagesFromStorage = + [self.responseParser parseAPIResponseDictionary:response + discardedMsgCount:&discardCount + fetchWaitTimeInSeconds:&fetchWaitTime]; + [self setMessageData:messagesFromStorage]; + completion(YES); + } else { + completion(NO); + } + }]; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.h b/Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.h new file mode 100644 index 00000000000..4e2777fd2eb --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.h @@ -0,0 +1,55 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMClientInfoFetcher.h" +#import "FIRIAMFetchFlow.h" +#import "FIRIAMFetchResponseParser.h" +#import "FIRIAMSDKSettings.h" +#import "FIRIAMServerMsgFetchStorage.h" + +NS_ASSUME_NONNULL_BEGIN + +// implementation of FIRIAMMessageFetcher by making Restful API requests to firebase +// in-app messaging services +@interface FIRIAMMsgFetcherUsingRestful : NSObject +/** + * Create an instance which uses NSURLSession to make the restful api call. + * + * @param serverHost API server host. + * @param fbProjectNumber project number used for the API call. It's the GCM_SENDER_ID + * field in GoogleService-Info.plist. + * @param fbAppId It's the GOOGLE_APP_ID field in GoogleService-Info.plist. + * @param apiKey API key. + * @param fetchStorage used to persist the fetched response. + * @param clientInfoFetcher used to fetch iid info for the current app. + * @param URLSession can be nil in which case the class would create NSURLSession + * internally to perform the network request. Having it here so that + * it's easier for doing mocking with unit testing. + */ +- (instancetype)initWithHost:(NSString *)serverHost + HTTPProtocol:(NSString *)HTTPProtocol + project:(NSString *)fbProjectNumber + firebaseApp:(NSString *)fbAppId + APIKey:(NSString *)apiKey + fetchStorage:(FIRIAMServerMsgFetchStorage *)fetchStorage + instanceIDFetcher:(FIRIAMClientInfoFetcher *)clientInfoFetcher + usingURLSession:(nullable NSURLSession *)URLSession + responseParser:(FIRIAMFetchResponseParser *)responseParser; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.m b/Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.m new file mode 100644 index 00000000000..7256906e3e2 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.m @@ -0,0 +1,272 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMFetchFlow.h" +#import "FIRIAMFetchResponseParser.h" +#import "FIRIAMMessageContentDataWithImageURL.h" +#import "FIRIAMMessageDefinition.h" +#import "FIRIAMMsgFetcherUsingRestful.h" +#import "FIRIAMSDKSettings.h" + +static NSInteger const SuccessHTTPStatusCode = 200; + +@interface FIRIAMMsgFetcherUsingRestful () +@property(readonly) NSURLSession *URLSession; +@property(readonly, copy, nonatomic) NSString *serverHostName; +@property(readonly, copy, nonatomic) NSString *appBundleID; +@property(readonly, copy, nonatomic) NSString *httpProtocol; +@property(readonly, copy, nonatomic) NSString *fbProjectNumber; +@property(readonly, copy, nonatomic) NSString *apiKey; +@property(readonly, copy, nonatomic) NSString *firebaseAppId; +@property(readonly, nonatomic) FIRIAMServerMsgFetchStorage *fetchStorage; +@property(readonly, nonatomic) FIRIAMClientInfoFetcher *clientInfoFetcher; +@property(readonly, nonatomic) FIRIAMFetchResponseParser *responseParser; +@end + +@implementation FIRIAMMsgFetcherUsingRestful +- (instancetype)initWithHost:(NSString *)serverHost + HTTPProtocol:(NSString *)HTTPProtocol + project:(NSString *)fbProjectNumber + firebaseApp:(NSString *)fbAppId + APIKey:(NSString *)apiKey + fetchStorage:(FIRIAMServerMsgFetchStorage *)fetchStorage + instanceIDFetcher:(FIRIAMClientInfoFetcher *)clientInfoFetcher + usingURLSession:(nullable NSURLSession *)URLSession + responseParser:(FIRIAMFetchResponseParser *)responseParser { + if (self = [super init]) { + _URLSession = URLSession ? URLSession : [NSURLSession sharedSession]; + _serverHostName = [serverHost copy]; + _fbProjectNumber = [fbProjectNumber copy]; + _firebaseAppId = [fbAppId copy]; + _httpProtocol = [HTTPProtocol copy]; + _apiKey = [apiKey copy]; + _clientInfoFetcher = clientInfoFetcher; + _fetchStorage = fetchStorage; + _appBundleID = [NSBundle mainBundle].bundleIdentifier; + _responseParser = responseParser; + } + return self; +} + +- (void)updatePostFetchData:(NSMutableDictionary *)postData + withImpressionList:(NSArray *)impressionList + instanceIDString:(nonnull NSString *)IIDValue + IIDToken:(nonnull NSString *)IIDToken { + NSMutableArray *impressionListForPost = [[NSMutableArray alloc] init]; + for (FIRIAMImpressionRecord *nextImpressionRecord in impressionList) { + NSDictionary *nextImpression = @{ + @"campaign_id" : nextImpressionRecord.messageID, + @"impression_timestamp_millis" : @(nextImpressionRecord.impressionTimeInSeconds * 1000) + }; + [impressionListForPost addObject:nextImpression]; + } + [postData setObject:impressionListForPost forKey:@"already_seen_campaigns"]; + + if (IIDValue) { + NSDictionary *clientAppInfo = @{ + @"gmp_app_id" : self.firebaseAppId, + @"app_instance_id" : IIDValue, + @"app_instance_id_token" : IIDToken + }; + [postData setObject:clientAppInfo forKey:@"requesting_client_app"]; + } + + NSMutableArray *clientSignals = [@{} mutableCopy]; + + // set client signal fields only when they are present + if ([self.clientInfoFetcher getAppVersion]) { + [clientSignals setValue:[self.clientInfoFetcher getAppVersion] forKey:@"app_version"]; + } + + if ([self.clientInfoFetcher getOSVersion]) { + [clientSignals setValue:[self.clientInfoFetcher getOSVersion] forKey:@"platform_version"]; + } + + if ([self.clientInfoFetcher getDeviceLanguageCode]) { + [clientSignals setValue:[self.clientInfoFetcher getDeviceLanguageCode] forKey:@"language_code"]; + } + + if ([self.clientInfoFetcher getTimezone]) { + [clientSignals setValue:[self.clientInfoFetcher getTimezone] forKey:@"time_zone"]; + } + + [postData setObject:clientSignals forKey:@"client_signals"]; +} + +- (void)fetchMessagesWithImpressionList:(NSArray *)impressonList + withIIDvalue:(NSString *)iidValue + IIDToken:(NSString *)iidToken + completion:(FIRIAMFetchMessageCompletionHandler)completion { + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + [request setHTTPMethod:@"POST"]; + + if (_appBundleID.length) { + // Handle the case in which the API key is being restricted to specific iOS app bundle, + // which can be set on Google Cloud console side for API key credentials. + [request addValue:_appBundleID forHTTPHeaderField:@"X-Ios-Bundle-Identifier"]; + } + + [request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + [request addValue:@"application/json" forHTTPHeaderField:@"Accept"]; + + NSMutableDictionary *postFetchDict = [[NSMutableDictionary alloc] init]; + [self updatePostFetchData:postFetchDict + withImpressionList:impressonList + instanceIDString:iidValue + IIDToken:iidToken]; + + NSData *postFetchData = [NSJSONSerialization dataWithJSONObject:postFetchDict + options:0 + error:nil]; + + NSString *requestURLString = [NSString + stringWithFormat:@"%@://%@/v1/sdkServing/projects/%@/eligibleCampaigns:fetch?key=%@", + self.httpProtocol, self.serverHostName, self.fbProjectNumber, self.apiKey]; + [request setURL:[NSURL URLWithString:requestURLString]]; + [request setHTTPBody:postFetchData]; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM130001", + @"Making a restful API request for pulling messages with fetch POST body as %@ " + "and request headers as %@", + postFetchDict, request.allHTTPHeaderFields); + + NSURLSessionDataTask *postDataTask = [self.URLSession + dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130002", + @"Internal error: encountered error in pulling messages from server" + ":%@", + error); + completion(nil, nil, 0, error); + } else { + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if (httpResponse.statusCode == SuccessHTTPStatusCode) { + // got response data successfully + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM130007", + @"Fetch API response headers are %@", [httpResponse allHeaderFields]); + + NSError *errorJson = nil; + NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data + options:kNilOptions + error:&errorJson]; + if (errorJson) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130003", + @"Failed to parse the response body as JSON string %@", errorJson); + completion(nil, nil, 0, errorJson); + } else { + NSInteger discardCount; + NSNumber *nextFetchWaitTimeFromResponse; + NSArray *messages = [self.responseParser + parseAPIResponseDictionary:responseDict + discardedMsgCount:&discardCount + fetchWaitTimeInSeconds:&nextFetchWaitTimeFromResponse]; + + if (messages) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM130012", + @"API request for fetching messages and parsing the response was " + "successful."); + [self.fetchStorage + saveResponseDictionary:responseDict + withCompletion:^(BOOL success) { + if (!success) + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130010", + @"Failed to persist server fetch response"); + }]; + // always report success regardless of whether we are able to persist into + // storage. they should get fixed in the next fetch cycle if it happens. + completion(messages, nextFetchWaitTimeFromResponse, discardCount, nil); + } else { + NSString *errorDesc = + @"Failed to recognize the fiam messages in the server response"; + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130011", @"%@", errorDesc); + NSError *error = + [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey : errorDesc}]; + completion(nil, nil, 0, error); + } + } + } else { + NSString *responseBody = [[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding]; + + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130004", + @"Failed restful api request to fetch in-app messages: seeing http " + @"status code as %ld with body as %@", + (long)httpResponse.statusCode, responseBody); + + NSError *error = [NSError errorWithDomain:NSURLErrorDomain + code:httpResponse.statusCode + userInfo:nil]; + completion(nil, nil, 0, error); + } + } else { + NSString *errorDesc = @"Got a non http response type from fetch endpoint"; + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130005", @"%@", errorDesc); + + NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey : errorDesc}]; + completion(nil, nil, 0, error); + } + } + }]; + + if (postDataTask == nil) { + NSString *errorDesc = + @"Internal error: NSURLSessionDataTask failed to be created due to possibly " + "incorrect parameters"; + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130006", @"%@", errorDesc); + NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey : errorDesc}]; + completion(nil, nil, 0, error); + } else { + [postDataTask resume]; + } +} + +#pragma mark - protocol FIRIAMMessageFetcher +- (void)fetchMessagesWithImpressionList:(NSArray *)impressonList + withCompletion:(FIRIAMFetchMessageCompletionHandler)completion { + // First step is to fetch the instance id value and token on the fly. We are not caching the data + // since the fetch operation frequency is low enough that we are not concerned about its impact + // on server load and this guarantees that we always have an up-to-date iid values and tokens. + [self.clientInfoFetcher + fetchFirebaseIIDDataWithProjectNumber:self.fbProjectNumber + withCompletion:^(NSString *_Nullable iid, NSString *_Nullable token, + NSError *_Nullable error) { + if (error) { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130008", + @"Not able to get iid value and/or token for " + @"talking to server: %@", + error.localizedDescription); + completion(nil, nil, 0, error); + } else { + [self fetchMessagesWithImpressionList:impressonList + withIIDvalue:iid + IIDToken:token + completion:completion]; + } + }]; +} +@end diff --git a/Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.h b/Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.h new file mode 100644 index 00000000000..61cfac3c710 --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.h @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Google + * + * 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 + +// A class that will persist response data fetched from server side into a local file on +// client side. This file can be used as the cache for messages after the app has been +// killed and before it's up for next server fetch. +@interface FIRIAMServerMsgFetchStorage : NSObject +- (void)saveResponseDictionary:(NSDictionary *)response + withCompletion:(void (^)(BOOL success))completion; +- (void)readResponseDictionary:(void (^)(NSDictionary *response, BOOL success))completion; + +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.m b/Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.m new file mode 100644 index 00000000000..3a08b61a99a --- /dev/null +++ b/Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.m @@ -0,0 +1,64 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRCore+InAppMessaging.h" +#import "FIRIAMServerMsgFetchStorage.h" +@implementation FIRIAMServerMsgFetchStorage +- (NSString *)determineCacheFilePath { + NSString *cachePath = + NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0]; + NSString *filePath = [NSString stringWithFormat:@"%@/firebase-iam-messages-cache", cachePath]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM150004", + @"Persistent file path for fetch response data is %@", filePath); + return filePath; +} + +- (void)saveResponseDictionary:(NSDictionary *)response + withCompletion:(void (^)(BOOL success))completion { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + if ([response writeToFile:[self determineCacheFilePath] atomically:YES]) { + completion(YES); + } else { + completion(NO); + } + }); +} + +- (void)readResponseDictionary:(void (^)(NSDictionary *response, BOOL success))completion { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + NSString *storageFilePath = [self determineCacheFilePath]; + if ([[NSFileManager defaultManager] fileExistsAtPath:storageFilePath]) { + NSDictionary *dictFromFile = + [[NSMutableDictionary dictionaryWithContentsOfFile:[self determineCacheFilePath]] copy]; + if (dictFromFile) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM150001", + @"Loaded response from fetch storage successfully."); + completion(dictFromFile, YES); + } else { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM150002", + @"Not able to read response from fetch storage."); + completion(dictFromFile, NO); + } + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM150003", + @"Local fetch storage file not existent yet: first time launch of the app."); + completion(nil, YES); + } + }); +} +@end diff --git a/Firebase/InAppMessaging/Public/FIRInAppMessaging.h b/Firebase/InAppMessaging/Public/FIRInAppMessaging.h new file mode 100644 index 00000000000..1f5afcbc932 --- /dev/null +++ b/Firebase/InAppMessaging/Public/FIRInAppMessaging.h @@ -0,0 +1,86 @@ +/* + * Copyright 2017 Google + * + * 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 FIRApp; + +#import "FIRInAppMessagingRendering.h" + +NS_ASSUME_NONNULL_BEGIN +/** + * The root object for in-app messaging iOS SDK. + * + * Note: Firebase InApp Messaging depends on using a Firebase Instance ID & token pair to be able + * to retrieve FIAM messages defined for the current app instance. By default, Firebase in-app + * messaging SDK would obtain the ID & token pair on app/SDK startup. As a result of using + * ID & token pair, some device client data (linked to the instance ID) would be collected and sent + * over to Firebase backend periodically. + * + * The app can tune the default data collection behavior via certain controls. They are listed in + * descending order below. If a higher-priority setting exists, lower level settings are ignored. + * + * 1. Dynamically turn on/off data collection behavior by setting the + * `automaticDataCollectionEnabled` property on the `FIRInAppMessaging` instance to true/false + * Swift or YES/NO (objective-c). + * 2. Set `FirebaseInAppMessagingAutomaticDataCollectionEnabled` to false in the app's plist file. + * 3. Global Firebase data collection setting. + **/ +NS_SWIFT_NAME(InAppMessaging) +@interface FIRInAppMessaging : NSObject +/** @fn inAppMessaging + @brief Gets the singleton FIRInAppMessaging object constructed from default Firebase App + settings. +*/ ++ (FIRInAppMessaging *)inAppMessaging NS_SWIFT_NAME(inAppMessaging()); + +/** + * Unavailable. Use +inAppMessaging instead. + */ +- (instancetype)init __attribute__((unavailable("Use +inAppMessaging instead."))); + +/** + * A boolean flag that can be used to suppress messaging display at runtime. It's + * initialized to false at app startup. Once set to true, fiam SDK would stop rendering any + * new messages until it's set back to false. + */ +@property(nonatomic) BOOL messageDisplaySuppressed; + +/** + * A boolean flag that can be set at runtime to allow/disallow fiam SDK automatically + * collect user data on app startup. Settings made via this property is persisted across app + * restarts and has higher priority over FirebaseInAppMessagingAutomaticDataCollectionEnabled + * flag (if present) in plist file. + */ +@property(nonatomic) BOOL automaticDataCollectionEnabled; + +/** + * This is the display component that will be used by FirebaseInAppMessaging to render messages. + * If it's nil (the default case when FirebaseIAppMessaging SDK starts), FirebaseInAppMessaging + * would only perform other non-rendering flows (fetching messages for example). SDK + * FirebaseInAppMessagingDisplay would set itself as the display component if it's included by + * the app. Any other custom implementation of FIRInAppMessagingDisplay would need to set this + * property so that it can be used for rendering fiam message UIs. + */ +@property(nonatomic) id messageDisplayComponent; + +/** + * This delegate should be set on the app side to receive message lifecycle events in app runtime. + */ +@property(nonatomic, weak) id delegate; + +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Public/FIRInAppMessagingRendering.h b/Firebase/InAppMessaging/Public/FIRInAppMessagingRendering.h new file mode 100644 index 00000000000..17de1ac2c0d --- /dev/null +++ b/Firebase/InAppMessaging/Public/FIRInAppMessagingRendering.h @@ -0,0 +1,309 @@ +/* + * Copyright 2018 Google + * + * 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 + +typedef NS_ENUM(NSInteger, FIRInAppMessagingDisplayMessageType) { + FIRInAppMessagingDisplayMessageTypeModal, + FIRInAppMessagingDisplayMessageTypeBanner, + FIRInAppMessagingDisplayMessageTypeImageOnly +}; + +typedef NS_ENUM(NSInteger, FIRInAppMessagingDisplayTriggerType) { + FIRInAppMessagingDisplayTriggerTypeOnAppForeground, + FIRInAppMessagingDisplayTriggerTypeOnAnalyticsEvent +}; + +/** Contains the display information for an action button. + */ +NS_SWIFT_NAME(InAppMessagingActionButton) +@interface FIRInAppMessagingActionButton : NSObject + +/** + * Gets the text string for the button + */ +@property(nonatomic, nonnull, copy, readonly) NSString *buttonText; + +/** + * Gets the button's text color. + */ +@property(nonatomic, copy, nonnull, readonly) UIColor *buttonTextColor; + +/** + * Gets the button's background color + */ +@property(nonatomic, copy, nonnull, readonly) UIColor *buttonBackgroundColor; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithButtonText:(NSString *)btnText + buttonTextColor:(UIColor *)textColor + backgroundColor:(UIColor *)bkgColor NS_DESIGNATED_INITIALIZER; +@end + +/** Contain display data for an image for a fiam message. + */ +NS_SWIFT_NAME(InAppMessagingImageData) +@interface FIRInAppMessagingImageData : NSObject +@property(nonatomic, nonnull, copy, readonly) NSString *imageURL; + +/** + * Gets the downloaded image data. It can be null if headless component fails to load it. + */ +@property(nonatomic, readonly, nullable) NSData *imageRawData; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithImageURL:(NSString *)imageURL + imageData:(NSData *)imageData NS_DESIGNATED_INITIALIZER; +@end + +/** Defines the metadata for the campaign to which a FIAM message belongs. + */ +@interface FIRInAppMessagingCampaignInfo : NSObject + +@property(nonatomic, nonnull, copy, readonly) NSString *messageID; +@property(nonatomic, nonnull, copy, readonly) NSString *campaignName; +@property(nonatomic, readonly) BOOL renderAsTestMessage; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMessageID:(NSString *)messageID + campaignName:(NSString *)campaignName + renderAsTestMessage:(BOOL)renderAsTestMessage; + +@end + +/** + * Base class representing a FIAM message to be displayed. Don't create instance + * of this class directly. Instantiate one of its subclasses instead. + */ +NS_SWIFT_NAME(InAppMessagingDisplayMessage) +@interface FIRInAppMessagingDisplayMessage : NSObject +@property(nonatomic, copy, nonnull, readonly) FIRInAppMessagingCampaignInfo *campaignInfo; +@property(nonatomic, readonly) FIRInAppMessagingDisplayMessageType type; +@property(nonatomic, readonly) FIRInAppMessagingDisplayTriggerType triggerType; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMessageID:(NSString *)messageID + campaignName:(NSString *)campaignName + renderAsTestMessage:(BOOL)renderAsTestMessage + messageType:(FIRInAppMessagingDisplayMessageType)messageType + triggerType:(FIRInAppMessagingDisplayTriggerType)triggerType; +@end + +/** Class for defining a modal message for display. + */ +NS_SWIFT_NAME(InAppMessagingModalDisplay) +@interface FIRInAppMessagingModalDisplay : FIRInAppMessagingDisplayMessage + +/** + * Gets the title for a modal fiam message. + */ +@property(nonatomic, nonnull, copy, readonly) NSString *title; + +/** + * Gets the image data for a modal fiam message. + */ +@property(nonatomic, nullable, copy, readonly) FIRInAppMessagingImageData *imageData; + +/** + * Gets the body text for a modal fiam message. + */ +@property(nonatomic, nullable, copy, readonly) NSString *bodyText; + +/** + * Gets the action button metadata for a modal fiam message. + */ +@property(nonatomic, nullable, readonly) FIRInAppMessagingActionButton *actionButton; + +/** + * Gets the action URL for a modal fiam message. + */ +@property(nonatomic, nullable, readonly) NSURL *actionURL; + +/** + * Gets the background color for a modal fiam message. + */ +@property(nonatomic, copy, nonnull) UIColor *displayBackgroundColor; + +/** + * Gets the color for text in modal fiam message. It would apply to both title and body text. + */ +@property(nonatomic, copy, nonnull) UIColor *textColor; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMessageID:(NSString *)messageID + campaignName:(NSString *)campaignName + renderAsTestMessage:(BOOL)renderAsTestMessage + triggerType:(FIRInAppMessagingDisplayTriggerType)triggerType + titleText:(NSString *)title + bodyText:(NSString *)bodyText + textColor:(UIColor *)textColor + backgroundColor:(UIColor *)backgroundColor + imageData:(nullable FIRInAppMessagingImageData *)imageData + actionButton:(nullable FIRInAppMessagingActionButton *)actionButton + actionURL:(nullable NSURL *)actionURL NS_DESIGNATED_INITIALIZER; +@end + +/** Class for defining a banner message for display. + */ +NS_SWIFT_NAME(InAppMessagingBannerDisplay) +@interface FIRInAppMessagingBannerDisplay : FIRInAppMessagingDisplayMessage +// Title is always required for modal messages. +@property(nonatomic, nonnull, copy, readonly) NSString *title; + +// Image, body, action URL are all optional for banner messages. +@property(nonatomic, nullable, copy, readonly) FIRInAppMessagingImageData *imageData; +@property(nonatomic, nullable, copy, readonly) NSString *bodyText; + +/** + * Gets banner's background color + */ +@property(nonatomic, copy, nonnull, readonly) UIColor *displayBackgroundColor; + +/** + * Gets the color for text in banner fiam message. It would apply to both title and body text. + */ +@property(nonatomic, copy, nonnull) UIColor *textColor; + +/** + * Gets the action URL for a banner fiam message. + */ +@property(nonatomic, nullable, readonly) NSURL *actionURL; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMessageID:(NSString *)messageID + campaignName:(NSString *)campaignName + renderAsTestMessage:(BOOL)renderAsTestMessage + triggerType:(FIRInAppMessagingDisplayTriggerType)triggerType + titleText:(NSString *)title + bodyText:(NSString *)bodyText + textColor:(UIColor *)textColor + backgroundColor:(UIColor *)backgroundColor + imageData:(nullable FIRInAppMessagingImageData *)imageData + actionURL:(nullable NSURL *)actionURL NS_DESIGNATED_INITIALIZER; +@end + +/** Class for defining a image-only message for display. + */ +NS_SWIFT_NAME(InAppMessagingImageOnlyDisplay) +@interface FIRInAppMessagingImageOnlyDisplay : FIRInAppMessagingDisplayMessage + +/** + * Gets the image for this message + */ +@property(nonatomic, nonnull, copy, readonly) FIRInAppMessagingImageData *imageData; + +/** + * Gets the action URL for an image-only fiam message. + */ +@property(nonatomic, nullable, readonly) NSURL *actionURL; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithMessageID:(NSString *)messageID + campaignName:(NSString *)campaignName + renderAsTestMessage:(BOOL)renderAsTestMessage + triggerType:(FIRInAppMessagingDisplayTriggerType)triggerType + imageData:(FIRInAppMessagingImageData *)imageData + actionURL:(nullable NSURL *)actionURL NS_DESIGNATED_INITIALIZER; +@end + +typedef NS_ENUM(NSInteger, FIRInAppMessagingDismissType) { + FIRInAppMessagingDismissTypeUserSwipe, // user swipes away the banner view + FIRInAppMessagingDismissTypeUserTapClose, // user clicks on close buttons + FIRInAppMessagingDismissTypeAuto, // automatic dismiss from banner view + FIRInAppMessagingDismissUnspecified, // message is dismissed, but not belonging to any + // above dismiss category. +}; + +// enum integer value used in as code for NSError reported from displayErrorEncountered: callback +typedef NS_ENUM(NSInteger, FIAMDisplayRenderErrorType) { + FIAMDisplayRenderErrorTypeImageDataInvalid, // provided image data is not valid for image + // rendering + FIAMDisplayRenderErrorTypeUnspecifiedError, // error not classified, mainly unexpected + // failure cases +}; + +/** + * A protocol defining those callbacks to be triggered by the message display component + * under appropriate conditions. + */ +NS_SWIFT_NAME(InAppMessagingDisplayDelegate) +@protocol FIRInAppMessagingDisplayDelegate +/** + * Called when the message is dismissed. Should be called from main thread. + * @param inAppMessage the message that was dismissed. + * @param dismissType specifies how the message is closed. + */ +- (void)messageDismissed:(FIRInAppMessagingDisplayMessage *)inAppMessage + dismissType:(FIRInAppMessagingDismissType)dismissType; + +/** + * Called when the message's action button is followed by the user. + * @param inAppMessage the message that was clicked. + */ +- (void)messageClicked:(FIRInAppMessagingDisplayMessage *)inAppMessage; + +/** + * Use this to mark a message as having gone through enough impression so that + * headless component can make appropriate impression tracking for it. + * + * Calling this is optional. + * + * When messageDismissedWithType: or messageClicked is + * triggered, the message would be marked as having a valid impression implicitly. + * Use impressionDetected if the UI implementation would like to mark valid + * impression in additional cases. One example is that the message is displayed for + * N seconds and then the app is killed by the user. Neither + * onMessageDismissedWithType or onMessageClicked would be triggered + * in this case. But if the app regards this as a valid impression and does not + * want the user to see the same message again, call impressionDetected to mark + * a valid impression. + * @param inAppMessage the message for which an impression was detected. + */ +- (void)impressionDetectedForMessage:(FIRInAppMessagingDisplayMessage *)inAppMessage; + +/** + * Called when the display component could not render the message due to various reason. + * It's essential for display component to call this when error does arise. On seeing + * this, the headless component of fiam would assume that a prior attempt to render a + * message has finished and therefore it's ready to render a new one when conditions are + * met. Missing this callback in failed rendering attempt would make headless + * component think a fiam message is still being rendered and therefore suppress any + * future message rendering. + * @param inAppMessage the message that encountered a display error. + */ +- (void)displayErrorForMessage:(FIRInAppMessagingDisplayMessage *)inAppMessage + error:(NSError *)error; +@end + +/** + * The protocol that a FIAM display component must implement. + */ +NS_SWIFT_NAME(InAppMessagingDisplay) +@protocol FIRInAppMessagingDisplay + +/** + * Method for rendering a specified message on client side. It's called from main thread. + * @param messageForDisplay the message object. It would be of one of the three message + * types at runtime. + * @param displayDelegate the callback object used to trigger notifications about certain + * conditions related to message rendering. + */ +- (void)displayMessage:(FIRInAppMessagingDisplayMessage *)messageForDisplay + displayDelegate:(id)displayDelegate; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Public/FirebaseInAppMessaging.h b/Firebase/InAppMessaging/Public/FirebaseInAppMessaging.h new file mode 100644 index 00000000000..c23e1f86343 --- /dev/null +++ b/Firebase/InAppMessaging/Public/FirebaseInAppMessaging.h @@ -0,0 +1,18 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInAppMessaging.h" +#import "FIRInAppMessagingRendering.h" diff --git a/Firebase/InAppMessaging/RenderingObjects/FIRInAppMessagingRenderingDataClasses.m b/Firebase/InAppMessaging/RenderingObjects/FIRInAppMessagingRenderingDataClasses.m new file mode 100644 index 00000000000..ac2bb53a3a2 --- /dev/null +++ b/Firebase/InAppMessaging/RenderingObjects/FIRInAppMessagingRenderingDataClasses.m @@ -0,0 +1,151 @@ +/* + * Copyright 2018 Google + * + * 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 "FIRInAppMessagingRendering.h" + +@implementation FIRInAppMessagingDisplayMessage + +- (instancetype)initWithMessageID:(NSString *)messageID + campaignName:(NSString *)campaignName + renderAsTestMessage:(BOOL)renderAsTestMessage + messageType:(FIRInAppMessagingDisplayMessageType)messageType + triggerType:(FIRInAppMessagingDisplayTriggerType)triggerType { + if (self = [super init]) { + _campaignInfo = [[FIRInAppMessagingCampaignInfo alloc] initWithMessageID:messageID + campaignName:campaignName + renderAsTestMessage:renderAsTestMessage]; + _type = messageType; + _triggerType = triggerType; + } + return self; +} +@end + +@implementation FIRInAppMessagingBannerDisplay +- (instancetype)initWithMessageID:(NSString *)messageID + campaignName:(NSString *)campaignName + renderAsTestMessage:(BOOL)renderAsTestMessage + triggerType:(FIRInAppMessagingDisplayTriggerType)triggerType + titleText:(NSString *)title + bodyText:(NSString *)bodyText + textColor:(UIColor *)textColor + backgroundColor:(UIColor *)backgroundColor + imageData:(nullable FIRInAppMessagingImageData *)imageData + actionURL:(nullable NSURL *)actionURL { + if (self = [super initWithMessageID:messageID + campaignName:campaignName + renderAsTestMessage:renderAsTestMessage + messageType:FIRInAppMessagingDisplayMessageTypeBanner + triggerType:triggerType]) { + _title = title; + _bodyText = bodyText; + _textColor = textColor; + _displayBackgroundColor = backgroundColor; + _imageData = imageData; + _actionURL = actionURL; + } + return self; +} +@end + +@implementation FIRInAppMessagingModalDisplay + +- (instancetype)initWithMessageID:(NSString *)messageID + campaignName:(NSString *)campaignName + renderAsTestMessage:(BOOL)renderAsTestMessage + triggerType:(FIRInAppMessagingDisplayTriggerType)triggerType + titleText:(NSString *)title + bodyText:(NSString *)bodyText + textColor:(UIColor *)textColor + backgroundColor:(UIColor *)backgroundColor + imageData:(nullable FIRInAppMessagingImageData *)imageData + actionButton:(nullable FIRInAppMessagingActionButton *)actionButton + actionURL:(nullable NSURL *)actionURL { + if (self = [super initWithMessageID:messageID + campaignName:campaignName + renderAsTestMessage:renderAsTestMessage + messageType:FIRInAppMessagingDisplayMessageTypeModal + triggerType:triggerType]) { + _title = title; + _bodyText = bodyText; + _textColor = textColor; + _displayBackgroundColor = backgroundColor; + _imageData = imageData; + _actionButton = actionButton; + _actionURL = actionURL; + } + return self; +} +@end + +@implementation FIRInAppMessagingImageOnlyDisplay + +- (instancetype)initWithMessageID:(NSString *)messageID + campaignName:(NSString *)campaignName + renderAsTestMessage:(BOOL)renderAsTestMessage + triggerType:(FIRInAppMessagingDisplayTriggerType)triggerType + imageData:(FIRInAppMessagingImageData *)imageData + actionURL:(nullable NSURL *)actionURL { + if (self = [super initWithMessageID:messageID + campaignName:campaignName + renderAsTestMessage:renderAsTestMessage + messageType:FIRInAppMessagingDisplayMessageTypeModal + triggerType:triggerType]) { + _imageData = imageData; + _actionURL = actionURL; + } + return self; +} +@end + +@implementation FIRInAppMessagingActionButton + +- (instancetype)initWithButtonText:(NSString *)btnText + buttonTextColor:(UIColor *)textColor + backgroundColor:(UIColor *)bkgColor { + if (self = [super init]) { + _buttonText = btnText; + _buttonTextColor = textColor; + _buttonBackgroundColor = bkgColor; + } + return self; +} +@end + +@implementation FIRInAppMessagingImageData +- (instancetype)initWithImageURL:(NSString *)imageURL imageData:(NSData *)imageData { + if (self = [super init]) { + _imageURL = imageURL; + _imageRawData = imageData; + } + return self; +} +@end + +@implementation FIRInAppMessagingCampaignInfo +- (instancetype)initWithMessageID:(NSString *)messageID + campaignName:(NSString *)campaignName + renderAsTestMessage:(BOOL)renderAsTestMessage { + if (self = [super init]) { + _messageID = messageID; + _campaignName = campaignName; + _renderAsTestMessage = renderAsTestMessage; + } + return self; +} +@end diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.h b/Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.h new file mode 100644 index 00000000000..a05d4b65fbb --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.h @@ -0,0 +1,46 @@ +/* + * Copyright 2018 Google + * + * 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 + +NS_ASSUME_NONNULL_BEGIN +// A class for handling action url following. +// It tries to handle these cases: +// 1 Follow a universal link. +// 2 Follow a custom url scheme link. +// 3 Follow other types of links. +@interface FIRIAMActionURLFollower : NSObject + +// Create an FIRIAMActionURLFollower object by inspecting the app's main bundle info. ++ (instancetype)actionURLFollower; + +- (instancetype)init NS_UNAVAILABLE; + +// initialize the instance with an array of supported custom url schemes and +// the main application object +- (instancetype)initWithCustomURLSchemeArray:(NSArray *)customURLScheme + withApplication:(UIApplication *)application NS_DESIGNATED_INITIALIZER; + +/** + * Follow a given URL. Report success in the completion block parameter. Notice that + * it can not always be fully sure about whether the operation is successful. So it's a clue + * in some cases. + * Check its implementation about the details in the following logic. + */ +- (void)followActionURL:(NSURL *)actionURL withCompletionBlock:(void (^)(BOOL success))completion; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.m b/Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.m new file mode 100644 index 00000000000..58416179dda --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.m @@ -0,0 +1,244 @@ +/* + * Copyright 2018 Google + * + * 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 + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMActionURLFollower.h" + +@interface FIRIAMActionURLFollower () +@property(nonatomic, readonly, nonnull, copy) NSSet *appCustomURLSchemesSet; +@property(nonatomic, readonly) BOOL isOldAppDelegateOpenURLDefined; +@property(nonatomic, readonly) BOOL isNewAppDelegateOpenURLDefined; +@property(nonatomic, readonly) BOOL isContinueUserActivityMethodDefined; + +@property(nonatomic, readonly, nullable) id appDelegate; +@property(nonatomic, readonly, nonnull) UIApplication *mainApplication; +@end + +@implementation FIRIAMActionURLFollower + ++ (FIRIAMActionURLFollower *)actionURLFollower { + static FIRIAMActionURLFollower *URLFollower; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + NSMutableArray *customSchemeURLs = [[NSMutableArray alloc] init]; + + // Reading the custom url list from the environment. + NSBundle *appBundle = [NSBundle mainBundle]; + if (appBundle) { + id URLTypesID = [appBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"]; + if ([URLTypesID isKindOfClass:[NSArray class]]) { + NSArray *urlTypesArray = (NSArray *)URLTypesID; + + for (id nextURLType in urlTypesArray) { + if ([nextURLType isKindOfClass:[NSDictionary class]]) { + NSDictionary *nextURLTypeDict = (NSDictionary *)nextURLType; + id nextSchemeArray = nextURLTypeDict[@"CFBundleURLSchemes"]; + if (nextSchemeArray && [nextSchemeArray isKindOfClass:[NSArray class]]) { + [customSchemeURLs addObjectsFromArray:nextSchemeArray]; + } + } + } + } + } + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM300010", + @"Detected %d custom url schems from environment", (int)customSchemeURLs.count); + + if ([NSThread isMainThread]) { + // We can not dispatch sychronously to main queue if we are already in main queue. That + // can cause deadlock. + URLFollower = [[FIRIAMActionURLFollower alloc] + initWithCustomURLSchemeArray:customSchemeURLs + withApplication:UIApplication.sharedApplication]; + } else { + // If we are not on main thread, dispatch it to main queue since it invovles calling UIKit + // methods, which are required to be carried out on main queue. + dispatch_sync(dispatch_get_main_queue(), ^{ + URLFollower = [[FIRIAMActionURLFollower alloc] + initWithCustomURLSchemeArray:customSchemeURLs + withApplication:UIApplication.sharedApplication]; + }); + } + }); + return URLFollower; +} + +- (instancetype)initWithCustomURLSchemeArray:(NSArray *)customURLScheme + withApplication:(UIApplication *)application { + if (self = [super init]) { + _appCustomURLSchemesSet = [NSSet setWithArray:customURLScheme]; + _mainApplication = application; + _appDelegate = [application delegate]; + + if (_appDelegate) { + _isOldAppDelegateOpenURLDefined = [_appDelegate + respondsToSelector:@selector(application:openURL:sourceApplication:annotation:)]; + + _isNewAppDelegateOpenURLDefined = + [_appDelegate respondsToSelector:@selector(application:openURL:options:)]; + + _isContinueUserActivityMethodDefined = [_appDelegate + respondsToSelector:@selector(application:continueUserActivity:restorationHandler:)]; + } + } + return self; +} + +- (void)followActionURL:(NSURL *)actionURL withCompletionBlock:(void (^)(BOOL success))completion { + // So this is the logic of the url following flow + // 1 If it's a http or https link + // 1.1 If delegate implements application:continueUserActivity:restorationHandler: and calling + // it returns YES: the flow stops here: we have finished the url-following action + // 1.2 In other cases: fall through to step 3 + // 2 If the URL scheme matches any element in appCustomURLSchemes + // 2.1 Triggers application:openURL:options: or + // application:openURL:sourceApplication:annotation: + // depending on their availability. + // 3 Use UIApplication openURL: or openURL:options:completionHandler: to have iOS system to deal + // with the url following. + // + // The rationale for doing step 1 and 2 instead of simply doing step 3 for all cases are: + // I) calling UIApplication openURL with the universal link targeted for current app would + // not cause the link being treated as a universal link. See apple doc at + // https://developer.apple.com/library/content/documentation/General/Conceptual/AppSearch/UniversalLinks.html + // So step 1 is trying to handle this gracefully + // II) If there are other apps on the same device declaring the same custom url scheme as for + // the current app, doing step 3 directly have the risk of triggering another app for + // handling the custom scheme url: See the note about "If more than one third-party" from + // https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/Inter-AppCommunication/Inter-AppCommunication.html + // So step 2 is to optimize user experience by short-circuiting the engagement with iOS + // system + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240007", @"Following action url %@", actionURL); + + if ([self.class isHttpOrHttpsScheme:actionURL]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240001", @"Try to treat it as a universal link."); + if ([self followURLWithContinueUserActivity:actionURL]) { + completion(YES); + return; // following the url has been fully handled by App Delegate's + // continueUserActivity method + } + } else if ([self isCustomSchemeForCurrentApp:actionURL]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240002", @"Custom URL scheme matches."); + if ([self followURLWithAppDelegateOpenURLActivity:actionURL]) { + completion(YES); + return; // following the url has been fully handled by App Delegate's openURL method + } + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240003", @"Open the url via iOS."); + [self followURLViaIOS:actionURL withCompletionBlock:completion]; +} + +// Try to handle the url as a custom scheme url link by triggering +// application:openURL:options: on App's delegate object directly. +// @returns YES if that delegate method is defined and returns YES. +- (BOOL)followURLWithAppDelegateOpenURLActivity:(NSURL *)url { + if (self.isNewAppDelegateOpenURLDefined) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM210008", + @"iOS 9+ version of App Delegate's application:openURL:options: method detected"); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" + return [self.appDelegate application:self.mainApplication openURL:url options:@{}]; +#pragma clang pop + } + + // if we come here, we can try to trigger the older version of openURL method on the app's + // delegate + if (self.isOldAppDelegateOpenURLDefined) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240009", + @"iOS 9 below version of App Delegate's openURL method detected"); + NSString *appBundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; + BOOL handled = [self.appDelegate application:self.mainApplication + openURL:url + sourceApplication:appBundleIdentifier + annotation:@{}]; + return handled; + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240010", + @"No approriate openURL method defined for App Delegate"); + return NO; +} + +// Try to handle the url as a universal link by triggering +// application:continueUserActivity:restorationHandler: on App's delegate object directly. +// @returns YES if that delegate method is defined and seeing a YES being returned from +// trigging it +- (BOOL)followURLWithContinueUserActivity:(NSURL *)url { + if (self.isContinueUserActivityMethodDefined) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240004", + @"App delegate responds to application:continueUserActivity:restorationHandler:." + "Simulating action url opening from a web browser."); + NSUserActivity *userActivity = + [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb]; + userActivity.webpageURL = url; + BOOL handled = [self.appDelegate application:self.mainApplication + continueUserActivity:userActivity + restorationHandler:^(NSArray *restorableObjects) { + // mimic system behavior of triggering restoreUserActivityState: + // method on each element of restorableObjects + for (id nextRestoreObject in restorableObjects) { + if ([nextRestoreObject isKindOfClass:[UIResponder class]]) { + UIResponder *responder = (UIResponder *)nextRestoreObject; + [responder restoreUserActivityState:userActivity]; + } + } + }]; + if (handled) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240005", + @"App handling acton URL returns YES, no more further action taken"); + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240004", @"App handling acton URL returns NO."); + } + return handled; + } else { + return NO; + } +} + +- (void)followURLViaIOS:(NSURL *)url withCompletionBlock:(void (^)(BOOL success))completion { + if ([self.mainApplication respondsToSelector:@selector(openURL:options:completionHandler:)]) { + NSDictionary *options = @{}; + [self.mainApplication + openURL:url + options:options + completionHandler:^(BOOL success) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240006", @"openURL result is %d", success); + completion(success); + }]; + } else { + // fallback to the older version of openURL + BOOL success = [self.mainApplication openURL:url]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240007", @"openURL result is %d", success); + completion(success); + } +} + +- (BOOL)isCustomSchemeForCurrentApp:(NSURL *)url { + NSString *schemeInLowerCase = [url.scheme lowercaseString]; + return [self.appCustomURLSchemesSet containsObject:schemeInLowerCase]; +} + ++ (BOOL)isHttpOrHttpsScheme:(NSURL *)url { + NSString *schemeInLowerCase = [url.scheme lowercaseString]; + return + [schemeInLowerCase isEqualToString:@"https"] || [schemeInLowerCase isEqualToString:@"http"]; +} +@end diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.h b/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.h new file mode 100644 index 00000000000..f106fbcc093 --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.h @@ -0,0 +1,56 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMActivityLogger.h" +#import "FIRIAMBookKeeper.h" +#import "FIRIAMDisplayExecutor.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMSDKSettings.h" +#import "FIRIAMServerMsgFetchStorage.h" + +NS_ASSUME_NONNULL_BEGIN +// A class for managing the objects/dependencies for supporting different fiam flows at runtime +@interface FIRIAMRuntimeManager : NSObject +@property(nonatomic, nonnull) FIRIAMSDKSettings *currentSetting; +@property(nonatomic, nonnull) FIRIAMActivityLogger *activityLogger; +@property(nonatomic, nonnull) FIRIAMBookKeeperViaUserDefaults *bookKeeper; +@property(nonatomic, nonnull) FIRIAMMessageClientCache *messageCache; +@property(nonatomic, nonnull) FIRIAMServerMsgFetchStorage *fetchResultStorage; +@property(nonatomic, nonnull) FIRIAMDisplayExecutor *displayExecutor; + +// Initialize fiam SDKs and start various flows with specified settings. +- (void)startRuntimeWithSDKSettings:(FIRIAMSDKSettings *)settings; + +// Pause runtime flows/functions to disable SDK functions at runtime +- (void)pause; + +// Resume runtime flows/functions. +- (void)resume; + +// allows app to programmatically turn on/off auto data collection for fiam, which also implies +// running/stopping fiam functionalities +@property(nonatomic) BOOL automaticDataCollectionEnabled; + +// Get the global singleton instance ++ (FIRIAMRuntimeManager *)getSDKRuntimeInstance; + +// a method used to suppress or allow message being displayed based on the parameter +// @param shouldSuppress if true, no new message is rendered by the sdk. +- (void)setShouldSuppressMessageDisplay:(BOOL)shouldSuppress; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.m b/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.m new file mode 100644 index 00000000000..550b38e8127 --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.m @@ -0,0 +1,433 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRCore+InAppMessaging.h" +#import "FIRIAMActivityLogger.h" +#import "FIRIAMAnalyticsEventLoggerImpl.h" +#import "FIRIAMBookKeeper.h" +#import "FIRIAMClearcutHttpRequestSender.h" +#import "FIRIAMClearcutLogStorage.h" +#import "FIRIAMClearcutLogger.h" +#import "FIRIAMClearcutUploader.h" +#import "FIRIAMClientInfoFetcher.h" +#import "FIRIAMDisplayCheckOnAnalyticEventsFlow.h" +#import "FIRIAMDisplayCheckOnAppForegroundFlow.h" +#import "FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h" +#import "FIRIAMDisplayExecutor.h" +#import "FIRIAMFetchOnAppForegroundFlow.h" +#import "FIRIAMFetchResponseParser.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMMsgFetcherUsingRestful.h" +#import "FIRIAMRuntimeManager.h" +#import "FIRIAMSDKModeManager.h" +#import "FIRInAppMessaging.h" + +@interface FIRInAppMessaging () +@property(nonatomic, readwrite, strong) id _Nullable analytics; +@end + +// A enum indicating 3 different possiblities of a setting about auto data collection. +typedef NS_ENUM(NSInteger, FIRIAMAutoDataCollectionSetting) { + // This indicates that the config is not explicitly set. + FIRIAMAutoDataCollectionSettingNone = 0, + + // This indicates that the setting explicitly enables the auto data collection. + FIRIAMAutoDataCollectionSettingEnabled = 1, + + // This indicates that the setting explicitly disables the auto data collection. + FIRIAMAutoDataCollectionSettingDisabled = 2, +}; + +@interface FIRIAMRuntimeManager () +@property(nonatomic, nonnull) FIRIAMMsgFetcherUsingRestful *restfulFetcher; +@property(nonatomic, nonnull) FIRIAMDisplayCheckOnAppForegroundFlow *displayOnAppForegroundFlow; +@property(nonatomic, nonnull) FIRIAMDisplayCheckOnFetchDoneNotificationFlow *displayOnFetchDoneFlow; +@property(nonatomic, nonnull) + FIRIAMDisplayCheckOnAnalyticEventsFlow *displayOnFIRAnalyticEventsFlow; + +@property(nonatomic, nonnull) FIRIAMFetchOnAppForegroundFlow *fetchOnAppForegroundFlow; +@property(nonatomic, nonnull) FIRIAMClientInfoFetcher *clientInfoFetcher; +@property(nonatomic, nonnull) FIRIAMFetchResponseParser *responseParser; +@end + +static NSString *const _userDefaultsKeyForFIAMProgammaticAutoDataCollectionSetting = + @"firebase-iam-sdk-auto-data-collection"; + +@implementation FIRIAMRuntimeManager { + // since we allow the SDK feature to be disabled/enabled at runtime, we need a field to track + // its state on this + BOOL _running; +} ++ (FIRIAMRuntimeManager *)getSDKRuntimeInstance { + static FIRIAMRuntimeManager *managerInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + managerInstance = [[FIRIAMRuntimeManager alloc] init]; + }); + + return managerInstance; +} + +// For protocol FIRIAMTestingModeListener. +- (void)testingModeSwitchedOn { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180015", + @"Dynamically switch to the display flow for testing mode instance."); + + [self.displayOnAppForegroundFlow stop]; + [self.displayOnFetchDoneFlow start]; +} + +- (FIRIAMAutoDataCollectionSetting)FIAMProgrammaticAutoDataCollectionSetting { + id settingEntry = [[NSUserDefaults standardUserDefaults] + objectForKey:_userDefaultsKeyForFIAMProgammaticAutoDataCollectionSetting]; + + if (![settingEntry isKindOfClass:[NSNumber class]]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180014", + @"No auto data collection enable setting entry detected." + "So no FIAM programmatic setting from the app."); + return FIRIAMAutoDataCollectionSettingNone; + } else { + if ([(NSNumber *)settingEntry boolValue]) { + return FIRIAMAutoDataCollectionSettingEnabled; + } else { + return FIRIAMAutoDataCollectionSettingDisabled; + } + } +} + +// the key for the plist entry to suppress auto start +static NSString *const kFirebaseInAppMessagingAutoDataCollectionKey = + @"FirebaseInAppMessagingAutomaticDataCollectionEnabled"; + +- (FIRIAMAutoDataCollectionSetting)FIAMPlistAutoDataCollectionSetting { + id fiamAutoDataCollectionPlistEntry = [[NSBundle mainBundle] + objectForInfoDictionaryKey:kFirebaseInAppMessagingAutoDataCollectionKey]; + + if ([fiamAutoDataCollectionPlistEntry isKindOfClass:[NSNumber class]]) { + BOOL fiamDataCollectionEnabledPlistSetting = + [(NSNumber *)fiamAutoDataCollectionPlistEntry boolValue]; + + if (fiamDataCollectionEnabledPlistSetting) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180011", + @"Auto data collection is explicitly enabled in FIAM plist entry."); + return FIRIAMAutoDataCollectionSettingEnabled; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180012", + @"Auto data collection is explicitly disabled in FIAM plist entry."); + return FIRIAMAutoDataCollectionSettingDisabled; + } + } else { + return FIRIAMAutoDataCollectionSettingNone; + } +} + +// Whether data collection is enabled by FIAM programmatic flag. +- (BOOL)automaticDataCollectionEnabled { + return + [self FIAMProgrammaticAutoDataCollectionSetting] != FIRIAMAutoDataCollectionSettingDisabled; +} + +// Sets FIAM's programmatic flag for auto data collection. +- (void)setAutomaticDataCollectionEnabled:(BOOL)automaticDataCollectionEnabled { + if (automaticDataCollectionEnabled) { + [self resume]; + } else { + [self pause]; + } +} + +- (BOOL)shouldRunSDKFlowsOnStartup { + // This can be controlled at 3 different levels in decsending priority. If a higher-priority + // setting exists, the lower level settings are ignored. + // 1. Setting made by the app by setting FIAM SDK's automaticDataCollectionEnabled flag. + // 2. FIAM specific data collection setting in plist file. + // 3. Global Firebase auto data collecting setting (carried over by currentSetting property). + + FIRIAMAutoDataCollectionSetting programmaticSetting = + [self FIAMProgrammaticAutoDataCollectionSetting]; + + if (programmaticSetting == FIRIAMAutoDataCollectionSettingEnabled) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180010", + @"FIAM auto data-collection is explicitly enabled, start SDK flows."); + return true; + } else if (programmaticSetting == FIRIAMAutoDataCollectionSettingDisabled) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180013", + @"FIAM auto data-collection is explicitly disabled, do not start SDK flows."); + return false; + } else { + // No explicit setting from fiam's programmatic setting. Checking next level down. + FIRIAMAutoDataCollectionSetting fiamPlistDataCollectionSetting = + [self FIAMPlistAutoDataCollectionSetting]; + + if (fiamPlistDataCollectionSetting == FIRIAMAutoDataCollectionSettingNone) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180018", + @"No programmatic or plist setting at FIAM level. Fallback to global Firebase " + "level setting."); + return self.currentSetting.isFirebaseAutoDataCollectionEnabled; + } else { + return fiamPlistDataCollectionSetting == FIRIAMAutoDataCollectionSettingEnabled; + } + } +} + +- (void)resume { + // persist the setting + [[NSUserDefaults standardUserDefaults] + setObject:@(YES) + forKey:_userDefaultsKeyForFIAMProgammaticAutoDataCollectionSetting]; + + @synchronized(self) { + if (!_running) { + [self.fetchOnAppForegroundFlow start]; + [self.displayOnAppForegroundFlow start]; + [self.displayOnFIRAnalyticEventsFlow start]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180019", + @"Start Firebase In-App Messaging flows from inactive."); + _running = YES; + } else { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM180004", + @"Runtime is already active, resume is just a no-op"); + } + } +} + +- (void)pause { + // persist the setting + [[NSUserDefaults standardUserDefaults] + setObject:@(NO) + forKey:_userDefaultsKeyForFIAMProgammaticAutoDataCollectionSetting]; + + @synchronized(self) { + if (_running) { + [self.fetchOnAppForegroundFlow stop]; + [self.displayOnAppForegroundFlow stop]; + [self.displayOnFIRAnalyticEventsFlow stop]; + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180006", + @"Shutdown Firebase In-App Messaging flows."); + _running = NO; + } else { + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM180005", + @"No runtime active yet, pause is just a no-op"); + } + } +} + +- (void)setShouldSuppressMessageDisplay:(BOOL)shouldSuppress { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180003", @"Message display suppress set to %@", + @(shouldSuppress)); + self.displayExecutor.suppressMessageDisplay = shouldSuppress; +} + +- (void)startRuntimeWithSDKSettings:(FIRIAMSDKSettings *)settings { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{ + [self internalStartRuntimeWithSDKSettings:settings]; + }); +} + +- (void)internalStartRuntimeWithSDKSettings:(FIRIAMSDKSettings *)settings { + if (_running) { + // Runtime has been started previously. Stop all the flows first. + [self.fetchOnAppForegroundFlow stop]; + [self.displayOnAppForegroundFlow stop]; + [self.displayOnFIRAnalyticEventsFlow stop]; + } + + self.currentSetting = settings; + + FIRIAMTimerWithNSDate *timeFetcher = [[FIRIAMTimerWithNSDate alloc] init]; + NSTimeInterval start = [timeFetcher currentTimestampInSeconds]; + + self.activityLogger = + [[FIRIAMActivityLogger alloc] initWithMaxCountBeforeReduce:settings.loggerMaxCountBeforeReduce + withSizeAfterReduce:settings.loggerSizeAfterReduce + verboseMode:settings.loggerInVerboseMode + loadFromCache:YES]; + + self.responseParser = [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:timeFetcher]; + + self.bookKeeper = [[FIRIAMBookKeeperViaUserDefaults alloc] + initWithUserDefaults:[NSUserDefaults standardUserDefaults]]; + + self.messageCache = [[FIRIAMMessageClientCache alloc] initWithBookkeeper:self.bookKeeper + usingResponseParser:self.responseParser]; + self.fetchResultStorage = [[FIRIAMServerMsgFetchStorage alloc] init]; + self.clientInfoFetcher = [[FIRIAMClientInfoFetcher alloc] init]; + + self.restfulFetcher = + [[FIRIAMMsgFetcherUsingRestful alloc] initWithHost:settings.apiServerHost + HTTPProtocol:settings.apiHttpProtocol + project:settings.firebaseProjectNumber + firebaseApp:settings.firebaseAppId + APIKey:settings.apiKey + fetchStorage:self.fetchResultStorage + instanceIDFetcher:self.clientInfoFetcher + usingURLSession:nil + responseParser:self.responseParser]; + + // start fetch on app foreground flow + FIRIAMFetchSetting *fetchSetting = [[FIRIAMFetchSetting alloc] init]; + fetchSetting.fetchMinIntervalInMinutes = settings.fetchMinIntervalInMinutes; + + // start render on app foreground flow + FIRIAMDisplaySetting *appForegroundDisplaysetting = [[FIRIAMDisplaySetting alloc] init]; + appForegroundDisplaysetting.displayMinIntervalInMinutes = + settings.appFGRenderMinIntervalInMinutes; + + // clearcut log expires after 14 days: give up on attempting to deliver them any more + NSInteger ctLogExpiresInSeconds = 14 * 24 * 60 * 60; + + FIRIAMClearcutLogStorage *ctLogStorage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:ctLogExpiresInSeconds + withTimeFetcher:timeFetcher]; + + FIRIAMClearcutHttpRequestSender *clearcutRequestSender = [[FIRIAMClearcutHttpRequestSender alloc] + initWithClearcutHost:settings.clearcutServerHost + usingTimeFetcher:timeFetcher + withOSMajorVersion:[self.clientInfoFetcher getOSMajorVersion]]; + + FIRIAMClearcutUploader *ctUploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:clearcutRequestSender + timeFetcher:timeFetcher + logStorage:ctLogStorage + usingStrategy:settings.clearcutStrategy + usingUserDefaults:nil]; + + FIRIAMClearcutLogger *clearcutLogger = + [[FIRIAMClearcutLogger alloc] initWithFBProjectNumber:settings.firebaseProjectNumber + fbAppId:settings.firebaseAppId + clientInfoFetcher:self.clientInfoFetcher + usingTimeFetcher:timeFetcher + usingUploader:ctUploader]; + + FIRIAMAnalyticsEventLoggerImpl *analyticsEventLogger = [[FIRIAMAnalyticsEventLoggerImpl alloc] + initWithClearcutLogger:clearcutLogger + usingTimeFetcher:timeFetcher + usingUserDefaults:nil + analytics:[FIRInAppMessaging inAppMessaging].analytics]; + + FIRIAMSDKModeManager *sdkModeManager = + [[FIRIAMSDKModeManager alloc] initWithUserDefaults:NSUserDefaults.standardUserDefaults + testingModeListener:self]; + + self.fetchOnAppForegroundFlow = + [[FIRIAMFetchOnAppForegroundFlow alloc] initWithSetting:fetchSetting + messageCache:self.messageCache + messageFetcher:self.restfulFetcher + timeFetcher:timeFetcher + bookKeeper:self.bookKeeper + activityLogger:self.activityLogger + analyticsEventLogger:analyticsEventLogger + FIRIAMSDKModeManager:sdkModeManager]; + + FIRIAMActionURLFollower *actionFollower = [FIRIAMActionURLFollower actionURLFollower]; + + self.displayExecutor = + [[FIRIAMDisplayExecutor alloc] initWithInAppMessaging:[FIRInAppMessaging inAppMessaging] + setting:appForegroundDisplaysetting + messageCache:self.messageCache + timeFetcher:timeFetcher + bookKeeper:self.bookKeeper + actionURLFollower:actionFollower + activityLogger:self.activityLogger + analyticsEventLogger:analyticsEventLogger]; + + // Setting the display component. It's needed in case headless SDK is initialized after + // the display component is already set on FIRInAppMessaging. + self.displayExecutor.messageDisplayComponent = + FIRInAppMessaging.inAppMessaging.messageDisplayComponent; + + // Both display flows are created on startup. But they would only be turned on (started) based on + // the sdk mode for the current instance + self.displayOnFetchDoneFlow = [[FIRIAMDisplayCheckOnFetchDoneNotificationFlow alloc] + initWithDisplayFlow:self.displayExecutor]; + self.displayOnAppForegroundFlow = + [[FIRIAMDisplayCheckOnAppForegroundFlow alloc] initWithDisplayFlow:self.displayExecutor]; + + self.displayOnFIRAnalyticEventsFlow = + [[FIRIAMDisplayCheckOnAnalyticEventsFlow alloc] initWithDisplayFlow:self.displayExecutor]; + + self.messageCache.analycisEventDislayCheckFlow = self.displayOnFIRAnalyticEventsFlow; + [self.messageCache + loadMessageDataFromServerFetchStorage:self.fetchResultStorage + withCompletion:^(BOOL success) { + // start flows regardless whether we can load messages from fetch + // storage successfully + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180001", + @"Message loading from fetch storage was done."); + + if ([self shouldRunSDKFlowsOnStartup]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180008", + @"Start SDK runtime components."); + + [self.clientInfoFetcher + fetchFirebaseIIDDataWithProjectNumber: + self.currentSetting.firebaseProjectNumber + withCompletion:^( + NSString *_Nullable iid, + NSString *_Nullable token, + NSError *_Nullable error) { + // Always dump the instance id into + // log on startup to help developers + // to find it for their app instance. + FIRLogDebug(kFIRLoggerInAppMessaging, + @"I-IAM180017", + @"Starting " + @"InAppMessaging runtime " + @"with " + "Instance ID %@", + iid); + }]; + + [self.fetchOnAppForegroundFlow start]; + [self.displayOnFIRAnalyticEventsFlow start]; + + self->_running = YES; + + if (sdkModeManager.currentMode == FIRIAMSDKModeTesting) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180007", + @"InAppMessaging testing mode enabled. App " + "foreground messages will be displayed following " + "fetch"); + [self.displayOnFetchDoneFlow start]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180020", + @"Start regular display flow for non-testing " + "instance mode"); + [self.displayOnAppForegroundFlow start]; + + // Simulate app going into foreground on startup + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + } + + // One-time triggering of checks for both fetch flow + // upon SDK/app startup. + [self.fetchOnAppForegroundFlow checkAndFetch]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180009", + @"No FIAM SDK startup due to settings."); + } + }]; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180002", + @"Firebase In-App Messaging SDK version %@ finished startup in %lf seconds " + "with these settings: %@", + [self.clientInfoFetcher getIAMSDKVersion], + (double)([timeFetcher currentTimestampInSeconds] - start), settings); +} +@end diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.h b/Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.h new file mode 100644 index 00000000000..65534186fec --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.h @@ -0,0 +1,73 @@ +/* + * Copyright 2018 Google + * + * 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 + +extern NSInteger const kFIRIAMMaxFetchInNewlyInstalledMode; + +/** + * At runtime a FIAM SDK client can function in one of the following modes: + * 1 Regular. This SDK client instance will conform to regular fetch minimal interval time policy. + * 2 Newly installed. This is a mode a newly installed SDK stays in until the first + * kFIRIAMMaxFetchInNewlyInstalledMode fetches have finished. In this mode, there is no + * minimal time interval between fetches: a fetch would be triggered as long as the app goes + * into foreground state. + * 3 Testing Instance. This app instance is targeted for test on device feature for fiam. When + * it's in this mode, no minimal time interval between fetches is applied. SDK turns itself + * into this mode on seeing test-on-client messages are returned in fetch responses. + */ + +typedef NS_ENUM(NSInteger, FIRIAMSDKMode) { + FIRIAMSDKModeRegular, + FIRIAMSDKModeTesting, + FIRIAMSDKModeNewlyInstalled +}; + +// turn the sdk mode enum integer value into a descriptive string +NSString *FIRIAMDescriptonStringForSDKMode(FIRIAMSDKMode mode); + +extern NSString *const kFIRIAMUserDefaultKeyForSDKMode; +extern NSString *const kFIRIAMUserDefaultKeyForServerFetchCount; +extern NSInteger const kFIRIAMMaxFetchInNewlyInstalledMode; + +@protocol FIRIAMTestingModeListener +// Triggered when the current app switches into testing mode from a using testing mode +- (void)testingModeSwitchedOn; +@end + +// A class for tracking and updating the SDK mode. The tracked mode related info is persisted +// so that it can be restored beyond app restarts +@interface FIRIAMSDKModeManager : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +// having NSUserDefaults as passed-in to help with unit testing +- (instancetype)initWithUserDefaults:(NSUserDefaults *)userDefaults + testingModeListener:(id)testingModeListener; + +// returns the current SDK mode +- (FIRIAMSDKMode)currentMode; + +// turn the current SDK into 'Testing Instance' mode. +- (void)becomeTestingInstance; +// inform the manager that one more fetch is done. This is to allow +// the manager to potentially graduate from the newly installed mode. +- (void)registerOneMoreFetch; + +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.m b/Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.m new file mode 100644 index 00000000000..b59c8f784e9 --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.m @@ -0,0 +1,113 @@ +/* + * Copyright 2018 Google + * + * 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 "FIRCore+InAppMessaging.h" +#import "FIRIAMSDKModeManager.h" + +NSString *FIRIAMDescriptonStringForSDKMode(FIRIAMSDKMode mode) { + switch (mode) { + case FIRIAMSDKModeTesting: + return @"Testing Instance"; + case FIRIAMSDKModeRegular: + return @"Regular"; + case FIRIAMSDKModeNewlyInstalled: + return @"Newly Installed"; + default: + FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM290003", @"Unknown sdk mode value %d", + (int)mode); + return @"Unknown"; + } +} + +@interface FIRIAMSDKModeManager () +@property(nonatomic, nonnull, readonly) NSUserDefaults *userDefaults; +// Make it weak so that we don't depend on its existence to avoid circular reference. +@property(nonatomic, readonly, weak) id testingModeListener; +@end + +NSString *const kFIRIAMUserDefaultKeyForSDKMode = @"firebase-iam-sdk-mode"; +NSString *const kFIRIAMUserDefaultKeyForServerFetchCount = @"firebase-iam-server-fetch-count"; +NSInteger const kFIRIAMMaxFetchInNewlyInstalledMode = 5; + +@implementation FIRIAMSDKModeManager { + FIRIAMSDKMode _sdkMode; + NSInteger _fetchCount; +} + +- (instancetype)initWithUserDefaults:(NSUserDefaults *)userDefaults + testingModeListener:(id)testingModeListener { + if (self = [super init]) { + _userDefaults = userDefaults; + _testingModeListener = testingModeListener; + + id modeEntry = [_userDefaults objectForKey:kFIRIAMUserDefaultKeyForSDKMode]; + if (modeEntry == nil) { + // no entry yet, it's a newly installed sdk instance + _sdkMode = FIRIAMSDKModeNewlyInstalled; + + // initialize the mode and fetch count in the persistent storage + [_userDefaults setObject:[NSNumber numberWithInt:_sdkMode] + forKey:kFIRIAMUserDefaultKeyForSDKMode]; + [_userDefaults setInteger:0 forKey:kFIRIAMUserDefaultKeyForServerFetchCount]; + } else { + _sdkMode = [(NSNumber *)modeEntry integerValue]; + _fetchCount = [_userDefaults integerForKey:kFIRIAMUserDefaultKeyForServerFetchCount]; + } + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM290001", + @"SDK is in mode of %@ and has seen %d fetches.", + FIRIAMDescriptonStringForSDKMode(_sdkMode), (int)_fetchCount); + } + return self; +} + +// inform the manager that one more fetch is done. This is to allow +// the manager to potentially graduate from the newly installed mode. +- (void)registerOneMoreFetch { + // we only care about the fetch count when sdk is in newly installed mode (so that it may + // graduate from that after certain number of fetches). + if (_sdkMode == FIRIAMSDKModeNewlyInstalled) { + if (++_fetchCount >= kFIRIAMMaxFetchInNewlyInstalledMode) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM290002", + @"Coming out of newly installed mode since there have been %d fetches", + (int)_fetchCount); + + _sdkMode = FIRIAMSDKModeRegular; + [_userDefaults setObject:[NSNumber numberWithInt:_sdkMode] + forKey:kFIRIAMUserDefaultKeyForSDKMode]; + } else { + [_userDefaults setInteger:_fetchCount forKey:kFIRIAMUserDefaultKeyForServerFetchCount]; + } + } +} + +- (void)becomeTestingInstance { + _sdkMode = FIRIAMSDKModeTesting; + [_userDefaults setObject:[NSNumber numberWithInt:_sdkMode] + forKey:kFIRIAMUserDefaultKeyForSDKMode]; + + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM290004", + @"Test mode enabled, notifying test mode listener."); + [self.testingModeListener testingModeSwitchedOn]; +} + +// returns the current SDK mode +- (FIRIAMSDKMode)currentMode { + return _sdkMode; +} +@end diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMSDKRuntimeErrorCodes.h b/Firebase/InAppMessaging/Runtime/FIRIAMSDKRuntimeErrorCodes.h new file mode 100644 index 00000000000..d7f3e2c8df8 --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMSDKRuntimeErrorCodes.h @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Google + * + * 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 + +typedef NS_ENUM(NSInteger, FIRIAMSDKRuntimeError) { + // fail to crawl the image url + FIRIAMSDKRuntimeErrorImageNotFetchable = 0, + + // crawling image url sees non-image type data being returned + FIRIAMSDKRuntimeErrorNonImageMimetypeFromImageURL = 1 +}; diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.h b/Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.h new file mode 100644 index 00000000000..63aecea9f8a --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.h @@ -0,0 +1,53 @@ +/* + * Copyright 2017 Google + * + * 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 FIRIAMClearcutStrategy; + +NS_ASSUME_NONNULL_BEGIN +@interface FIRIAMSDKSettings : NSObject +// settings related to communicating with in-app messaging server +@property(nonatomic, copy) NSString *firebaseProjectNumber; +@property(nonatomic, copy) NSString *firebaseAppId; +@property(nonatomic, copy) NSString *apiKey; +@property(nonatomic, copy) NSString *apiServerHost; +@property(nonatomic, copy) NSString *apiHttpProtocol; // http or https. It should be always + // https on production. Allow http to + // faciliate testing in non-prod environment +@property(nonatomic) NSTimeInterval fetchMinIntervalInMinutes; + +// settings related to activity logger +@property(nonatomic) NSInteger loggerMaxCountBeforeReduce; +@property(nonatomic) NSInteger loggerSizeAfterReduce; +@property(nonatomic) BOOL loggerInVerboseMode; + +// settings for controlling rendering frequency for messages rendered from app foreground triggers +@property(nonatomic) NSTimeInterval appFGRenderMinIntervalInMinutes; + +// host name for clearcut servers +@property(nonatomic, copy) NSString *clearcutServerHost; +// clearcut strategy +@property(nonatomic, strong) FIRIAMClearcutStrategy *clearcutStrategy; + +// The global flag at whole Firebase level for automatic data collection. On FIAM SDK startup, +// it would be retreived from FIRApp's corresponding setting. +@property(nonatomic, getter=isFirebaseAutoDataCollectionEnabled) + BOOL firebaseAutoDataCollectionEnabled; + +- (NSString *)description; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.m b/Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.m new file mode 100644 index 00000000000..aa24f4f93c8 --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.m @@ -0,0 +1,35 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMSDKSettings.h" + +@implementation FIRIAMSDKSettings + +- (NSString *)description { + return + [NSString stringWithFormat:@"APIServer:%@;ProjectNumber:%@; API_Key:%@;Clearcut Server:%@; " + "Fetch Minimal Interval:%lu seconds; Activity Logger Max:%lu; " + "Foreground Display Trigger Minimal Interval:%lu seconds;\n" + "Clearcut strategy:%@;Global Firebase auto data collection %@\n", + self.apiServerHost, self.firebaseProjectNumber, self.apiKey, + self.clearcutServerHost, + (unsigned long)(self.fetchMinIntervalInMinutes * 60), + (unsigned long)self.loggerMaxCountBeforeReduce, + (unsigned long)(self.appFGRenderMinIntervalInMinutes * 60), + self.clearcutStrategy, + self.firebaseAutoDataCollectionEnabled ? @"enabled" : @"disabled"]; +} +@end diff --git a/Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.h b/Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.h new file mode 100644 index 00000000000..e2c51390ec1 --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.h @@ -0,0 +1,33 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMSDKSettings.h" +#import "FIRInAppMessaging.h" + +/** + * This category extends FIRInAppMessaging with the configurations from FIRApp + */ +@interface FIRInAppMessaging (Bootstrap) + ++ (NSString *)getFiamServerHost; ++ (void)setFiamServerHostWithName:(NSString *)serverHost; + ++ (NSString *)getServer; + ++ (void)bootstrapIAMWithSettings:(FIRIAMSDKSettings *)settings; + ++ (void)bootstrapIAMFromFIRApp:(FIRApp *)app; +@end diff --git a/Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.m b/Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.m new file mode 100644 index 00000000000..1f3134adb44 --- /dev/null +++ b/Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.m @@ -0,0 +1,137 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRInAppMessaging+Bootstrap.h" + +#import +#import +#import +#import +#import + +#import "FIRCore+InAppMessaging.h" +#import "FIRIAMClearcutUploader.h" +#import "FIRIAMRuntimeManager.h" +#import "FIRIAMSDKSettings.h" +#import "FIROptionsInternal.h" +#import "NSString+FIRInterlaceStrings.h" + +@implementation FIRInAppMessaging (Bootstrap) + +static FIRIAMSDKSettings *_sdkSetting = nil; + +static NSString *_fiamServerHostName = @"firebaseinappmessaging.googleapis.com"; + ++ (NSString *)getFiamServerHost { + return _fiamServerHostName; +} + ++ (void)setFiamServerHostWithName:(NSString *)serverHost { + _fiamServerHostName = serverHost; +} + ++ (NSString *)getServer { + // Override to change to test server. + NSString *serverHostNameFirstComponent = @"pa.ogepscm"; + NSString *serverHostNameSecondComponent = @"lygolai.o"; + return [NSString fir_interlaceString:serverHostNameFirstComponent + withString:serverHostNameSecondComponent]; +} + ++ (void)bootstrapIAMFromFIRApp:(FIRApp *)app { + FIROptions *options = app.options; + NSError *error; + + if (!options.GCMSenderID.length) { + error = + [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain + code:0 + userInfo:@{ + NSLocalizedDescriptionKey : @"Google Sender ID must not be nil or empty." + }]; + + [self exitAppWithFatalError:error]; + } + + if (!options.APIKey.length) { + error = [NSError + errorWithDomain:kFirebaseInAppMessagingErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey : @"API key must not be nil or empty."}]; + + [self exitAppWithFatalError:error]; + } + + if (!options.googleAppID.length) { + error = + [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain + code:0 + userInfo:@{NSLocalizedDescriptionKey : @"Google App ID must not be nil."}]; + [self exitAppWithFatalError:error]; + } + + // following are the default sdk settings to be used by hosting app + _sdkSetting = [[FIRIAMSDKSettings alloc] init]; + _sdkSetting.apiServerHost = [FIRInAppMessaging getFiamServerHost]; + _sdkSetting.clearcutServerHost = [FIRInAppMessaging getServer]; + _sdkSetting.apiHttpProtocol = @"https"; + _sdkSetting.firebaseAppId = options.googleAppID; + _sdkSetting.firebaseProjectNumber = options.GCMSenderID; + _sdkSetting.apiKey = options.APIKey; + _sdkSetting.fetchMinIntervalInMinutes = 24 * 60; // fetch at most once every 24 hours + _sdkSetting.loggerMaxCountBeforeReduce = 100; + _sdkSetting.loggerSizeAfterReduce = 50; + _sdkSetting.appFGRenderMinIntervalInMinutes = 24 * 60; // render at most one message from + // app-foreground trigger every 24 hours + _sdkSetting.loggerInVerboseMode = NO; + + // TODO: once Firebase Core supports sending notifications at global Firebase level setting + // change, FIAM SDK would listen to it and respond to it. Until then, FIAM SDK only checks + // the setting once upon App/SDK startup. + _sdkSetting.firebaseAutoDataCollectionEnabled = app.isDataCollectionDefaultEnabled; + + if ([GULAppEnvironmentUtil isSimulator]) { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170004", + @"Running in simulator. Do realtime clearcut uploading."); + _sdkSetting.clearcutStrategy = + [[FIRIAMClearcutStrategy alloc] initWithMinWaitTimeInMills:0 + maxWaitTimeInMills:0 + failureBackoffTimeInMills:60 * 60 * 1000 // 60 mins + batchSendSize:50]; + } else { + FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170005", + @"Not running in simulator. Use regular clearcut uploading strategy."); + _sdkSetting.clearcutStrategy = + [[FIRIAMClearcutStrategy alloc] initWithMinWaitTimeInMills:5 * 60 * 1000 // 5 mins + maxWaitTimeInMills:12 * 60 * 60 * 1000 // 12 hours + failureBackoffTimeInMills:60 * 60 * 1000 // 60 mins + batchSendSize:50]; + } + + [[FIRIAMRuntimeManager getSDKRuntimeInstance] startRuntimeWithSDKSettings:_sdkSetting]; +} + ++ (void)bootstrapIAMWithSettings:(FIRIAMSDKSettings *)settings { + _sdkSetting = settings; + [[FIRIAMRuntimeManager getSDKRuntimeInstance] startRuntimeWithSDKSettings:_sdkSetting]; +} + ++ (void)exitAppWithFatalError:(NSError *)error { + [NSException raise:kFirebaseInAppMessagingErrorDomain + format:@"Error happened %@", error.localizedDescription]; +} + +@end diff --git a/Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.h b/Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.h new file mode 100644 index 00000000000..02a48bf0b95 --- /dev/null +++ b/Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.h @@ -0,0 +1,29 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMTimeFetcher.h" + +NS_ASSUME_NONNULL_BEGIN +// A class via which we can track elapsed time with the capability to pause and resume +// the tracking +@interface FIRIAMElapsedTimeTracker : NSObject +- (NSTimeInterval)trackedTimeSoFar; +- (void)pause; +- (void)resume; +- (instancetype)initWithTimeFetcher:(id)timeFetcher; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.m b/Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.m new file mode 100644 index 00000000000..79ec0dfdbbd --- /dev/null +++ b/Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.m @@ -0,0 +1,56 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMElapsedTimeTracker.h" +@interface FIRIAMElapsedTimeTracker () +@property(nonatomic) NSTimeInterval totalTrackedTimeSoFar; +@property(nonatomic) NSTimeInterval lastTrackingStartPoint; +@property(nonatomic, nonnull) id timeFetcher; +@property(nonatomic) BOOL tracking; +@end + +@implementation FIRIAMElapsedTimeTracker + +- (NSTimeInterval)trackedTimeSoFar { + if (_tracking) { + return self.totalTrackedTimeSoFar + [self.timeFetcher currentTimestampInSeconds] - + self.lastTrackingStartPoint; + } else { + return self.totalTrackedTimeSoFar; + } +} + +- (void)pause { + self.tracking = NO; + self.totalTrackedTimeSoFar += + [self.timeFetcher currentTimestampInSeconds] - self.lastTrackingStartPoint; +} + +- (void)resume { + self.tracking = YES; + self.lastTrackingStartPoint = [self.timeFetcher currentTimestampInSeconds]; +} + +- (instancetype)initWithTimeFetcher:(id)timeFetcher { + if (self = [super init]) { + _tracking = YES; + _timeFetcher = timeFetcher; + _totalTrackedTimeSoFar = 0; + _lastTrackingStartPoint = [timeFetcher currentTimestampInSeconds]; + } + return self; +} +@end diff --git a/Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.h b/Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.h new file mode 100644 index 00000000000..eacab44762e --- /dev/null +++ b/Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.h @@ -0,0 +1,28 @@ +/* + * Copyright 2017 Google + * + * 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 +// A protocol wrapping around function of getting timestamp. Created to help +// unit testing in which we need to control the elapsed time. +@protocol FIRIAMTimeFetcher +- (NSTimeInterval)currentTimestampInSeconds; +@end + +@interface FIRIAMTimerWithNSDate : NSObject +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.m b/Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.m new file mode 100644 index 00000000000..d32f5b24c2e --- /dev/null +++ b/Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.m @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMTimeFetcher.h" + +@implementation FIRIAMTimerWithNSDate +- (NSTimeInterval)currentTimestampInSeconds { + return [[NSDate date] timeIntervalSince1970]; +} +@end diff --git a/Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.h b/Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.h new file mode 100644 index 00000000000..70025110a08 --- /dev/null +++ b/Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.h @@ -0,0 +1,30 @@ +/* + * Copyright 2019 Google + * + * 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 + +// Extension on NSString that combines two strings. +@interface NSString (FIRInterlaceStrings) + +// Returns a combined string created from iterating over both strings alternately, +// beginning with stringOne's first character. ++ (NSString *)fir_interlaceString:(NSString *)stringOne withString:(NSString *)stringTwo; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.m b/Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.m new file mode 100644 index 00000000000..ddd1aa1e93c --- /dev/null +++ b/Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.m @@ -0,0 +1,42 @@ +/* + * Copyright 2019 Google + * + * 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 "NSString+FIRInterlaceStrings.h" + +@implementation NSString (InterlaceStrings) + ++ (NSString *)fir_interlaceString:(NSString *)stringOne withString:(NSString *)stringTwo { + NSMutableString *interlacedString = [NSMutableString string]; + + NSUInteger count = MAX(stringOne.length, stringTwo.length); + + for (NSUInteger i = 0; i < count; i++) { + if (i < stringOne.length) { + NSString *firstComponentChar = + [NSString stringWithFormat:@"%c", [stringOne characterAtIndex:i]]; + [interlacedString appendString:firstComponentChar]; + } + if (i < stringTwo.length) { + NSString *secondComponentChar = + [NSString stringWithFormat:@"%c", [stringTwo characterAtIndex:i]]; + [interlacedString appendString:secondComponentChar]; + } + } + + return interlacedString; +} + +@end diff --git a/Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.h b/Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.h new file mode 100644 index 00000000000..e7d1feebe54 --- /dev/null +++ b/Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.h @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Google + * + * 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 + +// Extension on UIColor to support conversion from a color hex string in the format +// of #XXXXXX +@interface UIColor (HexString) + +// Constructing UIColor object from a string with '#XXXXXX' format where 'XXXXXX' is +// the 6-digit hex value string of the rgb color. +// +// @param hexString hex string for the color. +// @return a UIColor parsed out of the hex string. Nil returned if the hexString is nil or does +// not conform the desired format. ++ (nullable UIColor *)firiam_colorWithHexString:(nullable NSString *)hexString; +@end diff --git a/Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.m b/Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.m new file mode 100644 index 00000000000..7768d646c93 --- /dev/null +++ b/Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.m @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Google + * + * 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 "UIColor+FIRIAMHexString.h" + +@implementation UIColor (HexString) ++ (UIColor *)firiam_colorWithHexString:(nullable NSString *)hexString { + if (hexString.length < 7) { + return nil; + } + + unsigned rgbValue = 0; + NSScanner *scanner = [NSScanner scannerWithString:hexString]; + [scanner setScanLocation:1]; // bypass '#' character + + if (![scanner scanHexInt:&rgbValue]) { + // no valid heximal value is detected + return nil; + } + + return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0 + green:((rgbValue & 0xFF00) >> 8) / 255.0 + blue:(rgbValue & 0xFF) / 255.0 + alpha:1.0]; +} +@end diff --git a/Firebase/InAppMessaging/firebase_28dp.png b/Firebase/InAppMessaging/firebase_28dp.png new file mode 100644 index 00000000000..51a87f47575 Binary files /dev/null and b/Firebase/InAppMessaging/firebase_28dp.png differ diff --git a/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewController.m b/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewController.m index 511f7dff694..80fa0c34708 100644 --- a/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewController.m +++ b/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewController.m @@ -14,8 +14,6 @@ * limitations under the License. */ -#import - #import "FIDBannerViewController.h" #import "FIRCore+InAppMessagingDisplay.h" @@ -86,6 +84,10 @@ @implementation FIDBannerViewController return bannerVC; } +- (FIRInAppMessagingDisplayMessage *)inAppMessage { + return self.bannerDisplayMessage; +} + - (void)setupRecognizers { UIPanGestureRecognizer *panSwipeRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanSwipe:)]; diff --git a/Firebase/InAppMessagingDisplay/CHANGELOG.md b/Firebase/InAppMessagingDisplay/CHANGELOG.md index fc00d942812..ece3d26671f 100644 --- a/Firebase/InAppMessagingDisplay/CHANGELOG.md +++ b/Firebase/InAppMessagingDisplay/CHANGELOG.md @@ -1,2 +1,8 @@ +# 2019-03-19 -- v0.13.1 +- Fixed a crash (#2498) that occurred when integrating In-App Messaging into NativeScript apps. + +# 2019-03-05 -- v0.13.0 +- Added a feature allowing developers to programmatically register a delegate for updates on in-app engagement (impression, click, display errors). + # 2018-09-25 -- v0.12.0 - UI functionality of Firebase In-App Messaging spun off into FirebaseInAppMessagingDisplay. diff --git a/Firebase/InAppMessagingDisplay/FIDBaseRenderingViewController.h b/Firebase/InAppMessagingDisplay/FIDBaseRenderingViewController.h index c1bd7154118..3d39bfb6faa 100644 --- a/Firebase/InAppMessagingDisplay/FIDBaseRenderingViewController.h +++ b/Firebase/InAppMessagingDisplay/FIDBaseRenderingViewController.h @@ -43,5 +43,9 @@ NS_ASSUME_NONNULL_BEGIN // Call this when end user wants to follow the action url - (void)followActionURL; + +// Returns the in-app message being displayed. Overridden by message type subclasses. +- (nullable FIRInAppMessagingDisplayMessage *)inAppMessage; + @end NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessagingDisplay/FIDBaseRenderingViewController.m b/Firebase/InAppMessagingDisplay/FIDBaseRenderingViewController.m index 8b416313a02..e250dfb634a 100644 --- a/Firebase/InAppMessagingDisplay/FIDBaseRenderingViewController.m +++ b/Firebase/InAppMessagingDisplay/FIDBaseRenderingViewController.m @@ -14,8 +14,6 @@ * limitations under the License. */ -#import - #import "FIDBaseRenderingViewController.h" #import "FIDTimeFetcher.h" #import "FIRCore+InAppMessagingDisplay.h" @@ -35,6 +33,10 @@ @interface FIDBaseRenderingViewController () @implementation FIDBaseRenderingViewController +- (nullable FIRInAppMessagingDisplayMessage *)inAppMessage { + return nil; +} + - (void)viewDidLoad { [super viewDidLoad]; @@ -102,8 +104,8 @@ - (void)minImpressionTimeReached { FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID200004", @"Min impression time has been reached."); - if ([self.displayDelegate respondsToSelector:@selector(impressionDetected)]) { - [self.displayDelegate impressionDetected]; + if ([self.displayDelegate respondsToSelector:@selector(impressionDetectedForMessage:)]) { + [self.displayDelegate impressionDetectedForMessage:[self inAppMessage]]; } [NSNotificationCenter.defaultCenter removeObserver:self]; @@ -133,7 +135,7 @@ - (void)dismissView:(FIRInAppMessagingDismissType)dismissType { self.view.window.rootViewController = nil; if (self.displayDelegate) { - [self.displayDelegate messageDismissedWithType:dismissType]; + [self.displayDelegate messageDismissed:[self inAppMessage] dismissType:dismissType]; } else { FIRLogWarning(kFIRLoggerInAppMessagingDisplay, @"I-FID200007", @"Display delegate is nil while message is being dismissed."); @@ -147,7 +149,7 @@ - (void)followActionURL { self.view.window.rootViewController = nil; if (self.displayDelegate) { - [self.displayDelegate messageClicked]; + [self.displayDelegate messageClicked:[self inAppMessage]]; } else { FIRLogWarning(kFIRLoggerInAppMessagingDisplay, @"I-FID200008", @"Display delegate is nil while trying to follow action URL."); diff --git a/Firebase/InAppMessagingDisplay/FIDRenderingWindowHelper.m b/Firebase/InAppMessagingDisplay/FIDRenderingWindowHelper.m index 6714dbd640e..c133efc60a3 100644 --- a/Firebase/InAppMessagingDisplay/FIDRenderingWindowHelper.m +++ b/Firebase/InAppMessagingDisplay/FIDRenderingWindowHelper.m @@ -24,8 +24,7 @@ + (UIWindow *)UIWindowForModalView { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - UIWindow *appWindow = [[[UIApplication sharedApplication] delegate] window]; - UIWindowForModal = [[UIWindow alloc] initWithFrame:[appWindow frame]]; + UIWindowForModal = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; UIWindowForModal.windowLevel = UIWindowLevelNormal; }); return UIWindowForModal; @@ -36,8 +35,7 @@ + (UIWindow *)UIWindowForBannerView { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - UIWindow *appWindow = [[[UIApplication sharedApplication] delegate] window]; - UIWindowForBanner = [[FIDBannerViewUIWindow alloc] initWithFrame:[appWindow frame]]; + UIWindowForBanner = [[FIDBannerViewUIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; UIWindowForBanner.windowLevel = UIWindowLevelNormal; }); @@ -49,8 +47,7 @@ + (UIWindow *)UIWindowForImageOnlyView { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - UIWindow *appWindow = [[[UIApplication sharedApplication] delegate] window]; - UIWindowForImageOnly = [[UIWindow alloc] initWithFrame:[appWindow frame]]; + UIWindowForImageOnly = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; UIWindowForImageOnly.windowLevel = UIWindowLevelNormal; }); diff --git a/Firebase/InAppMessagingDisplay/FIRIAMDefaultDisplayImpl.m b/Firebase/InAppMessagingDisplay/FIRIAMDefaultDisplayImpl.m index cf8005f755f..6b20a45f6f2 100644 --- a/Firebase/InAppMessagingDisplay/FIRIAMDefaultDisplayImpl.m +++ b/Firebase/InAppMessagingDisplay/FIRIAMDefaultDisplayImpl.m @@ -17,9 +17,9 @@ #import #import + #import #import - #import "FIDBannerViewController.h" #import "FIDImageOnlyViewController.h" #import "FIDModalViewController.h" @@ -78,7 +78,7 @@ + (void)displayModalViewWithMessageDefinition:(FIRInAppMessagingModalDisplay *)m NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingDisplayErrorDomain code:FIAMDisplayRenderErrorTypeUnspecifiedError userInfo:@{@"message" : @"resource bundle is missing"}]; - [displayDelegate displayErrorEncountered:error]; + [displayDelegate displayErrorForMessage:modalMessage error:error]; return; } @@ -96,7 +96,7 @@ + (void)displayModalViewWithMessageDefinition:(FIRInAppMessagingModalDisplay *)m NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingDisplayErrorDomain code:FIAMDisplayRenderErrorTypeUnspecifiedError userInfo:@{}]; - [displayDelegate displayErrorEncountered:error]; + [displayDelegate displayErrorForMessage:modalMessage error:error]; return; } @@ -115,7 +115,7 @@ + (void)displayBannerViewWithMessageDefinition:(FIRInAppMessagingBannerDisplay * NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingDisplayErrorDomain code:FIAMDisplayRenderErrorTypeUnspecifiedError userInfo:@{}]; - [displayDelegate displayErrorEncountered:error]; + [displayDelegate displayErrorForMessage:bannerMessage error:error]; return; } @@ -133,7 +133,7 @@ + (void)displayBannerViewWithMessageDefinition:(FIRInAppMessagingBannerDisplay * NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingDisplayErrorDomain code:FIAMDisplayRenderErrorTypeUnspecifiedError userInfo:@{}]; - [displayDelegate displayErrorEncountered:error]; + [displayDelegate displayErrorForMessage:bannerMessage error:error]; return; } @@ -153,7 +153,7 @@ + (void)displayImageOnlyViewWithMessageDefinition: NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingDisplayErrorDomain code:FIAMDisplayRenderErrorTypeUnspecifiedError userInfo:@{}]; - [displayDelegate displayErrorEncountered:error]; + [displayDelegate displayErrorForMessage:imageOnlyMessage error:error]; return; } @@ -171,7 +171,7 @@ + (void)displayImageOnlyViewWithMessageDefinition: NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingDisplayErrorDomain code:FIAMDisplayRenderErrorTypeUnspecifiedError userInfo:@{}]; - [displayDelegate displayErrorEncountered:error]; + [displayDelegate displayErrorForMessage:imageOnlyMessage error:error]; return; } @@ -182,7 +182,7 @@ + (void)displayImageOnlyViewWithMessageDefinition: } #pragma mark - protocol FIRInAppMessagingDisplay -- (void)displayMessage:(FIRInAppMessagingDisplayMessageBase *)messageForDisplay +- (void)displayMessage:(FIRInAppMessagingDisplayMessage *)messageForDisplay displayDelegate:(id)displayDelegate { if ([messageForDisplay isKindOfClass:[FIRInAppMessagingModalDisplay class]]) { FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID100000", @"Display a modal message"); @@ -208,7 +208,7 @@ - (void)displayMessage:(FIRInAppMessagingDisplayMessageBase *)messageForDisplay NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingDisplayErrorDomain code:FIAMDisplayRenderErrorTypeUnspecifiedError userInfo:@{}]; - [displayDelegate displayErrorEncountered:error]; + [displayDelegate displayErrorForMessage:messageForDisplay error:error]; } } @end diff --git a/Firebase/InAppMessagingDisplay/ImageOnly/FIDImageOnlyViewController.m b/Firebase/InAppMessagingDisplay/ImageOnly/FIDImageOnlyViewController.m index c395f5a0d19..e244a18aef4 100644 --- a/Firebase/InAppMessagingDisplay/ImageOnly/FIDImageOnlyViewController.m +++ b/Firebase/InAppMessagingDisplay/ImageOnly/FIDImageOnlyViewController.m @@ -14,8 +14,6 @@ * limitations under the License. */ -#import - #import "FIDImageOnlyViewController.h" #import "FIRCore+InAppMessagingDisplay.h" @@ -56,6 +54,10 @@ @implementation FIDImageOnlyViewController return imageOnlyVC; } +- (FIRInAppMessagingDisplayMessage *)inAppMessage { + return self.imageOnlyMessage; +} + - (IBAction)closeButtonClicked:(id)sender { [self dismissView:FIRInAppMessagingDismissTypeUserTapClose]; } @@ -151,7 +153,7 @@ - (void)viewWillAppear:(BOOL)animated { to:nil from:nil forEvent:nil]; - if (self.imageOnlyMessage.renderAsTestMessage) { + if (self.imageOnlyMessage.campaignInfo.renderAsTestMessage) { FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID110004", @"Flashing the close button since this is a test message."); [self flashCloseButton:self.closeButton]; diff --git a/Firebase/InAppMessagingDisplay/Modal/FIDModalViewController.m b/Firebase/InAppMessagingDisplay/Modal/FIDModalViewController.m index 8e31910c0e1..0c4b2b4f2a1 100644 --- a/Firebase/InAppMessagingDisplay/Modal/FIDModalViewController.m +++ b/Firebase/InAppMessagingDisplay/Modal/FIDModalViewController.m @@ -16,8 +16,6 @@ #import -#import - #import "FIDModalViewController.h" #import "FIRCore+InAppMessagingDisplay.h" @@ -95,6 +93,10 @@ @implementation FIDModalViewController return modalVC; } +- (FIRInAppMessagingDisplayMessage *)inAppMessage { + return self.modalDisplayMessage; +} + - (IBAction)closeButtonClicked:(id)sender { [self dismissView:FIRInAppMessagingDismissTypeUserTapClose]; } @@ -428,7 +430,7 @@ - (void)viewWillAppear:(BOOL)animated { from:nil forEvent:nil]; - if (self.modalDisplayMessage.renderAsTestMessage) { + if (self.modalDisplayMessage.campaignInfo.renderAsTestMessage) { FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300011", @"Flushing the close button since this is a test message."); [self flashCloseButton:self.closeButton]; diff --git a/Firebase/InAppMessagingDisplay/Public/FIRIAMDefaultDisplayImpl.h b/Firebase/InAppMessagingDisplay/Public/FIRIAMDefaultDisplayImpl.h index 41f6ba39d5a..6074c3e2819 100644 --- a/Firebase/InAppMessagingDisplay/Public/FIRIAMDefaultDisplayImpl.h +++ b/Firebase/InAppMessagingDisplay/Public/FIRIAMDefaultDisplayImpl.h @@ -14,9 +14,10 @@ * limitations under the License. */ -#import #import +#import + NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(InAppMessagingDefaultDisplayImpl) /** @@ -25,7 +26,7 @@ NS_SWIFT_NAME(InAppMessagingDefaultDisplayImpl) * to help UI Testing app access the UI layer directly. */ @interface FIRIAMDefaultDisplayImpl : NSObject -- (void)displayMessage:(FIRInAppMessagingDisplayMessageBase *)messageForDisplay +- (void)displayMessage:(FIRInAppMessagingDisplayMessage *)messageForDisplay displayDelegate:(id)displayDelegate; @end NS_ASSUME_NONNULL_END diff --git a/Firebase/InstanceID/CHANGELOG.md b/Firebase/InstanceID/CHANGELOG.md new file mode 100644 index 00000000000..74ff31004cd --- /dev/null +++ b/Firebase/InstanceID/CHANGELOG.md @@ -0,0 +1,148 @@ +# 2019-03-19 -- v3.8.0 +- Adding community support for tvOS. (#2428) +- Adding Firebase info to checkin. (#2509) +- Fixed a crash in FIRInstanceIDCheckinService. (#2548) + +# 2019-03-05 -- v3.7.0 +- Open source Firebase InstanceID. (#186) + +# 2019-02-20 -- v3.5.0 +- Always update keychain access control when adding new keychain to ensure it won't be blocked when device is locked. (#1399) + +# 2019-01-22 -- v3.4.0 +- Move all keychain write operations off the main thread. (#1399) +- Make keychain operations asynchronous where possible (given the current APIs) +- Avoid redundant keychain operations when it's been queried and cached before. + +# 2018-10-25 -- v3.3.0 +- Fixed a crash caused by keychain operation when accessing default access group. (#1399, #1393) +- Remove internal APIs that are no longer used. + +# 2018-09-25 -- v3.2.2 +- Fixed a crash caused by NSUserDefaults being called on background thread. + +# 2018-08-14 -- v3.2.1 +- Fixed an issue that checkin is not cached properly when app first started. (#1561) + +# 2018-07-31 -- v3.2.0 +- Added support for global Firebase data collection flag. (#1219) +- Improved message tracking sent by server API. +- Fixed an issue that InstanceID doesn't compile in app extensions, allowing its +dependents like remote config to be working inside the app extensions. + +# 2018-06-19 -- v3.1.1 +- Ensure the checkin and tokens are refreshed if firebase project changed. +- Fixed an issue that checkin should be turned off when FCM's autoInitEnabled flag is off. + +# 2018-06-12 -- v3.1.0 +- Added a new API to fetch InstanceID and Token with a completion handler. The completion handler returns a FIRInstanceIDResult with a instanceID and a token properties. +- Deprecated the token method. +- Added support to log a new customized label provided by developer. + +# 2018-05-08 -- v3.0.0 +- Removed deprecated method `setAPNSToken:type` defined in FIRInstanceID, please use `setAPNSToken:type` defined in FIRMessaging instead. +- Removed deprecated enum `FIRInstanceIDAPNSTokenType` defined in FIRInstanceID, please use `FIRMessagingAPNSTokenType` defined in FIRMessaging instead. +- Fixed an issue that FCM scheduled messages were not tracked successfully. + +# 2018-03-06 -- v2.0.10 +- Improved documentation on InstanceID usage for GDPR. +- Improved the keypair handling during GCM to FCM migration. If you are migrating from GCM to FCM, we encourage you to update to this version and above. + +# 2018-02-06 -- v2.0.9 +- Improved support for language targeting for FCM service. Server updates happen more efficiently when language changes. +- Improved support for FCM token auto generation enable/disable functions. + +# 2017-12-11 -- v2.0.8 +- Fixed a crash caused by a reflection call during logging. +- Updating server with the latest parameters and deprecating old ones. + +# 2017-11-27 -- v2.0.7 +- Improve identity reset process, ensuring all information is reset during Identity deletion. + +# 2017-11-06 -- v2.0.6 +- Make token refresh weekly. +- Fixed a crash when performing token operation. + +# 2017-10-11 -- v2.0.5 +- Improved support for working in shared Keychain environments. + +# 2017-09-26 -- v2.0.4 +- Fixed an issue where the FCM token was not associating correctly with an APNs + device token, depending on when the APNs device token was made available. +- Fixed an issue where FCM tokens for different Sender IDs were not associating + correctly with an APNs device token. +- Fixed an issue that was preventing the FCM direct channel from being + established on the first start after 24 hours of being opened. + +# 2017-09-13 -- v2.0.3 +- Fixed a race condition where a token was not being generated on first start, + if Firebase Messaging was included and the app did not register for remote + notifications. + +# 2017-08-25 -- v2.0.2 +- Fixed a startup performance regression, removing a call which was blocking the + main thread. + +# 2017-08-07 -- v2.0.1 +- Fixed issues with token and app identifier being inaccessible when the device + is locked. +- Fixed a crash if bundle identifier is nil, which is possible in some testing + environments. +- Fixed a small memory leak fetching a new token. +- Moved to a new and simplified token storage system. +- Moved to a new queuing system for token fetches and deletes. +- Simplified logic and code around configuration and logging. +- Added clarification about the 'apns_sandbox' parameter, in header comments. + +# 2017-05-08 -- v2.0.0 +- Introduced an improved interface for Swift 3 developers +- Deprecated some methods and properties after moving their logic to the + Firebase Cloud Messaging SDK +- Fixed an intermittent stability issue when a debug build of an app was + replaced with a release build of the same version +- Removed swizzling logic that was sometimes resulting in developers receiving + a validation notice about enabling push notification capabilities, even though + they weren't using push notifications +- Fixed a notification that would sometimes fire twice in quick succession + during the first run of an app + +# 2017-03-31 -- v1.0.10 + +- Improvements to token-fetching logic +- Fixed some warnings in Instance ID +- Improved error messages if Instance ID couldn't be initialized properly +- Improvements to console logging + +# 2017-01-31 -- v1.0.9 + +- Removed an error being mistakenly logged to the console. + +# 2016-07-06 -- v1.0.8 + +- Don't store InstanceID plists in Documents folder. + +# 2016-06-19 -- v1.0.7 + +- Fix remote-notifications warning on app submission. + +# 2016-05-16 -- v1.0.6 + +- Fix CocoaPod linter issues for InstanceID pod. + +# 2016-05-13 -- v1.0.5 + +- Fix Authorization errors for InstanceID tokens. + +# 2016-05-11 -- v1.0.4 + +- Reduce wait for InstanceID token during parallel requests. + +# 2016-04-18 -- v1.0.3 + +- Change flag to disable swizzling to *FirebaseAppDelegateProxyEnabled*. +- Fix incessant Keychain errors while accessing InstanceID. +- Fix max retries for fetching IID token. + +# 2016-04-18 -- v1.0.2 + +- Register for remote notifications on iOS8+ in the SDK itself. diff --git a/Firebase/InstanceID/FIRIMessageCode.h b/Firebase/InstanceID/FIRIMessageCode.h new file mode 100644 index 00000000000..db653d7cc48 --- /dev/null +++ b/Firebase/InstanceID/FIRIMessageCode.h @@ -0,0 +1,156 @@ +/* + * Copyright 2019 Google + * + * 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 + +// The format of the debug code will show in the log as: e.g. +// for code 1000, it will show as I-IID001000. +typedef NS_ENUM(NSInteger, FIRInstanceIDMessageCode) { + // DO NOT USE 2000, 2002. + kFIRInstanceIDMessageCodeFIRApp000 = 1000, // I-IID001000 + kFIRInstanceIDMessageCodeFIRApp001 = 1001, + kFIRInstanceIDMessageCodeFIRApp002 = 1002, + kFIRInstanceIDMessageCodeInternal001 = 2001, + kFIRInstanceIDMessageCodeInternal002 = 2002, + // FIRInstanceID.m + // DO NOT USE 4000. + kFIRInstanceIDMessageCodeInstanceID000 = 3000, + kFIRInstanceIDMessageCodeInstanceID001 = 3001, + kFIRInstanceIDMessageCodeInstanceID002 = 3002, + kFIRInstanceIDMessageCodeInstanceID003 = 3003, + kFIRInstanceIDMessageCodeInstanceID004 = 3004, + kFIRInstanceIDMessageCodeInstanceID005 = 3005, + kFIRInstanceIDMessageCodeInstanceID006 = 3006, + kFIRInstanceIDMessageCodeInstanceID007 = 3007, + kFIRInstanceIDMessageCodeInstanceID008 = 3008, + kFIRInstanceIDMessageCodeInstanceID009 = 3009, + kFIRInstanceIDMessageCodeInstanceID010 = 3010, + kFIRInstanceIDMessageCodeInstanceID011 = 3011, + kFIRInstanceIDMessageCodeInstanceID012 = 3012, + kFIRInstanceIDMessageCodeInstanceID013 = 3013, + kFIRInstanceIDMessageCodeInstanceID014 = 3014, + kFIRInstanceIDMessageCodeInstanceID015 = 3015, + kFIRInstanceIDMessageCodeRefetchingTokenForAPNS = 3016, + // FIRInstanceIDAuthService.m + kFIRInstanceIDMessageCodeAuthService000 = 5000, + kFIRInstanceIDMessageCodeAuthService001 = 5001, + kFIRInstanceIDMessageCodeAuthService002 = 5002, + kFIRInstanceIDMessageCodeAuthService003 = 5003, + kFIRInstanceIDMessageCodeAuthService004 = 5004, + kFIRInstanceIDMessageCodeAuthServiceCheckinInProgress = 5004, + + // FIRInstanceIDBackupExcludedPlist.m + kFIRInstanceIDMessageCodeBackupExcludedPlist000 = 6000, + kFIRInstanceIDMessageCodeBackupExcludedPlist001 = 6001, + kFIRInstanceIDMessageCodeBackupExcludedPlist002 = 6002, + kFIRInstanceIDMessageCodeBackupExcludedPlistInvalidPlistEnum = 6003, + // FIRInstanceIDCheckinService.m + kFIRInstanceIDMessageCodeService000 = 7000, + kFIRInstanceIDMessageCodeService001 = 7001, + kFIRInstanceIDMessageCodeService002 = 7002, + kFIRInstanceIDMessageCodeService003 = 7003, + kFIRInstanceIDMessageCodeService004 = 7004, + kFIRInstanceIDMessageCodeService005 = 7005, + kFIRInstanceIDMessageCodeService006 = 7006, + kFIRIntsanceIDInvalidNetworkSession = 7007, + // FIRInstanceIDCheckinStore.m + // DO NOT USE 8002, 8004 - 8008 + kFIRInstanceIDMessageCodeCheckinStore000 = 8000, + kFIRInstanceIDMessageCodeCheckinStore001 = 8001, + kFIRInstanceIDMessageCodeCheckinStore003 = 8003, + // FIRInstanceIDKeyPair.m + // DO NOT USE 9001, 9003 + kFIRInstanceIDMessageCodeKeyPair000 = 9000, + kFIRInstanceIDMessageCodeKeyPair002 = 9002, + kFIRInstanceIDMessageCodeKeyPairMigrationError = 9004, + kFIRInstanceIDMessageCodeKeyPairMigrationSuccess = 9005, + // FIRInstanceIDKeyPairStore.m + kFIRInstanceIDMessageCodeKeyPairStore000 = 10000, + kFIRInstanceIDMessageCodeKeyPairStore001 = 10001, + kFIRInstanceIDMessageCodeKeyPairStore002 = 10002, + kFIRInstanceIDMessageCodeKeyPairStore003 = 10003, + kFIRInstanceIDMessageCodeKeyPairStore004 = 10004, + kFIRInstanceIDMessageCodeKeyPairStore005 = 10005, + kFIRInstanceIDMessageCodeKeyPairStore006 = 10006, + kFIRInstanceIDMessageCodeKeyPairStore007 = 10007, + kFIRInstanceIDMessageCodeKeyPairStore008 = 10008, + kFIRInstanceIDMessageCodeKeyPairStoreCouldNotLoadKeyPair = 10009, + // FIRInstanceIDKeyPairUtilities.m + kFIRInstanceIDMessageCodeKeyPairUtilities000 = 11000, + kFIRInstanceIDMessageCodeKeyPairUtilities001 = 11001, + kFIRInstanceIDMessageCodeKeyPairUtilitiesFirstConcatenateParamNil = 11002, + + // DO NOT USE 12000 - 12014 + + // FIRInstanceIDStore.m + // DO NOT USE 13004, 13005, 13007, 13008, 13010, 13011, 13013, 13014 + kFIRInstanceIDMessageCodeStore000 = 13000, + kFIRInstanceIDMessageCodeStore001 = 13001, + kFIRInstanceIDMessageCodeStore002 = 13002, + kFIRInstanceIDMessageCodeStore003 = 13003, + kFIRInstanceIDMessageCodeStore006 = 13006, + kFIRInstanceIDMessageCodeStore009 = 13009, + kFIRInstanceIDMessageCodeStore012 = 13012, + // FIRInstanceIDTokenManager.m + // DO NOT USE 14002, 14005 + kFIRInstanceIDMessageCodeTokenManager000 = 14000, + kFIRInstanceIDMessageCodeTokenManager001 = 14001, + kFIRInstanceIDMessageCodeTokenManager003 = 14003, + kFIRInstanceIDMessageCodeTokenManager004 = 14004, + kFIRInstanceIDMessageCodeTokenManagerErrorDeletingFCMTokensOnAppReset = 14006, + kFIRInstanceIDMessageCodeTokenManagerDeletedFCMTokensOnAppReset = 14007, + kFIRInstanceIDMessageCodeTokenManagerSavedAppVersion = 14008, + kFIRInstanceIDMessageCodeTokenManagerErrorInvalidatingAllTokens = 14009, + kFIRInstanceIDMessageCodeTokenManagerAPNSChanged = 14010, + kFIRInstanceIDMessageCodeTokenManagerAPNSChangedTokenInvalidated = 14011, + kFIRInstanceIDMessageCodeTokenManagerInvalidateStaleToken = 14012, + // FIRInstanceIDTokenStore.m + // DO NOT USE 15002 - 15013 + kFIRInstanceIDMessageCodeTokenStore000 = 15000, + kFIRInstanceIDMessageCodeTokenStore001 = 15001, + kFIRInstanceIDMessageCodeTokenStoreExceptionUnarchivingTokenInfo = 15015, + + // DO NOT USE 16000, 18004 + + // FIRInstanceIDUtilities.m + kFIRInstanceIDMessageCodeUtilitiesMissingBundleIdentifier = 18000, + kFIRInstanceIDMessageCodeUtilitiesAppEnvironmentUtilNotAvailable = 18001, + kFIRInstanceIDMessageCodeUtilitiesCannotGetHardwareModel = 18002, + kFIRInstanceIDMessageCodeUtilitiesCannotGetSystemVersion = 18003, + // FIRInstanceIDTokenOperation.m + kFIRInstanceIDMessageCodeTokenOperationFailedToSignParams = 19000, + // FIRInstanceIDTokenFetchOperation.m + // DO NOT USE 20004, 20005 + kFIRInstanceIDMessageCodeTokenFetchOperationFetchRequest = 20000, + kFIRInstanceIDMessageCodeTokenFetchOperationRequestError = 20001, + kFIRInstanceIDMessageCodeTokenFetchOperationBadResponse = 20002, + kFIRInstanceIDMessageCodeTokenFetchOperationBadTokenStructure = 20003, + // FIRInstanceIDTokenDeleteOperation.m + kFIRInstanceIDMessageCodeTokenDeleteOperationFetchRequest = 21000, + kFIRInstanceIDMessageCodeTokenDeleteOperationRequestError = 21001, + kFIRInstanceIDMessageCodeTokenDeleteOperationBadResponse = 21002, + // FIRInstanceIDTokenInfo.m + kFIRInstanceIDMessageCodeTokenInfoBadAPNSInfo = 22000, + kFIRInstanceIDMessageCodeTokenInfoFirebaseAppIDChanged = 22001, + kFIRInstanceIDMessageCodeTokenInfoLocaleChanged = 22002, + // FIRInstanceIDKeychain.m + kFIRInstanceIDKeychainReadItemError = 23000, + kFIRInstanceIDKeychainAddItemError = 23001, + kFIRInstanceIDKeychainDeleteItemError = 23002, + kFIRInstanceIDKeychainCreateKeyPairError = 23003, + kFIRInstanceIDKeychainUpdateItemError = 23004, + +}; diff --git a/Firebase/InstanceID/FIRInstanceID+Private.h b/Firebase/InstanceID/FIRInstanceID+Private.h new file mode 100644 index 00000000000..524441778e2 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceID+Private.h @@ -0,0 +1,55 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceID.h" + +#import "FIRInstanceIDCheckinService.h" + +/** + * Internal API used by other Firebase SDK teams, including Messaging, Analytics and Remote config. + */ +@interface FIRInstanceID (Private) + +/** + * Return the cached checkin preferences on the disk. This is used internally only by Messaging. + * + * @return The cached checkin preferences on the client. + */ +- (nullable FIRInstanceIDCheckinPreferences *)cachedCheckinPreferences; + +/** + * Fetches checkin info for the app. If the app has valid cached checkin preferences + * they are returned instead of making a network request. + * + * @param handler The completion handler to invoke once the request has completed. + */ +- (void)fetchCheckinInfoWithHandler:(nullable FIRInstanceIDDeviceCheckinCompletion)handler; + +/** + * Get the InstanceID for the app. If an ID was created before and cached + * successfully we will return that ID. If no cached ID exists we create + * a new ID, cache it and return that. + * + * This is a blocking call and should not really be called on the main thread. + * + * @param error The error object that represents the error while trying to + * retrieve the instance id. + * + * @return The InstanceID for the app. + */ +- (nullable NSString *)appInstanceID:(NSError *_Nullable *_Nullable)error; + +@end diff --git a/Firebase/InstanceID/FIRInstanceID+Private.m b/Firebase/InstanceID/FIRInstanceID+Private.m new file mode 100644 index 00000000000..61d43a4207a --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceID+Private.m @@ -0,0 +1,46 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceID+Private.h" + +#import "FIRInstanceIDAuthService.h" +#import "FIRInstanceIDKeyPairStore.h" +#import "FIRInstanceIDTokenManager.h" + +@interface FIRInstanceID () + +@property(nonatomic, readonly, strong) FIRInstanceIDTokenManager *tokenManager; +@property(nonatomic, readonly, strong) FIRInstanceIDKeyPairStore *keyPairStore; + +@end + +@implementation FIRInstanceID (Private) + +- (FIRInstanceIDCheckinPreferences *)cachedCheckinPreferences { + return [self.tokenManager.authService checkinPreferences]; +} + +// This method just wraps our pre-configured auth service to make the request. +// This method is only needed by first-party users, like Remote Config. +- (void)fetchCheckinInfoWithHandler:(FIRInstanceIDDeviceCheckinCompletion)handler { + [self.tokenManager.authService fetchCheckinInfoWithHandler:handler]; +} + +- (NSString *)appInstanceID:(NSError **)error { + return [self.keyPairStore appIdentityWithError:error]; +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceID+Testing.h b/Firebase/InstanceID/FIRInstanceID+Testing.h new file mode 100644 index 00000000000..ba24926fc4f --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceID+Testing.h @@ -0,0 +1,42 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceID+Private.h" +#import "FIRInstanceID.h" +#import "FIRInstanceIDKeyPairStore.h" +#import "FIRInstanceIDTokenManager.h" + +@interface FIRInstanceID (Testing) + +@property(nonatomic, readwrite, strong) FIRInstanceIDTokenManager *tokenManager; +@property(nonatomic, readwrite, strong) FIRInstanceIDKeyPairStore *keyPairStore; +@property(nonatomic, readwrite, copy) NSString *fcmSenderID; + +/** + * Private initializer. + */ +- (instancetype)initPrivately; + +/** + * Actually makes InstanceID instantiate both the IID and Token-related subsystems. + */ +- (void)start; + ++ (int64_t)maxRetryCountForDefaultToken; ++ (int64_t)minIntervalForDefaultTokenRetry; ++ (int64_t)maxRetryIntervalForDefaultTokenInSeconds; + +@end diff --git a/Firebase/InstanceID/FIRInstanceID.m b/Firebase/InstanceID/FIRInstanceID.m new file mode 100644 index 00000000000..f5c4a6eb551 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceID.m @@ -0,0 +1,1196 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceID.h" + +#import +#import +#import +#import +#import +#import +#import "FIRInstanceID+Private.h" +#import "FIRInstanceIDAuthService.h" +#import "FIRInstanceIDCombinedHandler.h" +#import "FIRInstanceIDConstants.h" +#import "FIRInstanceIDDefines.h" +#import "FIRInstanceIDKeyPairStore.h" +#import "FIRInstanceIDLogger.h" +#import "FIRInstanceIDStore.h" +#import "FIRInstanceIDTokenInfo.h" +#import "FIRInstanceIDTokenManager.h" +#import "FIRInstanceIDUtilities.h" +#import "FIRInstanceIDVersionUtilities.h" +#import "NSError+FIRInstanceID.h" + +// Public constants +NSString *const kFIRInstanceIDScopeFirebaseMessaging = @"fcm"; + +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 +const NSNotificationName kFIRInstanceIDTokenRefreshNotification = + @"com.firebase.iid.notif.refresh-token"; +#else +NSString *const kFIRInstanceIDTokenRefreshNotification = @"com.firebase.iid.notif.refresh-token"; +#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + +NSString *const kFIRInstanceIDInvalidNilHandlerError = @"Invalid nil handler."; + +// Private constants +int64_t const kMaxRetryIntervalForDefaultTokenInSeconds = 20 * 60; // 20 minutes +int64_t const kMinRetryIntervalForDefaultTokenInSeconds = 10; // 10 seconds +// we retry only a max 5 times. +// TODO(chliangGoogle): If we still fail we should listen for the network change notification +// since GCM would have started Reachability. We only start retrying after we see a configuration +// change. +NSInteger const kMaxRetryCountForDefaultToken = 5; + +static NSString *const kEntitlementsAPSEnvironmentKey = @"Entitlements.aps-environment"; +static NSString *const kAPSEnvironmentDevelopmentValue = @"development"; +/// FIRMessaging selector that returns the current FIRMessaging auto init +/// enabled flag. +static NSString *const kFIRInstanceIDFCMSelectorAutoInitEnabled = @"isAutoInitEnabled"; +static NSString *const kFIRInstanceIDFCMSelectorInstance = @"messaging"; + +static NSString *const kFIRInstanceIDAPNSTokenType = @"APNSTokenType"; +static NSString *const kFIRIIDAppReadyToConfigureSDKNotification = + @"FIRAppReadyToConfigureSDKNotification"; +static NSString *const kFIRIIDAppNameKey = @"FIRAppNameKey"; +static NSString *const kFIRIIDErrorDomain = @"com.firebase.instanceid"; +static NSString *const kFIRIIDServiceInstanceID = @"InstanceID"; + +// This should be the same value as FIRErrorCodeInstanceIDFailed, which we can't import directly +static NSInteger const kFIRIIDErrorCodeInstanceIDFailed = -121; + +typedef void (^FIRInstanceIDKeyPairHandler)(FIRInstanceIDKeyPair *keyPair, NSError *error); + +/** + * The APNS token type for the app. If the token type is set to `UNKNOWN` + * InstanceID will implicitly try to figure out what the actual token type + * is from the provisioning profile. + * This must match FIRMessagingAPNSTokenType in FIRMessaging.h + */ +typedef NS_ENUM(NSInteger, FIRInstanceIDAPNSTokenType) { + /// Unknown token type. + FIRInstanceIDAPNSTokenTypeUnknown, + /// Sandbox token type. + FIRInstanceIDAPNSTokenTypeSandbox, + /// Production token type. + FIRInstanceIDAPNSTokenTypeProd, +} NS_SWIFT_NAME(InstanceIDAPNSTokenType); + +@interface FIRInstanceIDResult () +@property(nonatomic, readwrite, copy) NSString *instanceID; +@property(nonatomic, readwrite, copy) NSString *token; +@end + +@interface FIRInstanceID () + +// FIRApp configuration objects. +@property(nonatomic, readwrite, copy) NSString *fcmSenderID; +@property(nonatomic, readwrite, copy) NSString *firebaseAppID; + +// Raw APNS token data +@property(nonatomic, readwrite, strong) NSData *apnsTokenData; + +@property(nonatomic, readwrite) FIRInstanceIDAPNSTokenType apnsTokenType; +// String-based, internal representation of APNS token +@property(nonatomic, readwrite, copy) NSString *APNSTupleString; +// Token fetched from the server automatically for the default app. +@property(nonatomic, readwrite, copy) NSString *defaultFCMToken; + +@property(nonatomic, readwrite, strong) FIRInstanceIDTokenManager *tokenManager; +@property(nonatomic, readwrite, strong) FIRInstanceIDKeyPairStore *keyPairStore; + +// backoff and retry for default token +@property(nonatomic, readwrite, assign) NSInteger retryCountForDefaultToken; +@property(atomic, strong, nullable) + FIRInstanceIDCombinedHandler *defaultTokenFetchHandler; + +@end + +// InstanceID doesn't provide any functionality to other components, +// so it provides a private, empty protocol that it conforms to and use it for registration. + +@protocol FIRInstanceIDInstanceProvider +@end + +@interface FIRInstanceID () +@end + +@implementation FIRInstanceIDResult +- (id)copyWithZone:(NSZone *)zone { + FIRInstanceIDResult *result = [[[self class] allocWithZone:zone] init]; + result.instanceID = self.instanceID; + result.token = self.token; + return result; +} +@end + +@implementation FIRInstanceID + +// File static to support InstanceID tests that call [FIRInstanceID instanceID] after +// [FIRInstanceID instanceIDForTests]. +static FIRInstanceID *gInstanceID; + ++ (instancetype)instanceID { + // If the static instance was created, return it. This should only be set in tests and we should + // eventually use proper dependency injection for a better test structure. + if (gInstanceID != nil) { + return gInstanceID; + } + FIRApp *defaultApp = [FIRApp defaultApp]; // Missing configure will be logged here. + FIRInstanceID *instanceID = + (FIRInstanceID *)FIR_COMPONENT(FIRInstanceIDInstanceProvider, defaultApp.container); + return instanceID; +} + +- (instancetype)initPrivately { + self = [super init]; + if (self != nil) { + // Use automatic detection of sandbox, unless otherwise set by developer + _apnsTokenType = FIRInstanceIDAPNSTokenTypeUnknown; + } + return self; +} + ++ (FIRInstanceID *)instanceIDForTests { + gInstanceID = [[FIRInstanceID alloc] initPrivately]; + [gInstanceID start]; + return gInstanceID; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - Tokens + +- (NSString *)token { + if (!self.fcmSenderID.length) { + return nil; + } + + NSString *cachedToken = [self cachedTokenIfAvailable]; + + if (cachedToken) { + return cachedToken; + } else { + // If we've never had a cached default token, we should fetch one because unrelatedly, + // this request will help us determine whether the locally-generated Instance ID keypair is not + // unique, and therefore generate a new one. + [self defaultTokenWithHandler:nil]; + return nil; + } +} + +- (void)instanceIDWithHandler:(FIRInstanceIDResultHandler)handler { + FIRInstanceID_WEAKIFY(self); + [self getIDWithHandler:^(NSString *identity, NSError *error) { + FIRInstanceID_STRONGIFY(self); + // This is in main queue already + if (error) { + if (handler) { + handler(nil, error); + } + return; + } + FIRInstanceIDResult *result = [[FIRInstanceIDResult alloc] init]; + result.instanceID = identity; + NSString *cachedToken = [self cachedTokenIfAvailable]; + if (cachedToken) { + if (handler) { + result.token = cachedToken; + handler(result, nil); + } + // If no handler, simply return since client has generated iid and token. + return; + } + [self defaultTokenWithHandler:^(NSString *_Nullable token, NSError *_Nullable error) { + if (handler) { + if (error) { + handler(nil, error); + return; + } + result.token = token; + handler(result, nil); + } + }]; + }]; +} + +- (NSString *)cachedTokenIfAvailable { + FIRInstanceIDTokenInfo *cachedTokenInfo = + [self.tokenManager cachedTokenInfoWithAuthorizedEntity:self.fcmSenderID + scope:kFIRInstanceIDDefaultTokenScope]; + return cachedTokenInfo.token; +} + +- (void)setDefaultFCMToken:(NSString *)defaultFCMToken { + if (_defaultFCMToken && defaultFCMToken && [defaultFCMToken isEqualToString:_defaultFCMToken]) { + return; + } + + _defaultFCMToken = defaultFCMToken; + + // Sending this notification out will ensure that FIRMessaging has the updated + // default FCM token. + NSNotification *internalDefaultTokenNotification = + [NSNotification notificationWithName:kFIRInstanceIDDefaultGCMTokenNotification + object:_defaultFCMToken]; + [[NSNotificationQueue defaultQueue] enqueueNotification:internalDefaultTokenNotification + postingStyle:NSPostASAP]; +} + +- (void)tokenWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + options:(NSDictionary *)options + handler:(FIRInstanceIDTokenHandler)handler { + _FIRInstanceIDDevAssert(handler != nil && [authorizedEntity length] && [scope length], + @"Invalid authorizedEntity or scope to new token"); + if (!handler) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID000, + kFIRInstanceIDInvalidNilHandlerError); + return; + } + + NSMutableDictionary *tokenOptions = [NSMutableDictionary dictionary]; + if (options.count) { + [tokenOptions addEntriesFromDictionary:options]; + } + + NSString *APNSKey = kFIRInstanceIDTokenOptionsAPNSKey; + NSString *serverTypeKey = kFIRInstanceIDTokenOptionsAPNSIsSandboxKey; + + if (tokenOptions[APNSKey] != nil && tokenOptions[serverTypeKey] == nil) { + // APNS key was given, but server type is missing. Supply the server type with automatic + // checking. This can happen when the token is requested from FCM, which does not include a + // server type during its request. + tokenOptions[serverTypeKey] = @([self isSandboxApp]); + } + + // comparing enums to ints directly throws a warning + FIRInstanceIDErrorCode noError = INT_MAX; + FIRInstanceIDErrorCode errorCode = noError; + if (FIRInstanceIDIsValidGCMScope(scope) && !tokenOptions[APNSKey]) { + errorCode = kFIRInstanceIDErrorCodeMissingAPNSToken; + } else if (FIRInstanceIDIsValidGCMScope(scope) && + ![tokenOptions[APNSKey] isKindOfClass:[NSData class]]) { + errorCode = kFIRInstanceIDErrorCodeInvalidRequest; + } else if (![authorizedEntity length]) { + errorCode = kFIRInstanceIDErrorCodeInvalidAuthorizedEntity; + } else if (![scope length]) { + errorCode = kFIRInstanceIDErrorCodeInvalidScope; + } else if (!self.keyPairStore) { + errorCode = kFIRInstanceIDErrorCodeInvalidStart; + } + + FIRInstanceIDTokenHandler newHandler = ^(NSString *token, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler(token, error); + }); + }; + + if (errorCode != noError) { + newHandler(nil, [NSError errorWithFIRInstanceIDErrorCode:errorCode]); + return; + } + + // TODO(chliangGoogle): Add some validation logic that the APNs token data and sandbox value are + // supplied in the valid format (NSData and BOOL, respectively). + + // Add internal options + if (self.firebaseAppID) { + tokenOptions[kFIRInstanceIDTokenOptionsFirebaseAppIDKey] = self.firebaseAppID; + } + + FIRInstanceID_WEAKIFY(self); + FIRInstanceIDAuthService *authService = self.tokenManager.authService; + [authService + fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *preferences, NSError *error) { + FIRInstanceID_STRONGIFY(self); + if (error) { + newHandler(nil, error); + return; + } + + // Only use the token in the cache if the APNSInfo matches what the request's options has. + // It's possible for the request to be with a newer APNs device token, which should be + // honored. + FIRInstanceIDTokenInfo *cachedTokenInfo = + [self.tokenManager cachedTokenInfoWithAuthorizedEntity:authorizedEntity scope:scope]; + if (cachedTokenInfo) { + // Ensure that the cached token matches APNs data before returning it. + FIRInstanceIDAPNSInfo *optionsAPNSInfo = + [[FIRInstanceIDAPNSInfo alloc] initWithTokenOptionsDictionary:tokenOptions]; + // If either the APNs info is missing in both, or if they are an exact match, then we can + // use this cached token. + if ((!cachedTokenInfo.APNSInfo && !optionsAPNSInfo) || + [cachedTokenInfo.APNSInfo isEqualToAPNSInfo:optionsAPNSInfo]) { + newHandler(cachedTokenInfo.token, nil); + return; + } + } + + FIRInstanceID_WEAKIFY(self); + [self asyncLoadKeyPairWithHandler:^(FIRInstanceIDKeyPair *keyPair, NSError *error) { + FIRInstanceID_STRONGIFY(self); + + if (error) { + NSError *newError = + [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidKeyPair]; + newHandler(nil, newError); + + } else { + [self.tokenManager fetchNewTokenWithAuthorizedEntity:[authorizedEntity copy] + scope:[scope copy] + keyPair:keyPair + options:tokenOptions + handler:newHandler]; + } + }]; + }]; +} + +- (void)deleteTokenWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + handler:(FIRInstanceIDDeleteTokenHandler)handler { + _FIRInstanceIDDevAssert(handler != nil && [authorizedEntity length] && [scope length], + @"Invalid authorizedEntity or scope to delete token"); + + if (!handler) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID001, + kFIRInstanceIDInvalidNilHandlerError); + } + + // comparing enums to ints directly throws a warning + FIRInstanceIDErrorCode noError = INT_MAX; + FIRInstanceIDErrorCode errorCode = noError; + + if (![authorizedEntity length]) { + errorCode = kFIRInstanceIDErrorCodeInvalidAuthorizedEntity; + } else if (![scope length]) { + errorCode = kFIRInstanceIDErrorCodeInvalidScope; + } else if (!self.keyPairStore) { + errorCode = kFIRInstanceIDErrorCodeInvalidStart; + } + + FIRInstanceIDDeleteTokenHandler newHandler = ^(NSError *error) { + // If a default token is deleted successfully, reset the defaultFCMToken too. + if (!error && [authorizedEntity isEqualToString:self.fcmSenderID] && + [scope isEqualToString:kFIRInstanceIDDefaultTokenScope]) { + self.defaultFCMToken = nil; + } + dispatch_async(dispatch_get_main_queue(), ^{ + handler(error); + }); + }; + + if (errorCode != noError) { + newHandler([NSError errorWithFIRInstanceIDErrorCode:errorCode]); + return; + } + + FIRInstanceID_WEAKIFY(self); + FIRInstanceIDAuthService *authService = self.tokenManager.authService; + [authService + fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *preferences, NSError *error) { + FIRInstanceID_STRONGIFY(self); + if (error) { + newHandler(error); + return; + } + + FIRInstanceID_WEAKIFY(self); + [self asyncLoadKeyPairWithHandler:^(FIRInstanceIDKeyPair *keyPair, NSError *error) { + FIRInstanceID_STRONGIFY(self); + if (error) { + NSError *newError = + [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidKeyPair]; + newHandler(newError); + + } else { + [self.tokenManager deleteTokenWithAuthorizedEntity:authorizedEntity + scope:scope + keyPair:keyPair + handler:newHandler]; + } + }]; + }]; +} + +- (void)asyncLoadKeyPairWithHandler:(FIRInstanceIDKeyPairHandler)handler { + FIRInstanceID_WEAKIFY(self); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + FIRInstanceID_STRONGIFY(self); + + NSError *error = nil; + FIRInstanceIDKeyPair *keyPair = [self.keyPairStore loadKeyPairWithError:&error]; + dispatch_async(dispatch_get_main_queue(), ^{ + if (error) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInstanceID002, + @"Failed to retreieve keyPair %@", error); + if (handler) { + handler(nil, error); + } + } else if (!keyPair && !error) { + if (handler) { + handler(nil, + [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidKeyPair]); + } + } else { + if (handler) { + handler(keyPair, nil); + } + } + }); + }); +} + +#pragma mark - Identity + +- (void)getIDWithHandler:(FIRInstanceIDHandler)handler { + _FIRInstanceIDDevAssert(handler, @"Invalid nil handler to getIdentity"); + + if (!handler) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID003, + kFIRInstanceIDInvalidNilHandlerError); + return; + } + + void (^callHandlerOnMainThread)(NSString *, NSError *) = ^(NSString *identity, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler(identity, error); + }); + }; + + if (!self.keyPairStore) { + NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidStart]; + callHandlerOnMainThread(nil, error); + return; + } + + FIRInstanceID_WEAKIFY(self); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + FIRInstanceID_STRONGIFY(self); + NSError *error; + NSString *appIdentity = [self.keyPairStore appIdentityWithError:&error]; + // When getID is explicitly called, trigger getToken to make sure token always exists. + // This is to avoid ID conflict (ID is not checked for conflict until we generate a token) + if (appIdentity) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self token]; +#pragma clang diagnostic pop + } + callHandlerOnMainThread(appIdentity, error); + }); +} + +- (void)deleteIDWithHandler:(FIRInstanceIDDeleteHandler)handler { + _FIRInstanceIDDevAssert(handler, @"Invalid nil handler to delete Identity"); + + if (!handler) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID004, + kFIRInstanceIDInvalidNilHandlerError); + return; + } + + void (^callHandlerOnMainThread)(NSError *) = ^(NSError *error) { + if ([NSThread isMainThread]) { + handler(error); + return; + } + dispatch_async(dispatch_get_main_queue(), ^{ + handler(error); + }); + }; + + if (!self.keyPairStore) { + FIRInstanceIDErrorCode error = kFIRInstanceIDErrorCodeInvalidStart; + callHandlerOnMainThread([NSError errorWithFIRInstanceIDErrorCode:error]); + return; + } + + FIRInstanceID_WEAKIFY(self); + void (^deleteTokensHandler)(NSError *) = ^void(NSError *error) { + FIRInstanceID_STRONGIFY(self); + if (error) { + callHandlerOnMainThread(error); + return; + } + [self deleteIdentityWithHandler:^(NSError *error) { + callHandlerOnMainThread(error); + }]; + }; + + [self asyncLoadKeyPairWithHandler:^(FIRInstanceIDKeyPair *keyPair, NSError *error) { + FIRInstanceID_STRONGIFY(self); + if (error) { + NSError *newError = + [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidKeyPair]; + callHandlerOnMainThread(newError); + } else { + [self.tokenManager deleteAllTokensWithKeyPair:keyPair handler:deleteTokensHandler]; + } + }]; +} + +- (void)notifyIdentityReset { + [self deleteIdentityWithHandler:nil]; +} + +// Delete all the local cache checkin, IID and token. +- (void)deleteIdentityWithHandler:(FIRInstanceIDDeleteHandler)handler { + // Delete tokens. + [self.tokenManager deleteAllTokensLocallyWithHandler:^(NSError *deleteTokenError) { + // Reset FCM token. + self.defaultFCMToken = nil; + if (deleteTokenError) { + if (handler) { + handler(deleteTokenError); + } + return; + } + + // Delete Instance ID. + [self.keyPairStore + deleteSavedKeyPairWithSubtype:kFIRInstanceIDKeyPairSubType + handler:^(NSError *error) { + NSError *deletePlistError; + [self.keyPairStore + removeKeyPairCreationTimePlistWithError:&deletePlistError]; + if (error || deletePlistError) { + if (handler) { + // Prefer to use the delete Instance ID error. + error = [NSError + errorWithFIRInstanceIDErrorCode: + kFIRInstanceIDErrorCodeUnknown + userInfo:@{ + NSUnderlyingErrorKey : error + ? error + : deletePlistError + }]; + handler(error); + } + return; + } + // Delete checkin. + [self.tokenManager.authService + resetCheckinWithHandler:^(NSError *error) { + if (error) { + if (handler) { + handler(error); + } + return; + } + // Only request new token if FCM auto initialization is + // enabled. + if ([self isFCMAutoInitEnabled]) { + // Deletion succeeds! Requesting new checkin, IID and token. + // TODO(chliangGoogle) see if dispatch_after is necessary + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(0.5 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + [self defaultTokenWithHandler:nil]; + }); + } + if (handler) { + handler(nil); + } + }]; + }]; + }]; +} + +#pragma mark - Config + ++ (void)load { + [FIRApp registerInternalLibrary:(Class)self + withName:@"fire-iid" + withVersion:FIRInstanceIDCurrentLibraryVersion()]; +} + ++ (nonnull NSArray *)componentsToRegister { + FIRComponentCreationBlock creationBlock = + ^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) { + // Ensure it's cached so it returns the same instance every time instanceID is called. + *isCacheable = YES; + FIRInstanceID *instanceID = [[FIRInstanceID alloc] initPrivately]; + [instanceID start]; + return instanceID; + }; + FIRComponent *instanceIDProvider = + [FIRComponent componentWithProtocol:@protocol(FIRInstanceIDInstanceProvider) + instantiationTiming:FIRInstantiationTimingLazy + dependencies:@[] + creationBlock:creationBlock]; + return @[ instanceIDProvider ]; +} + ++ (void)configureWithApp:(FIRApp *)app { + if (!app.isDefaultApp) { + // Only configure for the default FIRApp. + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeFIRApp002, + @"Firebase Instance ID only works with the default app."); + return; + } + [[FIRInstanceID instanceID] configureInstanceIDWithOptions:app.options app:app]; +} + +- (void)configureInstanceIDWithOptions:(FIROptions *)options app:(FIRApp *)firApp { + NSString *GCMSenderID = options.GCMSenderID; + if (!GCMSenderID.length) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeFIRApp000, + @"Firebase not set up correctly, nil or empty senderID."); + [FIRInstanceID exitWithReason:@"GCM_SENDER_ID must not be nil or empty." forFirebaseApp:firApp]; + return; + } + + self.fcmSenderID = GCMSenderID; + self.firebaseAppID = firApp.options.googleAppID; + + // FCM generates a FCM token during app start for sending push notification to device. + // This is not needed for app extension. + if (![GULAppEnvironmentUtil isAppExtension]) { + [self didCompleteConfigure]; + } +} + ++ (NSError *)configureErrorWithReason:(nonnull NSString *)reason { + NSString *description = + [NSString stringWithFormat:@"Configuration failed for service %@.", kFIRIIDServiceInstanceID]; + if (!reason.length) { + reason = @"Unknown reason"; + } + + NSDictionary *userInfo = + @{NSLocalizedDescriptionKey : description, NSLocalizedFailureReasonErrorKey : reason}; + + return [NSError errorWithDomain:kFIRIIDErrorDomain + code:kFIRIIDErrorCodeInstanceIDFailed + userInfo:userInfo]; +} + +// If the firebaseApp is available we should send logs for the error through it before +// raising an exception. ++ (void)exitWithReason:(nonnull NSString *)reason forFirebaseApp:(FIRApp *)firebaseApp { + [firebaseApp sendLogsWithServiceName:kFIRIIDServiceInstanceID + version:FIRInstanceIDCurrentLibraryVersion() + error:[self configureErrorWithReason:reason]]; + + [NSException raise:kFIRIIDErrorDomain + format:@"Could not configure Firebase InstanceID. %@", reason]; +} + +// This is used to start any operations when we receive FirebaseSDK setup notification +// from FIRCore. +- (void)didCompleteConfigure { + NSString *cachedToken = [self cachedTokenIfAvailable]; + // When there is a cached token, do the token refresh. + if (cachedToken) { + // Clean up expired tokens by checking the token refresh policy. + if ([self.tokenManager checkForTokenRefreshPolicy]) { + // Default token is expired, fetch default token from server. + [self defaultTokenWithHandler:nil]; + } + // Notify FCM with the default token. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + self.defaultFCMToken = [self token]; +#pragma clang diagnostic pop + } else if ([self isFCMAutoInitEnabled]) { + // When there is no cached token, must check auto init is enabled. + // If it's disabled, don't initiate token generation/refresh. + // If no cache token and auto init is enabled, fetch a token from server. + [self defaultTokenWithHandler:nil]; + // Notify FCM with the default token. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + self.defaultFCMToken = [self token]; +#pragma clang diagnostic pop + } + // ONLY checkin when auto data collection is turned on. + if ([self isFCMAutoInitEnabled]) { + [self.tokenManager.authService scheduleCheckin:YES]; + } +} + +- (BOOL)isFCMAutoInitEnabled { + Class messagingClass = NSClassFromString(kFIRInstanceIDFCMSDKClassString); + // Firebase Messaging is not installed, auto init should be disabled since it's for FCM. + if (!messagingClass) { + return NO; + } + + // Messaging doesn't have the singleton method, auto init should be enabled since FCM exists. + SEL instanceSelector = NSSelectorFromString(kFIRInstanceIDFCMSelectorInstance); + if (![messagingClass respondsToSelector:instanceSelector]) { + return YES; + } + + // Get FIRMessaging shared instance. + IMP messagingInstanceIMP = [messagingClass methodForSelector:instanceSelector]; + id (*getMessagingInstance)(id, SEL) = (void *)messagingInstanceIMP; + id messagingInstance = getMessagingInstance(messagingClass, instanceSelector); + + // Messaging doesn't have the property, auto init should be enabled since FCM exists. + SEL autoInitSelector = NSSelectorFromString(kFIRInstanceIDFCMSelectorAutoInitEnabled); + if (![messagingInstance respondsToSelector:autoInitSelector]) { + return YES; + } + + // Get autoInitEnabled method. + IMP isAutoInitEnabledIMP = [messagingInstance methodForSelector:autoInitSelector]; + BOOL (*isAutoInitEnabled)(id, SEL) = (BOOL(*)(id, SEL))isAutoInitEnabledIMP; + + // Check FCM's isAutoInitEnabled property. + return isAutoInitEnabled(messagingInstance, autoInitSelector); +} + +// Actually makes InstanceID instantiate both the IID and Token-related subsystems. +- (void)start { + if (![FIRInstanceIDStore hasSubDirectory:kFIRInstanceIDSubDirectoryName]) { + [FIRInstanceIDStore createSubDirectory:kFIRInstanceIDSubDirectoryName]; + } + + [self setupTokenManager]; + [self setupKeyPairManager]; + [self setupNotificationListeners]; +} + +// Creates the token manager, which is used for fetching, caching, and retrieving tokens. +- (void)setupTokenManager { + self.tokenManager = [[FIRInstanceIDTokenManager alloc] init]; +} + +// Creates a key pair manager, which stores the public/private keys needed to generate an +// application instance ID. +- (void)setupKeyPairManager { + self.keyPairStore = [[FIRInstanceIDKeyPairStore alloc] init]; + if ([self.keyPairStore invalidateKeyPairsIfNeeded]) { + // Reset tokens right away when keypair is deleted, otherwise async call can make first query + // of token happens before reset old tokens during app start. + // TODO(chliangGoogle): Delete all tokens on server too, using + // deleteAllTokensWithKeyPair:handler:. This requires actually retrieving the invalid keypair + // from Keychain, which is something that the key pair store does not currently do. + [self.tokenManager deleteAllTokensLocallyWithHandler:nil]; + } +} + +- (void)setupNotificationListeners { + // To prevent double notifications remove observer from all events during setup. + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center removeObserver:self]; + [center addObserver:self + selector:@selector(notifyIdentityReset) + name:kFIRInstanceIDIdentityInvalidatedNotification + object:nil]; + [center addObserver:self + selector:@selector(notifyAPNSTokenIsSet:) + name:kFIRInstanceIDAPNSTokenNotification + object:nil]; +} + +#pragma mark - Private Helpers +/// Maximum retry count to fetch the default token. ++ (int64_t)maxRetryCountForDefaultToken { + return kMaxRetryCountForDefaultToken; +} + +/// Minimum interval in seconds between retries to fetch the default token. ++ (int64_t)minIntervalForDefaultTokenRetry { + return kMinRetryIntervalForDefaultTokenInSeconds; +} + +/// Maximum retry interval between retries to fetch default token. ++ (int64_t)maxRetryIntervalForDefaultTokenInSeconds { + return kMaxRetryIntervalForDefaultTokenInSeconds; +} + +- (NSInteger)retryIntervalToFetchDefaultToken { + if (self.retryCountForDefaultToken >= [[self class] maxRetryCountForDefaultToken]) { + return (NSInteger)[[self class] maxRetryIntervalForDefaultTokenInSeconds]; + } + // exponential backoff with a fixed initial retry time + // 11s, 22s, 44s, 88s ... + int64_t minInterval = [[self class] minIntervalForDefaultTokenRetry]; + return (NSInteger)MIN( + (1 << self.retryCountForDefaultToken) + minInterval * self.retryCountForDefaultToken, + kMaxRetryIntervalForDefaultTokenInSeconds); +} + +- (void)defaultTokenWithHandler:(nullable FIRInstanceIDTokenHandler)aHandler { + [self defaultTokenWithRetry:NO handler:aHandler]; +} + +/** + * @param retry Indicates if the method is called to perform a retry after a failed attempt. + * If `YES`, then actual token request will be performed even if `self.defaultTokenFetchHandler != + * nil` + */ +- (void)defaultTokenWithRetry:(BOOL)retry handler:(nullable FIRInstanceIDTokenHandler)aHandler { + BOOL shouldPerformRequest = retry || self.defaultTokenFetchHandler == nil; + + if (!self.defaultTokenFetchHandler) { + self.defaultTokenFetchHandler = [[FIRInstanceIDCombinedHandler alloc] init]; + } + + if (aHandler) { + [self.defaultTokenFetchHandler addHandler:aHandler]; + } + + if (!shouldPerformRequest) { + return; + } + + NSDictionary *instanceIDOptions = @{}; + BOOL hasFirebaseMessaging = NSClassFromString(kFIRInstanceIDFCMSDKClassString) != nil; + if (hasFirebaseMessaging && self.apnsTokenData) { + BOOL isSandboxApp = (self.apnsTokenType == FIRInstanceIDAPNSTokenTypeSandbox); + if (self.apnsTokenType == FIRInstanceIDAPNSTokenTypeUnknown) { + isSandboxApp = [self isSandboxApp]; + } + instanceIDOptions = @{ + kFIRInstanceIDTokenOptionsAPNSKey : self.apnsTokenData, + kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(isSandboxApp), + }; + } + + FIRInstanceID_WEAKIFY(self); + FIRInstanceIDTokenHandler newHandler = ^void(NSString *token, NSError *error) { + FIRInstanceID_STRONGIFY(self); + + if (error) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID009, + @"Failed to fetch default token %@", error); + + // This notification can be sent multiple times since we can't guarantee success at any point + // of time. + NSNotification *tokenFetchFailNotification = + [NSNotification notificationWithName:kFIRInstanceIDDefaultGCMTokenFailNotification + object:[error copy]]; + [[NSNotificationQueue defaultQueue] enqueueNotification:tokenFetchFailNotification + postingStyle:NSPostASAP]; + + self.retryCountForDefaultToken = (NSInteger)MIN(self.retryCountForDefaultToken + 1, + [[self class] maxRetryCountForDefaultToken]); + + // Do not retry beyond the maximum limit. + if (self.retryCountForDefaultToken < [[self class] maxRetryCountForDefaultToken]) { + NSInteger retryInterval = [self retryIntervalToFetchDefaultToken]; + [self retryGetDefaultTokenAfter:retryInterval]; + } else { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID007, + @"Failed to retrieve the default FCM token after %ld retries", + (long)self.retryCountForDefaultToken); + [self performDefaultTokenHandlerWithToken:nil error:error]; + } + } else { + // If somebody updated IID with APNS token while our initial request did not have it + // set we need to update it on the server. + NSData *deviceTokenInRequest = instanceIDOptions[kFIRInstanceIDTokenOptionsAPNSKey]; + BOOL isSandboxInRequest = + [instanceIDOptions[kFIRInstanceIDTokenOptionsAPNSIsSandboxKey] boolValue]; + // Note that APNSTupleStringInRequest will be nil if deviceTokenInRequest is nil + NSString *APNSTupleStringInRequest = FIRInstanceIDAPNSTupleStringForTokenAndServerType( + deviceTokenInRequest, isSandboxInRequest); + // If the APNs value either remained nil, or was the same non-nil value, the APNs value + // did not change. + BOOL APNSRemainedSameDuringFetch = + (self.APNSTupleString == nil && APNSTupleStringInRequest == nil) || + ([self.APNSTupleString isEqualToString:APNSTupleStringInRequest]); + if (!APNSRemainedSameDuringFetch && hasFirebaseMessaging) { + // APNs value did change mid-fetch, so the token should be re-fetched with the current APNs + // value. + [self retryGetDefaultTokenAfter:0]; + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeRefetchingTokenForAPNS, + @"Received APNS token while fetching default token. " + @"Refetching default token."); + // Do not notify and handle completion handler since this is a retry. + // Simply return. + return; + } else { + FIRInstanceIDLoggerInfo(kFIRInstanceIDMessageCodeInstanceID010, + @"Successfully fetched default token."); + } + // Post the required notifications if somebody is waiting. + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInstanceID008, @"Got default token %@", + token); + NSString *previousFCMToken = self.defaultFCMToken; + self.defaultFCMToken = token; + + // Only notify of token refresh if we have a new valid token that's different than before + if (self.defaultFCMToken.length && ![self.defaultFCMToken isEqualToString:previousFCMToken]) { + NSNotification *tokenRefreshNotification = + [NSNotification notificationWithName:kFIRInstanceIDTokenRefreshNotification + object:[self.defaultFCMToken copy]]; + [[NSNotificationQueue defaultQueue] enqueueNotification:tokenRefreshNotification + postingStyle:NSPostASAP]; + + [self performDefaultTokenHandlerWithToken:token error:nil]; + } + } + }; + + [self tokenWithAuthorizedEntity:self.fcmSenderID + scope:kFIRInstanceIDDefaultTokenScope + options:instanceIDOptions + handler:newHandler]; +} + +/** + * + */ +- (void)performDefaultTokenHandlerWithToken:(NSString *)token error:(NSError *)error { + if (!self.defaultTokenFetchHandler) { + return; + } + + [self.defaultTokenFetchHandler combinedHandler](token, error); + self.defaultTokenFetchHandler = nil; +} + +- (void)retryGetDefaultTokenAfter:(NSTimeInterval)retryInterval { + FIRInstanceID_WEAKIFY(self); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(retryInterval * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + FIRInstanceID_STRONGIFY(self); + // Pass nil: no new handlers to be added, currently existing handlers + // will be called + [self defaultTokenWithRetry:YES handler:nil]; + }); +} + +#pragma mark - APNS Token +// This should only be triggered from FCM. +- (void)notifyAPNSTokenIsSet:(NSNotification *)notification { + NSData *token = notification.object; + if (!token || ![token isKindOfClass:[NSData class]]) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInternal002, @"Invalid APNS token type %@", + NSStringFromClass([notification.object class])); + return; + } + NSInteger type = [notification.userInfo[kFIRInstanceIDAPNSTokenType] integerValue]; + + // The APNS token is being added, or has changed (rare) + if ([self.apnsTokenData isEqualToData:token]) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInstanceID011, + @"Trying to reset APNS token to the same value. Will return"); + return; + } + // Use this token type for when we have to automatically fetch tokens in the future + self.apnsTokenType = type; + BOOL isSandboxApp = (type == FIRInstanceIDAPNSTokenTypeSandbox); + if (self.apnsTokenType == FIRInstanceIDAPNSTokenTypeUnknown) { + isSandboxApp = [self isSandboxApp]; + } + self.apnsTokenData = [token copy]; + self.APNSTupleString = FIRInstanceIDAPNSTupleStringForTokenAndServerType(token, isSandboxApp); + + // Pro-actively invalidate the default token, if the APNs change makes it + // invalid. Previously, we invalidated just before fetching the token. + NSArray *invalidatedTokens = + [self.tokenManager updateTokensToAPNSDeviceToken:self.apnsTokenData isSandbox:isSandboxApp]; + + // Re-fetch any invalidated tokens automatically, this time with the current APNs token, so that + // they are up-to-date. + if (invalidatedTokens.count > 0) { + FIRInstanceID_WEAKIFY(self); + [self asyncLoadKeyPairWithHandler:^(FIRInstanceIDKeyPair *keyPair, NSError *error) { + FIRInstanceID_STRONGIFY(self); + + NSMutableDictionary *tokenOptions = [@{ + kFIRInstanceIDTokenOptionsAPNSKey : self.apnsTokenData, + kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(isSandboxApp) + } mutableCopy]; + if (self.firebaseAppID) { + tokenOptions[kFIRInstanceIDTokenOptionsFirebaseAppIDKey] = self.firebaseAppID; + } + + for (FIRInstanceIDTokenInfo *tokenInfo in invalidatedTokens) { + if ([tokenInfo.token isEqualToString:self.defaultFCMToken]) { + // We will perform a special fetch for the default FCM token, so that the delegate methods + // are called. For all others, we will do an internal re-fetch. + [self defaultTokenWithHandler:nil]; + } else { + [self.tokenManager fetchNewTokenWithAuthorizedEntity:tokenInfo.authorizedEntity + scope:tokenInfo.scope + keyPair:keyPair + options:tokenOptions + handler:^(NSString *_Nullable token, + NSError *_Nullable error){ + + }]; + } + } + }]; + } +} + +- (BOOL)isSandboxApp { + static BOOL isSandboxApp = YES; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + isSandboxApp = ![self isProductionApp]; + }); + return isSandboxApp; +} + +- (BOOL)isProductionApp { + const BOOL defaultAppTypeProd = YES; + + NSError *error = nil; + + Class envClass = NSClassFromString(@"FIRAppEnvironmentUtil"); + SEL isSimulatorSelector = NSSelectorFromString(@"isSimulator"); + if ([envClass respondsToSelector:isSimulatorSelector]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + if ([envClass performSelector:isSimulatorSelector]) { +#pragma clang diagnostic pop + [self logAPNSConfigurationError:@"Running InstanceID on a simulator doesn't have APNS. " + @"Use prod profile by default."]; + return defaultAppTypeProd; + } + } + + NSString *path = [[[NSBundle mainBundle] bundlePath] + stringByAppendingPathComponent:@"embedded.mobileprovision"]; + + // Apps distributed via AppStore or TestFlight use the Production APNS certificates. + SEL isFromAppStoreSelector = NSSelectorFromString(@"isFromAppStore"); + if ([envClass respondsToSelector:isFromAppStoreSelector]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + if ([envClass performSelector:isFromAppStoreSelector]) { +#pragma clang diagnostic pop + return defaultAppTypeProd; + } + } + + SEL isAppStoreReceiptSandboxSelector = NSSelectorFromString(@"isAppStoreReceiptSandbox"); + if ([envClass respondsToSelector:isAppStoreReceiptSandboxSelector]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + if ([envClass performSelector:isAppStoreReceiptSandboxSelector] && !path.length) { +#pragma clang diagnostic pop + // Distributed via TestFlight + return defaultAppTypeProd; + } + } + + NSMutableData *profileData = [NSMutableData dataWithContentsOfFile:path options:0 error:&error]; + + if (!profileData.length || error) { + NSString *errorString = + [NSString stringWithFormat:@"Error while reading embedded mobileprovision %@", error]; + [self logAPNSConfigurationError:errorString]; + return defaultAppTypeProd; + } + + // The "embedded.mobileprovision" sometimes contains characters with value 0, which signals the + // end of a c-string and halts the ASCII parser, or with value > 127, which violates strict 7-bit + // ASCII. Replace any 0s or invalid characters in the input. + uint8_t *profileBytes = (uint8_t *)profileData.bytes; + for (int i = 0; i < profileData.length; i++) { + uint8_t currentByte = profileBytes[i]; + if (!currentByte || currentByte > 127) { + profileBytes[i] = '.'; + } + } + + NSString *embeddedProfile = [[NSString alloc] initWithBytesNoCopy:profileBytes + length:profileData.length + encoding:NSASCIIStringEncoding + freeWhenDone:NO]; + + if (error || !embeddedProfile.length) { + NSString *errorString = + [NSString stringWithFormat:@"Error while reading embedded mobileprovision %@", error]; + [self logAPNSConfigurationError:errorString]; + return defaultAppTypeProd; + } + + NSScanner *scanner = [NSScanner scannerWithString:embeddedProfile]; + NSString *plistContents; + if ([scanner scanUpToString:@"" intoString:&plistContents]) { + plistContents = [plistContents stringByAppendingString:@""]; + } + } + + if (!plistContents.length) { + return defaultAppTypeProd; + } + + NSData *data = [plistContents dataUsingEncoding:NSUTF8StringEncoding]; + if (!data.length) { + [self logAPNSConfigurationError:@"Couldn't read plist fetched from embedded mobileprovision"]; + return defaultAppTypeProd; + } + + NSError *plistMapError; + id plistData = [NSPropertyListSerialization propertyListWithData:data + options:NSPropertyListImmutable + format:nil + error:&plistMapError]; + if (plistMapError || ![plistData isKindOfClass:[NSDictionary class]]) { + NSString *errorString = + [NSString stringWithFormat:@"Error while converting assumed plist to dict %@", + plistMapError.localizedDescription]; + [self logAPNSConfigurationError:errorString]; + return defaultAppTypeProd; + } + NSDictionary *plistMap = (NSDictionary *)plistData; + + if ([plistMap valueForKeyPath:@"ProvisionedDevices"]) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInstanceID012, + @"Provisioning profile has specifically provisioned devices, " + @"most likely a Dev profile."); + } + + NSString *apsEnvironment = [plistMap valueForKeyPath:kEntitlementsAPSEnvironmentKey]; + NSString *debugString __unused = + [NSString stringWithFormat:@"APNS Environment in profile: %@", apsEnvironment]; + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInstanceID013, @"%@", debugString); + + // No aps-environment in the profile. + if (!apsEnvironment.length) { + [self logAPNSConfigurationError:@"No aps-environment set. If testing on a device APNS is not " + @"correctly configured. Please recheck your provisioning " + @"profiles. If testing on a simulator this is fine since APNS " + @"doesn't work on the simulator."]; + return defaultAppTypeProd; + } + + if ([apsEnvironment isEqualToString:kAPSEnvironmentDevelopmentValue]) { + return NO; + } + + return defaultAppTypeProd; +} + +/// Log error messages only when Messaging exists in the pod. +- (void)logAPNSConfigurationError:(NSString *)errorString { + BOOL hasFirebaseMessaging = NSClassFromString(kFIRInstanceIDFCMSDKClassString) != nil; + if (hasFirebaseMessaging) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeInstanceID014, @"%@", errorString); + } else { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInstanceID015, @"%@", errorString); + } +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDAPNSInfo.h b/Firebase/InstanceID/FIRInstanceIDAPNSInfo.h new file mode 100644 index 00000000000..92b2469b1fb --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDAPNSInfo.h @@ -0,0 +1,64 @@ +/* + * Copyright 2019 Google + * + * 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 + +/** + * Represents an APNS device token and whether its environment is for sandbox. + * It can read from and write to an NSDictionary for simple serialization. + */ +@interface FIRInstanceIDAPNSInfo : NSObject + +/// The APNs device token, provided by the OS to the application delegate +@property(nonatomic, readonly, strong) NSData *deviceToken; +/// Represents whether or not this is deviceToken is for the sandbox +/// environment, or production. +@property(nonatomic, readonly, getter=isSandbox) BOOL sandbox; + +/** + * Initializes the receiver with an APNs device token, and boolean + * representing whether that token is for the sandbox environment. + * + * @param deviceToken The APNs device token typically provided by the + * operating system. + * @param isSandbox YES if the APNs device token is for the sandbox + * environment, or NO if it is for production. + * @return An instance of FIRInstanceIDAPNSInfo. + */ +- (instancetype)initWithDeviceToken:(NSData *)deviceToken isSandbox:(BOOL)isSandbox; + +/** + * Initializes the receiver from a token options dictionary containing data + * within the `kFIRInstanceIDTokenOptionsAPNSKey` and + * `kFIRInstanceIDTokenOptionsAPNSIsSandboxKey` keys. The token should be an + * NSData blob, and the sandbox value should be an NSNumber + * representing a boolean value. + * + * @param dictionary A dictionary containing values under the keys + * `kFIRInstanceIDTokenOptionsAPNSKey` and + * `kFIRInstanceIDTokenOptionsAPNSIsSandboxKey`. + * @return An instance of FIRInstanceIDAPNSInfo, or nil if the + * dictionary data was invalid or missing. + */ +- (nullable instancetype)initWithTokenOptionsDictionary:(NSDictionary *)dictionary; + +- (BOOL)isEqualToAPNSInfo:(FIRInstanceIDAPNSInfo *)otherInfo; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InstanceID/FIRInstanceIDAPNSInfo.m b/Firebase/InstanceID/FIRInstanceIDAPNSInfo.m new file mode 100644 index 00000000000..d1f9d080024 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDAPNSInfo.m @@ -0,0 +1,79 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDAPNSInfo.h" + +#import "FIRInstanceIDConstants.h" + +/// The key used to find the APNs device token in an archive. +NSString *const kFIRInstanceIDAPNSInfoTokenKey = @"device_token"; +/// The key used to find the sandbox value in an archive. +NSString *const kFIRInstanceIDAPNSInfoSandboxKey = @"sandbox"; + +@implementation FIRInstanceIDAPNSInfo + +- (instancetype)initWithDeviceToken:(NSData *)deviceToken isSandbox:(BOOL)isSandbox { + self = [super init]; + if (self) { + _deviceToken = [deviceToken copy]; + _sandbox = isSandbox; + } + return self; +} + +- (instancetype)initWithTokenOptionsDictionary:(NSDictionary *)dictionary { + id deviceToken = dictionary[kFIRInstanceIDTokenOptionsAPNSKey]; + if (![deviceToken isKindOfClass:[NSData class]]) { + return nil; + } + + id isSandbox = dictionary[kFIRInstanceIDTokenOptionsAPNSIsSandboxKey]; + if (![isSandbox isKindOfClass:[NSNumber class]]) { + return nil; + } + self = [super init]; + if (self) { + _deviceToken = (NSData *)deviceToken; + _sandbox = ((NSNumber *)isSandbox).boolValue; + } + return self; +} + +#pragma mark - NSCoding + +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { + id deviceToken = [aDecoder decodeObjectForKey:kFIRInstanceIDAPNSInfoTokenKey]; + if (![deviceToken isKindOfClass:[NSData class]]) { + return nil; + } + BOOL isSandbox = [aDecoder decodeBoolForKey:kFIRInstanceIDAPNSInfoSandboxKey]; + return [self initWithDeviceToken:(NSData *)deviceToken isSandbox:isSandbox]; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self.deviceToken forKey:kFIRInstanceIDAPNSInfoTokenKey]; + [aCoder encodeBool:self.sandbox forKey:kFIRInstanceIDAPNSInfoSandboxKey]; +} + +- (BOOL)isEqualToAPNSInfo:(FIRInstanceIDAPNSInfo *)otherInfo { + if ([super isEqual:otherInfo]) { + return YES; + } + return ([self.deviceToken isEqualToData:otherInfo.deviceToken] && + self.isSandbox == otherInfo.isSandbox); +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDAuthKeyChain.h b/Firebase/InstanceID/FIRInstanceIDAuthKeyChain.h new file mode 100644 index 00000000000..347dddac19c --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDAuthKeyChain.h @@ -0,0 +1,98 @@ +/* + * Copyright 2019 Google + * + * 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 + +extern NSString *__nonnull const kFIRInstanceIDKeychainWildcardIdentifier; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Wrapper around storing FCM auth data in iOS keychain. + */ +@interface FIRInstanceIDAuthKeychain : NSObject + +/** + * Designated Initializer. Init a generic `SecClassGenericPassword` keychain with `identifier` + * as the `kSecAttrGeneric`. + * + * @param identifier The generic attribute to be used by the keychain. + * + * @return A Keychain object with `kSecAttrGeneric` attribute set to identifier. + */ +- (instancetype)initWithIdentifier:(NSString *)identifier; + +/** + * Get keychain items matching the given service and account. The service and/or account + * can be a wildcard (`kFIRInstanceIDKeychainWildcardIdentifier`), which case the query + * will include all items matching any services and/or accounts. + * + * @param service The kSecAttrService used to save the password. Can be wildcard. + * @param account The kSecAttrAccount used to save the password. Can be wildcard. + * + * @return An array of |NSData|s matching the provided inputs. + */ +- (NSArray *)itemsMatchingService:(NSString *)service account:(NSString *)account; + +/** + * Get keychain item for a given service and account. + * + * @param service The kSecAttrService used to save the password. + * @param account The kSecAttrAccount used to save the password. + * + * @return A cached keychain item for a given account and service, or nil if it was not + * found or could not be retrieved. + */ +- (NSData *)dataForService:(NSString *)service account:(NSString *)account; + +/** + * Remove the cached items from the keychain matching the service, account and access group. + * In case the items do not exist, YES is returned but with a valid error object with code + * `errSecItemNotFound`. + * + * @param service The kSecAttrService used to save the password. + * @param account The kSecAttrAccount used to save the password. + * @param handler The callback handler which is invoked when the remove operation is complete, with + * an error if there is any. + */ +- (void)removeItemsMatchingService:(NSString *)service + account:(NSString *)account + handler:(nullable void (^)(NSError *error))handler; + +/** + * Set the data for a given service and account with a specific accessibility. If + * accessibility is NULL we use `kSecAttrAccessibleAlwaysThisDeviceOnly` which + * prevents backup and restore to iCloud, and works for app extension that can + * execute right after a device is restarted (and not unlocked). + * + * @param data The data to save. + * @param service The `kSecAttrService` used to save the password. + * @param accessibility The `kSecAttrAccessibility` used to save the password. If NULL + * set this to `kSecAttrAccessibleAlwaysThisDeviceOnly`. + * @param account The `kSecAttrAccount` used to save the password. + * @param handler The callback handler which is invoked when the add operation is complete, + * with an error if there is any. + * + */ +- (void)setData:(NSData *)data + forService:(NSString *)service + accessibility:(nullable CFTypeRef)accessibility + account:(NSString *)account + handler:(nullable void (^)(NSError *))handler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InstanceID/FIRInstanceIDAuthKeyChain.m b/Firebase/InstanceID/FIRInstanceIDAuthKeyChain.m new file mode 100644 index 00000000000..f75362fd33e --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDAuthKeyChain.m @@ -0,0 +1,215 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDAuthKeyChain.h" +#import "FIRInstanceIDKeychain.h" +#import "FIRInstanceIDLogger.h" + +/** + * The error type representing why we couldn't read data from the keychain. + */ +typedef NS_ENUM(int, FIRInstanceIDKeychainErrorType) { + kFIRInstanceIDKeychainErrorBadArguments = -1301, +}; + +NSString *const kFIRInstanceIDKeychainWildcardIdentifier = @"*"; + +@interface FIRInstanceIDAuthKeychain () + +@property(nonatomic, copy) NSString *generic; +// cachedKeychainData is keyed by service and account, the value is an array of NSData. +// It is used to cache the tokens per service, per account, as well as checkin data per service, +// per account inside the keychain. +@property(nonatomic) + NSMutableDictionary *> *> + *cachedKeychainData; + +@end + +@implementation FIRInstanceIDAuthKeychain + +- (instancetype)initWithIdentifier:(NSString *)identifier { + self = [super init]; + if (self) { + _generic = [identifier copy]; + _cachedKeychainData = [[NSMutableDictionary alloc] init]; + } + return self; +} + ++ (NSMutableDictionary *)keychainQueryForService:(NSString *)service + account:(NSString *)account + generic:(NSString *)generic { + NSDictionary *query = @{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword}; + + NSMutableDictionary *finalQuery = [NSMutableDictionary dictionaryWithDictionary:query]; + if ([generic length] && ![kFIRInstanceIDKeychainWildcardIdentifier isEqualToString:generic]) { + finalQuery[(__bridge NSString *)kSecAttrGeneric] = generic; + } + if ([account length] && ![kFIRInstanceIDKeychainWildcardIdentifier isEqualToString:account]) { + finalQuery[(__bridge NSString *)kSecAttrAccount] = account; + } + if ([service length] && ![kFIRInstanceIDKeychainWildcardIdentifier isEqualToString:service]) { + finalQuery[(__bridge NSString *)kSecAttrService] = service; + } + return finalQuery; +} + +- (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account { + return [[self class] keychainQueryForService:service account:account generic:self.generic]; +} + +- (NSArray *)itemsMatchingService:(NSString *)service account:(NSString *)account { + // If query wildcard service, it asks for all the results, which always query from keychain. + if (![service isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier] && + ![account isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier] && + _cachedKeychainData[service][account]) { + // As long as service, account array exist, even it's empty, it means we've queried it before, + // returns the cache value. + return _cachedKeychainData[service][account]; + } + + NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account]; + + NSMutableArray *results; + keychainQuery[(__bridge id)kSecReturnData] = (__bridge id)kCFBooleanTrue; + keychainQuery[(__bridge id)kSecReturnAttributes] = (__bridge id)kCFBooleanTrue; + keychainQuery[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll; + // FIRInstanceIDKeychain should only take a query and return a result, will handle the query here. + NSArray *passwordInfos = + CFBridgingRelease([[FIRInstanceIDKeychain sharedInstance] itemWithQuery:keychainQuery]); + + if (!passwordInfos) { + // Nothing was found, simply return from this sync block. + // Make sure to label the cache entry empty, signaling that we've queried this entry. + if ([service isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier] || + [account isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier]) { + // Do not update cache if it's wildcard query. + return @[]; + } else if (_cachedKeychainData[service]) { + [_cachedKeychainData[service] setObject:@[] forKey:account]; + } else { + [_cachedKeychainData setObject:[@{account : @[]} mutableCopy] forKey:service]; + } + return @[]; + } + NSInteger numPasswords = passwordInfos.count; + results = [[NSMutableArray alloc] init]; + for (NSUInteger i = 0; i < numPasswords; i++) { + NSDictionary *passwordInfo = [passwordInfos objectAtIndex:i]; + if (passwordInfo[(__bridge id)kSecValueData]) { + [results addObject:passwordInfo[(__bridge id)kSecValueData]]; + } + } + + // We query the keychain because it didn't exist in cache, now query is done, update the result in + // the cache. + if ([service isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier] || + [account isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier]) { + // Do not update cache if it's wildcard query. + return [results copy]; + } else if (_cachedKeychainData[service]) { + [_cachedKeychainData[service] setObject:[results copy] forKey:account]; + } else { + NSMutableDictionary *entry = [@{account : [results copy]} mutableCopy]; + [_cachedKeychainData setObject:entry forKey:service]; + } + return [results copy]; +} + +- (NSData *)dataForService:(NSString *)service account:(NSString *)account { + NSArray *items = [self itemsMatchingService:service account:account]; + // If items is nil or empty, nil will be returned. + return items.firstObject; +} + +- (void)removeItemsMatchingService:(NSString *)service + account:(NSString *)account + handler:(void (^)(NSError *error))handler { + if ([service isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier]) { + // Delete all keychain items. + _cachedKeychainData = [[NSMutableDictionary alloc] init]; + } else if ([account isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier]) { + // Delete all entries under service, + if (_cachedKeychainData[service]) { + _cachedKeychainData[service] = [[NSMutableDictionary alloc] init]; + } + } else if (_cachedKeychainData[service]) { + // We should keep the service/account entry instead of nil so we know + // it's "empty entry" instead of "not query from keychain yet". + [_cachedKeychainData[service] setObject:@[] forKey:account]; + } else { + [_cachedKeychainData setObject:[@{account : @[]} mutableCopy] forKey:service]; + } + NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account]; + [[FIRInstanceIDKeychain sharedInstance] removeItemWithQuery:keychainQuery handler:handler]; +} + +- (void)setData:(NSData *)data + forService:(NSString *)service + accessibility:(CFTypeRef)accessibility + account:(NSString *)account + handler:(void (^)(NSError *))handler { + if ([service isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier] || + [account isEqualToString:kFIRInstanceIDKeychainWildcardIdentifier]) { + if (handler) { + handler([NSError errorWithDomain:kFIRInstanceIDKeychainErrorDomain + code:kFIRInstanceIDKeychainErrorBadArguments + userInfo:nil]); + } + return; + } + [self removeItemsMatchingService:service + account:account + handler:^(NSError *error) { + if (error) { + if (handler) { + handler(error); + } + return; + } + if (data.length > 0) { + NSMutableDictionary *keychainQuery = + [self keychainQueryForService:service account:account]; + keychainQuery[(__bridge id)kSecValueData] = data; + + if (accessibility != NULL) { + keychainQuery[(__bridge id)kSecAttrAccessible] = + (__bridge id)accessibility; + } else { + // Defaults to No backup + keychainQuery[(__bridge id)kSecAttrAccessible] = + (__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly; + } + [[FIRInstanceIDKeychain sharedInstance] + addItemWithQuery:keychainQuery + handler:handler]; + } + }]; + // Set the cache value. This must happen after removeItemsMatchingService:account:handler was + // called, so the cache value was reset before setting a new value. + if (_cachedKeychainData[service]) { + if (_cachedKeychainData[service][account]) { + _cachedKeychainData[service][account] = @[ data ]; + } else { + [_cachedKeychainData[service] setObject:@[ data ] forKey:account]; + } + } else { + [_cachedKeychainData setObject:[@{account : @[ data ]} mutableCopy] forKey:service]; + } +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDAuthService.h b/Firebase/InstanceID/FIRInstanceIDAuthService.h new file mode 100644 index 00000000000..1fb715e7c87 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDAuthService.h @@ -0,0 +1,91 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDCheckinService.h" + +@class FIRInstanceIDCheckinPreferences; +@class FIRInstanceIDStore; + +/** + * FIRInstanceIDAuthService is responsible for retrieving, caching, and supplying checkin info + * for the rest of Instance ID. A checkin can be scheduled, meaning that it will keep retrying the + * checkin request until it is successful. A checkin can also be requested directly, with a + * completion handler. + */ +@interface FIRInstanceIDAuthService : NSObject + +/** + * Used only for testing. In addition to taking a store (for locally caching the checkin info), it + * also takes a checkinService. + */ +- (instancetype)initWithCheckinService:(FIRInstanceIDCheckinService *)checkinService + store:(FIRInstanceIDStore *)store; + +/** + * Initializes the auth service given a store (which provides the local caching of checkin info). + * This initializer will create its own instance of FIRInstanceIDCheckinService. + */ +- (instancetype)initWithStore:(FIRInstanceIDStore *)store; + +#pragma mark - Checkin Service + +/** + * Checks if the current deviceID and secret are valid or not. + * + * @return YES if the checkin credentials are valid else NO. + */ +- (BOOL)hasValidCheckinInfo; + +/** + * Fetch checkin info from the server. This would usually refresh the existing + * checkin credentials for the current app. + * + * @param handler The completion handler to invoke once the checkin info has been + * refreshed. + */ +- (void)fetchCheckinInfoWithHandler:(FIRInstanceIDDeviceCheckinCompletion)handler; + +/** + * Schedule checkin. Will hit the network only if the currently loaded checkin + * preferences are stale. + * + * @param immediately YES if we want it to be scheduled immediately else NO. + */ +- (void)scheduleCheckin:(BOOL)immediately; + +/** + * Returns the checkin preferences currently loaded in memory. The Checkin preferences + * can be either valid or invalid. + * + * @return The checkin preferences loaded in memory. + */ +- (FIRInstanceIDCheckinPreferences *)checkinPreferences; + +/** + * Cancels any ongoing checkin fetch, if any. + */ +- (void)stopCheckinRequest; + +/** + * Resets the checkin information. + * + * @param handler The callback handler which is invoked when checkin reset is complete, + * with an error if there is any. + */ +- (void)resetCheckinWithHandler:(void (^)(NSError *error))handler; + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDAuthService.m b/Firebase/InstanceID/FIRInstanceIDAuthService.m new file mode 100644 index 00000000000..8c33c440898 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDAuthService.m @@ -0,0 +1,302 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDAuthService.h" + +#import "FIRInstanceIDCheckinPreferences+Internal.h" +#import "FIRInstanceIDCheckinPreferences.h" +#import "FIRInstanceIDCheckinPreferences_Private.h" +#import "FIRInstanceIDConstants.h" +#import "FIRInstanceIDDefines.h" +#import "FIRInstanceIDLogger.h" +#import "FIRInstanceIDStore.h" +#import "NSError+FIRInstanceID.h" + +// Max time interval between checkin retry in seconds. +static const int64_t kMaxCheckinRetryIntervalInSeconds = 1 << 5; + +@interface FIRInstanceIDAuthService () + +// Used to retrieve and cache the checkin info to disk and Keychain. +@property(nonatomic, readwrite, strong) FIRInstanceIDStore *store; +// Used to perform single checkin fetches. +@property(nonatomic, readwrite, strong) FIRInstanceIDCheckinService *checkinService; +// The current checkin info. It will be compared to what is retrieved to determine whether it is +// different than what is in the cache. +@property(nonatomic, readwrite, strong) FIRInstanceIDCheckinPreferences *checkinPreferences; + +// This array will track multiple handlers waiting for checkin to be performed. When a checkin +// request completes, all the handlers will be notified. +// Changes to the checkinHandlers array should happen in a thread-safe manner. +@property(nonatomic, readonly, strong) + NSMutableArray *checkinHandlers; + +// This is set to true if there is a checkin request in-flight. +@property(atomic, readwrite, assign) BOOL isCheckinInProgress; +// This timer is used a perform checkin retries. It is cancellable. +@property(atomic, readwrite, strong) NSTimer *scheduledCheckinTimer; +// The number of times checkin has been retried during a scheduled checkin. +@property(atomic, readwrite, assign) int checkinRetryCount; + +@end + +@implementation FIRInstanceIDAuthService + +- (instancetype)initWithCheckinService:(FIRInstanceIDCheckinService *)checkinService + store:(FIRInstanceIDStore *)store { + self = [super init]; + if (self) { + _store = store; + _checkinPreferences = [_store cachedCheckinPreferences]; + _checkinService = checkinService; + _checkinHandlers = [NSMutableArray array]; + } + return self; +} + +- (void)dealloc { + [_scheduledCheckinTimer invalidate]; +} + +- (instancetype)initWithStore:(FIRInstanceIDStore *)store { + FIRInstanceIDCheckinService *checkinService = [[FIRInstanceIDCheckinService alloc] init]; + return [self initWithCheckinService:checkinService store:store]; +} + +#pragma mark - Schedule Checkin + +- (void)scheduleCheckin:(BOOL)immediately { + // Checkin is still valid, so a remote checkin is not required. + if ([self.checkinPreferences hasValidCheckinInfo]) { + return; + } + + // Checkin is already scheduled, so this (non-immediate) request can be ignored. + if (!immediately && [self.scheduledCheckinTimer isValid]) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeAuthService000, + @"Checkin sync already scheduled. Will not schedule."); + return; + } + + if (immediately) { + [self performScheduledCheckin]; + } else { + int64_t checkinRetryDuration = [self calculateNextCheckinRetryIntervalInSeconds]; + [self startCheckinTimerWithDuration:(NSTimeInterval)checkinRetryDuration]; + } +} + +- (void)startCheckinTimerWithDuration:(NSTimeInterval)timerDuration { + self.scheduledCheckinTimer = + [NSTimer scheduledTimerWithTimeInterval:timerDuration + target:self + selector:@selector(onScheduledCheckinTimerFired:) + userInfo:nil + repeats:NO]; + // Add some tolerance to the timer, to allow iOS to be more flexible with this timer + self.scheduledCheckinTimer.tolerance = 0.5; +} + +- (void)clearScheduledCheckinTimer { + [self.scheduledCheckinTimer invalidate]; + self.scheduledCheckinTimer = nil; +} + +- (void)onScheduledCheckinTimerFired:(NSTimer *)timer { + [self performScheduledCheckin]; +} + +- (void)performScheduledCheckin { + // No checkin scheduled as of now. + [self clearScheduledCheckinTimer]; + + // Checkin is still valid, so a remote checkin is not required. + if ([self.checkinPreferences hasValidCheckinInfo]) { + return; + } + + FIRInstanceID_WEAKIFY(self); + [self + fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *preferences, NSError *error) { + FIRInstanceID_STRONGIFY(self); + self.checkinRetryCount++; + + if (error) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeAuthService001, @"Checkin error %@.", + error); + + dispatch_async(dispatch_get_main_queue(), ^{ + // Schedule another checkin + [self scheduleCheckin:NO]; + }); + + } else { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeAuthService002, @"Checkin success."); + } + }]; +} + +- (int64_t)calculateNextCheckinRetryIntervalInSeconds { + // persistent failures can lead to overflow prevent that. + if (self.checkinRetryCount >= 10) { + return kMaxCheckinRetryIntervalInSeconds; + } + return MIN(1 << self.checkinRetryCount, kMaxCheckinRetryIntervalInSeconds); +} + +#pragma mark - Checkin Service + +- (BOOL)hasValidCheckinInfo { + return [self.checkinPreferences hasValidCheckinInfo]; +} + +- (void)fetchCheckinInfoWithHandler:(nonnull FIRInstanceIDDeviceCheckinCompletion)handler { + // Perform any changes to self.checkinHandlers and _isCheckinInProgress in a thread-safe way. + @synchronized(self) { + [self.checkinHandlers addObject:handler]; + + if (_isCheckinInProgress) { + // Nothing more to do until our checkin request is done + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeAuthServiceCheckinInProgress, + @"Checkin is in progress\n"); + return; + } + } + + // Checkin is still valid, so a remote checkin is not required. + if ([self.checkinPreferences hasValidCheckinInfo]) { + [self notifyCheckinHandlersWithCheckin:self.checkinPreferences error:nil]; + return; + } + + @synchronized(self) { + _isCheckinInProgress = YES; + } + [self.checkinService + checkinWithExistingCheckin:self.checkinPreferences + completion:^(FIRInstanceIDCheckinPreferences *checkinPreferences, + NSError *error) { + @synchronized(self) { + self->_isCheckinInProgress = NO; + } + if (error) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeAuthService003, + @"Failed to checkin device %@", error); + [self notifyCheckinHandlersWithCheckin:nil error:error]; + return; + } + + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeAuthService004, + @"Successfully got checkin credentials"); + BOOL hasSameCachedPreferences = + [self cachedCheckinMatchesCheckin:checkinPreferences]; + checkinPreferences.hasPreCachedAuthCredentials = hasSameCachedPreferences; + + // Update to the most recent checkin preferences + self.checkinPreferences = checkinPreferences; + + // Save the checkin info to disk + // Keychain might not be accessible, so confirm that checkin preferences can + // be saved + [self.store + saveCheckinPreferences:checkinPreferences + handler:^(NSError *checkinSaveError) { + if (checkinSaveError && !hasSameCachedPreferences) { + // The checkin info was new, but it couldn't be + // written to the Keychain. Delete any stuff that was + // cached in memory. This doesn't delete any + // previously persisted preferences. + FIRInstanceIDLoggerError( + kFIRInstanceIDMessageCodeService004, + @"Unable to save checkin info, resetting " + @"checkin preferences " + "in memory."); + [checkinPreferences reset]; + [self + notifyCheckinHandlersWithCheckin:nil + error: + checkinSaveError]; + } else { + // The checkin is either new, or it was the same (and + // it couldn't be saved). Either way, report that the + // checkin preferences were received successfully. + [self notifyCheckinHandlersWithCheckin: + checkinPreferences + error:nil]; + if (!hasSameCachedPreferences) { + // Checkin is new. + // Notify any listeners that might be waiting for + // checkin to be fetched, such as Firebase + // Messaging (for its MCS connection). + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] + postNotificationName: + kFIRInstanceIDCheckinFetchedNotification + object:nil]; + }); + } + } + }]; + }]; +} + +- (FIRInstanceIDCheckinPreferences *)checkinPreferences { + return _checkinPreferences; +} + +- (void)stopCheckinRequest { + [self.checkinService stopFetching]; +} + +- (void)resetCheckinWithHandler:(void (^)(NSError *error))handler { + [self.store removeCheckinPreferencesWithHandler:^(NSError *error) { + if (!error) { + self.checkinPreferences = nil; + } + if (handler) { + handler(error); + } + }]; +} + +#pragma mark - Private + +/** + * Goes through the current list of checkin handlers and fires them with the same checkin and/or + * error info. The checkin handlers will get cleared after. + */ +- (void)notifyCheckinHandlersWithCheckin:(nullable FIRInstanceIDCheckinPreferences *)checkin + error:(nullable NSError *)error { + @synchronized(self) { + for (FIRInstanceIDDeviceCheckinCompletion handler in self.checkinHandlers) { + handler(checkin, error); + } + [self.checkinHandlers removeAllObjects]; + } +} + +/** + * Given a |checkin|, it will compare it to the current checkinPreferences to see if the + * deviceID and secretToken are the same. + */ +- (BOOL)cachedCheckinMatchesCheckin:(FIRInstanceIDCheckinPreferences *)checkin { + if (self.checkinPreferences && checkin) { + return ([self.checkinPreferences.deviceID isEqualToString:checkin.deviceID] && + [self.checkinPreferences.secretToken isEqualToString:checkin.secretToken]); + } + return NO; +} +@end diff --git a/Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.h b/Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.h new file mode 100644 index 00000000000..bccaced8903 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.h @@ -0,0 +1,81 @@ +/* + * Copyright 2019 Google + * + * 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 + +@interface FIRInstanceIDBackupExcludedPlist : NSObject + +/** + * Caches the plist contents in memory so we don't hit the disk each time we want + * to query something in the plist. This is loaded lazily i.e. if you write to the + * plist the contents you want to write will be stored here if the write was + * successful. The other case where it is loaded is if you read the plist contents + * by calling `contentAsDictionary`. + * + * In case you write to the plist and then try to read the file using + * `contentAsDictionary` we would just return the cachedPlistContents since it would + * represent the disk contents. + */ +@property(nonatomic, readonly, strong) NSDictionary *cachedPlistContents; + +/** + * Init a backup excluded plist file. + * + * @param fileName The filename for the plist file. + * @param subDirectory The subdirectory in Application Support to save the plist. + * + * @return Helper which allows to read write data to a backup excluded plist. + */ +- (instancetype)initWithFileName:(NSString *)fileName subDirectory:(NSString *)subDirectory; + +/** + * Write dictionary data to the backup excluded plist file. If the file does not exist + * it would be created before writing to it. + * + * @param dict The data to be written to the plist. + * @param error The error object if any while writing the data. + * + * @return YES if the write was successful else NO. + */ +- (BOOL)writeDictionary:(NSDictionary *)dict error:(NSError **)error; + +/** + * Delete the backup excluded plist created with the above filename. + * + * @param error The error object if any while deleting the file. + * + * @return YES If the delete was successful else NO. + */ +- (BOOL)deleteFile:(NSError **)error; + +/** + * The contents of the plist file. We also store the contents of the file in-memory. + * If the in-memory contents are valid we return the in-memory contents else we read + * the file from disk. + * + * @return A dictionary object that contains the contents of the plist file if the file + * exists else nil. + */ +- (NSDictionary *)contentAsDictionary; + +/** + * Check if the plist exists on the disk or not. + * + * @return YES if the file exists on the disk else NO. + */ +- (BOOL)doesFileExist; + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.m b/Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.m new file mode 100644 index 00000000000..2c322224fa8 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.m @@ -0,0 +1,206 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDBackupExcludedPlist.h" + +#import "FIRInstanceIDDefines.h" +#import "FIRInstanceIDLogger.h" + +typedef enum : NSUInteger { + FIRInstanceIDPlistDirectoryUnknown, + FIRInstanceIDPlistDirectoryDocuments, + FIRInstanceIDPlistDirectoryApplicationSupport, +} FIRInstanceIDPlistDirectory; + +@interface FIRInstanceIDBackupExcludedPlist () + +@property(nonatomic, readwrite, copy) NSString *fileName; +@property(nonatomic, readwrite, copy) NSString *subDirectoryName; +@property(nonatomic, readwrite, assign) BOOL fileInStandardDirectory; + +@property(nonatomic, readwrite, strong) NSDictionary *cachedPlistContents; + +@end + +@implementation FIRInstanceIDBackupExcludedPlist + +- (instancetype)initWithFileName:(NSString *)fileName subDirectory:(NSString *)subDirectory { + self = [super init]; + if (self) { + _fileName = [fileName copy]; + _subDirectoryName = [subDirectory copy]; +#if TARGET_OS_IOS + _fileInStandardDirectory = [self moveToApplicationSupportSubDirectory:subDirectory]; +#else + // For tvOS and macOS, we never store the content in document folder, so + // the migration is unnecessary. + _fileInStandardDirectory = YES; +#endif + } + return self; +} + +- (BOOL)writeDictionary:(NSDictionary *)dict error:(NSError **)error { + NSString *path = [self plistPathInDirectory:[self plistDirectory]]; + if (![dict writeToFile:path atomically:YES]) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeBackupExcludedPlist000, + @"Failed to write to %@.plist", self.fileName); + return NO; + } + + // Successfully wrote contents -- change the in-memory contents + self.cachedPlistContents = [dict copy]; + + _FIRInstanceIDDevAssert([[NSFileManager defaultManager] fileExistsAtPath:path], + @"Error writing data to non-backed up plist %@.plist", self.fileName); + + NSURL *URL = [NSURL fileURLWithPath:path]; + if (error) { + *error = nil; + } + + NSDictionary *preferences = [URL resourceValuesForKeys:@[ NSURLIsExcludedFromBackupKey ] + error:error]; + if ([preferences[NSURLIsExcludedFromBackupKey] boolValue]) { + return YES; + } + + BOOL success = [URL setResourceValue:@(YES) forKey:NSURLIsExcludedFromBackupKey error:error]; + if (!success) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeBackupExcludedPlist001, + @"Error excluding %@ from backup, %@", [URL lastPathComponent], + error ? *error : @""); + } + return success; +} + +- (BOOL)deleteFile:(NSError **)error { + BOOL success = YES; + NSString *path = [self plistPathInDirectory:[self plistDirectory]]; + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + success = [[NSFileManager defaultManager] removeItemAtPath:path error:error]; + } + // remove the in-memory contents + self.cachedPlistContents = nil; + return success; +} + +- (NSDictionary *)contentAsDictionary { + if (!self.cachedPlistContents) { + NSString *path = [self plistPathInDirectory:[self plistDirectory]]; + if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { + self.cachedPlistContents = [[NSDictionary alloc] initWithContentsOfFile:path]; + } + } + return self.cachedPlistContents; +} + +- (BOOL)moveToApplicationSupportSubDirectory:(NSString *)subDirectoryName { + NSArray *directoryPaths = + NSSearchPathForDirectoriesInDomains([self supportedDirectory], NSUserDomainMask, YES); + // This only going to happen inside iOS so it is an applicationSupportDirectory. + NSString *applicationSupportDirPath = directoryPaths.lastObject; + NSArray *components = @[ applicationSupportDirPath, subDirectoryName ]; + NSString *subDirectoryPath = [NSString pathWithComponents:components]; + BOOL hasSubDirectory; + if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath + isDirectory:&hasSubDirectory]) { + // Cannot move to non-existent directory + return NO; + } + + if ([self doesFileExistInDirectory:FIRInstanceIDPlistDirectoryDocuments]) { + NSString *oldPlistPath = [self plistPathInDirectory:FIRInstanceIDPlistDirectoryDocuments]; + NSString *newPlistPath = + [self plistPathInDirectory:FIRInstanceIDPlistDirectoryApplicationSupport]; + if ([self doesFileExistInDirectory:FIRInstanceIDPlistDirectoryApplicationSupport]) { + // File exists in both Documents and ApplicationSupport + return NO; + } + NSError *moveError; + if (![[NSFileManager defaultManager] moveItemAtPath:oldPlistPath + toPath:newPlistPath + error:&moveError]) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeBackupExcludedPlist002, + @"Failed to move file %@ from %@ to %@. Error: %@", self.fileName, + oldPlistPath, newPlistPath, moveError); + return NO; + } + } + // We moved the file if it existed, otherwise we didn't need to do anything + return YES; +} + +- (BOOL)doesFileExist { + return [self doesFileExistInDirectory:[self plistDirectory]]; +} + +#pragma mark - Private + +- (FIRInstanceIDPlistDirectory)plistDirectory { + if (_fileInStandardDirectory) { + return FIRInstanceIDPlistDirectoryApplicationSupport; + } else { + return FIRInstanceIDPlistDirectoryDocuments; + }; +} + +- (NSString *)plistPathInDirectory:(FIRInstanceIDPlistDirectory)directory { + return [self pathWithName:self.fileName inDirectory:directory]; +} + +- (NSString *)pathWithName:(NSString *)plistName + inDirectory:(FIRInstanceIDPlistDirectory)directory { + NSArray *directoryPaths; + NSArray *components = @[]; + NSString *plistNameWithExtension = [NSString stringWithFormat:@"%@.plist", plistName]; + switch (directory) { + case FIRInstanceIDPlistDirectoryDocuments: + directoryPaths = + NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + components = @[ directoryPaths.lastObject, plistNameWithExtension ]; + break; + + case FIRInstanceIDPlistDirectoryApplicationSupport: + directoryPaths = + NSSearchPathForDirectoriesInDomains([self supportedDirectory], NSUserDomainMask, YES); + components = @[ directoryPaths.lastObject, _subDirectoryName, plistNameWithExtension ]; + break; + + default: + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeBackupExcludedPlistInvalidPlistEnum, + @"Invalid plist directory type: %lu", (unsigned long)directory); + NSAssert(NO, @"Invalid plist directory type: %lu", (unsigned long)directory); + break; + } + + return [NSString pathWithComponents:components]; +} + +- (BOOL)doesFileExistInDirectory:(FIRInstanceIDPlistDirectory)directory { + NSString *path = [self plistPathInDirectory:directory]; + return [[NSFileManager defaultManager] fileExistsAtPath:path]; +} + +- (NSSearchPathDirectory)supportedDirectory { +#if TARGET_OS_TV + return NSCachesDirectory; +#else + return NSApplicationSupportDirectory; +#endif +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h b/Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h new file mode 100644 index 00000000000..a62fad1d5d7 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h @@ -0,0 +1,64 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDCheckinPreferences.h" + +@interface FIRInstanceIDCheckinPreferences (Internal) + +/** + * Parse the checkin auth credentials saved in the Keychain to initialize checkin + * preferences. + * + * @param keychainContent The checkin auth credentials saved in the Keychain. + * + * @return A valid checkin preferences object if the checkin auth credentials in the + * keychain can be parsed successfully else nil. + */ ++ (FIRInstanceIDCheckinPreferences *)preferencesFromKeychainContents:(NSString *)keychainContent; + +/** + * Default initializer for InstanceID checkin preferences. + * + * @param deviceID The deviceID for the app. + * @param secretToken The secret token the app uses to authenticate with the server. + * + * @return A checkin preferences object with given deviceID and secretToken. + */ +- (instancetype)initWithDeviceID:(NSString *)deviceID secretToken:(NSString *)secretToken; + +/** + * Update checkin preferences from the preferences dict persisted as a plist. The dict contains + * all the checkin preferences retrieved from the server except the deviceID and secret which + * are stored in the Keychain. + * + * @param checkinPlistContent The checkin preferences saved in a plist on the disk. + */ +- (void)updateWithCheckinPlistContents:(NSDictionary *)checkinPlistContent; + +/** + * Reset the current checkin preferences object. + */ +- (void)reset; + +/** + * The string that contains the checkin auth credentials i.e. deviceID and secret. This + * needs to be stored in the Keychain. + * + * @return The checkin auth credential string containing the deviceID and secret. + */ +- (NSString *)checkinKeychainContent; + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.m b/Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.m new file mode 100644 index 00000000000..88cc40a1be7 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.m @@ -0,0 +1,112 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDCheckinPreferences+Internal.h" + +#import "FIRInstanceIDCheckinService.h" +#import "FIRInstanceIDUtilities.h" + +static NSString *const kCheckinKeychainContentSeparatorString = @"|"; + +@interface FIRInstanceIDCheckinPreferences () + +@property(nonatomic, readwrite, copy) NSString *deviceID; +@property(nonatomic, readwrite, copy) NSString *secretToken; +@property(nonatomic, readwrite, copy) NSString *digest; +@property(nonatomic, readwrite, copy) NSString *versionInfo; +@property(nonatomic, readwrite, copy) NSString *deviceDataVersion; + +@property(nonatomic, readwrite, strong) NSMutableDictionary *gServicesData; +@property(nonatomic, readwrite, assign) int64_t lastCheckinTimestampMillis; + +@end + +@implementation FIRInstanceIDCheckinPreferences (Internal) + ++ (FIRInstanceIDCheckinPreferences *)preferencesFromKeychainContents:(NSString *)keychainContent { + NSString *deviceID = [self checkinDeviceIDFromKeychainContent:keychainContent]; + NSString *secret = [self checkinSecretFromKeychainContent:keychainContent]; + if ([deviceID length] && [secret length]) { + return [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:deviceID secretToken:secret]; + } else { + return nil; + } +} + +- (instancetype)initWithDeviceID:(NSString *)deviceID secretToken:(NSString *)secretToken { + self = [super init]; + if (self) { + self.deviceID = [deviceID copy]; + self.secretToken = [secretToken copy]; + } + return self; +} + +- (void)reset { + self.deviceID = nil; + self.secretToken = nil; + self.digest = nil; + self.versionInfo = nil; + self.gServicesData = nil; + self.deviceDataVersion = nil; + self.lastCheckinTimestampMillis = 0; +} + +- (void)updateWithCheckinPlistContents:(NSDictionary *)checkinPlistContent { + for (NSString *key in checkinPlistContent) { + if ([kFIRInstanceIDDigestStringKey isEqualToString:key]) { + self.digest = [checkinPlistContent[key] copy]; + } else if ([kFIRInstanceIDVersionInfoStringKey isEqualToString:key]) { + self.versionInfo = [checkinPlistContent[key] copy]; + } else if ([kFIRInstanceIDLastCheckinTimeKey isEqualToString:key]) { + self.lastCheckinTimestampMillis = [checkinPlistContent[key] longLongValue]; + } else if ([kFIRInstanceIDGServicesDictionaryKey isEqualToString:key]) { + self.gServicesData = [checkinPlistContent[key] mutableCopy]; + } else if ([kFIRInstanceIDDeviceDataVersionKey isEqualToString:key]) { + self.deviceDataVersion = [checkinPlistContent[key] copy]; + } + // Otherwise we have some keys we don't care about + } +} + +- (NSString *)checkinKeychainContent { + if ([self.deviceID length] && [self.secretToken length]) { + return [NSString stringWithFormat:@"%@%@%@", self.deviceID, + kCheckinKeychainContentSeparatorString, self.secretToken]; + } else { + return nil; + } +} + ++ (NSString *)checkinDeviceIDFromKeychainContent:(NSString *)keychainContent { + return [self checkinKeychainContent:keychainContent forIndex:0]; +} + ++ (NSString *)checkinSecretFromKeychainContent:(NSString *)keychainContent { + return [self checkinKeychainContent:keychainContent forIndex:1]; +} + ++ (NSString *)checkinKeychainContent:(NSString *)keychainContent forIndex:(int)index { + NSArray *keychainComponents = + [keychainContent componentsSeparatedByString:kCheckinKeychainContentSeparatorString]; + if (index >= 0 && index < 2 && [keychainComponents count] == 2) { + return keychainComponents[index]; + } else { + return nil; + } +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDCheckinPreferences.h b/Firebase/InstanceID/FIRInstanceIDCheckinPreferences.h new file mode 100644 index 00000000000..fe459af041e --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDCheckinPreferences.h @@ -0,0 +1,63 @@ +/* + * Copyright 2019 Google + * + * 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 + +/** + * The preferences InstanceID loads from checkin server. The deviceID and secret that checkin + * provides is used to authenticate all future requests to the server. Besides the deviceID + * and secret the other information that checkin provides is stored in a plist on the device. + * The deviceID and secret are persisted in the device keychain. + */ +@interface FIRInstanceIDCheckinPreferences : NSObject + +/** + * DeviceID and secretToken are the checkin auth credentials and are stored in the Keychain. + */ +@property(nonatomic, readonly, copy) NSString *deviceID; +@property(nonatomic, readonly, copy) NSString *secretToken; + +/** + * All the other checkin preferences other than deviceID and secret are stored in a plist. + */ +@property(nonatomic, readonly, copy) NSString *deviceDataVersion; +@property(nonatomic, readonly, copy) NSString *digest; +@property(nonatomic, readonly, copy) NSString *versionInfo; +@property(nonatomic, readonly, strong) NSMutableDictionary *gServicesData; +@property(nonatomic, readonly, assign) int64_t lastCheckinTimestampMillis; + +/** + * The content retrieved from checkin server that should be persisted in a plist. This + * doesn't contain the deviceID and secret which are stored in the Keychain since they + * should be more private. + * + * @return The checkin preferences that should be persisted in a plist. + */ +- (NSDictionary *)checkinPlistContents; + +/** + * Return whether checkin info exists, valid or not. + */ +- (BOOL)hasCheckinInfo; + +/** + * Verify if checkin preferences are valid or not. + * + * @return YES if valid checkin preferences else NO. + */ +- (BOOL)hasValidCheckinInfo; + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDCheckinPreferences.m b/Firebase/InstanceID/FIRInstanceIDCheckinPreferences.m new file mode 100644 index 00000000000..2479a85a97f --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDCheckinPreferences.m @@ -0,0 +1,97 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDCheckinPreferences.h" + +#import +#import "FIRInstanceIDCheckinService.h" +#import "FIRInstanceIDDefines.h" +#import "FIRInstanceIDUtilities.h" + +const NSTimeInterval kFIRInstanceIDDefaultCheckinInterval = 7 * 24 * 60 * 60; // 7 days. + +@interface FIRInstanceIDCheckinPreferences () + +@property(nonatomic, readwrite, copy) NSString *deviceID; +@property(nonatomic, readwrite, copy) NSString *secretToken; +@property(nonatomic, readwrite, copy) NSString *digest; +@property(nonatomic, readwrite, copy) NSString *versionInfo; +@property(nonatomic, readwrite, copy) NSString *deviceDataVersion; + +@property(nonatomic, readwrite, strong) NSMutableDictionary *gServicesData; +@property(nonatomic, readwrite, assign) int64_t lastCheckinTimestampMillis; + +// This flag indicates that we have already saved the above deviceID and secret +// to our keychain and hence we don't need to save again. This is helpful since +// on checkin refresh we can avoid writing to the Keychain which can sometimes +// be very buggy. For info check this https://forums.developer.apple.com/thread/4743 +@property(nonatomic, readwrite, assign) BOOL hasPreCachedAuthCredentials; + +@end + +@implementation FIRInstanceIDCheckinPreferences + +- (NSDictionary *)checkinPlistContents { + NSMutableDictionary *checkinPlistContents = [NSMutableDictionary dictionary]; + checkinPlistContents[kFIRInstanceIDDigestStringKey] = self.digest ?: @""; + checkinPlistContents[kFIRInstanceIDVersionInfoStringKey] = self.versionInfo ?: @""; + checkinPlistContents[kFIRInstanceIDDeviceDataVersionKey] = self.deviceDataVersion ?: @""; + checkinPlistContents[kFIRInstanceIDLastCheckinTimeKey] = @(self.lastCheckinTimestampMillis); + checkinPlistContents[kFIRInstanceIDGServicesDictionaryKey] = + [self.gServicesData count] ? self.gServicesData : @{}; + return checkinPlistContents; +} + +- (BOOL)hasCheckinInfo { + return (self.deviceID.length && self.secretToken.length); +} + +- (BOOL)hasValidCheckinInfo { + int64_t currentTimestampInMillis = FIRInstanceIDCurrentTimestampInMilliseconds(); + int64_t timeSinceLastCheckinInMillis = currentTimestampInMillis - self.lastCheckinTimestampMillis; + _FIRInstanceIDDevAssert(timeSinceLastCheckinInMillis >= 0, + @"FCM error: cannot have last checkin timestamp in future"); + BOOL hasCheckinInfo = [self hasCheckinInfo]; + NSString *lastLocale = + [[GULUserDefaults standardUserDefaults] stringForKey:kFIRInstanceIDUserDefaultsKeyLocale]; + // If it's app's first time open and checkin is already fetched and no locale information is + // stored, then checkin info is valid. We should not checkin again because locale is considered + // "changed". + if (hasCheckinInfo && !lastLocale) { + NSString *currentLocale = FIRInstanceIDCurrentLocale(); + [[GULUserDefaults standardUserDefaults] setObject:currentLocale + forKey:kFIRInstanceIDUserDefaultsKeyLocale]; + return YES; + } + + // If locale has changed, checkin info is no longer valid. + // Also update locale information if changed. (Only do it here not in token refresh) + if (FIRInstanceIDHasLocaleChanged()) { + NSString *currentLocale = FIRInstanceIDCurrentLocale(); + [[GULUserDefaults standardUserDefaults] setObject:currentLocale + forKey:kFIRInstanceIDUserDefaultsKeyLocale]; + return NO; + } + + return (hasCheckinInfo && + (timeSinceLastCheckinInMillis / 1000.0 < kFIRInstanceIDDefaultCheckinInterval)); +} + +- (void)setHasPreCachedAuthCredentials:(BOOL)hasPreCachedAuthCredentials { + _hasPreCachedAuthCredentials = hasPreCachedAuthCredentials; +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDCheckinPreferences_Private.h b/Firebase/InstanceID/FIRInstanceIDCheckinPreferences_Private.h new file mode 100644 index 00000000000..23b55e16548 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDCheckinPreferences_Private.h @@ -0,0 +1,27 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDCheckinPreferences.h" + +/** Checkin refresh interval. **/ +FOUNDATION_EXPORT const NSTimeInterval kFIRInstanceIDDefaultCheckinInterval; + +@interface FIRInstanceIDCheckinPreferences () + +- (BOOL)hasPreCachedAuthCredentials; +- (void)setHasPreCachedAuthCredentials:(BOOL)hasPreCachedAuthCredentials; + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDCheckinService.h b/Firebase/InstanceID/FIRInstanceIDCheckinService.h new file mode 100644 index 00000000000..9d05eb4514a --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDCheckinService.h @@ -0,0 +1,82 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDUtilities.h" + +NS_ASSUME_NONNULL_BEGIN + +// keys in Checkin preferences +FOUNDATION_EXPORT NSString *const kFIRInstanceIDDeviceAuthIdKey; +FOUNDATION_EXPORT NSString *const kFIRInstanceIDSecretTokenKey; +FOUNDATION_EXPORT NSString *const kFIRInstanceIDDigestStringKey; +FOUNDATION_EXPORT NSString *const kFIRInstanceIDLastCheckinTimeKey; +FOUNDATION_EXPORT NSString *const kFIRInstanceIDVersionInfoStringKey; +FOUNDATION_EXPORT NSString *const kFIRInstanceIDGServicesDictionaryKey; +FOUNDATION_EXPORT NSString *const kFIRInstanceIDDeviceDataVersionKey; +FOUNDATION_EXPORT NSString *const kFIRInstanceIDFirebaseUserAgentKey; + +@class FIRInstanceIDCheckinPreferences; + +/** + * @related FIRInstanceIDCheckinService + * + * The completion handler invoked once the fetch from Checkin server finishes. + * For successful fetches we returned checkin information by the checkin service + * and `nil` error, else we return the appropriate error object as reported by the + * Checkin Service. + * + * @param checkinPreferences The checkin preferences as fetched from the server. + * @param error The error object which fetching GServices data. + */ +typedef void (^FIRInstanceIDDeviceCheckinCompletion)( + FIRInstanceIDCheckinPreferences *_Nullable checkinPreferences, NSError *_Nullable error); + +/** + * Register the device with Checkin Service and get back the `authID`, `secret + * token` etc. for the client. Checkin results are cached in the + * `FIRInstanceIDCache` and periodically refreshed to prevent them from being stale. + * Each client needs to register with checkin before registering with InstanceID. + */ +@interface FIRInstanceIDCheckinService : NSObject + +/** + * Execute a device checkin request to obtain an deviceID, secret token, + * gService data. + * + * @param existingCheckin An existing checkin preference object, if available. + * @param completion Completion hander called on success or failure of device checkin. + */ +- (void)checkinWithExistingCheckin:(nullable FIRInstanceIDCheckinPreferences *)existingCheckin + completion:(FIRInstanceIDDeviceCheckinCompletion)completion; + +/** + * This would stop any request that the service made to the checkin backend and also + * release any callback handlers that it holds. + */ +- (void)stopFetching; + +/** + * Set test block for mock testing network requests. + * + * @param block The block to invoke as a mock response from the network. + */ ++ (void)setCheckinTestBlock:(nullable FIRInstanceIDURLRequestTestBlock)block; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InstanceID/FIRInstanceIDCheckinService.m b/Firebase/InstanceID/FIRInstanceIDCheckinService.m new file mode 100644 index 00000000000..8a2711d6231 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDCheckinService.m @@ -0,0 +1,240 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDCheckinService.h" + +#import +#import "FIRInstanceIDCheckinPreferences+Internal.h" +#import "FIRInstanceIDCheckinPreferences_Private.h" +#import "FIRInstanceIDDefines.h" +#import "FIRInstanceIDLogger.h" +#import "FIRInstanceIDStore.h" +#import "FIRInstanceIDUtilities.h" +#import "NSError+FIRInstanceID.h" + +static NSString *const kDeviceCheckinURL = @"https://device-provisioning.googleapis.com/checkin"; + +// keys in Checkin preferences +NSString *const kFIRInstanceIDDeviceAuthIdKey = @"GMSInstanceIDDeviceAuthIdKey"; +NSString *const kFIRInstanceIDSecretTokenKey = @"GMSInstanceIDSecretTokenKey"; +NSString *const kFIRInstanceIDDigestStringKey = @"GMSInstanceIDDigestKey"; +NSString *const kFIRInstanceIDLastCheckinTimeKey = @"GMSInstanceIDLastCheckinTimestampKey"; +NSString *const kFIRInstanceIDVersionInfoStringKey = @"GMSInstanceIDVersionInfo"; +NSString *const kFIRInstanceIDGServicesDictionaryKey = @"GMSInstanceIDGServicesData"; +NSString *const kFIRInstanceIDDeviceDataVersionKey = @"GMSInstanceIDDeviceDataVersion"; +NSString *const kFIRInstanceIDFirebaseUserAgentKey = @"X-firebase-client"; + +static NSUInteger const kCheckinType = 2; // DeviceType IOS in l/w/a/_checkin.proto +static NSUInteger const kCheckinVersion = 2; +static NSUInteger const kFragment = 0; + +static FIRInstanceIDURLRequestTestBlock testBlock; + +@interface FIRInstanceIDCheckinService () + +@property(nonatomic, readwrite, strong) NSURLSession *session; + +@end + +@implementation FIRInstanceIDCheckinService +; + +- (instancetype)init { + self = [super init]; + if (self) { + // Create an URLSession once, even though checkin should happen about once a day + NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; + config.timeoutIntervalForResource = 60.0f; // 1 minute + config.allowsCellularAccess = YES; + _session = [NSURLSession sessionWithConfiguration:config]; + _session.sessionDescription = @"com.google.iid-checkin"; + } + return self; +} + +- (void)dealloc { + testBlock = nil; + [self.session invalidateAndCancel]; +} + +- (void)checkinWithExistingCheckin:(FIRInstanceIDCheckinPreferences *)existingCheckin + completion:(FIRInstanceIDDeviceCheckinCompletion)completion { + _FIRInstanceIDDevAssert(completion != nil, @"completion required"); + + if (self.session == nil) { + FIRInstanceIDLoggerError(kFIRIntsanceIDInvalidNetworkSession, + @"Inconsistent state: NSURLSession has been invalidated"); + NSError *error = + [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeRegistrarFailedToCheckIn]; + completion(nil, error); + return; + } + + NSURL *url = [NSURL URLWithString:kDeviceCheckinURL]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + + [request setValue:@"application/json" forHTTPHeaderField:@"content-type"]; + [request setValue:[FIRApp firebaseUserAgent] + forHTTPHeaderField:kFIRInstanceIDFirebaseUserAgentKey]; + + NSDictionary *checkinParameters = [self checkinParametersWithExistingCheckin:existingCheckin]; + NSData *checkinData = [NSJSONSerialization dataWithJSONObject:checkinParameters + options:0 + error:nil]; + request.HTTPMethod = @"POST"; + request.HTTPBody = checkinData; + + void (^handler)(NSData *, NSURLResponse *, NSError *) = + ^(NSData *data, NSURLResponse *response, NSError *error) { + if (error) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeService000, + @"Device checkin HTTP fetch error. Error Code: %ld", + (long)error.code); + completion(nil, error); + return; + } + + NSError *serializationError; + NSDictionary *dataResponse = [NSJSONSerialization JSONObjectWithData:data + options:0 + error:&serializationError]; + if (serializationError) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeService001, + @"Error serializing json object. Error Code: %ld", + _FIRInstanceID_L(serializationError.code)); + completion(nil, serializationError); + return; + } + + NSString *deviceAuthID = [dataResponse[@"android_id"] stringValue]; + NSString *secretToken = [dataResponse[@"security_token"] stringValue]; + if ([deviceAuthID length] == 0) { + NSError *error = + [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidRequest]; + completion(nil, error); + return; + } + + int64_t lastCheckinTimestampMillis = [dataResponse[@"time_msec"] longLongValue]; + int64_t currentTimestampMillis = FIRInstanceIDCurrentTimestampInMilliseconds(); + // Somehow the server clock gets out of sync with the device clock. + // Reset the last checkin timestamp in case this happens. + if (lastCheckinTimestampMillis > currentTimestampMillis) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeService002, + @"Invalid last checkin timestamp in future."); + lastCheckinTimestampMillis = currentTimestampMillis; + } + + NSString *deviceDataVersionInfo = dataResponse[@"device_data_version_info"] ?: @""; + NSString *digest = dataResponse[@"digest"] ?: @""; + + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeService003, + @"Checkin successful with authId: %@, " + @"digest: %@, " + @"lastCheckinTimestamp: %lld", + deviceAuthID, digest, lastCheckinTimestampMillis); + + NSString *versionInfo = dataResponse[@"version_info"] ?: @""; + NSMutableDictionary *gservicesData = [NSMutableDictionary dictionary]; + + // Read gServices data. + NSArray *flatSettings = dataResponse[@"setting"]; + for (NSDictionary *dict in flatSettings) { + if (dict[@"name"] && dict[@"value"]) { + gservicesData[dict[@"name"]] = dict[@"value"]; + } else { + _FIRInstanceIDDevAssert(NO, @"Invalid setting in checkin response: (%@: %@)", + dict[@"name"], dict[@"value"]); + } + } + + FIRInstanceIDCheckinPreferences *checkinPreferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:deviceAuthID + secretToken:secretToken]; + NSDictionary *preferences = @{ + kFIRInstanceIDDigestStringKey : digest, + kFIRInstanceIDVersionInfoStringKey : versionInfo, + kFIRInstanceIDLastCheckinTimeKey : @(lastCheckinTimestampMillis), + kFIRInstanceIDGServicesDictionaryKey : gservicesData, + kFIRInstanceIDDeviceDataVersionKey : deviceDataVersionInfo, + }; + [checkinPreferences updateWithCheckinPlistContents:preferences]; + completion(checkinPreferences, nil); + }; + // Test block + if (testBlock) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeService005, + @"Test block set, will not hit the server"); + testBlock(request, handler); + return; + } + + NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request completionHandler:handler]; + [task resume]; +} + +- (void)stopFetching { + [self.session invalidateAndCancel]; + // The session cannot be reused after invalidation. Dispose it to prevent accident reusing. + self.session = nil; +} + +#pragma mark - Private + +- (NSDictionary *)checkinParametersWithExistingCheckin: + (nullable FIRInstanceIDCheckinPreferences *)checkinPreferences { + NSString *deviceModel = FIRInstanceIDDeviceModel(); + NSString *systemVersion = FIRInstanceIDOperatingSystemVersion(); + NSString *osVersion = [NSString stringWithFormat:@"IOS_%@", systemVersion]; + + // Get locale from GCM if GCM exists else use system API. + NSString *locale = FIRInstanceIDCurrentLocale(); + + NSInteger userNumber = 0; // Multi Profile may change this. + NSInteger userSerialNumber = 0; // Multi Profile may change this + + uint32_t loggingID = arc4random(); + NSString *timeZone = [NSTimeZone localTimeZone].name; + int64_t lastCheckingTimestampMillis = checkinPreferences.lastCheckinTimestampMillis; + + NSDictionary *checkinParameters = @{ + @"checkin" : @{ + @"iosbuild" : @{@"model" : deviceModel, @"os_version" : osVersion}, + @"type" : @(kCheckinType), + @"user_number" : @(userNumber), + @"last_checkin_msec" : @(lastCheckingTimestampMillis), + }, + @"fragment" : @(kFragment), + @"logging_id" : @(loggingID), + @"locale" : locale, + @"version" : @(kCheckinVersion), + @"digest" : checkinPreferences.digest ?: @"", + @"timezone" : timeZone, + @"user_serial_number" : @(userSerialNumber), + @"id" : @([checkinPreferences.deviceID longLongValue]), + @"security_token" : @([checkinPreferences.secretToken longLongValue]), + }; + + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeService006, @"Checkin parameters: %@", + checkinParameters); + return checkinParameters; +} + ++ (void)setCheckinTestBlock:(FIRInstanceIDURLRequestTestBlock)block { + testBlock = [block copy]; +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDCheckinStore.h b/Firebase/InstanceID/FIRInstanceIDCheckinStore.h new file mode 100644 index 00000000000..5e1b1194740 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDCheckinStore.h @@ -0,0 +1,108 @@ +/* + * Copyright 2019 Google + * + * 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 FIRInstanceIDAuthKeychain; +@class FIRInstanceIDBackupExcludedPlist; +@class FIRInstanceIDCheckinPreferences; + +// These values exposed for testing +extern NSString *const kFIRInstanceIDCheckinKeychainService; +extern NSString *const kFIRInstanceIDLegacyCheckinKeychainAccount; +extern NSString *const kFIRInstanceIDLegacyCheckinKeychainService; + +/** + * Checkin preferences backing store. + */ +@interface FIRInstanceIDCheckinStore : NSObject + +/** + * Designated Initializer. Initialize a checkin store with the given backup excluded + * plist filename. + * + * @param checkinFilename The backup excluded plist filename to persist checkin + * preferences. + * + * @param subDirectoryName Sub-directory in standard directory where we write + * InstanceID plist. + * + * @return Store to persist checkin preferences. + */ +- (instancetype)initWithCheckinPlistFileName:(NSString *)checkinFilename + subDirectoryName:(NSString *)subDirectoryName; + +/** + * Initialize a checkin store with the given backup excluded plist and keychain. + * + * @param plist The backup excluded plist to persist checkin preferences. + * @param keychain The keychain used to persist checkin auth preferences. + * + * @return Store to persist checkin preferences. + */ +- (instancetype)initWithCheckinPlist:(FIRInstanceIDBackupExcludedPlist *)plist + keychain:(FIRInstanceIDAuthKeychain *)keychain; + +/** + * Checks whether the backup excluded checkin preferences are present on the disk or not. + * + * @return YES if the backup excluded checkin plist exists on the disks else NO. + */ +- (BOOL)hasCheckinPlist; + +#pragma mark - Save + +/** + * Save the checkin preferences to backing store. + * + * @param preferences Checkin preferences to save. + * @param handler The callback handler which is invoked when the operation is complete, + * with an error if there is any. + */ +- (void)saveCheckinPreferences:(FIRInstanceIDCheckinPreferences *)preferences + handler:(void (^)(NSError *error))handler; + +#pragma mark - Delete + +/** + * Remove the cached checkin preferences. + * + * @param handler The callback handler which is invoked when the operation is complete, + * with an error if there is any. + */ +- (void)removeCheckinPreferencesWithHandler:(void (^)(NSError *error))handler; + +#pragma mark - Get + +/** + * Get the cached device secret. If we cannot access it for some reason we + * return the appropriate error object. + * + * @return The cached checkin preferences if present else nil. + */ +- (FIRInstanceIDCheckinPreferences *)cachedCheckinPreferences; + +/** + * Migrate the checkin item from old service/account to the new one. + * The new account is dynamic as it uses bundle ID. + * This is to ensure checkin is not shared across apps, but still the same + * if app has used GCM before. + * This call should only happen once. + * + */ +- (void)migrateCheckinItemIfNeeded; + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDCheckinStore.m b/Firebase/InstanceID/FIRInstanceIDCheckinStore.m new file mode 100644 index 00000000000..96f80712625 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDCheckinStore.m @@ -0,0 +1,239 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDCheckinStore.h" + +#import "FIRInstanceIDAuthKeyChain.h" +#import "FIRInstanceIDBackupExcludedPlist.h" +#import "FIRInstanceIDCheckinPreferences+Internal.h" +#import "FIRInstanceIDCheckinPreferences_Private.h" +#import "FIRInstanceIDCheckinService.h" +#import "FIRInstanceIDLogger.h" +#import "FIRInstanceIDUtilities.h" +#import "FIRInstanceIDVersionUtilities.h" +#import "NSError+FIRInstanceID.h" + +static NSString *const kFIRInstanceIDCheckinKeychainGeneric = @"com.google.iid"; + +NSString *const kFIRInstanceIDCheckinKeychainService = @"com.google.iid.checkin"; +NSString *const kFIRInstanceIDLegacyCheckinKeychainAccount = @"com.google.iid.checkin-account"; +NSString *const kFIRInstanceIDLegacyCheckinKeychainService = @"com.google.iid.checkin-service"; + +// Checkin plist used to have the deviceID and secret stored in them and that's why they +// had 6 items in it. Since the deviceID and secret have been moved to the keychain +// there would only be 4 items. +static const NSInteger kOldCheckinPlistCount = 6; + +@interface FIRInstanceIDCheckinStore () + +@property(nonatomic, readwrite, strong) FIRInstanceIDBackupExcludedPlist *plist; +@property(nonatomic, readwrite, strong) FIRInstanceIDAuthKeychain *keychain; +// Checkin will store items under +// Keychain account: , +// Keychain service: |kFIRInstanceIDCheckinKeychainService| +@property(nonatomic, readonly) NSString *bundleIdentifierForKeychainAccount; + +@end + +@implementation FIRInstanceIDCheckinStore + +- (instancetype)initWithCheckinPlistFileName:(NSString *)checkinFilename + subDirectoryName:(NSString *)subDirectoryName { + FIRInstanceIDBackupExcludedPlist *plist = + [[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:checkinFilename + subDirectory:subDirectoryName]; + + FIRInstanceIDAuthKeychain *keychain = + [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDCheckinKeychainGeneric]; + return [self initWithCheckinPlist:plist keychain:keychain]; +} + +- (instancetype)initWithCheckinPlist:(FIRInstanceIDBackupExcludedPlist *)plist + keychain:(FIRInstanceIDAuthKeychain *)keychain { + self = [super init]; + if (self) { + _plist = plist; + _keychain = keychain; + } + return self; +} + +- (BOOL)hasCheckinPlist { + return [self.plist doesFileExist]; +} + +- (NSString *)bundleIdentifierForKeychainAccount { + static NSString *bundleIdentifier; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + bundleIdentifier = FIRInstanceIDAppIdentifier(); + }); + return bundleIdentifier; +} + +- (void)saveCheckinPreferences:(FIRInstanceIDCheckinPreferences *)preferences + handler:(void (^)(NSError *error))handler { + NSDictionary *checkinPlistContents = [preferences checkinPlistContents]; + NSString *checkinKeychainContent = [preferences checkinKeychainContent]; + + if (![checkinKeychainContent length]) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeCheckinStore000, + @"Failed to get checkin keychain content from memory."); + if (handler) { + handler([NSError + errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeRegistrarFailedToCheckIn]); + } + return; + } + if (![checkinPlistContents count]) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeCheckinStore001, + @"Failed to get checkin plist contents from memory."); + if (handler) { + handler([NSError + errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeRegistrarFailedToCheckIn]); + } + return; + } + + // Save the deviceID and secret in the Keychain + __block BOOL shouldContinue = YES; + if (!preferences.hasPreCachedAuthCredentials) { + NSData *data = [checkinKeychainContent dataUsingEncoding:NSUTF8StringEncoding]; + [self.keychain setData:data + forService:kFIRInstanceIDCheckinKeychainService + accessibility:nil + account:self.bundleIdentifierForKeychainAccount + handler:^(NSError *error) { + if (error) { + if (handler) { + handler(error); + } + shouldContinue = NO; + return; + } + }]; + } + if (!shouldContinue) { + return; + } + + // Save all other checkin preferences in a plist + NSError *error; + if (![self.plist writeDictionary:checkinPlistContents error:&error]) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeCheckinStore003, + @"Failed to save checkin plist contents." + @"Will delete auth credentials"); + [self.keychain removeItemsMatchingService:kFIRInstanceIDCheckinKeychainService + account:self.bundleIdentifierForKeychainAccount + handler:nil]; + if (handler) { + handler(error); + } + return; + } + handler(nil); +} + +- (void)removeCheckinPreferencesWithHandler:(void (^)(NSError *error))handler { + // Remove deviceID and secret from Keychain + [self.keychain + removeItemsMatchingService:kFIRInstanceIDCheckinKeychainService + account:self.bundleIdentifierForKeychainAccount + handler:^(NSError *error) { + if (error) { + if (handler) { + handler(error); + } + return; + } + // Delete the checkin preferences plist + NSError *deletePlistError; + [self.plist deleteFile:&deletePlistError]; + + // Try to remove from old location as well because migration + // is no longer needed. Consider this is either a fresh install + // or an identity wipe. + [self.keychain + removeItemsMatchingService:kFIRInstanceIDLegacyCheckinKeychainService + account:kFIRInstanceIDLegacyCheckinKeychainAccount + handler:nil]; + handler(deletePlistError); + }]; +} + +- (FIRInstanceIDCheckinPreferences *)cachedCheckinPreferences { + // Query the keychain for deviceID and secret + NSData *item = [self.keychain dataForService:kFIRInstanceIDCheckinKeychainService + account:self.bundleIdentifierForKeychainAccount]; + + // Check info found in keychain + NSString *checkinKeychainContent = [[NSString alloc] initWithData:item + encoding:NSUTF8StringEncoding]; + FIRInstanceIDCheckinPreferences *checkinPreferences = + [FIRInstanceIDCheckinPreferences preferencesFromKeychainContents:checkinKeychainContent]; + + NSDictionary *checkinPlistContents = [self.plist contentAsDictionary]; + + NSString *plistDeviceAuthID = checkinPlistContents[kFIRInstanceIDDeviceAuthIdKey]; + NSString *plistSecretToken = checkinPlistContents[kFIRInstanceIDSecretTokenKey]; + + // If deviceID and secret not found in the keychain verify that we don't have them in the + // checkin preferences plist. + if (![checkinPreferences.deviceID length] && ![checkinPreferences.secretToken length]) { + if ([plistDeviceAuthID length] && [plistSecretToken length]) { + // Couldn't find checkin credentials in keychain but found them in the plist. + checkinPreferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:plistDeviceAuthID + secretToken:plistSecretToken]; + } else { + // Couldn't find checkin credentials in keychain nor plist + return nil; + } + } else if (kOldCheckinPlistCount == checkinPlistContents.count) { + // same check as above but just to be extra sure that we cover all upgrade cases properly. + // TODO(chliangGoogle): Remove this case, after verifying it's not needed + if ([plistDeviceAuthID length] && [plistSecretToken length]) { + checkinPreferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:plistDeviceAuthID + secretToken:plistSecretToken]; + } + } + + [checkinPreferences updateWithCheckinPlistContents:checkinPlistContents]; + return checkinPreferences; +} + +- (void)migrateCheckinItemIfNeeded { + // Check for checkin in the old location, using the legacy keys + // Query the keychain for deviceID and secret + NSData *dataInOldLocation = + [self.keychain dataForService:kFIRInstanceIDLegacyCheckinKeychainService + account:kFIRInstanceIDLegacyCheckinKeychainAccount]; + if (dataInOldLocation) { + // Save to new location + [self.keychain setData:dataInOldLocation + forService:kFIRInstanceIDCheckinKeychainService + accessibility:NULL + account:self.bundleIdentifierForKeychainAccount + handler:nil]; + // Remove from old location + [self.keychain removeItemsMatchingService:kFIRInstanceIDLegacyCheckinKeychainService + account:kFIRInstanceIDLegacyCheckinKeychainAccount + handler:nil]; + } +} + +@end diff --git a/Firestore/Source/Core/FSTListenSequence.h b/Firebase/InstanceID/FIRInstanceIDCombinedHandler.h similarity index 57% rename from Firestore/Source/Core/FSTListenSequence.h rename to Firebase/InstanceID/FIRInstanceIDCombinedHandler.h index c9f798cc875..dcb5429b953 100644 --- a/Firestore/Source/Core/FSTListenSequence.h +++ b/Firebase/InstanceID/FIRInstanceIDCombinedHandler.h @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google + * Copyright 2019 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,22 +16,15 @@ #import -#include "Firestore/core/src/firebase/firestore/model/types.h" - NS_ASSUME_NONNULL_BEGIN /** - * FSTListenSequence is a monotonic sequence. It is initialized with a minimum value to - * exceed. All subsequent calls to next will return increasing values. + * A generic class to combine several handler blocks into a single block in a thread-safe manner */ -@interface FSTListenSequence : NSObject - -- (instancetype)initStartingAfter:(firebase::firestore::model::ListenSequenceNumber)after - NS_DESIGNATED_INITIALIZER; - -- (id)init NS_UNAVAILABLE; +@interface FIRInstanceIDCombinedHandler : NSObject -- (firebase::firestore::model::ListenSequenceNumber)next; +- (void)addHandler:(void (^)(ResultType _Nullable result, NSError* _Nullable error))handler; +- (void (^)(ResultType _Nullable result, NSError* _Nullable error))combinedHandler; @end diff --git a/Firebase/InstanceID/FIRInstanceIDCombinedHandler.m b/Firebase/InstanceID/FIRInstanceIDCombinedHandler.m new file mode 100644 index 00000000000..bc6be6c1085 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDCombinedHandler.m @@ -0,0 +1,64 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDCombinedHandler.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^FIRInstanseIDHandler)(id _Nullable result, NSError *_Nullable error); + +@interface FIRInstanceIDCombinedHandler () +@property(atomic, readonly, strong) NSMutableArray *handlers; +@end + +NS_ASSUME_NONNULL_END + +@implementation FIRInstanceIDCombinedHandler + +- (instancetype)init { + self = [super init]; + if (self) { + _handlers = [NSMutableArray array]; + } + return self; +} + +- (void)addHandler:(FIRInstanseIDHandler)handler { + if (!handler) { + return; + } + + @synchronized(self) { + [self.handlers addObject:handler]; + } +} + +- (FIRInstanseIDHandler)combinedHandler { + FIRInstanseIDHandler combinedHandler = nil; + + @synchronized(self) { + NSArray *handlers = [self.handlers copy]; + combinedHandler = ^(id result, NSError *error) { + for (FIRInstanseIDHandler handler in handlers) { + handler(result, error); + } + }; + } + + return combinedHandler; +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDConstants.h b/Firebase/InstanceID/FIRInstanceIDConstants.h new file mode 100644 index 00000000000..cd2e131551d --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDConstants.h @@ -0,0 +1,63 @@ +/* + * Copyright 2019 Google + * + * 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 + +#pragma mark - Commands + +/** + * Value included in a structured response or GCM message from IID, indicating + * an identity reset. + */ +FOUNDATION_EXPORT NSString *const kFIRInstanceID_CMD_RST; + +#pragma mark - Notifications + +/// Notification used to deliver GCM messages for InstanceID. +FOUNDATION_EXPORT NSString *const kFIRInstanceIDCheckinFetchedNotification; +FOUNDATION_EXPORT NSString *const kFIRInstanceIDAPNSTokenNotification; +FOUNDATION_EXPORT NSString *const kFIRInstanceIDDefaultGCMTokenNotification; +FOUNDATION_EXPORT NSString *const kFIRInstanceIDDefaultGCMTokenFailNotification; + +FOUNDATION_EXPORT NSString *const kFIRInstanceIDIdentityInvalidatedNotification; + +#pragma mark - Miscellaneous + +/// The scope used to save the IID "*" scope token. This is used for saving the +/// IID auth token that we receive from the server. This feature was never +/// implemented on the server side. +FOUNDATION_EXPORT NSString *const kFIRInstanceIDAllScopeIdentifier; +/// The scope used to save the IID "*" scope token. +FOUNDATION_EXPORT NSString *const kFIRInstanceIDDefaultTokenScope; + +/// Subdirectory in search path directory to store InstanceID preferences. +FOUNDATION_EXPORT NSString *const kFIRInstanceIDSubDirectoryName; + +/// The key for APNS token in options dictionary. +FOUNDATION_EXPORT NSString *const kFIRInstanceIDTokenOptionsAPNSKey; + +/// The key for APNS token environment type in options dictionary. +FOUNDATION_EXPORT NSString *const kFIRInstanceIDTokenOptionsAPNSIsSandboxKey; + +/// The key for GMP AppID sent in registration requests. +FOUNDATION_EXPORT NSString *const kFIRInstanceIDTokenOptionsFirebaseAppIDKey; + +/// The key to enable auto-register by swizzling AppDelegate's methods. +FOUNDATION_EXPORT NSString *const kFIRInstanceIDAppDelegateProxyEnabledInfoPlistKey; + +/// Error code for missing entitlements in Keychain. iOS Keychain error +/// https://forums.developer.apple.com/thread/4743 +FOUNDATION_EXPORT const int kFIRInstanceIDSecMissingEntitlementErrorCode; diff --git a/Firebase/InstanceID/FIRInstanceIDConstants.m b/Firebase/InstanceID/FIRInstanceIDConstants.m new file mode 100644 index 00000000000..81f4620e0a4 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDConstants.m @@ -0,0 +1,46 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDConstants.h" + +// Commands +NSString *const kFIRInstanceID_CMD_RST = @"RST"; + +// NOTIFICATIONS +NSString *const kFIRInstanceIDCheckinFetchedNotification = @"com.google.gcm.notif-checkin-fetched"; +NSString *const kFIRInstanceIDAPNSTokenNotification = @"com.firebase.iid.notif.apns-token"; +NSString *const kFIRInstanceIDDefaultGCMTokenNotification = @"com.firebase.iid.notif.fcm-token"; +NSString *const kFIRInstanceIDDefaultGCMTokenFailNotification = + @"com.firebase.iid.notif.fcm-token-fail"; + +NSString *const kFIRInstanceIDIdentityInvalidatedNotification = @"com.google.iid.identity-invalid"; + +// Miscellaneous +NSString *const kFIRInstanceIDAllScopeIdentifier = @"iid-all"; +NSString *const kFIRInstanceIDDefaultTokenScope = @"*"; +NSString *const kFIRInstanceIDSubDirectoryName = @"Google/FirebaseInstanceID"; + +// Registration Options +NSString *const kFIRInstanceIDTokenOptionsAPNSKey = @"apns_token"; +NSString *const kFIRInstanceIDTokenOptionsAPNSIsSandboxKey = @"apns_sandbox"; +NSString *const kFIRInstanceIDTokenOptionsFirebaseAppIDKey = @"gmp_app_id"; + +NSString *const kFIRInstanceIDAppDelegateProxyEnabledInfoPlistKey = + @"FirebaseAppDelegateProxyEnabled"; + +// iOS Keychain error https://forums.developer.apple.com/thread/4743 +// An undocumented error code hence need to be redeclared. +const int kFIRInstanceIDSecMissingEntitlementErrorCode = -34018; diff --git a/Firebase/InstanceID/FIRInstanceIDDefines.h b/Firebase/InstanceID/FIRInstanceIDDefines.h new file mode 100644 index 00000000000..764dfe5e021 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDDefines.h @@ -0,0 +1,70 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRInstanceIDLib_FIRInstanceIDDefines_h +#define FIRInstanceIDLib_FIRInstanceIDDefines_h + +#define _FIRInstanceID_VERBOSE_LOGGING 1 + +// Verbose Logging +#if (_FIRInstanceID_VERBOSE_LOGGING) +#define FIRInstanceID_DEV_VERBOSE_LOG(...) NSLog(__VA_ARGS__) +#else +#define FIRInstanceID_DEV_VERBOSE_LOG(...) \ + do { \ + } while (0) +#endif // VERBOSE_LOGGING + +// WEAKIFY & STRONGIFY +// Helper macro. +#define _FIRInstanceID_WEAKNAME(VAR) VAR##_weak_ + +#define FIRInstanceID_WEAKIFY(VAR) __weak __typeof__(VAR) _FIRInstanceID_WEAKNAME(VAR) = (VAR); + +#define FIRInstanceID_STRONGIFY(VAR) \ + _Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored \"-Wshadow\"") \ + __strong __typeof__(VAR) VAR = _FIRInstanceID_WEAKNAME(VAR); \ + _Pragma("clang diagnostic pop") + +// Type Conversions (used for NSInteger etc) +#ifndef _FIRInstanceID_L +#define _FIRInstanceID_L(v) (long)(v) +#endif + +#endif + +// Debug Assert +#ifndef _FIRInstanceIDDevAssert +// we directly invoke the NSAssert handler so we can pass on the varargs +// (NSAssert doesn't have a macro we can use that takes varargs) +#if !defined(NS_BLOCK_ASSERTIONS) +#define _FIRInstanceIDDevAssert(condition, ...) \ + do { \ + if (!(condition)) { \ + [[NSAssertionHandler currentHandler] \ + handleFailureInFunction:(NSString *)[NSString stringWithUTF8String:__PRETTY_FUNCTION__] \ + file:(NSString *)[NSString stringWithUTF8String:__FILE__] \ + lineNumber:__LINE__ \ + description:__VA_ARGS__]; \ + } \ + } while (0) +#else // !defined(NS_BLOCK_ASSERTIONS) +#define _FIRInstanceIDDevAssert(condition, ...) \ + do { \ + } while (0) +#endif // !defined(NS_BLOCK_ASSERTIONS) + +#endif // _FIRInstanceIDDevAssert diff --git a/Firebase/InstanceID/FIRInstanceIDKeyPair.h b/Firebase/InstanceID/FIRInstanceIDKeyPair.h new file mode 100644 index 00000000000..a1aa5e1add6 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDKeyPair.h @@ -0,0 +1,78 @@ +/* + * Copyright 2019 Google + * + * 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 + +@interface FIRInstanceIDKeyPair : NSObject + +- (instancetype)init __attribute__(( + unavailable("Use -initWithPrivateKey:publicKey:publicKeyData:privateKeyData: instead."))); +; + +/** + * Initialize a new 2048 bit RSA keypair. This also stores the keypair in the Keychain + * Preferences. + * + * @param publicKey The publicKey stored in Keychain. + * @param privateKey The privateKey stored in Keychain. + * @param publicKeyData The publicKey in NSData format. + * @param privateKeyData The privateKey in NSData format. + * + * @return A new KeyPair instance with the generated public and private key. + */ +- (instancetype)initWithPrivateKey:(SecKeyRef)privateKey + publicKey:(SecKeyRef)publicKey + publicKeyData:(NSData *)publicKeyData + privateKeyData:(NSData *)privateKeyData NS_DESIGNATED_INITIALIZER; + +/** + * The public key in the RSA 20148 bit generated KeyPair. + * + * @return The 2048 bit RSA KeyPair's public key. + */ +@property(nonatomic, readonly, strong) NSData *publicKeyData; + +/** + * The private key in the RSA 20148 bit generated KeyPair. + * + * @return The 2048 bit RSA KeyPair's private key. + */ +@property(nonatomic, readonly, strong) NSData *privateKeyData; + +#pragma mark - Info + +/** + * Checks if the private and public keyPair are valid or not. + * + * @return YES if keypair is valid else NO. + */ +- (BOOL)isValid; + +/** + * The public key in the RSA 2048 bit generated KeyPair. + * + * @return The 2048 bit RSA KeyPair's public key. + */ +- (SecKeyRef)publicKey; + +/** + * The private key in the RSA 2048 bit generated KeyPair. + * + * @return The 2048 bit RSA KeyPair's private key. + */ +- (SecKeyRef)privateKey; + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDKeyPair.m b/Firebase/InstanceID/FIRInstanceIDKeyPair.m new file mode 100644 index 00000000000..52b27c2efa6 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDKeyPair.m @@ -0,0 +1,73 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDKeyPair.h" + +#import + +#import "FIRInstanceIDKeyPairUtilities.h" +#import "FIRInstanceIDKeychain.h" +#import "FIRInstanceIDLogger.h" +#import "NSError+FIRInstanceID.h" + +@interface FIRInstanceIDKeyPair () { + SecKeyRef _privateKey; + SecKeyRef _publicKey; +} + +@property(nonatomic, readwrite, strong) NSData *publicKeyData; +@property(nonatomic, readwrite, strong) NSData *privateKeyData; +@end + +@implementation FIRInstanceIDKeyPair +- (instancetype)initWithPrivateKey:(SecKeyRef)privateKey + publicKey:(SecKeyRef)publicKey + publicKeyData:(NSData *)publicKeyData + privateKeyData:(NSData *)privateKeyData { + self = [super init]; + if (self) { + _privateKey = privateKey; + _publicKey = publicKey; + _publicKeyData = publicKeyData; + _privateKeyData = privateKeyData; + } + return self; +} + +- (void)dealloc { + if (_privateKey) { + CFRelease(_privateKey); + } + if (_publicKey) { + CFRelease(_publicKey); + } +} + +#pragma mark - Info + +- (BOOL)isValid { + return _privateKey != NULL && _publicKey != NULL; +} + +- (SecKeyRef)publicKey { + return _publicKey; +} + +- (SecKeyRef)privateKey { + return _privateKey; +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDKeyPairStore.h b/Firebase/InstanceID/FIRInstanceIDKeyPairStore.h new file mode 100644 index 00000000000..02c2896bb00 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDKeyPairStore.h @@ -0,0 +1,85 @@ +/* + * Copyright 2019 Google + * + * 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 FIRInstanceIDKeyPair; + +extern NSString *const kFIRInstanceIDKeyPairSubType; + +@class FIRInstanceIDKeyPairStore; + +@interface FIRInstanceIDKeyPairStore : NSObject + +/** + * Invalidates the cached keypairs in the Keychain, if needed. The keypair metadata plist is + * checked for existence. If the plist file does not exist, it is a signal of a new installation, + * and therefore the key pairs are not valid. + * + * Returns YES if keypair has been invalidated. + */ +- (BOOL)invalidateKeyPairsIfNeeded; + +/** + * Delete the cached RSA keypair from Keychain with the given subtype. + * + * @param subtype The subtype used to cache the RSA keypair in Keychain. + * @param handler The callback handler which is invoked when the keypair deletion is + * complete, with an error if there is any. + */ +- (void)deleteSavedKeyPairWithSubtype:(NSString *)subtype handler:(void (^)(NSError *))handler; + +/** + * Delete the plist that caches KeyPair generation timestamps. + * + * @param error The error if any while deleting the plist else nil. + * + * @return YES if the delete was successful else NO. + */ +- (BOOL)removeKeyPairCreationTimePlistWithError:(NSError **)error; + +/** + * Loads a cached KeyPair if it exists in the Keychain else generate a new + * one. If a keyPair already exists in memory this will just return that. This should + * not be called from the main thread since it could potentially lead to creating a new + * RSA-2048 bit keyPair which is an expensive operation. + * + * @param error The error, if any, while accessing the Keychain. + * + * @return A valid 2048 bit RSA key pair. + */ +- (FIRInstanceIDKeyPair *)loadKeyPairWithError:(NSError **)error; + +/** + * Check if the Keychain has any cached keypairs or not. + * + * @return YES if the Keychain has cached RSA KeyPairs else NO. + */ +- (BOOL)hasCachedKeyPairs; + +/** + * Return an identifier for the app instance. The result is a short identifier that can + * be used as a key when storing information about the app. This method will return the same + * ID as long as the application identity remains active. If the identity has been revoked or + * expired the method will generate and return a new identifier. + * + * @param error The error if any while loading the RSA KeyPair. + * + * @return The identifier, as url safe string. + */ +- (NSString *)appIdentityWithError:(NSError *__autoreleasing *)error; + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDKeyPairStore.m b/Firebase/InstanceID/FIRInstanceIDKeyPairStore.m new file mode 100644 index 00000000000..7b715a7c556 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDKeyPairStore.m @@ -0,0 +1,528 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDKeyPairStore.h" + +#import "FIRInstanceIDBackupExcludedPlist.h" +#import "FIRInstanceIDConstants.h" +#import "FIRInstanceIDDefines.h" +#import "FIRInstanceIDKeyPair.h" +#import "FIRInstanceIDKeyPairUtilities.h" +#import "FIRInstanceIDKeychain.h" +#import "FIRInstanceIDLogger.h" +#import "FIRInstanceIDUtilities.h" +#import "NSError+FIRInstanceID.h" + +// NOTE: These values should be in sync with what InstanceID saves in as. +static NSString *const kFIRInstanceIDKeyPairStoreFileName = @"com.google.iid-keypair"; + +static NSString *const kFIRInstanceIDStoreKeyGenerationTime = @"cre"; + +static NSString *const kFIRInstanceIDStoreKeyPrefix = @"com.google.iid-"; +static NSString *const kFIRInstanceIDStoreKeyPublic = @"|P|"; +static NSString *const kFIRInstanceIDStoreKeyPrivate = @"|K|"; +static NSString *const kFIRInstanceIDStoreKeySubtype = @"|S|"; + +static NSString *const kFIRInstanceIDKeyPairPublicTagPrefix = @"com.google.iid.keypair.public-"; +static NSString *const kFIRInstanceIDKeyPairPrivateTagPrefix = @"com.google.iid.keypair.private-"; + +static const int kMaxMissingEntitlementErrorCount = 3; + +NSString *const kFIRInstanceIDKeyPairSubType = @""; + +// Query the key with NSData format +NSData *FIRInstanceIDKeyDataWithTag(NSString *tag) { + _FIRInstanceIDDevAssert([tag length], @"Invalid tag for keychain specified"); + if (![tag length]) { + return NULL; + } + NSDictionary *queryKey = FIRInstanceIDKeyPairQuery(tag, YES, YES); + CFTypeRef result = [[FIRInstanceIDKeychain sharedInstance] itemWithQuery:queryKey]; + if (!result) { + return NULL; + } + return (__bridge NSData *)result; +} + +// Query the key given a tag +SecKeyRef FIRInstanceIDCachedKeyRefWithTag(NSString *tag) { + _FIRInstanceIDDevAssert([tag length], @"Invalid tag for keychain specified"); + if (!tag.length) { + return NULL; + } + NSDictionary *queryKey = FIRInstanceIDKeyPairQuery(tag, YES, NO); + CFTypeRef result = [[FIRInstanceIDKeychain sharedInstance] itemWithQuery:queryKey]; + return (SecKeyRef)result; +} + +// Check if keypair has been migrated from the legacy to the new version +BOOL FIRInstanceIDHasMigratedKeyPair(NSString *legacyPublicKeyTag, NSString *newPublicKeyTag) { + NSData *oldPublicKeyData = FIRInstanceIDKeyDataWithTag(legacyPublicKeyTag); + NSData *newPublicKeyData = FIRInstanceIDKeyDataWithTag(newPublicKeyTag); + return [oldPublicKeyData isEqualToData:newPublicKeyData]; +} + +// The legacy value is hardcoded to be the same key. This is a potential problem in shared keychain +// environments. +NSString *FIRInstanceIDLegacyPublicTagWithSubtype(NSString *subtype) { + NSString *prefix = kFIRInstanceIDStoreKeyPrefix; + return [NSString stringWithFormat:@"%@%@%@", prefix, subtype, kFIRInstanceIDStoreKeyPublic]; +} + +// The legacy value is hardcoded to be the same key. This is a potential problem in shared keychain +// environments. +NSString *FIRInstanceIDLegacyPrivateTagWithSubtype(NSString *subtype) { + NSString *prefix = kFIRInstanceIDStoreKeyPrefix; + return [NSString stringWithFormat:@"%@%@%@", prefix, subtype, kFIRInstanceIDStoreKeyPrivate]; +} + +NSString *FIRInstanceIDPublicTagWithSubtype(NSString *subtype) { + static NSString *publicTag; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *mainAppBundleID = FIRInstanceIDAppIdentifier(); + publicTag = + [NSString stringWithFormat:@"%@%@", kFIRInstanceIDKeyPairPublicTagPrefix, mainAppBundleID]; + }); + return publicTag; +} + +NSString *FIRInstanceIDPrivateTagWithSubtype(NSString *subtype) { + static NSString *privateTag; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *mainAppBundleID = FIRInstanceIDAppIdentifier(); + privateTag = + [NSString stringWithFormat:@"%@%@", kFIRInstanceIDKeyPairPrivateTagPrefix, mainAppBundleID]; + }); + return privateTag; +} + +NSString *FIRInstanceIDCreationTimeKeyWithSubtype(NSString *subtype) { + return [NSString stringWithFormat:@"%@%@%@", subtype, kFIRInstanceIDStoreKeySubtype, + kFIRInstanceIDStoreKeyGenerationTime]; +} + +@interface FIRInstanceIDKeyPairStore () + +@property(nonatomic, readwrite, strong) FIRInstanceIDBackupExcludedPlist *plist; +@property(nonatomic, readwrite, strong) FIRInstanceIDKeyPair *keyPair; +@property(nonatomic, readwrite, assign) NSInteger keychainEntitlementsErrorCount; + +@end + +@implementation FIRInstanceIDKeyPairStore + +- (instancetype)init { + self = [super init]; + if (self) { + NSString *fileName = [[self class] keyStoreFileName]; + _plist = + [[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:fileName + subDirectory:kFIRInstanceIDSubDirectoryName]; + } + return self; +} + +- (BOOL)invalidateKeyPairsIfNeeded { + // Currently keypairs are always invalidated if self.plist is missing. This normally indicates + // a fresh install (or an uninstall/reinstall). In those situations the key pairs should be + // deleted. + // NOTE: Although this class refers to multiple key pairs, with different subtypes, in practice + // only a single subtype is currently supported. (b/64906549) + if (![self.plist doesFileExist]) { + // A fresh install, clear all the key pairs in the key chain. Do not perform migration as all + // key pairs are gone. + [self deleteSavedKeyPairWithSubtype:kFIRInstanceIDKeyPairSubType handler:nil]; + return YES; + } + // Not a fresh install, perform migration at early state. + [self migrateKeyPairCacheIfNeededWithHandler:nil]; + return NO; +} + +- (BOOL)hasCachedKeyPairs { + NSError *error; + if ([self cachedKeyPairWithSubtype:kFIRInstanceIDKeyPairSubType error:&error] == nil) { + if (error) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeKeyPairStore000, + @"Failed to get cached keyPair %@", error); + } + error = nil; + [self removeKeyPairCreationTimePlistWithError:&error]; + if (error) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeKeyPairStore001, + @"Failed to remove keyPair creationTime plist %@", error); + } + return NO; + } + return YES; +} + +- (NSString *)appIdentityWithError:(NSError *__autoreleasing *)error { + // Load the keyPair from Keychain (or generate a key pair, if this is the first run of the app). + FIRInstanceIDKeyPair *keyPair = [self loadKeyPairWithError:error]; + if (!keyPair) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPairStoreCouldNotLoadKeyPair, + @"Keypair could not be loaded from Keychain. Error: %@", (*error)); + return nil; + } + + if (error) { + *error = nil; + } + NSString *appIdentity = FIRInstanceIDAppIdentity(keyPair); + if (!appIdentity.length) { + if (error) { + *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeUnknown]; + } + } + return appIdentity; +} + +- (FIRInstanceIDKeyPair *)loadKeyPairWithError:(NSError **)error { + // In case we call this from different threads we don't want to generate or fetch the + // keyPair multiple times. Once we have a keyPair in the cache it would mostly be used + // from there. + @synchronized(self) { + if ([self.keyPair isValid]) { + return self.keyPair; + } + + if (self.keychainEntitlementsErrorCount >= kMaxMissingEntitlementErrorCount) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeKeyPairStore002, + @"Keychain not accessible, Entitlements missing error (-34018). " + @"Will not check token in cache."); + return nil; + } + + if (!self.keyPair) { + self.keyPair = [self validCachedKeyPairWithSubtype:kFIRInstanceIDKeyPairSubType error:error]; + } + + if ((*error).code == kFIRInstanceIDSecMissingEntitlementErrorCode) { + self.keychainEntitlementsErrorCount++; + } + + if (!self.keyPair) { + self.keyPair = [self generateAndSaveKeyWithSubtype:kFIRInstanceIDKeyPairSubType + creationTime:FIRInstanceIDCurrentTimestampInSeconds() + error:error]; + } + } + return self.keyPair; +} + +// TODO(chliangGoogle: Remove subtype support, as it's not being used. +- (FIRInstanceIDKeyPair *)generateAndSaveKeyWithSubtype:(NSString *)subtype + creationTime:(int64_t)creationTime + error:(NSError **)error { + NSString *publicKeyTag = FIRInstanceIDPublicTagWithSubtype(subtype); + NSString *privateKeyTag = FIRInstanceIDPrivateTagWithSubtype(subtype); + FIRInstanceIDKeyPair *keyPair = + [[FIRInstanceIDKeychain sharedInstance] generateKeyPairWithPrivateTag:privateKeyTag + publicTag:publicKeyTag]; + + if (![keyPair isValid]) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPairStore003, + @"Unable to generate keypair."); + return nil; + } + + NSString *creationTimeKey = FIRInstanceIDCreationTimeKeyWithSubtype(subtype); + NSDictionary *keyPairData = @{creationTimeKey : @(creationTime)}; + + if (error) { + *error = nil; + } + NSMutableDictionary *allKeyPairs = [[self.plist contentAsDictionary] mutableCopy]; + if (allKeyPairs.count) { + [allKeyPairs addEntriesFromDictionary:keyPairData]; + } else { + allKeyPairs = [keyPairData mutableCopy]; + } + if (![self.plist writeDictionary:allKeyPairs error:error]) { + [FIRInstanceIDKeyPairStore deleteKeyPairWithPrivateTag:privateKeyTag + publicTag:publicKeyTag + handler:nil]; + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPairStore004, + @"Failed to save keypair data to plist %@", error ? *error : @""); + return nil; + } + + return keyPair; +} + +- (FIRInstanceIDKeyPair *)validCachedKeyPairWithSubtype:(NSString *)subtype + error:(NSError **)error { + // On a new install (or if the ID was deleted), the plist will be missing, which should trigger + // a reset of the key pairs in Keychain (if they exist). + NSDictionary *allKeyPairs = [self.plist contentAsDictionary]; + NSString *creationTimeKey = FIRInstanceIDCreationTimeKeyWithSubtype(subtype); + + if (allKeyPairs[creationTimeKey] > 0) { + return [self cachedKeyPairWithSubtype:subtype error:error]; + } else { + // There is no need to reset keypair again here as FIRInstanceID init call is always + // going to be ahead of this call, which already trigger keypair reset if it's new install + FIRInstanceIDErrorCode code = kFIRInstanceIDErrorCodeInvalidKeyPairCreationTime; + if (error) { + *error = [NSError errorWithFIRInstanceIDErrorCode:code]; + } + return nil; + } +} + +- (FIRInstanceIDKeyPair *)cachedKeyPairWithSubtype:(NSString *)subtype + error:(NSError *__autoreleasing *)error { + // base64 encoded keys + NSString *publicKeyTag = FIRInstanceIDPublicTagWithSubtype(subtype); + NSString *privateKeyTag = FIRInstanceIDPrivateTagWithSubtype(subtype); + return [FIRInstanceIDKeyPairStore keyPairForPrivateKeyTag:privateKeyTag + publicKeyTag:publicKeyTag + error:error]; +} + ++ (FIRInstanceIDKeyPair *)keyPairForPrivateKeyTag:(NSString *)privateKeyTag + publicKeyTag:(NSString *)publicKeyTag + error:(NSError *__autoreleasing *)error { + _FIRInstanceIDDevAssert([privateKeyTag length] && [publicKeyTag length], + @"Invalid tags for keypair"); + if (![privateKeyTag length] || ![publicKeyTag length]) { + if (error) { + *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidKeyPairTags]; + } + return nil; + } + + SecKeyRef privateKeyRef = FIRInstanceIDCachedKeyRefWithTag(privateKeyTag); + SecKeyRef publicKeyRef = FIRInstanceIDCachedKeyRefWithTag(publicKeyTag); + + if (!privateKeyRef || !publicKeyRef) { + if (error) { + *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeMissingKeyPair]; + } + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeKeyPair000, + @"No keypair info is retrieved with tag %@", privateKeyTag); + return nil; + } + + NSData *publicKeyData = FIRInstanceIDKeyDataWithTag(publicKeyTag); + NSData *privateKeyData = FIRInstanceIDKeyDataWithTag(privateKeyTag); + + FIRInstanceIDKeyPair *keyPair = [[FIRInstanceIDKeyPair alloc] initWithPrivateKey:privateKeyRef + publicKey:publicKeyRef + publicKeyData:publicKeyData + privateKeyData:privateKeyData]; + return keyPair; +} + +// Migrates from keypair saved under legacy keys (hardcoded value) to dynamic keys (stable, but +// unique for the app's bundle id +- (void)migrateKeyPairCacheIfNeededWithHandler:(void (^)(NSError *error))handler { + // Attempt to load keypair using legacy keys + NSString *legacyPublicKeyTag = + FIRInstanceIDLegacyPublicTagWithSubtype(kFIRInstanceIDKeyPairSubType); + NSString *legacyPrivateKeyTag = + FIRInstanceIDLegacyPrivateTagWithSubtype(kFIRInstanceIDKeyPairSubType); + NSError *error; + FIRInstanceIDKeyPair *keyPair = + [FIRInstanceIDKeyPairStore keyPairForPrivateKeyTag:legacyPrivateKeyTag + publicKeyTag:legacyPublicKeyTag + error:&error]; + if (![keyPair isValid]) { + if (handler) { + handler(nil); + } + return; + } + + // Check whether migration already done. + NSString *publicKeyTag = FIRInstanceIDPublicTagWithSubtype(kFIRInstanceIDKeyPairSubType); + if (FIRInstanceIDHasMigratedKeyPair(legacyPublicKeyTag, publicKeyTag)) { + if (handler) { + handler(nil); + } + return; + } + + // Also cache locally since we are sure to use the migrated key pair. + self.keyPair = keyPair; + + // Either new key pair doesn't exist or it's different than legacy key pair, start the migration. + NSString *privateKeyTag = FIRInstanceIDPrivateTagWithSubtype(kFIRInstanceIDKeyPairSubType); + [self updateKeyRef:keyPair.publicKey + withTag:publicKeyTag + handler:^(NSError *error) { + if (error) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPairMigrationError, + @"Unable to migrate key pair from legacy ones."); + } + [self updateKeyRef:keyPair.privateKey + withTag:privateKeyTag + handler:^(NSError *error) { + if (error) { + FIRInstanceIDLoggerError( + kFIRInstanceIDMessageCodeKeyPairMigrationError, + @"Unable to migrate key pair from legacy ones."); + return; + } + FIRInstanceIDLoggerDebug( + kFIRInstanceIDMessageCodeKeyPairMigrationSuccess, + @"Successfully migrated the key pair from legacy ones."); + if (handler) { + handler(error); + } + }]; + }]; +} + +// Used for migrating from legacy tags to updated tags. The legacy keychain is not deleted for +// backward compatibility. +// TODO(chliangGoogle) Delete the legacy keychain when GCM is fully deprecated. +- (void)updateKeyRef:(SecKeyRef)keyRef + withTag:(NSString *)tag + handler:(void (^)(NSError *error))handler { + NSData *updatedTagData = [tag dataUsingEncoding:NSUTF8StringEncoding]; + + // Always delete the old keychain before adding a new one to avoid conflicts. + NSDictionary *deleteQuery = @{ + (__bridge id)kSecAttrApplicationTag : updatedTagData, + (__bridge id)kSecClass : (__bridge id)kSecClassKey, + (__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeRSA, + (__bridge id)kSecReturnRef : @(YES), + }; + + [[FIRInstanceIDKeychain sharedInstance] + removeItemWithQuery:deleteQuery + handler:^(NSError *error) { + if (error) { + if (handler) { + handler(error); + } + return; + } + NSDictionary *addQuery = @{ + (__bridge id)kSecAttrApplicationTag : updatedTagData, + (__bridge id)kSecClass : (__bridge id)kSecClassKey, + (__bridge id)kSecValueRef : (__bridge id)keyRef, + (__bridge id) + kSecAttrAccessible : (__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly, + }; + [[FIRInstanceIDKeychain sharedInstance] addItemWithQuery:addQuery + handler:^(NSError *addError) { + if (handler) { + handler(addError); + } + }]; + }]; +} + +- (void)deleteSavedKeyPairWithSubtype:(NSString *)subtype + handler:(void (^)(NSError *error))handler { + NSDictionary *allKeyPairs = [self.plist contentAsDictionary]; + + NSString *publicKeyTag = FIRInstanceIDPublicTagWithSubtype(subtype); + NSString *privateKeyTag = FIRInstanceIDPrivateTagWithSubtype(subtype); + NSString *creationTimeKey = FIRInstanceIDCreationTimeKeyWithSubtype(subtype); + + // remove the creation time + if (allKeyPairs[creationTimeKey] > 0) { + NSMutableDictionary *newKeyPairs = [NSMutableDictionary dictionaryWithDictionary:allKeyPairs]; + [newKeyPairs removeObjectForKey:creationTimeKey]; + + NSError *plistError; + if (![self.plist writeDictionary:newKeyPairs error:&plistError]) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPairStore006, + @"Unable to remove keypair creation time from plist %@", plistError); + } + } + + [FIRInstanceIDKeyPairStore + deleteKeyPairWithPrivateTag:privateKeyTag + publicTag:publicKeyTag + handler:^(NSError *error) { + // Delete legacy key pairs from GCM/FCM If they exist. All key pairs + // should be deleted when app is newly installed. + NSString *legacyPublicKeyTag = + FIRInstanceIDLegacyPublicTagWithSubtype(subtype); + NSString *legacyPrivateKeyTag = + FIRInstanceIDLegacyPrivateTagWithSubtype(subtype); + [FIRInstanceIDKeyPairStore + deleteKeyPairWithPrivateTag:legacyPrivateKeyTag + publicTag:legacyPublicKeyTag + handler:nil]; + if (error) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPairStore007, + @"Unable to remove RSA keypair, error: %@", + error); + if (handler) { + handler(error); + } + } else { + self.keyPair = nil; + if (handler) { + handler(nil); + } + } + }]; +} + ++ (void)deleteKeyPairWithPrivateTag:(NSString *)privateTag + publicTag:(NSString *)publicTag + handler:(void (^)(NSError *))handler { + NSDictionary *queryPublicKey = FIRInstanceIDKeyPairQuery(publicTag, NO, NO); + NSDictionary *queryPrivateKey = FIRInstanceIDKeyPairQuery(privateTag, NO, NO); + + // Always remove public key first because it is the key we generate IID. + [[FIRInstanceIDKeychain sharedInstance] removeItemWithQuery:queryPublicKey + handler:^(NSError *error) { + if (error) { + if (handler) { + handler(error); + } + return; + } + [[FIRInstanceIDKeychain sharedInstance] + removeItemWithQuery:queryPrivateKey + handler:^(NSError *error) { + if (error) { + if (handler) { + handler(error); + } + return; + } + if (handler) { + handler(nil); + } + }]; + }]; +} + +- (BOOL)removeKeyPairCreationTimePlistWithError:(NSError *__autoreleasing *)error { + if (![self.plist deleteFile:error]) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPairStore008, + @"Unable to delete keypair creation times plist"); + return NO; + } + return YES; +} + ++ (NSString *)keyStoreFileName { + return kFIRInstanceIDKeyPairStoreFileName; +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDKeyPairUtilities.h b/Firebase/InstanceID/FIRInstanceIDKeyPairUtilities.h new file mode 100644 index 00000000000..b8baa6af2a8 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDKeyPairUtilities.h @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Google + * + * 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 FIRInstanceIDKeyPair; + +/** + * A web-safe base64 encoded string with no padding. + * + * @param data The data to encode. + * + * @return A web-safe base 64 encoded string with no padding. + */ +FOUNDATION_EXPORT NSString *FIRInstanceIDWebSafeBase64(NSData *data); + +FOUNDATION_EXPORT NSData *FIRInstanceIDSHA1(NSData *data); + +FOUNDATION_EXPORT NSDictionary *FIRInstanceIDKeyPairQuery(NSString *tag, + BOOL addReturnAttr, + BOOL returnData); + +FOUNDATION_EXPORT NSString *FIRInstanceIDAppIdentity(FIRInstanceIDKeyPair *keyPair); diff --git a/Firebase/InstanceID/FIRInstanceIDKeyPairUtilities.m b/Firebase/InstanceID/FIRInstanceIDKeyPairUtilities.m new file mode 100644 index 00000000000..3298752f5f2 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDKeyPairUtilities.m @@ -0,0 +1,84 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDKeyPairUtilities.h" + +#import + +#import "FIRInstanceIDDefines.h" +#import "FIRInstanceIDKeyPair.h" +#import "FIRInstanceIDLogger.h" +#import "FIRInstanceIDStringEncoding.h" + +NSString *FIRInstanceIDWebSafeBase64(NSData *data) { + // Websafe encoding with no padding. + FIRInstanceIDStringEncoding *encoding = + [FIRInstanceIDStringEncoding rfc4648Base64WebsafeStringEncoding]; + [encoding setDoPad:NO]; + return [encoding encode:data]; +} + +NSData *FIRInstanceIDSHA1(NSData *data) { + unsigned int outputLength = CC_SHA1_DIGEST_LENGTH; + unsigned char output[outputLength]; + unsigned int length = (unsigned int)[data length]; + + CC_SHA1(data.bytes, length, output); + return [NSMutableData dataWithBytes:output length:outputLength]; +} + +NSDictionary *FIRInstanceIDKeyPairQuery(NSString *tag, BOOL addReturnAttr, BOOL returnData) { + NSMutableDictionary *queryKey = [NSMutableDictionary dictionary]; + NSData *tagData = [tag dataUsingEncoding:NSUTF8StringEncoding]; + + queryKey[(__bridge id)kSecClass] = (__bridge id)kSecClassKey; + queryKey[(__bridge id)kSecAttrApplicationTag] = tagData; + queryKey[(__bridge id)kSecAttrKeyType] = (__bridge id)kSecAttrKeyTypeRSA; + if (addReturnAttr) { + if (returnData) { + queryKey[(__bridge id)kSecReturnData] = @(YES); + } else { + queryKey[(__bridge id)kSecReturnRef] = @(YES); + } + } + return queryKey; +} + +NSString *FIRInstanceIDAppIdentity(FIRInstanceIDKeyPair *keyPair) { + // An Instance-ID is a 64 bit (8 byte) integer with a fixed 4-bit header of 0111 (=^ 0x7). + // The variable 60 bits are obtained by truncating the SHA1 of the app-instance's public key. + SecKeyRef publicKeyRef = [keyPair publicKey]; + if (!publicKeyRef) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeKeyPair002, + @"Unable to create a valid asymmetric crypto key"); + return nil; + } + NSData *publicKeyData = keyPair.publicKeyData; + NSData *publicKeySHA1 = FIRInstanceIDSHA1(publicKeyData); + + const uint8_t *bytes = publicKeySHA1.bytes; + NSMutableData *identityData = [NSMutableData dataWithData:publicKeySHA1]; + + uint8_t b0 = bytes[0]; + // Take the first byte and make the initial four 7 by initially making the initial 4 bits 0 + // and then adding 0x70 to it. + b0 = 0x70 + (0xF & b0); + // failsafe should give you back b0 itself + b0 = (b0 & 0xFF); + [identityData replaceBytesInRange:NSMakeRange(0, 1) withBytes:&b0]; + NSData *data = [identityData subdataWithRange:NSMakeRange(0, 8 * sizeof(Byte))]; + return FIRInstanceIDWebSafeBase64(data); +} diff --git a/Firebase/InstanceID/FIRInstanceIDKeychain.h b/Firebase/InstanceID/FIRInstanceIDKeychain.h new file mode 100644 index 00000000000..0bd2a49333b --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDKeychain.h @@ -0,0 +1,76 @@ +/* + * Copyright 2019 Google + * + * 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 + +/* The Keychain error domain */ +extern NSString *const kFIRInstanceIDKeychainErrorDomain; + +@class FIRInstanceIDKeyPair; + +/* + * Wrapping the keychain operations in a serialize queue. This is to avoid keychain operation + * blocking main queue. + */ +@interface FIRInstanceIDKeychain : NSObject + +/** + * FIRInstanceIDKeychain. + * + * @return A shared instance of FIRInstanceIDKeychain. + */ ++ (instancetype)sharedInstance; + +/** + * Get keychain items matching the given a query. + * + * @param keychainQuery The keychain query. + * + * @return An CFTypeRef result matching the provided inputs. + */ +- (CFTypeRef)itemWithQuery:(NSDictionary *)keychainQuery; + +/** + * Remove the cached items from the keychain matching the query. + * + * @param keychainQuery The keychain query. + * @param handler The callback handler which is invoked when the remove operation is + * complete, with an error if there is any. + */ +- (void)removeItemWithQuery:(NSDictionary *)keychainQuery handler:(void (^)(NSError *error))handler; + +/** + * Add the item with a given query. + * + * @param keychainQuery The keychain query. + * @param handler The callback handler which is invoked when the add operation is + * complete, with an error if there is any. + */ +- (void)addItemWithQuery:(NSDictionary *)keychainQuery handler:(void (^)(NSError *))handler; + +#pragma mark - Keypair +/** + * Generate a public/private key pair given their tags. + * + * @param privateTag The private tag associated with the private key. + * @param publicTag The public tag associated with the public key. + * + * @return A new FIRInstanceIDKeyPair object. + */ +- (FIRInstanceIDKeyPair *)generateKeyPairWithPrivateTag:(NSString *)privateTag + publicTag:(NSString *)publicTag; + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDKeychain.m b/Firebase/InstanceID/FIRInstanceIDKeychain.m new file mode 100644 index 00000000000..c5f1606508d --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDKeychain.m @@ -0,0 +1,175 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDKeychain.h" + +#import "FIRInstanceIDKeyPair.h" +#import "FIRInstanceIDKeyPairUtilities.h" +#import "FIRInstanceIDLogger.h" + +NSString *const kFIRInstanceIDKeychainErrorDomain = @"com.google.iid"; + +static const NSUInteger kRSA2048KeyPairSize = 2048; + +@interface FIRInstanceIDKeychain () { + dispatch_queue_t _keychainOperationQueue; +} + +@end + +@implementation FIRInstanceIDKeychain + ++ (instancetype)sharedInstance { + static FIRInstanceIDKeychain *sharedInstance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[FIRInstanceIDKeychain alloc] init]; + }); + return sharedInstance; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _keychainOperationQueue = + dispatch_queue_create("com.google.FirebaseInstanceID.Keychain", DISPATCH_QUEUE_SERIAL); + } + return self; +} + +- (CFTypeRef)itemWithQuery:(NSDictionary *)keychainQuery { + __block SecKeyRef keyRef = NULL; + dispatch_sync(_keychainOperationQueue, ^{ + OSStatus status = + SecItemCopyMatching((__bridge CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyRef); + + if (status != noErr) { + if (keyRef) { + CFRelease(keyRef); + } + FIRInstanceIDLoggerDebug( + kFIRInstanceIDKeychainReadItemError, + @"No info is retrieved from Keychain OSStatus: %d with the keychain query %@", + (int)status, keychainQuery); + } + }); + return keyRef; +} + +- (void)removeItemWithQuery:(NSDictionary *)keychainQuery + handler:(void (^)(NSError *error))handler { + dispatch_async(_keychainOperationQueue, ^{ + OSStatus status = SecItemDelete((__bridge CFDictionaryRef)keychainQuery); + if (status != noErr) { + FIRInstanceIDLoggerDebug( + kFIRInstanceIDKeychainDeleteItemError, + @"Couldn't delete item from Keychain OSStatus: %d with the keychain query %@", + (int)status, keychainQuery); + } + + if (handler) { + NSError *error; + // When item is not found, it should NOT be considered as an error. The operation should + // continue. + if (status != noErr && status != errSecItemNotFound) { + error = [NSError errorWithDomain:kFIRInstanceIDKeychainErrorDomain + code:status + userInfo:nil]; + } + dispatch_async(dispatch_get_main_queue(), ^{ + handler(error); + }); + } + }); +} + +- (void)addItemWithQuery:(NSDictionary *)keychainQuery handler:(void (^)(NSError *))handler { + dispatch_async(_keychainOperationQueue, ^{ + OSStatus status = SecItemAdd((__bridge CFDictionaryRef)keychainQuery, NULL); + + if (handler) { + NSError *error; + if (status != noErr) { + FIRInstanceIDLoggerWarning(kFIRInstanceIDKeychainAddItemError, + @"Couldn't add item to Keychain OSStatus: %d", (int)status); + error = [NSError errorWithDomain:kFIRInstanceIDKeychainErrorDomain + code:status + userInfo:nil]; + } + dispatch_async(dispatch_get_main_queue(), ^{ + handler(error); + }); + } + }); +} + +- (FIRInstanceIDKeyPair *)generateKeyPairWithPrivateTag:(NSString *)privateTag + publicTag:(NSString *)publicTag { + // TODO(chliangGoogle) this is called by appInstanceID, which is an internal API used by other + // Firebase teams, will see if we can make it async. + NSData *publicTagData = [publicTag dataUsingEncoding:NSUTF8StringEncoding]; + NSData *privateTagData = [privateTag dataUsingEncoding:NSUTF8StringEncoding]; + + NSDictionary *privateKeyAttr = @{ + (__bridge id)kSecAttrIsPermanent : @YES, + (__bridge id)kSecAttrApplicationTag : privateTagData, + (__bridge id)kSecAttrLabel : @"Firebase InstanceID Key Pair Private Key", + (__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly, + }; + + NSDictionary *publicKeyAttr = @{ + (__bridge id)kSecAttrIsPermanent : @YES, + (__bridge id)kSecAttrApplicationTag : publicTagData, + (__bridge id)kSecAttrLabel : @"Firebase InstanceID Key Pair Public Key", + (__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly, + }; + + NSDictionary *keyPairAttributes = @{ + (__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeRSA, + (__bridge id)kSecAttrLabel : @"Firebase InstanceID Key Pair", + (__bridge id)kSecAttrKeySizeInBits : @(kRSA2048KeyPairSize), + (__bridge id)kSecPrivateKeyAttrs : privateKeyAttr, + (__bridge id)kSecPublicKeyAttrs : publicKeyAttr, + }; + + __block SecKeyRef privateKey = NULL; + __block SecKeyRef publicKey = NULL; + dispatch_sync(_keychainOperationQueue, ^{ + // SecKeyGeneratePair does not allow you to set kSetAttrAccessible on the keys. We need the keys + // to be accessible even when the device is locked (i.e. app is woken up during a push + // notification, or some background refresh). + OSStatus status = + SecKeyGeneratePair((__bridge CFDictionaryRef)keyPairAttributes, &publicKey, &privateKey); + if (status != noErr || publicKey == NULL || privateKey == NULL) { + FIRInstanceIDLoggerWarning(kFIRInstanceIDKeychainCreateKeyPairError, + @"Couldn't create keypair from Keychain OSStatus: %d", + (int)status); + } + }); + // Extract the actual public and private key data from the Keychain + NSDictionary *publicKeyDataQuery = FIRInstanceIDKeyPairQuery(publicTag, YES, YES); + NSDictionary *privateKeyDataQuery = FIRInstanceIDKeyPairQuery(privateTag, YES, YES); + + NSData *publicKeyData = (__bridge NSData *)[self itemWithQuery:publicKeyDataQuery]; + NSData *privateKeyData = (__bridge NSData *)[self itemWithQuery:privateKeyDataQuery]; + + return [[FIRInstanceIDKeyPair alloc] initWithPrivateKey:privateKey + publicKey:publicKey + publicKeyData:publicKeyData + privateKeyData:privateKeyData]; +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDLogger.h b/Firebase/InstanceID/FIRInstanceIDLogger.h new file mode 100644 index 00000000000..ab93976ca44 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDLogger.h @@ -0,0 +1,66 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRIMessageCode.h" + +// The convenience macros are only defined if they haven't already been defined. +#ifndef FIRInstanceIDLoggerInfo + +// Convenience macros that log to the shared GTMLogger instance. These macros +// are how users should typically log to FIRInstanceIDLogger. +#define FIRInstanceIDLoggerDebug(code, ...) \ + [FIRInstanceIDSharedLogger() logFuncDebug:__func__ messageCode:code msg:__VA_ARGS__] +#define FIRInstanceIDLoggerInfo(code, ...) \ + [FIRInstanceIDSharedLogger() logFuncInfo:__func__ messageCode:code msg:__VA_ARGS__] +#define FIRInstanceIDLoggerNotice(code, ...) \ + [FIRInstanceIDSharedLogger() logFuncNotice:__func__ messageCode:code msg:__VA_ARGS__] +#define FIRInstanceIDLoggerWarning(code, ...) \ + [FIRInstanceIDSharedLogger() logFuncWarning:__func__ messageCode:code msg:__VA_ARGS__] +#define FIRInstanceIDLoggerError(code, ...) \ + [FIRInstanceIDSharedLogger() logFuncError:__func__ messageCode:code msg:__VA_ARGS__] + +#endif // !defined(FIRInstanceIDLoggerInfo) + +@interface FIRInstanceIDLogger : NSObject + +- (void)logFuncDebug:(const char *)func + messageCode:(FIRInstanceIDMessageCode)messageCode + msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4); + +- (void)logFuncInfo:(const char *)func + messageCode:(FIRInstanceIDMessageCode)messageCode + msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4); + +- (void)logFuncNotice:(const char *)func + messageCode:(FIRInstanceIDMessageCode)messageCode + msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4); + +- (void)logFuncWarning:(const char *)func + messageCode:(FIRInstanceIDMessageCode)messageCode + msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4); + +- (void)logFuncError:(const char *)func + messageCode:(FIRInstanceIDMessageCode)messageCode + msg:(NSString *)fmt, ... NS_FORMAT_FUNCTION(3, 4); + +@end + +/** + * Instantiates and/or returns a shared GTMLogger used exclusively + * for InstanceID log messages. + * @return the shared GTMLogger instance + */ +FIRInstanceIDLogger *FIRInstanceIDSharedLogger(void); diff --git a/Firebase/InstanceID/FIRInstanceIDLogger.m b/Firebase/InstanceID/FIRInstanceIDLogger.m new file mode 100644 index 00000000000..2600d3ba8f5 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDLogger.m @@ -0,0 +1,92 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDLogger.h" + +#import + +// Re-definition of FIRLogger service, as it is not included in :FIRAppHeaders target +NSString *const kFIRInstanceIDLoggerService = @"[Firebase/InstanceID]"; + +@implementation FIRInstanceIDLogger + +#pragma mark - Log Helpers + ++ (NSString *)formatMessageCode:(FIRInstanceIDMessageCode)messageCode { + return [NSString stringWithFormat:@"I-IID%06ld", (long)messageCode]; +} + +- (void)logFuncDebug:(const char *)func + messageCode:(FIRInstanceIDMessageCode)messageCode + msg:(NSString *)fmt, ... { + va_list args; + va_start(args, fmt); + FIRLogBasic(FIRLoggerLevelDebug, kFIRInstanceIDLoggerService, + [FIRInstanceIDLogger formatMessageCode:messageCode], fmt, args); + va_end(args); +} + +- (void)logFuncInfo:(const char *)func + messageCode:(FIRInstanceIDMessageCode)messageCode + msg:(NSString *)fmt, ... { + va_list args; + va_start(args, fmt); + FIRLogBasic(FIRLoggerLevelInfo, kFIRInstanceIDLoggerService, + [FIRInstanceIDLogger formatMessageCode:messageCode], fmt, args); + va_end(args); +} + +- (void)logFuncNotice:(const char *)func + messageCode:(FIRInstanceIDMessageCode)messageCode + msg:(NSString *)fmt, ... { + va_list args; + va_start(args, fmt); + FIRLogBasic(FIRLoggerLevelNotice, kFIRInstanceIDLoggerService, + [FIRInstanceIDLogger formatMessageCode:messageCode], fmt, args); + va_end(args); +} + +- (void)logFuncWarning:(const char *)func + messageCode:(FIRInstanceIDMessageCode)messageCode + msg:(NSString *)fmt, ... { + va_list args; + va_start(args, fmt); + FIRLogBasic(FIRLoggerLevelWarning, kFIRInstanceIDLoggerService, + [FIRInstanceIDLogger formatMessageCode:messageCode], fmt, args); + va_end(args); +} + +- (void)logFuncError:(const char *)func + messageCode:(FIRInstanceIDMessageCode)messageCode + msg:(NSString *)fmt, ... { + va_list args; + va_start(args, fmt); + FIRLogBasic(FIRLoggerLevelError, kFIRInstanceIDLoggerService, + [FIRInstanceIDLogger formatMessageCode:messageCode], fmt, args); + va_end(args); +} + +@end + +FIRInstanceIDLogger *FIRInstanceIDSharedLogger() { + static dispatch_once_t onceToken; + static FIRInstanceIDLogger *logger; + dispatch_once(&onceToken, ^{ + logger = [[FIRInstanceIDLogger alloc] init]; + }); + + return logger; +} diff --git a/Firebase/InstanceID/FIRInstanceIDStore.h b/Firebase/InstanceID/FIRInstanceIDStore.h new file mode 100644 index 00000000000..d1f26348707 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDStore.h @@ -0,0 +1,183 @@ +/* + * Copyright 2019 Google + * + * 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 + +@class FIRInstanceIDBackupExcludedPlist; +@class FIRInstanceIDCheckinPreferences; +@class FIRInstanceIDCheckinStore; +@class FIRInstanceIDTokenInfo; +@class FIRInstanceIDTokenStore; + +@class FIRInstanceIDStore; +@protocol FIRInstanceIDStoreDelegate + +/** + * This is called when the store has decided to invalide its tokens associated with the + * previous checkin credentials. After deleting the tokens locally, it calls this method + * to notify the delegate of the change. If possible, the delegate should use this time + * to request the invalidation of the tokens on the server as well. + */ +- (void)store:(FIRInstanceIDStore *)store + didDeleteFCMScopedTokensForCheckin:(FIRInstanceIDCheckinPreferences *)checkin; + +@end + +/** + * Used to persist the InstanceID tokens. This is also used to cache the Checkin + * credentials. The store also checks for stale entries in the store and + * let's us know if things in the store are stale or not. It does not however + * acts on stale entries in anyway. + */ +@interface FIRInstanceIDStore : NSObject + +/** + * The delegate set in the initializer which is notified of changes in the store. + */ +@property(nonatomic, readonly, weak) NSObject *delegate; + +- (instancetype)init __attribute__((unavailable("Use initWithDelegate: instead."))); + +/** + * Initialize a default store to persist InstanceID tokens and options. + * + * @param delegate The delegate with which to be notified of changes in the store. + * @return Store to persist InstanceID tokens. + */ +- (instancetype)initWithDelegate:(NSObject *)delegate; + +/** + * Initialize a store with the token store used to persist tokens, and a checkin store. + * Used for testing. + * + * @param checkinStore Persistent store that persists checkin preferences. + * @param tokenStore Persistent store that persists tokens. + * + * @return Store to persist InstanceID tokens and options. + */ +- (instancetype)initWithCheckinStore:(FIRInstanceIDCheckinStore *)checkinStore + tokenStore:(FIRInstanceIDTokenStore *)tokenStore + delegate:(NSObject *)delegate + NS_DESIGNATED_INITIALIZER; + +#pragma mark - Save +/** + * Save the instanceID token info to the store. + * + * @param tokenInfo The token info to store. + * @param handler The callback handler which is invoked when the operation is complete, + * with an error if there is any. + */ +- (void)saveTokenInfo:(FIRInstanceIDTokenInfo *)tokenInfo handler:(void (^)(NSError *))handler; + +#pragma mark - Get + +/** + * Get the cached token info. + * + * @param authorizedEntity The authorized entity for which we want the token. + * @param scope The scope for which we want the token. + * + * @return The cached token info if any for the given authorizedEntity and scope else + * returns nil. + */ +- (nullable FIRInstanceIDTokenInfo *)tokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope; +/** + * Return all cached token infos from the Keychain. + * + * @return The cached token infos, if any, that are stored in the Keychain. + */ +- (NSArray *)cachedTokenInfos; + +#pragma mark - Delete + +/** + * Remove the cached token for a given authorizedEntity and scope. If the token was never + * cached or deleted from the cache before this is a no-op. + * + * @param authorizedEntity The authorizedEntity for the cached token. + * @param scope The scope for the cached token + */ +- (void)removeCachedTokenWithAuthorizedEntity:(NSString *)authorizedEntity scope:(NSString *)scope; + +/** + * Removes all cached tokens from the persistent store. In case deleting the cached tokens + * fails we try to delete the backup excluded plist that stores the tokens. + * + * @param handler The callback handler which is invoked when the operation is complete, + * with an error if there is any. + * + */ +- (void)removeAllCachedTokensWithHandler:(nullable void (^)(NSError *error))handler; + +#pragma mark - Persisting Checkin Preferences + +/** + * Save the checkin preferences + * + * @param preferences Checkin preferences to save. + * @param handler The callback handler which is invoked when the operation is complete, + * with an error if there is any. + */ +- (void)saveCheckinPreferences:(FIRInstanceIDCheckinPreferences *)preferences + handler:(nullable void (^)(NSError *error))handler; + +/** + * Return the cached checkin preferences. + * + * @return Checkin preferences. + */ +- (FIRInstanceIDCheckinPreferences *)cachedCheckinPreferences; + +/** + * Remove the cached checkin preferences from the store. + * + * @param handler The callback handler which is invoked when the operation is complete, + * with an error if there is any. + */ +- (void)removeCheckinPreferencesWithHandler:(nullable void (^)(NSError *error))handler; + +#pragma mark - Standard Directory sub-directory + +/** + * Check if supported directory has InstanceID subdirectory + * + * @return YES if the Application Support directory has InstanceID subdirectory else NO. + */ ++ (BOOL)hasSubDirectory:(NSString *)subDirectoryName; + +/** + * Create InstanceID subdirectory in Application support directory. + * + * @return YES if the subdirectory was created successfully else NO. + */ ++ (BOOL)createSubDirectory:(NSString *)subDirectoryName; + +/** + * Removes Application Support subdirectory for InstanceID. + * + * @param error The error object if any while trying to delete the sub-directory. + * + * @return YES if the deletion was successful else NO. + */ ++ (BOOL)removeSubDirectory:(NSString *)subDirectoryName error:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InstanceID/FIRInstanceIDStore.m b/Firebase/InstanceID/FIRInstanceIDStore.m new file mode 100644 index 00000000000..7467cdac2d3 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDStore.m @@ -0,0 +1,240 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDStore.h" + +#import "FIRInstanceIDCheckinPreferences.h" +#import "FIRInstanceIDCheckinStore.h" +#import "FIRInstanceIDConstants.h" +#import "FIRInstanceIDLogger.h" +#import "FIRInstanceIDTokenStore.h" +#import "FIRInstanceIDVersionUtilities.h" + +// NOTE: These values should be in sync with what InstanceID saves in as. +static NSString *const kCheckinFileName = @"g-checkin"; + +// APNS token (use the old key value i.e. with prefix GMS) +static NSString *const kFIRInstanceIDLibraryVersion = @"GMSInstanceID-version"; + +@interface FIRInstanceIDStore () + +@property(nonatomic, readwrite, strong) FIRInstanceIDCheckinStore *checkinStore; +@property(nonatomic, readwrite, strong) FIRInstanceIDTokenStore *tokenStore; + +@end + +@implementation FIRInstanceIDStore + +- (instancetype)initWithDelegate:(NSObject *)delegate { + FIRInstanceIDCheckinStore *checkinStore = [[FIRInstanceIDCheckinStore alloc] + initWithCheckinPlistFileName:kCheckinFileName + subDirectoryName:kFIRInstanceIDSubDirectoryName]; + + FIRInstanceIDTokenStore *tokenStore = [FIRInstanceIDTokenStore defaultStore]; + + return [self initWithCheckinStore:checkinStore tokenStore:tokenStore delegate:delegate]; +} + +- (instancetype)initWithCheckinStore:(FIRInstanceIDCheckinStore *)checkinStore + tokenStore:(FIRInstanceIDTokenStore *)tokenStore + delegate:(NSObject *)delegate { + self = [super init]; + if (self) { + _checkinStore = checkinStore; + _tokenStore = tokenStore; + _delegate = delegate; + [self resetCredentialsIfNeeded]; + } + return self; +} + +#pragma mark - Upgrades + ++ (BOOL)hasSubDirectory:(NSString *)subDirectoryName { + NSString *subDirectoryPath = [self pathForSupportSubDirectory:subDirectoryName]; + BOOL isDirectory; + if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath + isDirectory:&isDirectory]) { + return NO; + } else if (!isDirectory) { + return NO; + } + return YES; +} + ++ (NSSearchPathDirectory)supportedDirectory { +#if TARGET_OS_TV + return NSCachesDirectory; +#else + return NSApplicationSupportDirectory; +#endif +} + ++ (NSString *)pathForSupportSubDirectory:(NSString *)subDirectoryName { + NSArray *directoryPaths = + NSSearchPathForDirectoriesInDomains([self supportedDirectory], NSUserDomainMask, YES); + NSString *dirPath = directoryPaths.lastObject; + NSArray *components = @[ dirPath, subDirectoryName ]; + return [NSString pathWithComponents:components]; +} + ++ (BOOL)createSubDirectory:(NSString *)subDirectoryName { + NSString *subDirectoryPath = [self pathForSupportSubDirectory:subDirectoryName]; + BOOL hasSubDirectory; + + if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath + isDirectory:&hasSubDirectory]) { + NSError *error; + [[NSFileManager defaultManager] createDirectoryAtPath:subDirectoryPath + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (error) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeStore000, + @"Cannot create directory %@, error: %@", subDirectoryPath, error); + return NO; + } + } else { + if (!hasSubDirectory) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeStore001, + @"Found file instead of directory at %@", subDirectoryPath); + return NO; + } + } + return YES; +} + ++ (BOOL)removeSubDirectory:(NSString *)subDirectoryName error:(NSError **)error { + if ([self hasSubDirectory:subDirectoryName]) { + NSString *subDirectoryPath = [self pathForSupportSubDirectory:subDirectoryName]; + BOOL isDirectory; + if ([[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath + isDirectory:&isDirectory]) { + return [[NSFileManager defaultManager] removeItemAtPath:subDirectoryPath error:error]; + } + } + return YES; +} + +/** + * Reset the keychain preferences if the app had been deleted earlier and then reinstalled. + * Keychain preferences are not cleared in the above scenario so explicitly clear them. + * + * In case of an iCloud backup and restore the Keychain preferences should already be empty + * since the Keychain items are marked with `*BackupThisDeviceOnly`. + */ +- (void)resetCredentialsIfNeeded { + BOOL checkinPlistExists = [self.checkinStore hasCheckinPlist]; + // Checkin info existed in backup excluded plist. Should not be a fresh install. + if (checkinPlistExists) { + // FCM user can still have the old version of checkin, migration should only happen once. + [self.checkinStore migrateCheckinItemIfNeeded]; + return; + } + + // reset checkin in keychain if a fresh install. + // set the old checkin preferences to unregister pre-registered tokens + FIRInstanceIDCheckinPreferences *oldCheckinPreferences = + [self.checkinStore cachedCheckinPreferences]; + + if (oldCheckinPreferences) { + [self.checkinStore removeCheckinPreferencesWithHandler:^(NSError *error) { + if (!error) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeStore002, + @"Removed cached checkin preferences from Keychain."); + } else { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeStore003, + @"Couldn't remove cached checkin preferences. Error: %@", error); + } + if (oldCheckinPreferences.deviceID.length && oldCheckinPreferences.secretToken.length) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeStore006, + @"App reset detected. Will delete server registrations."); + // We don't really need to delete old FCM tokens created via IID auth tokens since + // those tokens are already hashed by APNS token as the has so creating a new + // token should automatically delete the old-token. + [self.delegate store:self didDeleteFCMScopedTokensForCheckin:oldCheckinPreferences]; + } else { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeStore009, + @"App reset detected but no valid checkin auth preferences found." + @" Will not delete server registrations."); + } + }]; + } +} + +#pragma mark - Get + +- (FIRInstanceIDTokenInfo *)tokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope { + // TODO(chliangGoogle): If we don't have the token plist we should delete all the tokens from + // the keychain. This is because not having the plist signifies a backup and restore operation. + // In case the keychain has any tokens these would now be stale and therefore should be + // deleted. + if (![authorizedEntity length] || ![scope length]) { + return nil; + } + FIRInstanceIDTokenInfo *info = [self.tokenStore tokenInfoWithAuthorizedEntity:authorizedEntity + scope:scope]; + return info; +} + +- (NSArray *)cachedTokenInfos { + return [self.tokenStore cachedTokenInfos]; +} + +#pragma mark - Save + +- (void)saveTokenInfo:(FIRInstanceIDTokenInfo *)tokenInfo + handler:(void (^)(NSError *error))handler { + [self.tokenStore saveTokenInfo:tokenInfo handler:handler]; +} + +#pragma mark - Delete + +- (void)removeCachedTokenWithAuthorizedEntity:(NSString *)authorizedEntity scope:(NSString *)scope { + if (![authorizedEntity length] || ![scope length]) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeStore012, + @"Will not delete token with invalid entity: %@, scope: %@", + authorizedEntity, scope); + return; + } + [self.tokenStore removeTokenWithAuthorizedEntity:authorizedEntity scope:scope]; +} + +- (void)removeAllCachedTokensWithHandler:(void (^)(NSError *error))handler { + [self.tokenStore removeAllTokensWithHandler:handler]; +} + +#pragma mark - FIRInstanceIDCheckinCache protocol + +- (void)saveCheckinPreferences:(FIRInstanceIDCheckinPreferences *)preferences + handler:(void (^)(NSError *error))handler { + [self.checkinStore saveCheckinPreferences:preferences handler:handler]; +} + +- (FIRInstanceIDCheckinPreferences *)cachedCheckinPreferences { + return [self.checkinStore cachedCheckinPreferences]; +} + +- (void)removeCheckinPreferencesWithHandler:(void (^)(NSError *))handler { + [self.checkinStore removeCheckinPreferencesWithHandler:^(NSError *error) { + if (handler) { + handler(error); + } + }]; +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDStringEncoding.h b/Firebase/InstanceID/FIRInstanceIDStringEncoding.h new file mode 100644 index 00000000000..8f2f369739b --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDStringEncoding.h @@ -0,0 +1,66 @@ +// +// GTMStringEncoding.h +// +// Copyright 2010 Google Inc. +// +// 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. +// + +// This is a copy of GTMStringEncoding. FIRInstanceID wants to avoid +// a CocoaPods GTM dependency. Hence we use our own version of StringEncoding. + +#import + +// A generic class for arbitrary base-2 to 128 string encoding and decoding. +@interface FIRInstanceIDStringEncoding : NSObject { + @private + NSData *charMapData_; + char *charMap_; + int reverseCharMap_[128]; + int shift_; + unsigned int mask_; + BOOL doPad_; + char paddingChar_; + int padLen_; +} + ++ (id)rfc4648Base64WebsafeStringEncoding; + +// Create a new, autoreleased GTMStringEncoding object with the given string, +// as described below. ++ (id)stringEncodingWithString:(NSString *)string; + +// Initialize a new GTMStringEncoding object with the string. +// +// The length of the string must be a power of 2, at least 2 and at most 128. +// Only 7-bit ASCII characters are permitted in the string. +// +// These characters are the canonical set emitted during encoding. +// If the characters have alternatives (e.g. case, easily transposed) then use +// addDecodeSynonyms: to configure them. +- (id)initWithString:(NSString *)string; + +// Indicates whether padding is performed during encoding. +- (BOOL)doPad; +- (void)setDoPad:(BOOL)doPad; + +// Sets the padding character to use during encoding. +- (void)setPaddingChar:(char)c; + +// Encode a raw binary buffer to a 7-bit ASCII string. +- (NSString *)encode:(NSData *)data; + +// Decode a 7-bit ASCII string to a raw binary buffer. +- (NSData *)decode:(NSString *)string; + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDStringEncoding.m b/Firebase/InstanceID/FIRInstanceIDStringEncoding.m new file mode 100644 index 00000000000..64b6d3efed3 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDStringEncoding.m @@ -0,0 +1,202 @@ +#import "FIRInstanceIDDefines.h" + +// +// FIRInstanceIDStringEncoding.m +// +// Copyright 2009 Google Inc. +// +// 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. +// + +// This is a copy of GTMStringEncoding. FIRInstanceID wants to avoid +// a CocoaPods GTM dependency. Hence we use our own version of StringEncoding. + +#import "FIRInstanceIDStringEncoding.h" + +enum { kUnknownChar = -1, kPaddingChar = -2, kIgnoreChar = -3 }; + +@implementation FIRInstanceIDStringEncoding + ++ (id)rfc4648Base64WebsafeStringEncoding { + FIRInstanceIDStringEncoding *ret = [self + stringEncodingWithString:@"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"]; + + [ret setPaddingChar:'=']; + [ret setDoPad:YES]; + return ret; +} + +static inline int lcm(int a, int b) { + for (int aa = a, bb = b;;) { + if (aa == bb) + return aa; + else if (aa < bb) + aa += a; + else + bb += b; + } +} + ++ (id)stringEncodingWithString:(NSString *)string { + return [[FIRInstanceIDStringEncoding alloc] initWithString:string]; +} + +- (id)initWithString:(NSString *)string { + if ((self = [super init])) { + charMapData_ = [string dataUsingEncoding:NSASCIIStringEncoding]; + if (!charMapData_) { + // Unable to convert string to ASCII + return nil; + } + charMap_ = (char *)[charMapData_ bytes]; + NSUInteger length = [charMapData_ length]; + if (length < 2 || length > 128 || length & (length - 1)) { + // Length not a power of 2 between 2 and 128 + return nil; + } + + memset(reverseCharMap_, kUnknownChar, sizeof(reverseCharMap_)); + for (unsigned int i = 0; i < length; i++) { + if (reverseCharMap_[(int)charMap_[i]] != kUnknownChar) { + // Duplicate character at |i| + return nil; + } + reverseCharMap_[(int)charMap_[i]] = i; + } + + for (NSUInteger i = 1; i < length; i <<= 1) shift_++; + mask_ = (1 << shift_) - 1; + padLen_ = lcm(8, shift_) / shift_; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"", 1 << shift_, charMapData_]; +} + +- (BOOL)doPad { + return doPad_; +} + +- (void)setDoPad:(BOOL)doPad { + doPad_ = doPad; +} + +- (void)setPaddingChar:(char)c { + paddingChar_ = c; + reverseCharMap_[(int)c] = kPaddingChar; +} + +- (NSString *)encode:(NSData *)inData { + NSUInteger inLen = [inData length]; + if (inLen <= 0) { + // Empty input + return @""; + } + unsigned char *inBuf = (unsigned char *)[inData bytes]; + NSUInteger inPos = 0; + + NSUInteger outLen = (inLen * 8 + shift_ - 1) / shift_; + if (doPad_) { + outLen = ((outLen + padLen_ - 1) / padLen_) * padLen_; + } + NSMutableData *outData = [NSMutableData dataWithLength:outLen]; + unsigned char *outBuf = (unsigned char *)[outData mutableBytes]; + NSUInteger outPos = 0; + + unsigned int buffer = inBuf[inPos++]; + int bitsLeft = 8; + while (bitsLeft > 0 || inPos < inLen) { + if (bitsLeft < shift_) { + if (inPos < inLen) { + buffer <<= 8; + buffer |= (inBuf[inPos++] & 0xff); + bitsLeft += 8; + } else { + int pad = shift_ - bitsLeft; + buffer <<= pad; + bitsLeft += pad; + } + } + unsigned int idx = (buffer >> (bitsLeft - shift_)) & mask_; + bitsLeft -= shift_; + outBuf[outPos++] = charMap_[idx]; + } + + if (doPad_) { + while (outPos < outLen) outBuf[outPos++] = paddingChar_; + } + + _FIRInstanceIDDevAssert(outPos == outLen, @"Underflowed output buffer"); + [outData setLength:outPos]; + + return [[NSString alloc] initWithData:outData encoding:NSASCIIStringEncoding]; +} + +- (NSData *)decode:(NSString *)inString { + char *inBuf = (char *)[inString cStringUsingEncoding:NSASCIIStringEncoding]; + if (!inBuf) { + // Unable to convert buffer to ASCII + return nil; + } + NSUInteger inLen = strlen(inBuf); + + NSUInteger outLen = inLen * shift_ / 8; + NSMutableData *outData = [NSMutableData dataWithLength:outLen]; + unsigned char *outBuf = (unsigned char *)[outData mutableBytes]; + NSUInteger outPos = 0; + + int buffer = 0; + int bitsLeft = 0; + BOOL expectPad = NO; + for (NSUInteger i = 0; i < inLen; i++) { + int val = reverseCharMap_[(int)inBuf[i]]; + switch (val) { + case kIgnoreChar: + break; + case kPaddingChar: + expectPad = YES; + break; + case kUnknownChar: + // Unexpected data at input pos |i| + return nil; + default: + if (expectPad) { + // Expected further padding characters + return nil; + } + buffer <<= shift_; + buffer |= val & mask_; + bitsLeft += shift_; + if (bitsLeft >= 8) { + outBuf[outPos++] = (unsigned char)(buffer >> (bitsLeft - 8)); + bitsLeft -= 8; + } + break; + } + } + + if (bitsLeft && buffer & ((1 << bitsLeft) - 1)) { + // Incomplete trailing data + return nil; + } + + // Shorten buffer if needed due to padding chars + _FIRInstanceIDDevAssert(outPos <= outLen, @"Overflowed buffer"); + [outData setLength:outPos]; + + return outData; +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDTokenDeleteOperation.h b/Firebase/InstanceID/FIRInstanceIDTokenDeleteOperation.h new file mode 100644 index 00000000000..58368d04172 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDTokenDeleteOperation.h @@ -0,0 +1,31 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDTokenOperation.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRInstanceIDTokenDeleteOperation : FIRInstanceIDTokenOperation + +- (instancetype)initWithAuthorizedEntity:(nullable NSString *)authorizedEntity + scope:(nullable NSString *)scope + checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences + keyPair:(nullable FIRInstanceIDKeyPair *)keyPair + action:(FIRInstanceIDTokenAction)action; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InstanceID/FIRInstanceIDTokenDeleteOperation.m b/Firebase/InstanceID/FIRInstanceIDTokenDeleteOperation.m new file mode 100644 index 00000000000..365f321bd55 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDTokenDeleteOperation.m @@ -0,0 +1,120 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDTokenDeleteOperation.h" + +#import "FIRInstanceIDCheckinPreferences.h" +#import "FIRInstanceIDDefines.h" +#import "FIRInstanceIDLogger.h" +#import "FIRInstanceIDTokenOperation+Private.h" +#import "FIRInstanceIDURLQueryItem.h" +#import "FIRInstanceIDUtilities.h" +#import "NSError+FIRInstanceID.h" + +@implementation FIRInstanceIDTokenDeleteOperation + +- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences + keyPair:(FIRInstanceIDKeyPair *)keyPair + action:(FIRInstanceIDTokenAction)action { + self = [super initWithAction:action + forAuthorizedEntity:authorizedEntity + scope:scope + options:nil + checkinPreferences:checkinPreferences + keyPair:keyPair]; + if (self) { + } + return self; +} + +- (void)performTokenOperation { + NSString *authHeader = + [FIRInstanceIDTokenOperation HTTPAuthHeaderFromCheckin:self.checkinPreferences]; + NSMutableURLRequest *request = [FIRInstanceIDTokenOperation requestWithAuthHeader:authHeader]; + + // Build form-encoded body + NSString *deviceAuthID = self.checkinPreferences.deviceID; + NSMutableArray *queryItems = + [FIRInstanceIDTokenOperation standardQueryItemsWithDeviceID:deviceAuthID scope:self.scope]; + [queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"delete" value:@"true"]]; + if (self.action == FIRInstanceIDTokenActionDeleteTokenAndIID) { + [queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"iid-operation" + value:@"delete"]]; + } + if (self.authorizedEntity) { + [queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"sender" + value:self.authorizedEntity]]; + } + // Typically we include our public key-signed url items, but in some cases (like deleting all FCM + // tokens), we don't. + if (self.keyPair != nil) { + [queryItems addObjectsFromArray:[self queryItemsWithKeyPair:self.keyPair]]; + } + + NSString *content = FIRInstanceIDQueryFromQueryItems(queryItems); + request.HTTPBody = [content dataUsingEncoding:NSUTF8StringEncoding]; + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenDeleteOperationFetchRequest, + @"Unregister request to %@ content: %@", FIRInstanceIDRegisterServer(), + content); + + FIRInstanceID_WEAKIFY(self); + void (^requestHandler)(NSData *, NSURLResponse *, NSError *) = + ^(NSData *data, NSURLResponse *response, NSError *error) { + FIRInstanceID_STRONGIFY(self); + [self handleResponseWithData:data response:response error:error]; + }; + + // Test block + if (self.testBlock) { + self.testBlock(request, requestHandler); + return; + } + + NSURLSession *session = [FIRInstanceIDTokenOperation sharedURLSession]; + self.dataTask = [session dataTaskWithRequest:request completionHandler:requestHandler]; + [self.dataTask resume]; +} + +- (void)handleResponseWithData:(NSData *)data + response:(NSURLResponse *)response + error:(NSError *)error { + if (error) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenDeleteOperationRequestError, + @"Device unregister HTTP fetch error. Error code: %ld", + _FIRInstanceID_L(error.code)); + [self finishWithResult:FIRInstanceIDTokenOperationError token:nil error:error]; + return; + } + + NSString *dataResponse = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if (dataResponse.length == 0) { + NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeUnknown]; + [self finishWithResult:FIRInstanceIDTokenOperationError token:nil error:error]; + return; + } + + if (![dataResponse hasPrefix:@"deleted="] && ![dataResponse hasPrefix:@"token="]) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenDeleteOperationBadResponse, + @"Invalid unregister response %@", response); + NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeUnknown]; + [self finishWithResult:FIRInstanceIDTokenOperationError token:nil error:error]; + return; + } + [self finishWithResult:FIRInstanceIDTokenOperationSucceeded token:nil error:nil]; +} +@end diff --git a/Firebase/InstanceID/FIRInstanceIDTokenFetchOperation.h b/Firebase/InstanceID/FIRInstanceIDTokenFetchOperation.h new file mode 100644 index 00000000000..83ac71411c5 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDTokenFetchOperation.h @@ -0,0 +1,29 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDTokenOperation.h" + +NS_ASSUME_NONNULL_BEGIN +@interface FIRInstanceIDTokenFetchOperation : FIRInstanceIDTokenOperation + +- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + options:(nullable NSDictionary *)options + checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences + keyPair:(FIRInstanceIDKeyPair *)keyPair; + +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InstanceID/FIRInstanceIDTokenFetchOperation.m b/Firebase/InstanceID/FIRInstanceIDTokenFetchOperation.m new file mode 100644 index 00000000000..c2df1f7ed0b --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDTokenFetchOperation.m @@ -0,0 +1,200 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDTokenFetchOperation.h" + +#import "FIRInstanceIDCheckinPreferences.h" +#import "FIRInstanceIDConstants.h" +#import "FIRInstanceIDDefines.h" +#import "FIRInstanceIDLogger.h" +#import "FIRInstanceIDTokenOperation+Private.h" +#import "FIRInstanceIDURLQueryItem.h" +#import "FIRInstanceIDUtilities.h" +#import "NSError+FIRInstanceID.h" + +// We can have a static int since this error should theoretically only +// happen once (for the first time). If it repeats there is something +// else that is wrong. +static int phoneRegistrationErrorRetryCount = 0; +static const int kMaxPhoneRegistrationErrorRetryCount = 10; + +@implementation FIRInstanceIDTokenFetchOperation + +- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + options:(nullable NSDictionary *)options + checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences + keyPair:(FIRInstanceIDKeyPair *)keyPair { + self = [super initWithAction:FIRInstanceIDTokenActionFetch + forAuthorizedEntity:authorizedEntity + scope:scope + options:options + checkinPreferences:checkinPreferences + keyPair:keyPair]; + if (self) { + } + return self; +} + +- (void)performTokenOperation { + NSString *authHeader = + [FIRInstanceIDTokenOperation HTTPAuthHeaderFromCheckin:self.checkinPreferences]; + NSMutableURLRequest *request = [[self class] requestWithAuthHeader:authHeader]; + NSString *checkinVersionInfo = self.checkinPreferences.versionInfo; + [request setValue:checkinVersionInfo forHTTPHeaderField:@"info"]; + + // Build form-encoded body + NSString *deviceAuthID = self.checkinPreferences.deviceID; + NSMutableArray *queryItems = + [[self class] standardQueryItemsWithDeviceID:deviceAuthID scope:self.scope]; + [queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"sender" + value:self.authorizedEntity]]; + [queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"X-subtype" + value:self.authorizedEntity]]; + + [queryItems addObjectsFromArray:[self queryItemsWithKeyPair:self.keyPair]]; + + // Create query items from passed-in options + id apnsTokenData = self.options[kFIRInstanceIDTokenOptionsAPNSKey]; + id apnsSandboxValue = self.options[kFIRInstanceIDTokenOptionsAPNSIsSandboxKey]; + if ([apnsTokenData isKindOfClass:[NSData class]] && + [apnsSandboxValue isKindOfClass:[NSNumber class]]) { + NSString *APNSString = FIRInstanceIDAPNSTupleStringForTokenAndServerType( + apnsTokenData, ((NSNumber *)apnsSandboxValue).boolValue); + // The name of the query item happens to be the same as the dictionary key + FIRInstanceIDURLQueryItem *item = + [FIRInstanceIDURLQueryItem queryItemWithName:kFIRInstanceIDTokenOptionsAPNSKey + value:APNSString]; + [queryItems addObject:item]; + } + id firebaseAppID = self.options[kFIRInstanceIDTokenOptionsFirebaseAppIDKey]; + if ([firebaseAppID isKindOfClass:[NSString class]]) { + // The name of the query item happens to be the same as the dictionary key + FIRInstanceIDURLQueryItem *item = + [FIRInstanceIDURLQueryItem queryItemWithName:kFIRInstanceIDTokenOptionsFirebaseAppIDKey + value:(NSString *)firebaseAppID]; + [queryItems addObject:item]; + } + + NSString *content = FIRInstanceIDQueryFromQueryItems(queryItems); + request.HTTPBody = [content dataUsingEncoding:NSUTF8StringEncoding]; + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenFetchOperationFetchRequest, + @"Register request to %@ content: %@", FIRInstanceIDRegisterServer(), + content); + + FIRInstanceID_WEAKIFY(self); + void (^requestHandler)(NSData *, NSURLResponse *, NSError *) = + ^(NSData *data, NSURLResponse *response, NSError *error) { + FIRInstanceID_STRONGIFY(self); + [self handleResponseWithData:data response:response error:error]; + }; + + // Test block + if (self.testBlock) { + self.testBlock(request, requestHandler); + return; + } + + NSURLSession *session = [FIRInstanceIDTokenOperation sharedURLSession]; + self.dataTask = [session dataTaskWithRequest:request completionHandler:requestHandler]; + [self.dataTask resume]; +} + +#pragma mark - Request Handling + +- (void)handleResponseWithData:(NSData *)data + response:(NSURLResponse *)response + error:(NSError *)error { + if (error) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenFetchOperationRequestError, + @"Token fetch HTTP error. Error Code: %ld", (long)error.code); + [self finishWithResult:FIRInstanceIDTokenOperationError token:nil error:error]; + return; + } + NSString *dataResponse = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + + if (dataResponse.length == 0) { + NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeUnknown]; + [self finishWithResult:FIRInstanceIDTokenOperationError token:nil error:error]; + return; + } + NSDictionary *parsedResponse = [self parseFetchTokenResponse:dataResponse]; + _FIRInstanceIDDevAssert(parsedResponse.count, @"Invalid registration response"); + + if ([parsedResponse[@"token"] length]) { + [self finishWithResult:FIRInstanceIDTokenOperationSucceeded + token:parsedResponse[@"token"] + error:nil]; + return; + } + + NSString *errorValue = parsedResponse[@"Error"]; + NSError *responseError; + if (errorValue.length) { + NSArray *errorComponents = [errorValue componentsSeparatedByString:@":"]; + // HACK (Kansas replication delay), PHONE_REGISTRATION_ERROR on App + // uninstall and reinstall. + if ([errorComponents containsObject:@"PHONE_REGISTRATION_ERROR"]) { + // Encountered issue http://b/27043795 + // Retry register until successful or another error encountered or a + // certain number of tries are over. + + if (phoneRegistrationErrorRetryCount < kMaxPhoneRegistrationErrorRetryCount) { + const int nextRetryInterval = 1 << phoneRegistrationErrorRetryCount; + FIRInstanceID_WEAKIFY(self); + + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(nextRetryInterval * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + FIRInstanceID_STRONGIFY(self); + phoneRegistrationErrorRetryCount++; + [self performTokenOperation]; + }); + return; + } + } else if ([errorComponents containsObject:kFIRInstanceID_CMD_RST]) { + // Server detected the identity we use is no longer valid. + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:kFIRInstanceIDIdentityInvalidatedNotification object:nil]; + + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeInternal001, + @"Identity is invalid. Server request identity reset."); + responseError = + [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidIdentity]; + } + } + if (!responseError) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenFetchOperationBadResponse, + @"Invalid fetch response, expected 'token' or 'Error' key"); + responseError = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeUnknown]; + } + [self finishWithResult:FIRInstanceIDTokenOperationError token:nil error:responseError]; +} + +// expect a response e.g. "token=\nGOOG.ttl=123" +- (NSDictionary *)parseFetchTokenResponse:(NSString *)response { + NSArray *lines = [response componentsSeparatedByString:@"\n"]; + NSMutableDictionary *parsedResponse = [NSMutableDictionary dictionary]; + for (NSString *line in lines) { + NSArray *keyAndValue = [line componentsSeparatedByString:@"="]; + if ([keyAndValue count] > 1) { + parsedResponse[keyAndValue[0]] = keyAndValue[1]; + } + } + return parsedResponse; +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDTokenInfo.h b/Firebase/InstanceID/FIRInstanceIDTokenInfo.h new file mode 100644 index 00000000000..34ad7166253 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDTokenInfo.h @@ -0,0 +1,82 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDAPNSInfo.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Represents an Instance ID token, and all of the relevant information + * associated with it. It can read from and write to an NSDictionary object, for + * simple serialization. + */ +@interface FIRInstanceIDTokenInfo : NSObject + +/// The authorized entity (also known as Sender ID), associated with the token. +@property(nonatomic, readonly, copy) NSString *authorizedEntity; +/// The scope associated with the token. This is an arbitrary string, typically "*". +@property(nonatomic, readonly, copy) NSString *scope; +/// The token value itself, with which all other properties are associated. +@property(nonatomic, readonly, copy) NSString *token; + +// These properties are nullable because they might not exist for tokens fetched from +// legacy storage formats. + +/// The app version that this token represents. +@property(nonatomic, readonly, copy, nullable) NSString *appVersion; +/// The Firebase app ID (also known as GMP App ID), that this token is associated with. +@property(nonatomic, readonly, copy, nullable) NSString *firebaseAppID; + +/// Tokens may not always be associated with an APNs token, and may be associated after +/// being created. +@property(nonatomic, strong, nullable) FIRInstanceIDAPNSInfo *APNSInfo; +/// The time that this token info was updated. The cache time is writeable, since in +/// some cases the token info may be refreshed from the server. In those situations, +/// the cacheTime would be updated. +@property(nonatomic, copy, nullable) NSDate *cacheTime; + +/** + * Initializes a FIRInstanceIDTokenInfo object with the required parameters. These + * parameters represent all the relevant associated data with a token. + * + * @param authorizedEntity The authorized entity (also known as Sender ID). + * @param scope The scope of the token, typically "*" meaning + * it's a "default scope". + * @param token The token value itself. + * @param appVersion The application version that this token is associated with. + * @param firebaseAppID The Firebase app ID which this token is associated with. + * @return An instance of FIRInstanceIDTokenInfo. + */ +- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + token:(NSString *)token + appVersion:(nullable NSString *)appVersion + firebaseAppID:(nullable NSString *)firebaseAppID; + +/** + * Check whether the token is still fresh based on: + * 1. Last fetch token is within the 7 days. + * 2. Language setting is not changed. + * 3. App version is current. + * 4. GMP App ID is current. + */ +- (BOOL)isFresh; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InstanceID/FIRInstanceIDTokenInfo.m b/Firebase/InstanceID/FIRInstanceIDTokenInfo.m new file mode 100644 index 00000000000..a4d284b2dc9 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDTokenInfo.m @@ -0,0 +1,188 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDTokenInfo.h" + +#import "FIRInstanceIDLogger.h" +#import "FIRInstanceIDUtilities.h" + +/** + * @enum Token Info Dictionary Key Constants + * @discussion The keys that are checked when a token info is + * created from a dictionary. The same keys are used + * when decoding/encoding an archive. + */ +/// Specifies a dictonary key whose value represents the authorized entity, or +/// Sender ID for the token. +static NSString *const kFIRInstanceIDAuthorizedEntityKey = @"authorized_entity"; +/// Specifies a dictionary key whose value represents the scope of the token, +/// typically "*". +static NSString *const kFIRInstanceIDScopeKey = @"scope"; +/// Specifies a dictionary key which represents the token value itself. +static NSString *const kFIRInstanceIDTokenKey = @"token"; +/// Specifies a dictionary key which represents the app version associated +/// with the token. +static NSString *const kFIRInstanceIDAppVersionKey = @"app_version"; +/// Specifies a dictionary key which represents the GMP App ID associated with +/// the token. +static NSString *const kFIRInstanceIDFirebaseAppIDKey = @"firebase_app_id"; +/// Specifies a dictionary key representing an archive for a +/// `FIRInstanceIDAPNSInfo` object. +static NSString *const kFIRInstanceIDAPNSInfoKey = @"apns_info"; +/// Specifies a dictionary key representing the "last cached" time for the token. +static NSString *const kFIRInstanceIDCacheTimeKey = @"cache_time"; +/// Default interval that token stays fresh. +const NSTimeInterval kDefaultFetchTokenInterval = 7 * 24 * 60 * 60; // 7 days. + +@implementation FIRInstanceIDTokenInfo + +- (instancetype)initWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + token:(NSString *)token + appVersion:(NSString *)appVersion + firebaseAppID:(NSString *)firebaseAppID { + self = [super init]; + if (self) { + _authorizedEntity = [authorizedEntity copy]; + _scope = [scope copy]; + _token = [token copy]; + _appVersion = [appVersion copy]; + _firebaseAppID = [firebaseAppID copy]; + } + return self; +} + +- (BOOL)isFresh { + // Last fetch token cache time could be null if token is from legacy storage format. Then token is + // considered not fresh and should be refreshed and overwrite with the latest storage format. + if (!_cacheTime) { + return NO; + } + + // Check if app has just been updated to a new version. + NSString *currentAppVersion = FIRInstanceIDCurrentAppVersion(); + if (!_appVersion || ![_appVersion isEqualToString:currentAppVersion]) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenManager004, + @"Invalidating cached token for %@ (%@) due to app version change.", + _authorizedEntity, _scope); + return NO; + } + + // Check if GMP App ID has changed + NSString *currentFirebaseAppID = FIRInstanceIDFirebaseAppID(); + if (!_firebaseAppID || ![_firebaseAppID isEqualToString:currentFirebaseAppID]) { + FIRInstanceIDLoggerDebug( + kFIRInstanceIDMessageCodeTokenInfoFirebaseAppIDChanged, + @"Invalidating cached token due to Firebase App IID change from %@ to %@", _firebaseAppID, + currentFirebaseAppID); + return NO; + } + + // Check whether locale has changed, if yes, token needs to be updated with server for locale + // information. + if (FIRInstanceIDHasLocaleChanged()) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenInfoLocaleChanged, + @"Invalidating cached token due to locale change"); + return NO; + } + + // Locale is not changed, check whether token has been fetched within 7 days. + NSTimeInterval lastFetchTokenTimestamp = [_cacheTime timeIntervalSince1970]; + NSTimeInterval currentTimestamp = FIRInstanceIDCurrentTimestampInSeconds(); + NSTimeInterval timeSinceLastFetchToken = currentTimestamp - lastFetchTokenTimestamp; + return (timeSinceLastFetchToken < kDefaultFetchTokenInterval); +} +#pragma mark - NSCoding + +- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder { + // These value cannot be nil + + id authorizedEntity = [aDecoder decodeObjectForKey:kFIRInstanceIDAuthorizedEntityKey]; + if (![authorizedEntity isKindOfClass:[NSString class]]) { + return nil; + } + + id scope = [aDecoder decodeObjectForKey:kFIRInstanceIDScopeKey]; + if (![scope isKindOfClass:[NSString class]]) { + return nil; + } + + id token = [aDecoder decodeObjectForKey:kFIRInstanceIDTokenKey]; + if (![token isKindOfClass:[NSString class]]) { + return nil; + } + + // These values are nullable, so only fail the decode if the type does not match + + id appVersion = [aDecoder decodeObjectForKey:kFIRInstanceIDAppVersionKey]; + if (appVersion && ![appVersion isKindOfClass:[NSString class]]) { + return nil; + } + + id firebaseAppID = [aDecoder decodeObjectForKey:kFIRInstanceIDFirebaseAppIDKey]; + if (firebaseAppID && ![firebaseAppID isKindOfClass:[NSString class]]) { + return nil; + } + + id rawAPNSInfo = [aDecoder decodeObjectForKey:kFIRInstanceIDAPNSInfoKey]; + if (rawAPNSInfo && ![rawAPNSInfo isKindOfClass:[NSData class]]) { + return nil; + } + + FIRInstanceIDAPNSInfo *APNSInfo = nil; + if (rawAPNSInfo) { + @try { + APNSInfo = [NSKeyedUnarchiver unarchiveObjectWithData:rawAPNSInfo]; + } @catch (NSException *exception) { + FIRInstanceIDLoggerInfo(kFIRInstanceIDMessageCodeTokenInfoBadAPNSInfo, + @"Could not parse raw APNS Info while parsing archived token info."); + APNSInfo = nil; + } @finally { + } + } + + id cacheTime = [aDecoder decodeObjectForKey:kFIRInstanceIDCacheTimeKey]; + if (cacheTime && ![cacheTime isKindOfClass:[NSDate class]]) { + return nil; + } + + self = [super init]; + if (self) { + _authorizedEntity = authorizedEntity; + _scope = scope; + _token = token; + _appVersion = appVersion; + _firebaseAppID = firebaseAppID; + _APNSInfo = APNSInfo; + _cacheTime = cacheTime; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self.authorizedEntity forKey:kFIRInstanceIDAuthorizedEntityKey]; + [aCoder encodeObject:self.scope forKey:kFIRInstanceIDScopeKey]; + [aCoder encodeObject:self.token forKey:kFIRInstanceIDTokenKey]; + [aCoder encodeObject:self.appVersion forKey:kFIRInstanceIDAppVersionKey]; + [aCoder encodeObject:self.firebaseAppID forKey:kFIRInstanceIDFirebaseAppIDKey]; + if (self.APNSInfo) { + NSData *rawAPNSInfo = [NSKeyedArchiver archivedDataWithRootObject:self.APNSInfo]; + [aCoder encodeObject:rawAPNSInfo forKey:kFIRInstanceIDAPNSInfoKey]; + } + [aCoder encodeObject:self.cacheTime forKey:kFIRInstanceIDCacheTimeKey]; +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDTokenManager.h b/Firebase/InstanceID/FIRInstanceIDTokenManager.h new file mode 100644 index 00000000000..491b99c4a86 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDTokenManager.h @@ -0,0 +1,149 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceID.h" + +@class FIRInstanceIDAuthService; +@class FIRInstanceIDCheckinPreferences; +@class FIRInstanceIDKeyPair; +@class FIRInstanceIDTokenInfo; +@class FIRInstanceIDStore; + +typedef NS_OPTIONS(NSUInteger, FIRInstanceIDInvalidTokenReason) { + FIRInstanceIDInvalidTokenReasonNone = 0, // 0 + FIRInstanceIDInvalidTokenReasonAppVersion = (1 << 0), // 0...00001 + FIRInstanceIDInvalidTokenReasonAPNSToken = (1 << 1), // 0...00010 +}; + +/** + * Manager for the InstanceID token requests i.e `newToken` and `deleteToken`. This + * manages the overall interaction of the `InstanceIDStore`, the token register + * service and the callbacks associated with `GCMInstanceID`. + */ +@interface FIRInstanceIDTokenManager : NSObject + +/// Expose the auth service, so it can be used by others +@property(nonatomic, readonly, strong) FIRInstanceIDAuthService *authService; + +/** + * Fetch new token for the given authorizedEntity and scope. This makes an + * asynchronous request to the InstanceID backend to create a new token for + * the service and returns it. This will replace any old token for the given + * authorizedEntity and scope that has been cached before. + * + * @param authorizedEntity The authorized entity for the token, should not be nil. + * @param scope The scope for the token, should not be nil. + * @param keyPair The keyPair that represents the app identity. + * @param options The options to be added to the fetch request. + * @param handler The handler to be invoked once we have the token or the + * fetch request to InstanceID backend results in an error. Also + * since it's a public handler it should always be called + * asynchronously. This should be non-nil. + */ +- (void)fetchNewTokenWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + keyPair:(FIRInstanceIDKeyPair *)keyPair + options:(NSDictionary *)options + handler:(FIRInstanceIDTokenHandler)handler; + +/** + * Return the cached token info, if one exists, for the given authorizedEntity and scope. + * + * @param authorizedEntity The authorized entity for the token. + * @param scope The scope for the token. + * + * @return The cached token info, if available, matching the parameters. + */ +- (FIRInstanceIDTokenInfo *)cachedTokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope; + +/** + * Delete the token for the given authorizedEntity and scope. If the token has + * been cached, it will be deleted from the store. It will also make an + * asynchronous request to the InstanceID backend to invalidate the token. + * + * @param authorizedEntity The authorized entity for the token, should not be nil. + * @param scope The scope for the token, should not be nil. + * @param keyPair The keyPair that represents the app identity. + * @param handler The handler to be invoked once the delete request to + * InstanceID backend has returned. If the request was + * successful we invoke the handler with a nil error; + * otherwise we call it with an appropriate error. Also since + * it's a public handler it should always be called + * asynchronously. This should be non-nil. + */ +- (void)deleteTokenWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + keyPair:(FIRInstanceIDKeyPair *)keyPair + handler:(FIRInstanceIDDeleteTokenHandler)handler; + +/** + * Deletes all cached tokens from the persistent store. This method should only be triggered + * when InstanceID is deleted + * + * @param keyPair The keyPair for the given app. + * @param handler The handler to be invoked once the delete request to InstanceID backend + * has returned. If the request was successful we invoke the handler with + * a nil error; else we pass in an appropriate error. This should be non-nil + * and be called asynchronously. + */ +- (void)deleteAllTokensWithKeyPair:(FIRInstanceIDKeyPair *)keyPair + handler:(FIRInstanceIDDeleteHandler)handler; + +/** + * Deletes all cached tokens from the persistent store. + * @param handler The callback handler which is invoked when tokens deletion is complete, + * with an error if there is any. + * + */ +- (void)deleteAllTokensLocallyWithHandler:(void (^)(NSError *error))handler; + +/** + * Stop any ongoing token operations. + */ +- (void)stopAllTokenOperations; + +#pragma mark - Invalidating Cached Tokens + +/** + * Invalidate any cached tokens, if the app version has changed since last launch or if the token + * is cached for more than 7 days. + * + * @return Whether we should fetch default token from server. + * + * @discussion This should safely be called prior to any tokens being retrieved from + * the cache or being fetched from the network. + */ +- (BOOL)checkForTokenRefreshPolicy; + +/** + * Upon being provided with different APNs or sandbox, any locally cached tokens + * should be deleted, and the new APNs token should be cached. + * + * @discussion It is possible for this method to be called while token operations are + * in-progress or queued. In this case, the in-flight token operations will have stale + * APNs information. The default token is checked for being out-of-date by Instance ID, + * and re-fetched. Custom tokens are not currently checked. + * + * @param deviceToken The APNS device token, provided by the operating system. + * @param isSandbox YES if the device token is for the sandbox environment, NO otherwise. + * + * @return The array of FIRInstanceIDTokenInfo objects which were invalidated. + */ +- (NSArray *)updateTokensToAPNSDeviceToken:(NSData *)deviceToken + isSandbox:(BOOL)isSandbox; + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDTokenManager.m b/Firebase/InstanceID/FIRInstanceIDTokenManager.m new file mode 100644 index 00000000000..0c4f644f35a --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDTokenManager.m @@ -0,0 +1,341 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDTokenManager.h" + +#import "FIRInstanceIDAuthKeyChain.h" +#import "FIRInstanceIDAuthService.h" +#import "FIRInstanceIDCheckinPreferences.h" +#import "FIRInstanceIDConstants.h" +#import "FIRInstanceIDDefines.h" +#import "FIRInstanceIDLogger.h" +#import "FIRInstanceIDStore.h" +#import "FIRInstanceIDTokenDeleteOperation.h" +#import "FIRInstanceIDTokenFetchOperation.h" +#import "FIRInstanceIDTokenInfo.h" +#import "FIRInstanceIDTokenOperation.h" +#import "NSError+FIRInstanceID.h" + +@interface FIRInstanceIDTokenManager () + +@property(nonatomic, readwrite, strong) FIRInstanceIDStore *instanceIDStore; +@property(nonatomic, readwrite, strong) FIRInstanceIDAuthService *authService; +@property(nonatomic, readonly, strong) NSOperationQueue *tokenOperations; + +@property(nonatomic, readwrite, strong) FIRInstanceIDAPNSInfo *currentAPNSInfo; + +@end + +@implementation FIRInstanceIDTokenManager + +- (instancetype)init { + self = [super init]; + if (self) { + _instanceIDStore = [[FIRInstanceIDStore alloc] initWithDelegate:self]; + _authService = [[FIRInstanceIDAuthService alloc] initWithStore:_instanceIDStore]; + [self configureTokenOperations]; + } + return self; +} + +- (void)dealloc { + [self stopAllTokenOperations]; +} + +- (void)configureTokenOperations { + _tokenOperations = [[NSOperationQueue alloc] init]; + _tokenOperations.name = @"com.google.iid-token-operations"; + // For now, restrict the operations to be serial, because in some cases (like if the + // authorized entity and scope are the same), order matters. + // If we have to deal with several different token requests simultaneously, it would be a good + // idea to add some better intelligence around this (performing unrelated token operations + // simultaneously, etc.). + _tokenOperations.maxConcurrentOperationCount = 1; + if ([_tokenOperations respondsToSelector:@selector(qualityOfService)]) { + _tokenOperations.qualityOfService = NSOperationQualityOfServiceUtility; + } +} + +- (void)fetchNewTokenWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + keyPair:(FIRInstanceIDKeyPair *)keyPair + options:(NSDictionary *)options + handler:(FIRInstanceIDTokenHandler)handler { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenManager000, + @"Fetch new token for authorizedEntity: %@, scope: %@", authorizedEntity, + scope); + FIRInstanceIDTokenFetchOperation *operation = + [self createFetchOperationWithAuthorizedEntity:authorizedEntity + scope:scope + options:options + keyPair:keyPair]; + FIRInstanceID_WEAKIFY(self); + FIRInstanceIDTokenOperationCompletion completion = + ^(FIRInstanceIDTokenOperationResult result, NSString *_Nullable token, + NSError *_Nullable error) { + FIRInstanceID_STRONGIFY(self); + if (error) { + handler(nil, error); + return; + } + NSString *firebaseAppID = options[kFIRInstanceIDTokenOptionsFirebaseAppIDKey]; + FIRInstanceIDTokenInfo *tokenInfo = [[FIRInstanceIDTokenInfo alloc] + initWithAuthorizedEntity:authorizedEntity + scope:scope + token:token + appVersion:FIRInstanceIDCurrentAppVersion() + firebaseAppID:firebaseAppID]; + tokenInfo.APNSInfo = [[FIRInstanceIDAPNSInfo alloc] initWithTokenOptionsDictionary:options]; + + [self.instanceIDStore + saveTokenInfo:tokenInfo + handler:^(NSError *error) { + if (!error) { + // Do not send the token back in case the save was unsuccessful. Since with + // the new asychronous fetch mechanism this can lead to infinite loops, for + // example, we will return a valid token even though we weren't able to store + // it in our cache. The first token will lead to a onTokenRefresh callback + // wherein the user again calls `getToken` but since we weren't able to save + // it we won't hit the cache but hit the server again leading to an infinite + // loop. + FIRInstanceIDLoggerDebug( + kFIRInstanceIDMessageCodeTokenManager001, + @"Token fetch successful, token: %@, authorizedEntity: %@, scope:%@", + token, authorizedEntity, scope); + + if (handler) { + handler(token, nil); + } + } else { + if (handler) { + handler(nil, error); + } + } + }]; + }; + // Add completion handler, and ensure it's called on the main queue + [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result, + NSString *_Nullable token, NSError *_Nullable error) { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(result, token, error); + }); + }]; + [self.tokenOperations addOperation:operation]; +} + +- (FIRInstanceIDTokenInfo *)cachedTokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope { + return [self.instanceIDStore tokenInfoWithAuthorizedEntity:authorizedEntity scope:scope]; +} + +- (void)deleteTokenWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + keyPair:(FIRInstanceIDKeyPair *)keyPair + handler:(FIRInstanceIDDeleteTokenHandler)handler { + if ([self.instanceIDStore tokenInfoWithAuthorizedEntity:authorizedEntity scope:scope]) { + [self.instanceIDStore removeCachedTokenWithAuthorizedEntity:authorizedEntity scope:scope]; + } + // Does not matter if we cannot find it in the cache. Still make an effort to unregister + // from the server. + FIRInstanceIDCheckinPreferences *checkinPreferences = self.authService.checkinPreferences; + FIRInstanceIDTokenDeleteOperation *operation = + [self createDeleteOperationWithAuthorizedEntity:authorizedEntity + scope:scope + checkinPreferences:checkinPreferences + keyPair:keyPair + action:FIRInstanceIDTokenActionDeleteToken]; + + if (handler) { + [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result, + NSString *_Nullable token, NSError *_Nullable error) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler(error); + }); + }]; + } + [self.tokenOperations addOperation:operation]; +} + +- (void)deleteAllTokensWithKeyPair:(FIRInstanceIDKeyPair *)keyPair + handler:(FIRInstanceIDDeleteHandler)handler { + // delete all tokens + FIRInstanceIDCheckinPreferences *checkinPreferences = self.authService.checkinPreferences; + if (!checkinPreferences) { + // The checkin is already deleted. No need to trigger the token delete operation as client no + // longer has the checkin information for server to delete. + dispatch_async(dispatch_get_main_queue(), ^{ + handler(nil); + }); + return; + } + FIRInstanceIDTokenDeleteOperation *operation = + [self createDeleteOperationWithAuthorizedEntity:kFIRInstanceIDKeychainWildcardIdentifier + scope:kFIRInstanceIDKeychainWildcardIdentifier + checkinPreferences:checkinPreferences + keyPair:keyPair + action:FIRInstanceIDTokenActionDeleteTokenAndIID]; + if (handler) { + [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result, + NSString *_Nullable token, NSError *_Nullable error) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler(error); + }); + }]; + } + [self.tokenOperations addOperation:operation]; +} + +- (void)deleteAllTokensLocallyWithHandler:(void (^)(NSError *error))handler { + [self.instanceIDStore removeAllCachedTokensWithHandler:handler]; +} + +- (void)stopAllTokenOperations { + [self.authService stopCheckinRequest]; + [self.tokenOperations cancelAllOperations]; +} + +#pragma mark - FIRInstanceIDStoreDelegate + +- (void)store:(FIRInstanceIDStore *)store + didDeleteFCMScopedTokensForCheckin:(FIRInstanceIDCheckinPreferences *)checkin { + // Make a best effort try to delete the old client related state on the FCM server. This is + // required to delete old pubusb registrations which weren't cleared when the app was deleted. + // + // This is only a one time effort. If this call fails the client would still receive duplicate + // pubsub notifications if he is again subscribed to the same topic. + // + // The client state should be cleared on the server for the provided checkin preferences. + FIRInstanceIDTokenDeleteOperation *operation = + [self createDeleteOperationWithAuthorizedEntity:nil + scope:nil + checkinPreferences:checkin + keyPair:nil + action:FIRInstanceIDTokenActionDeleteToken]; + [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result, + NSString *_Nullable token, NSError *_Nullable error) { + if (error) { + FIRInstanceIDMessageCode code = + kFIRInstanceIDMessageCodeTokenManagerErrorDeletingFCMTokensOnAppReset; + FIRInstanceIDLoggerDebug(code, @"Failed to delete GCM server registrations on app reset."); + } else { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenManagerDeletedFCMTokensOnAppReset, + @"Successfully deleted GCM server registrations on app reset"); + } + }]; + + [self.tokenOperations addOperation:operation]; +} + +#pragma mark - Unit Testing Stub Helpers +// We really have this method so that we can more easily stub it out for unit testing +- (FIRInstanceIDTokenFetchOperation *) + createFetchOperationWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + options:(NSDictionary *)options + keyPair:(FIRInstanceIDKeyPair *)keyPair { + FIRInstanceIDCheckinPreferences *checkinPreferences = self.authService.checkinPreferences; + FIRInstanceIDTokenFetchOperation *operation = + [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:authorizedEntity + scope:scope + options:options + checkinPreferences:checkinPreferences + keyPair:keyPair]; + return operation; +} + +// We really have this method so that we can more easily stub it out for unit testing +- (FIRInstanceIDTokenDeleteOperation *) + createDeleteOperationWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences + keyPair:(FIRInstanceIDKeyPair *)keyPair + action:(FIRInstanceIDTokenAction)action { + FIRInstanceIDTokenDeleteOperation *operation = + [[FIRInstanceIDTokenDeleteOperation alloc] initWithAuthorizedEntity:authorizedEntity + scope:scope + checkinPreferences:checkinPreferences + keyPair:keyPair + action:action]; + return operation; +} + +#pragma mark - Invalidating Cached Tokens +- (BOOL)checkForTokenRefreshPolicy { + // We know at least one cached token exists. + BOOL shouldFetchDefaultToken = NO; + NSArray *tokenInfos = [self.instanceIDStore cachedTokenInfos]; + + NSMutableArray *tokenInfosToDelete = + [NSMutableArray arrayWithCapacity:tokenInfos.count]; + for (FIRInstanceIDTokenInfo *tokenInfo in tokenInfos) { + BOOL isTokenFresh = [tokenInfo isFresh]; + if (isTokenFresh) { + // Token is fresh, do nothing. + continue; + } + if ([tokenInfo.scope isEqualToString:kFIRInstanceIDDefaultTokenScope]) { + // Default token is expired, do not mark for deletion. Fetch directly from server to + // replace the current one. + shouldFetchDefaultToken = YES; + } else { + // Non-default token is expired, mark for deletion. + [tokenInfosToDelete addObject:tokenInfo]; + } + FIRInstanceIDLoggerDebug( + kFIRInstanceIDMessageCodeTokenManagerInvalidateStaleToken, + @"Invalidating cached token for %@ (%@) due to token is no longer fresh.", + tokenInfo.authorizedEntity, tokenInfo.scope); + } + for (FIRInstanceIDTokenInfo *tokenInfoToDelete in tokenInfosToDelete) { + [self.instanceIDStore removeCachedTokenWithAuthorizedEntity:tokenInfoToDelete.authorizedEntity + scope:tokenInfoToDelete.scope]; + } + return shouldFetchDefaultToken; +} + +- (NSArray *)updateTokensToAPNSDeviceToken:(NSData *)deviceToken + isSandbox:(BOOL)isSandbox { + // Each cached IID token that is missing an APNSInfo, or has an APNSInfo associated should be + // checked and invalidated if needed. + FIRInstanceIDAPNSInfo *APNSInfo = [[FIRInstanceIDAPNSInfo alloc] initWithDeviceToken:deviceToken + isSandbox:isSandbox]; + if ([self.currentAPNSInfo isEqualToAPNSInfo:APNSInfo]) { + return @[]; + } + self.currentAPNSInfo = APNSInfo; + + NSArray *tokenInfos = [self.instanceIDStore cachedTokenInfos]; + NSMutableArray *tokenInfosToDelete = + [NSMutableArray arrayWithCapacity:tokenInfos.count]; + for (FIRInstanceIDTokenInfo *cachedTokenInfo in tokenInfos) { + // Check if the cached APNSInfo is nil, or if it is an old APNSInfo. + if (!cachedTokenInfo.APNSInfo || + ![cachedTokenInfo.APNSInfo isEqualToAPNSInfo:self.currentAPNSInfo]) { + // Mark for invalidation. + [tokenInfosToDelete addObject:cachedTokenInfo]; + } + } + for (FIRInstanceIDTokenInfo *tokenInfoToDelete in tokenInfosToDelete) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenManagerAPNSChangedTokenInvalidated, + @"Invalidating cached token for %@ (%@) due to APNs token change.", + tokenInfoToDelete.authorizedEntity, tokenInfoToDelete.scope); + [self.instanceIDStore removeCachedTokenWithAuthorizedEntity:tokenInfoToDelete.authorizedEntity + scope:tokenInfoToDelete.scope]; + } + return tokenInfosToDelete; +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDTokenOperation+Private.h b/Firebase/InstanceID/FIRInstanceIDTokenOperation+Private.h new file mode 100644 index 00000000000..68d9db18396 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDTokenOperation+Private.h @@ -0,0 +1,67 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDTokenOperation.h" + +#import "FIRInstanceIDUtilities.h" + +@class FIRInstanceIDKeyPair; +@class FIRInstanceIDURLQueryItem; + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRInstanceIDTokenOperation (Private) + +@property(atomic, strong) NSURLSessionDataTask *dataTask; +@property(readonly, strong) + NSMutableArray *completionHandlers; + +// For testing only +@property(nonatomic, readwrite, copy) FIRInstanceIDURLRequestTestBlock testBlock; + ++ (NSURLSession *)sharedURLSession; + +#pragma mark - Initialization +- (instancetype)initWithAction:(FIRInstanceIDTokenAction)action + forAuthorizedEntity:(nullable NSString *)authorizedEntity + scope:(NSString *)scope + options:(nullable NSDictionary *)options + checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences + keyPair:(FIRInstanceIDKeyPair *)keyPair; + +#pragma mark - Request Construction ++ (NSMutableURLRequest *)requestWithAuthHeader:(NSString *)authHeaderString; ++ (NSMutableArray *)standardQueryItemsWithDeviceID:(NSString *)deviceID + scope:(NSString *)scope; +- (NSArray *)queryItemsWithKeyPair:(FIRInstanceIDKeyPair *)keyPair; + +#pragma mark - HTTP Headers +/** + * Given a valid checkin preferences object, it will return a string that can be used + * in the "Authorization" HTTP header to authenticate this request. + * + * @param checkin The valid checkin preferences object, with a deviceID and secretToken. + */ ++ (NSString *)HTTPAuthHeaderFromCheckin:(FIRInstanceIDCheckinPreferences *)checkin; + +#pragma mark - Result +- (void)finishWithResult:(FIRInstanceIDTokenOperationResult)result + token:(nullable NSString *)token + error:(nullable NSError *)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InstanceID/FIRInstanceIDTokenOperation.h b/Firebase/InstanceID/FIRInstanceIDTokenOperation.h new file mode 100644 index 00000000000..1a1842cf287 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDTokenOperation.h @@ -0,0 +1,73 @@ +/* + * Copyright 2019 Google + * + * 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 FIRInstanceIDKeyPair; +@class FIRInstanceIDCheckinPreferences; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Represents the action taken on an FCM token. + */ +typedef NS_ENUM(NSInteger, FIRInstanceIDTokenAction) { + FIRInstanceIDTokenActionFetch, + FIRInstanceIDTokenActionDeleteToken, + FIRInstanceIDTokenActionDeleteTokenAndIID, +}; + +/** + * Represents the possible results of a token operation. + */ +typedef NS_ENUM(NSInteger, FIRInstanceIDTokenOperationResult) { + FIRInstanceIDTokenOperationSucceeded, + FIRInstanceIDTokenOperationError, + FIRInstanceIDTokenOperationCancelled, +}; + +/** + * Callback to invoke once the HTTP call to FIRMessaging backend for updating + * subscription finishes. + * + * @param result The result of the operation. + * @param token If the action for fetching a token and the request was successful, this will hold + * the value of the token. Otherwise nil. + * @param error The error which occurred while performing the token operation. This will be nil + * in case the operation was successful, or if the operation was cancelled. + */ +typedef void (^FIRInstanceIDTokenOperationCompletion)(FIRInstanceIDTokenOperationResult result, + NSString *_Nullable token, + NSError *_Nullable error); + +@interface FIRInstanceIDTokenOperation : NSOperation + +@property(nonatomic, readonly) FIRInstanceIDTokenAction action; +@property(nonatomic, readonly, nullable) NSString *authorizedEntity; +@property(nonatomic, readonly, nullable) NSString *scope; +@property(nonatomic, readonly, nullable) NSDictionary *options; +@property(nonatomic, readonly, strong) FIRInstanceIDCheckinPreferences *checkinPreferences; +@property(nonatomic, readonly, strong) FIRInstanceIDKeyPair *keyPair; + +@property(nonatomic, readonly) FIRInstanceIDTokenOperationResult result; + +- (instancetype)init NS_UNAVAILABLE; + +- (void)addCompletionHandler:(FIRInstanceIDTokenOperationCompletion)handler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InstanceID/FIRInstanceIDTokenOperation.m b/Firebase/InstanceID/FIRInstanceIDTokenOperation.m new file mode 100644 index 00000000000..dcfdb842482 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDTokenOperation.m @@ -0,0 +1,243 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDTokenOperation.h" + +#import "FIRInstanceIDCheckinPreferences.h" +#import "FIRInstanceIDDefines.h" +#import "FIRInstanceIDKeyPair.h" +#import "FIRInstanceIDKeyPairUtilities.h" +#import "FIRInstanceIDLogger.h" +#import "FIRInstanceIDURLQueryItem.h" +#import "FIRInstanceIDUtilities.h" +#import "NSError+FIRInstanceID.h" + +static const NSInteger kFIRInstanceIDPlatformVersionIOS = 2; + +static NSString *const kFIRInstanceIDParamInstanceID = @"appid"; +// Scope parameter that defines the service using the token +static NSString *const kFIRInstanceIDParamScope = @"X-scope"; +// Defines the SDK version +static NSString *const kFIRInstanceIDParamFCMLibVersion = @"X-cliv"; + +@interface FIRInstanceIDTokenOperation () { + BOOL _isFinished; + BOOL _isExecuting; +} + +@property(nonatomic, readwrite, strong) FIRInstanceIDCheckinPreferences *checkinPreferences; +@property(nonatomic, readwrite, strong) FIRInstanceIDKeyPair *keyPair; + +@property(atomic, strong) NSURLSessionDataTask *dataTask; +@property(readonly, strong) + NSMutableArray *completionHandlers; + +// For testing only +@property(nonatomic, readwrite, copy) FIRInstanceIDURLRequestTestBlock testBlock; + +@end + +@implementation FIRInstanceIDTokenOperation + ++ (NSURLSession *)sharedURLSession { + static NSURLSession *tokenOperationSharedSession; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; + config.timeoutIntervalForResource = 60.0f; // 1 minute + tokenOperationSharedSession = [NSURLSession sessionWithConfiguration:config]; + tokenOperationSharedSession.sessionDescription = @"com.google.iid.tokens.session"; + }); + return tokenOperationSharedSession; +} + +- (instancetype)initWithAction:(FIRInstanceIDTokenAction)action + forAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + options:(NSDictionary *)options + checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences + keyPair:(FIRInstanceIDKeyPair *)keyPair { + self = [super init]; + if (self) { + _action = action; + _authorizedEntity = [authorizedEntity copy]; + _scope = [scope copy]; + _options = [options copy]; + _checkinPreferences = checkinPreferences; + _keyPair = keyPair; + _completionHandlers = [NSMutableArray array]; + + _isExecuting = NO; + _isFinished = NO; + } + return self; +} + +- (void)dealloc { + _testBlock = nil; + _authorizedEntity = nil; + _scope = nil; + _options = nil; + _checkinPreferences = nil; + _keyPair = nil; + [_completionHandlers removeAllObjects]; + _completionHandlers = nil; +} + +- (void)addCompletionHandler:(FIRInstanceIDTokenOperationCompletion)handler { + [self.completionHandlers addObject:handler]; +} + +- (BOOL)isAsynchronous { + return YES; +} + +- (BOOL)isExecuting { + return _isExecuting; +} + +- (void)setExecuting:(BOOL)executing { + [self willChangeValueForKey:@"isExecuting"]; + _isExecuting = executing; + [self didChangeValueForKey:@"isExecuting"]; +} + +- (BOOL)isFinished { + return _isFinished; +} + +- (void)setFinished:(BOOL)finished { + [self willChangeValueForKey:@"isFinished"]; + _isFinished = finished; + [self didChangeValueForKey:@"isFinished"]; +} + +- (void)start { + if (self.isCancelled) { + [self finishWithResult:FIRInstanceIDTokenOperationCancelled token:nil error:nil]; + return; + } + + // Quickly validate whether or not the operation has all it needs to begin + BOOL checkinfoAvailable = [self.checkinPreferences hasCheckinInfo]; + _FIRInstanceIDDevAssert(checkinfoAvailable, @"Cannot fetch token invalid checkin state"); + if (!checkinfoAvailable) { + FIRInstanceIDErrorCode errorCode = kFIRInstanceIDErrorCodeRegistrarFailedToCheckIn; + [self finishWithResult:FIRInstanceIDTokenOperationError + token:nil + error:[NSError errorWithFIRInstanceIDErrorCode:errorCode]]; + return; + } + + [self setExecuting:YES]; + + [self performTokenOperation]; +} + +- (void)finishWithResult:(FIRInstanceIDTokenOperationResult)result + token:(nullable NSString *)token + error:(nullable NSError *)error { + // Add a check to prevent this finish from being called more than once. + if (self.isFinished) { + return; + } + self.dataTask = nil; + _result = result; + // TODO(chliangGoogle): Call these in the main thread? + for (FIRInstanceIDTokenOperationCompletion completionHandler in self.completionHandlers) { + completionHandler(result, token, error); + } + + [self setExecuting:NO]; + [self setFinished:YES]; +} + +- (void)cancel { + [super cancel]; + [self.dataTask cancel]; + [self finishWithResult:FIRInstanceIDTokenOperationCancelled token:nil error:nil]; +} + +- (void)performTokenOperation { +} + +#pragma mark - Request Construction ++ (NSMutableURLRequest *)requestWithAuthHeader:(NSString *)authHeaderString { + NSURL *url = [NSURL URLWithString:FIRInstanceIDRegisterServer()]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + + // Add HTTP headers + [request setValue:authHeaderString forHTTPHeaderField:@"Authorization"]; + [request setValue:FIRInstanceIDAppIdentifier() forHTTPHeaderField:@"app"]; + request.HTTPMethod = @"POST"; + return request; +} + ++ (NSMutableArray *)standardQueryItemsWithDeviceID:(NSString *)deviceID + scope:(NSString *)scope { + NSMutableArray *queryItems = [NSMutableArray arrayWithCapacity:8]; + + // E.g. X-osv=10.2.1 + NSString *systemVersion = FIRInstanceIDOperatingSystemVersion(); + [queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"X-osv" value:systemVersion]]; + // E.g. device= + if (deviceID) { + [queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"device" value:deviceID]]; + } + // E.g. X-scope=fcm + if (scope) { + [queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:kFIRInstanceIDParamScope + value:scope]]; + } + // E.g. plat=2 + NSString *platform = [NSString stringWithFormat:@"%ld", (long)kFIRInstanceIDPlatformVersionIOS]; + [queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"plat" value:platform]]; + // E.g. app=com.myapp.foo + NSString *appIdentifier = FIRInstanceIDAppIdentifier(); + [queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"app" value:appIdentifier]]; + // E.g. app_ver=1.5 + NSString *appVersion = FIRInstanceIDCurrentAppVersion(); + [queryItems addObject:[FIRInstanceIDURLQueryItem queryItemWithName:@"app_ver" value:appVersion]]; + // E.g. X-cliv=fiid-1.2.3 + NSString *fcmLibraryVersion = + [NSString stringWithFormat:@"fiid-%@", FIRInstanceIDCurrentGCMVersion()]; + if (fcmLibraryVersion.length) { + FIRInstanceIDURLQueryItem *gcmLibVersion = + [FIRInstanceIDURLQueryItem queryItemWithName:kFIRInstanceIDParamFCMLibVersion + value:fcmLibraryVersion]; + [queryItems addObject:gcmLibVersion]; + } + + return queryItems; +} + +- (NSArray *)queryItemsWithKeyPair:(FIRInstanceIDKeyPair *)keyPair { + NSMutableArray *items = [NSMutableArray arrayWithCapacity:3]; + // appid= + NSString *instanceID = FIRInstanceIDAppIdentity(keyPair); + [items addObject:[FIRInstanceIDURLQueryItem queryItemWithName:kFIRInstanceIDParamInstanceID + value:instanceID]]; + return items; +} + +#pragma mark - HTTP Header + ++ (NSString *)HTTPAuthHeaderFromCheckin:(FIRInstanceIDCheckinPreferences *)checkin { + NSString *deviceID = checkin.deviceID; + NSString *secret = checkin.secretToken; + return [NSString stringWithFormat:@"AidLogin %@:%@", deviceID, secret]; +} +@end diff --git a/Firebase/InstanceID/FIRInstanceIDTokenStore.h b/Firebase/InstanceID/FIRInstanceIDTokenStore.h new file mode 100644 index 00000000000..861c87b9962 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDTokenStore.h @@ -0,0 +1,106 @@ +/* + * Copyright 2019 Google + * + * 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 FIRInstanceIDAPNSInfo; +@class FIRInstanceIDAuthKeychain; +@class FIRInstanceIDTokenInfo; + +/** + * This class is responsible for retrieving and saving `FIRInstanceIDTokenInfo` objects from the + * keychain. The keychain keys that are used are: + * Account:
(e.g. com.mycompany.myapp) + * Service: : (e.g. 1234567890:*) + */ +@interface FIRInstanceIDTokenStore : NSObject + +NS_ASSUME_NONNULL_BEGIN + +/** + * Create a default InstanceID token store. Uses a valid Keychain object as it's + * persistent backing store. + * + * @return A valid token store object. + */ ++ (instancetype)defaultStore; + +- (instancetype)init __attribute__((unavailable("Use -initWithKeychain: instead."))); + +/** + * Initialize a token store object with a Keychain object. Used for testing. + * + * @param keychain The Keychain object to use as the backing store for tokens. + * + * @return A valid token store object with the given Keychain as backing store. + */ +- (instancetype)initWithKeychain:(FIRInstanceIDAuthKeychain *)keychain; + +#pragma mark - Get + +/** + * Get the cached token from the Keychain. + * + * @param authorizedEntity The authorized entity for the token. + * @param scope The scope for the token. + * + * @return The cached token info if any for the given authorizedEntity and scope else + * nil. + */ +- (nullable FIRInstanceIDTokenInfo *)tokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope; + +/** + * Return all cached token infos from the Keychain. + * + * @return The cached token infos, if any, that are stored in the Keychain. + */ +- (NSArray *)cachedTokenInfos; + +#pragma mark - Save + +/** + * Save the instanceID token info to the persistent store. + * + * @param tokenInfo The token info to store. + * @param handler The callback handler which is invoked when token saving is complete, + * with an error if there is any. + */ +- (void)saveTokenInfo:(FIRInstanceIDTokenInfo *)tokenInfo + handler:(nullable void (^)(NSError *))handler; + +#pragma mark - Delete + +/** + * Remove the cached token from Keychain. + * + * @param authorizedEntity The authorized entity for the token. + * @param scope The scope for the token. + * + */ +- (void)removeTokenWithAuthorizedEntity:(NSString *)authorizedEntity scope:(NSString *)scope; + +/** + * Remove all the cached tokens from the Keychain. + * @param handler The callback handler which is invoked when tokens deletion is complete, + * with an error if there is any. + * + */ +- (void)removeAllTokensWithHandler:(nullable void (^)(NSError *))handler; + +NS_ASSUME_NONNULL_END + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDTokenStore.m b/Firebase/InstanceID/FIRInstanceIDTokenStore.m new file mode 100644 index 00000000000..baadc895db3 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDTokenStore.m @@ -0,0 +1,137 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDTokenStore.h" + +#import "FIRInstanceIDAuthKeyChain.h" +#import "FIRInstanceIDConstants.h" +#import "FIRInstanceIDLogger.h" +#import "FIRInstanceIDTokenInfo.h" +#import "FIRInstanceIDUtilities.h" + +static NSString *const kFIRInstanceIDTokenKeychainId = @"com.google.iid-tokens"; + +@interface FIRInstanceIDTokenStore () + +@property(nonatomic, readwrite, strong) FIRInstanceIDAuthKeychain *keychain; + +@end + +@implementation FIRInstanceIDTokenStore + ++ (instancetype)defaultStore { + FIRInstanceIDAuthKeychain *tokenKeychain = + [[FIRInstanceIDAuthKeychain alloc] initWithIdentifier:kFIRInstanceIDTokenKeychainId]; + return [[FIRInstanceIDTokenStore alloc] initWithKeychain:tokenKeychain]; +} + +- (instancetype)initWithKeychain:(FIRInstanceIDAuthKeychain *)keychain { + self = [super init]; + if (self) { + _keychain = keychain; + } + return self; +} + +#pragma mark - Get + ++ (NSString *)serviceKeyForAuthorizedEntity:(NSString *)authorizedEntity scope:(NSString *)scope { + return [NSString stringWithFormat:@"%@:%@", authorizedEntity, scope]; +} + +- (nullable FIRInstanceIDTokenInfo *)tokenInfoWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope { + NSString *account = FIRInstanceIDAppIdentifier(); + NSString *service = [[self class] serviceKeyForAuthorizedEntity:authorizedEntity scope:scope]; + NSData *item = [self.keychain dataForService:service account:account]; + if (!item) { + return nil; + } + // Token infos created from legacy storage don't have appVersion, firebaseAppID, or APNSInfo. + FIRInstanceIDTokenInfo *tokenInfo = [[self class] tokenInfoFromKeychainItem:item]; + return tokenInfo; +} + +- (NSArray *)cachedTokenInfos { + NSString *account = FIRInstanceIDAppIdentifier(); + NSArray *items = + [self.keychain itemsMatchingService:kFIRInstanceIDKeychainWildcardIdentifier account:account]; + NSMutableArray *tokenInfos = + [NSMutableArray arrayWithCapacity:items.count]; + for (NSData *item in items) { + FIRInstanceIDTokenInfo *tokenInfo = [[self class] tokenInfoFromKeychainItem:item]; + if (tokenInfo) { + [tokenInfos addObject:tokenInfo]; + } + } + return tokenInfos; +} + ++ (nullable FIRInstanceIDTokenInfo *)tokenInfoFromKeychainItem:(NSData *)item { + // Check if it is saved as an archived FIRInstanceIDTokenInfo, otherwise return nil. + FIRInstanceIDTokenInfo *tokenInfo = nil; + // NOTE: Passing in nil to unarchiveObjectWithData will result in an iOS error logged + // in the console on iOS 10 and below. Avoid by checking item.data's existence. + if (item) { + @try { + tokenInfo = [NSKeyedUnarchiver unarchiveObjectWithData:item]; + } @catch (NSException *exception) { + FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeTokenStoreExceptionUnarchivingTokenInfo, + @"Unable to parse token info from Keychain item; item was in an " + @"invalid format"); + tokenInfo = nil; + } @finally { + } + } + return tokenInfo; +} + +#pragma mark - Save +// Token Infos will be saved under these Keychain keys: +// Account:
(e.g. com.mycompany.myapp) +// Service: : (e.g. 1234567890:*) +- (void)saveTokenInfo:(FIRInstanceIDTokenInfo *)tokenInfo + handler:(void (^)(NSError *))handler { // Keep the cachetime up-to-date. + tokenInfo.cacheTime = [NSDate date]; + // Always write to the Keychain, so that the cacheTime is up-to-date. + NSData *tokenInfoData = [NSKeyedArchiver archivedDataWithRootObject:tokenInfo]; + NSString *account = FIRInstanceIDAppIdentifier(); + NSString *service = [[self class] serviceKeyForAuthorizedEntity:tokenInfo.authorizedEntity + scope:tokenInfo.scope]; + [self.keychain setData:tokenInfoData + forService:service + accessibility:NULL + account:account + handler:handler]; +} + +#pragma mark - Delete + +- (void)removeTokenWithAuthorizedEntity:(nonnull NSString *)authorizedEntity + scope:(nonnull NSString *)scope { + NSString *account = FIRInstanceIDAppIdentifier(); + NSString *service = [[self class] serviceKeyForAuthorizedEntity:authorizedEntity scope:scope]; + [self.keychain removeItemsMatchingService:service account:account handler:nil]; +} + +- (void)removeAllTokensWithHandler:(void (^)(NSError *error))handler { + NSString *account = FIRInstanceIDAppIdentifier(); + [self.keychain removeItemsMatchingService:kFIRInstanceIDKeychainWildcardIdentifier + account:account + handler:handler]; +} + +@end diff --git a/Firebase/InstanceID/FIRInstanceIDURLQueryItem.h b/Firebase/InstanceID/FIRInstanceIDURLQueryItem.h new file mode 100644 index 00000000000..3a3a1d7cd7b --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDURLQueryItem.h @@ -0,0 +1,39 @@ +/* + * Copyright 2019 Google + * + * 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 + +// Stand-in for NSURLQueryItem, which is only available on iOS 8.0 and up. +@interface FIRInstanceIDURLQueryItem : NSObject + +@property(nonatomic, readonly) NSString *name; +@property(nonatomic, readonly) NSString *value; + ++ (instancetype)queryItemWithName:(NSString *)name value:(NSString *)value; +- (instancetype)initWithName:(NSString *)name value:(NSString *)value; + +@end + +/** + * Given an array of query items, construct a URL query. On iOS 8.0 and above, this will use + * NSURLQueryItems internally to perform the string creation, and will be done manually in iOS + * 7 and below. + */ +NSString *FIRInstanceIDQueryFromQueryItems(NSArray *queryItems); + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InstanceID/FIRInstanceIDURLQueryItem.m b/Firebase/InstanceID/FIRInstanceIDURLQueryItem.m new file mode 100644 index 00000000000..59b4865558c --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDURLQueryItem.m @@ -0,0 +1,55 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDURLQueryItem.h" + +@implementation FIRInstanceIDURLQueryItem + ++ (instancetype)queryItemWithName:(NSString *)name value:(NSString *)value { + return [[[self class] alloc] initWithName:name value:value]; +} + +- (instancetype)initWithName:(NSString *)name value:(NSString *)value { + self = [super init]; + if (self) { + _name = [name copy]; + _value = [value copy]; + } + return self; +} +@end + +NSString *FIRInstanceIDQueryFromQueryItems(NSArray *queryItems) { + if ([NSURLQueryItem class]) { + // We are iOS 8.0 and above. Convert to NSURLQueryItems and get query that way + // to take advantage of any automatic encoding + NSMutableArray *urlItems = + [NSMutableArray arrayWithCapacity:queryItems.count]; + for (FIRInstanceIDURLQueryItem *queryItem in queryItems) { + [urlItems addObject:[NSURLQueryItem queryItemWithName:queryItem.name value:queryItem.value]]; + } + NSURLComponents *components = [[NSURLComponents alloc] init]; + components.queryItems = urlItems; + return components.query; + } else { + // We are on iOS 7.0. Manually create the query string + NSMutableArray *pairs = [NSMutableArray arrayWithCapacity:queryItems.count]; + for (FIRInstanceIDURLQueryItem *queryItem in queryItems) { + [pairs addObject:[NSString stringWithFormat:@"%@=%@", queryItem.name, queryItem.value]]; + } + return [pairs componentsJoinedByString:@"&"]; + } +} diff --git a/Firebase/InstanceID/FIRInstanceIDUtilities.h b/Firebase/InstanceID/FIRInstanceIDUtilities.h new file mode 100644 index 00000000000..da6ebad3390 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDUtilities.h @@ -0,0 +1,85 @@ +/* + * Copyright 2019 Google + * + * 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 + +/// FIRMessaging Class that responds to the FIRMessaging SDK version selector. +/// Verify at runtime if the class exists and implements the required method. +FOUNDATION_EXPORT NSString *const kFIRInstanceIDFCMSDKClassString; + +/// locale key stored in GULUserDefaults +FOUNDATION_EXPORT NSString *const kFIRInstanceIDUserDefaultsKeyLocale; + +#pragma mark - Test Blocks + +/** + * Response block for mock registration requests made during tests. + * + * @param data The data as returned by the mock request. + * @param response The response as returned by the mock request. + * @param error The error if any as returned by the mock request. + */ +typedef void (^FIRInstanceIDURLRequestTestResponseBlock)(NSData *data, + NSURLResponse *response, + NSError *error); + +/** + * Test block to mock registration requests response. + * + * @param request The request to mock response for. + * @param response The response block for the mocked request. + */ +typedef void (^FIRInstanceIDURLRequestTestBlock)(NSURLRequest *request, + FIRInstanceIDURLRequestTestResponseBlock response); + +#pragma mark - URL Helpers + +FOUNDATION_EXPORT NSString *FIRInstanceIDRegisterServer(void); + +#pragma mark - Time + +FOUNDATION_EXPORT int64_t FIRInstanceIDCurrentTimestampInSeconds(void); +FOUNDATION_EXPORT int64_t FIRInstanceIDCurrentTimestampInMilliseconds(void); + +#pragma mark - App Info + +FOUNDATION_EXPORT NSString *FIRInstanceIDCurrentAppVersion(void); +FOUNDATION_EXPORT NSString *FIRInstanceIDAppIdentifier(void); +FOUNDATION_EXPORT NSString *FIRInstanceIDFirebaseAppID(void); + +#pragma mark - Device Info + +FOUNDATION_EXPORT NSString *FIRInstanceIDDeviceModel(void); +FOUNDATION_EXPORT NSString *FIRInstanceIDOperatingSystemVersion(void); +FOUNDATION_EXPORT BOOL FIRInstanceIDHasLocaleChanged(void); + +#pragma mark - Helpers + +FOUNDATION_EXPORT BOOL FIRInstanceIDIsValidGCMScope(NSString *scope); +FOUNDATION_EXPORT NSString *FIRInstanceIDStringForAPNSDeviceToken(NSData *deviceToken); +FOUNDATION_EXPORT NSString *FIRInstanceIDAPNSTupleStringForTokenAndServerType(NSData *deviceToken, + BOOL isSandbox); + +#pragma mark - GCM Helpers +/// Returns the current GCM version if GCM library is found else returns nil. +FOUNDATION_EXPORT NSString *FIRInstanceIDCurrentGCMVersion(void); + +/// Returns the current locale. If GCM is present it queries GCM for a +/// Context Manager specific locale. Otherwise, it returns the system's first +/// preferred language (which may be set independently from locale). If the +/// system returns no preferred languages, this method returns the most common +/// language for the user's given locale. Guaranteed to return a nonnull value. +FOUNDATION_EXPORT NSString *FIRInstanceIDCurrentLocale(void); diff --git a/Firebase/InstanceID/FIRInstanceIDUtilities.m b/Firebase/InstanceID/FIRInstanceIDUtilities.m new file mode 100644 index 00000000000..6b7ca960494 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDUtilities.m @@ -0,0 +1,194 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDUtilities.h" + +#import +#import + +#import +#import +#import "FIRInstanceID.h" +#import "FIRInstanceIDConstants.h" +#import "FIRInstanceIDDefines.h" +#import "FIRInstanceIDLogger.h" + +// Convert the macro to a string +#define STR_EXPAND(x) #x +#define STR(x) STR_EXPAND(x) + +static NSString *const kFIRInstanceIDAPNSSandboxPrefix = @"s_"; +static NSString *const kFIRInstanceIDAPNSProdPrefix = @"p_"; + +/// FIRMessaging Class that responds to the FIRMessaging SDK version selector. +/// Verify at runtime if the class exists and implements the required method. +NSString *const kFIRInstanceIDFCMSDKClassString = @"FIRMessaging"; + +/// FIRMessaging selector that returns the current FIRMessaging library version. +static NSString *const kFIRInstanceIDFCMSDKVersionSelectorString = @"FIRMessagingSDKVersion"; + +/// FIRMessaging selector that returns the current device locale. +static NSString *const kFIRInstanceIDFCMSDKLocaleSelectorString = @"FIRMessagingSDKCurrentLocale"; + +NSString *const kFIRInstanceIDUserDefaultsKeyLocale = + @"com.firebase.instanceid.user_defaults.locale"; // locale key stored in GULUserDefaults + +/// Static values which will be populated once retrieved using +/// |FIRInstanceIDRetrieveEnvironmentInfoFromFirebaseCore|. +static NSString *operatingSystemVersion; +static NSString *hardwareDeviceModel; + +#pragma mark - URL Helpers + +NSString *FIRInstanceIDRegisterServer() { + return @"https://fcmtoken.googleapis.com/register"; +} + +#pragma mark - Time + +int64_t FIRInstanceIDCurrentTimestampInSeconds() { + return (int64_t)[[NSDate date] timeIntervalSince1970]; +} + +int64_t FIRInstanceIDCurrentTimestampInMilliseconds() { + return (int64_t)(FIRInstanceIDCurrentTimestampInSeconds() * 1000.0); +} + +#pragma mark - App Info + +NSString *FIRInstanceIDCurrentAppVersion() { + NSString *version = [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"]; + if (![version length]) { + return @""; + } + return version; +} + +NSString *FIRInstanceIDAppIdentifier() { + NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; + if (!bundleIdentifier.length) { + FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeUtilitiesMissingBundleIdentifier, + @"The mainBundle's bundleIdentifier returned '%@'. Bundle identifier " + @"expected to be non-empty.", + bundleIdentifier); + return @""; + } + return bundleIdentifier; +} + +NSString *FIRInstanceIDFirebaseAppID() { + return [FIROptions defaultOptions].googleAppID; +} + +#pragma mark - Device Info +// Get the device model from Firebase Core's App Environment Util +NSString *FIRInstanceIDDeviceModel() { + static dispatch_once_t once; + dispatch_once(&once, ^{ + struct utsname systemInfo; + if (uname(&systemInfo) == 0) { + hardwareDeviceModel = [NSString stringWithUTF8String:systemInfo.machine]; + } + }); + return hardwareDeviceModel; +} + +// Get the system version from Firebase Core's App Environment Util +NSString *FIRInstanceIDOperatingSystemVersion() { +#if TARGET_OS_IOS || TARGET_OS_TV + return [UIDevice currentDevice].systemVersion; +#elif TARGET_OS_OSX + return [NSProcessInfo processInfo].operatingSystemVersionString; +#endif +} + +BOOL FIRInstanceIDHasLocaleChanged() { + NSString *lastLocale = + [[GULUserDefaults standardUserDefaults] stringForKey:kFIRInstanceIDUserDefaultsKeyLocale]; + NSString *currentLocale = FIRInstanceIDCurrentLocale(); + if (lastLocale) { + if ([currentLocale isEqualToString:lastLocale]) { + return NO; + } + } + return YES; +} + +#pragma mark - Helpers + +BOOL FIRInstanceIDIsValidGCMScope(NSString *scope) { + return [scope compare:kFIRInstanceIDScopeFirebaseMessaging + options:NSCaseInsensitiveSearch] == NSOrderedSame; +} + +NSString *FIRInstanceIDStringForAPNSDeviceToken(NSData *deviceToken) { + NSMutableString *APNSToken = [NSMutableString string]; + unsigned char *bytes = (unsigned char *)[deviceToken bytes]; + for (int i = 0; i < (int)deviceToken.length; i++) { + [APNSToken appendFormat:@"%02x", bytes[i]]; + } + return APNSToken; +} + +NSString *FIRInstanceIDAPNSTupleStringForTokenAndServerType(NSData *deviceToken, BOOL isSandbox) { + if (deviceToken == nil) { + // A nil deviceToken leads to an invalid tuple string, so return nil. + return nil; + } + NSString *prefix = isSandbox ? kFIRInstanceIDAPNSSandboxPrefix : kFIRInstanceIDAPNSProdPrefix; + NSString *APNSString = FIRInstanceIDStringForAPNSDeviceToken(deviceToken); + NSString *APNSTupleString = [NSString stringWithFormat:@"%@%@", prefix, APNSString]; + + return APNSTupleString; +} + +#pragma mark - GCM Helpers + +NSString *FIRInstanceIDCurrentGCMVersion() { + Class versionClass = NSClassFromString(kFIRInstanceIDFCMSDKClassString); + SEL versionSelector = NSSelectorFromString(kFIRInstanceIDFCMSDKVersionSelectorString); + if ([versionClass respondsToSelector:versionSelector]) { + IMP getVersionIMP = [versionClass methodForSelector:versionSelector]; + NSString *(*getVersion)(id, SEL) = (void *)getVersionIMP; + return getVersion(versionClass, versionSelector); + } + return nil; +} + +NSString *FIRInstanceIDCurrentLocale() { + Class localeClass = NSClassFromString(kFIRInstanceIDFCMSDKClassString); + SEL localeSelector = NSSelectorFromString(kFIRInstanceIDFCMSDKLocaleSelectorString); + + if ([localeClass respondsToSelector:localeSelector]) { + IMP getLocaleIMP = [localeClass methodForSelector:localeSelector]; + NSString *(*getLocale)(id, SEL) = (void *)getLocaleIMP; + NSString *fcmLocale = getLocale(localeClass, localeSelector); + if (fcmLocale != nil) { + return fcmLocale; + } + } + + NSString *systemLanguage = [[NSLocale preferredLanguages] firstObject]; + if (systemLanguage != nil) { + return systemLanguage; + } + + if (@available(iOS 10.0, *)) { + return [NSLocale currentLocale].languageCode; + } else { + return nil; + } +} diff --git a/Firebase/InstanceID/FIRInstanceIDVersionUtilities.h b/Firebase/InstanceID/FIRInstanceIDVersionUtilities.h new file mode 100644 index 00000000000..ec5a76c5dcb --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDVersionUtilities.h @@ -0,0 +1,35 @@ +/* + * Copyright 2019 Google + * + * 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 + +/** + * Parsing utility for InstanceID Library versions. InstanceID lib follows semantic versioning. + * This provides utilities to parse the library versions to enable features and do + * updates based on appropriate library versions. + * + * Some example semantic versions are 1.0.1, 2.1.0, 2.1.1, 2.2.0-alpha1, 2.2.1-beta1 + */ + +FOUNDATION_EXPORT NSString *FIRInstanceIDCurrentLibraryVersion(void); +/// Returns the current Major version of GCM library. +FOUNDATION_EXPORT int FIRInstanceIDCurrentLibraryVersionMajor(void); +/// Returns the current Minor version of GCM library. +FOUNDATION_EXPORT int FIRInstanceIDCurrentLibraryVersionMinor(void); +/// Returns the current Patch version of GCM library. +FOUNDATION_EXPORT int FIRInstanceIDCurrentLibraryVersionPatch(void); +/// Returns YES if current library version is `beta` else NO. +FOUNDATION_EXPORT BOOL FIRInstanceIDCurrentLibraryVersionIsBeta(void); diff --git a/Firebase/InstanceID/FIRInstanceIDVersionUtilities.m b/Firebase/InstanceID/FIRInstanceIDVersionUtilities.m new file mode 100644 index 00000000000..d6b0945f683 --- /dev/null +++ b/Firebase/InstanceID/FIRInstanceIDVersionUtilities.m @@ -0,0 +1,91 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceIDVersionUtilities.h" + +#import "FIRInstanceIDDefines.h" + +// Convert the macro to a string +#define STR(x) STR_EXPAND(x) +#define STR_EXPAND(x) #x + +static NSString *const kSemanticVersioningSeparator = @"."; +static NSString *const kBetaVersionPrefix = @"-beta"; + +static NSString *libraryVersion; + +static int majorVersion; +static int minorVersion; +static int patchVersion; +static int betaVersion; + +void FIRInstanceIDParseCurrentLibraryVersion() { + static NSArray *allVersions; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableString *daylightVersion = + [NSMutableString stringWithUTF8String:STR(FIRInstanceID_LIB_VERSION)]; + // Parse versions + // major, minor, patch[-beta#] + allVersions = [daylightVersion componentsSeparatedByString:kSemanticVersioningSeparator]; + _FIRInstanceIDDevAssert(allVersions.count == 3, @"Invalid versioning of FIRInstanceID library"); + if (allVersions.count == 3) { + majorVersion = [allVersions[0] intValue]; + minorVersion = [allVersions[1] intValue]; + + // Parse patch and beta versions + NSArray *patchAndBetaVersion = + [allVersions[2] componentsSeparatedByString:kBetaVersionPrefix]; + _FIRInstanceIDDevAssert(patchAndBetaVersion.count <= 2, + @"Invalid versioning of FIRInstanceID library"); + + if (patchAndBetaVersion.count == 2) { + patchVersion = [patchAndBetaVersion[0] intValue]; + betaVersion = [patchAndBetaVersion[1] intValue]; + } else if (patchAndBetaVersion.count == 1) { + patchVersion = [patchAndBetaVersion[0] intValue]; + } + } + + // Copy library version + libraryVersion = [daylightVersion copy]; + }); +} + +NSString *FIRInstanceIDCurrentLibraryVersion() { + FIRInstanceIDParseCurrentLibraryVersion(); + return libraryVersion; +} + +int FIRInstanceIDCurrentLibraryVersionMajor() { + FIRInstanceIDParseCurrentLibraryVersion(); + return majorVersion; +} + +int FIRInstanceIDCurrentLibraryVersionMinor() { + FIRInstanceIDParseCurrentLibraryVersion(); + return minorVersion; +} + +int FIRInstanceIDCurrentLibraryVersionPatch() { + FIRInstanceIDParseCurrentLibraryVersion(); + return patchVersion; +} + +BOOL FIRInstanceIDCurrentLibraryVersionIsBeta() { + FIRInstanceIDParseCurrentLibraryVersion(); + return betaVersion > 0; +} diff --git a/Firebase/InstanceID/NSError+FIRInstanceID.h b/Firebase/InstanceID/NSError+FIRInstanceID.h new file mode 100644 index 00000000000..b533dc4a99c --- /dev/null +++ b/Firebase/InstanceID/NSError+FIRInstanceID.h @@ -0,0 +1,70 @@ +/* + * Copyright 2019 Google + * + * 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_EXPORT NSString *const kFIRInstanceIDDomain; + +typedef NS_ENUM(NSUInteger, FIRInstanceIDErrorCode) { + // Unknown error. + kFIRInstanceIDErrorCodeUnknown = 0, + + // Http related errors. + kFIRInstanceIDErrorCodeAuthentication = 1, + kFIRInstanceIDErrorCodeNoAccess = 2, + kFIRInstanceIDErrorCodeTimeout = 3, + kFIRInstanceIDErrorCodeNetwork = 4, + + // Another operation is in progress. + kFIRInstanceIDErrorCodeOperationInProgress = 5, + + // Failed to perform device check in. + kFIRInstanceIDErrorCodeRegistrarFailedToCheckIn = 6, + + kFIRInstanceIDErrorCodeInvalidRequest = 7, + + // InstanceID generic errors + kFIRInstanceIDErrorCodeMissingDeviceID = 501, + + // InstanceID Token specific errors + kFIRInstanceIDErrorCodeMissingAPNSToken = 1001, + kFIRInstanceIDErrorCodeMissingAPNSServerType = 1002, + kFIRInstanceIDErrorCodeInvalidAuthorizedEntity = 1003, + kFIRInstanceIDErrorCodeInvalidScope = 1004, + kFIRInstanceIDErrorCodeInvalidStart = 1005, + kFIRInstanceIDErrorCodeInvalidKeyPair = 1006, + + // InstanceID Identity specific errors + // Generic InstanceID keypair error + kFIRInstanceIDErrorCodeMissingKeyPair = 2001, + kFIRInstanceIDErrorCodeInvalidKeyPairTags = 2002, + kFIRInstanceIDErrorCodeInvalidKeyPairCreationTime = 2005, + kFIRInstanceIDErrorCodeInvalidIdentity = 2006, + +}; + +@interface NSError (FIRInstanceID) + +@property(nonatomic, readonly) FIRInstanceIDErrorCode instanceIDErrorCode; + ++ (NSError *)errorWithFIRInstanceIDErrorCode:(FIRInstanceIDErrorCode)errorCode; + ++ (NSError *)errorWithFIRInstanceIDErrorCode:(FIRInstanceIDErrorCode)errorCode + userInfo:(NSDictionary *)userInfo; + ++ (NSError *)FIRInstanceIDErrorMissingCheckin; + +@end diff --git a/Firebase/InstanceID/NSError+FIRInstanceID.m b/Firebase/InstanceID/NSError+FIRInstanceID.m new file mode 100644 index 00000000000..560a5df0ec0 --- /dev/null +++ b/Firebase/InstanceID/NSError+FIRInstanceID.m @@ -0,0 +1,44 @@ +/* + * Copyright 2019 Google + * + * 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 "NSError+FIRInstanceID.h" + +NSString *const kFIRInstanceIDDomain = @"com.firebase.iid"; + +@implementation NSError (FIRInstanceID) + +- (FIRInstanceIDErrorCode)instanceIDErrorCode { + return (FIRInstanceIDErrorCode)self.code; +} + ++ (NSError *)errorWithFIRInstanceIDErrorCode:(FIRInstanceIDErrorCode)errorCode { + return [NSError errorWithFIRInstanceIDErrorCode:errorCode userInfo:nil]; +} + ++ (NSError *)errorWithFIRInstanceIDErrorCode:(FIRInstanceIDErrorCode)errorCode + userInfo:(NSDictionary *)userInfo { + return [NSError errorWithDomain:kFIRInstanceIDDomain code:errorCode userInfo:userInfo]; +} + ++ (NSError *)FIRInstanceIDErrorMissingCheckin { + NSDictionary *userInfo = @{@"msg" : @"Missing device credentials. Retry later."}; + + return [NSError errorWithDomain:kFIRInstanceIDDomain + code:kFIRInstanceIDErrorCodeMissingDeviceID + userInfo:userInfo]; +} + +@end diff --git a/Firebase/InstanceID/Public/FIRInstanceID.h b/Firebase/InstanceID/Public/FIRInstanceID.h new file mode 100644 index 00000000000..d95995acfa3 --- /dev/null +++ b/Firebase/InstanceID/Public/FIRInstanceID.h @@ -0,0 +1,320 @@ +/* + * Copyright 2019 Google + * + * 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 + +@class FIRInstanceIDResult; +/** + * @memberof FIRInstanceID + * + * The scope to be used when fetching/deleting a token for Firebase Messaging. + */ +FOUNDATION_EXPORT NSString *const kFIRInstanceIDScopeFirebaseMessaging + NS_SWIFT_NAME(InstanceIDScopeFirebaseMessaging); + +#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 +/** + * Called when the system determines that tokens need to be refreshed. + * This method is also called if Instance ID has been reset in which + * case, tokens and FCM topic subscriptions also need to be refreshed. + * + * Instance ID service will throttle the refresh event across all devices + * to control the rate of token updates on application servers. + */ +FOUNDATION_EXPORT const NSNotificationName kFIRInstanceIDTokenRefreshNotification + NS_SWIFT_NAME(InstanceIDTokenRefresh); +#else +/** + * Called when the system determines that tokens need to be refreshed. + * This method is also called if Instance ID has been reset in which + * case, tokens and FCM topic subscriptions also need to be refreshed. + * + * Instance ID service will throttle the refresh event across all devices + * to control the rate of token updates on application servers. + */ +FOUNDATION_EXPORT NSString *const kFIRInstanceIDTokenRefreshNotification + NS_SWIFT_NAME(InstanceIDTokenRefreshNotification); +#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + +/** + * @related FIRInstanceID + * + * The completion handler invoked when the InstanceID token returns. If + * the call fails we return the appropriate `error code` as described below. + * + * @param token The valid token as returned by InstanceID backend. + * + * @param error The error describing why generating a new token + * failed. See the error codes below for a more detailed + * description. + */ +typedef void (^FIRInstanceIDTokenHandler)(NSString *__nullable token, NSError *__nullable error) + NS_SWIFT_NAME(InstanceIDTokenHandler); + +/** + * @related FIRInstanceID + * + * The completion handler invoked when the InstanceID `deleteToken` returns. If + * the call fails we return the appropriate `error code` as described below + * + * @param error The error describing why deleting the token failed. + * See the error codes below for a more detailed description. + */ +typedef void (^FIRInstanceIDDeleteTokenHandler)(NSError *error) + NS_SWIFT_NAME(InstanceIDDeleteTokenHandler); + +/** + * @related FIRInstanceID + * + * The completion handler invoked when the app identity is created. If the + * identity wasn't created for some reason we return the appropriate error code. + * + * @param identity A valid identity for the app instance, nil if there was an error + * while creating an identity. + * @param error The error if fetching the identity fails else nil. + */ +typedef void (^FIRInstanceIDHandler)(NSString *__nullable identity, NSError *__nullable error) + NS_SWIFT_NAME(InstanceIDHandler); + +/** + * @related FIRInstanceID + * + * The completion handler invoked when the app identity and all the tokens associated + * with it are deleted. Returns a valid error object in case of failure else nil. + * + * @param error The error if deleting the identity and all the tokens associated with + * it fails else nil. + */ +typedef void (^FIRInstanceIDDeleteHandler)(NSError *__nullable error) + NS_SWIFT_NAME(InstanceIDDeleteHandler); + +/** + * @related FIRInstanceID + * + * The completion handler invoked when the app identity and token are fetched. If the + * identity wasn't created for some reason we return the appropriate error code. + * + * @param result The result containing an identity for the app instance and a valid token, + * nil if there was an error while creating the result. + * @param error The error if fetching the identity or token fails else nil. + */ +typedef void (^FIRInstanceIDResultHandler)(FIRInstanceIDResult *__nullable result, + NSError *__nullable error) + NS_SWIFT_NAME(InstanceIDResultHandler); + +/** + * Public errors produced by InstanceID. + */ +typedef NS_ENUM(NSUInteger, FIRInstanceIDError) { + // Http related errors. + + /// Unknown error. + FIRInstanceIDErrorUnknown = 0, + + /// Auth Error -- GCM couldn't validate request from this client. + FIRInstanceIDErrorAuthentication = 1, + + /// NoAccess -- InstanceID service cannot be accessed. + FIRInstanceIDErrorNoAccess = 2, + + /// Timeout -- Request to InstanceID backend timed out. + FIRInstanceIDErrorTimeout = 3, + + /// Network -- No network available to reach the servers. + FIRInstanceIDErrorNetwork = 4, + + /// OperationInProgress -- Another similar operation in progress, + /// bailing this one. + FIRInstanceIDErrorOperationInProgress = 5, + + /// InvalidRequest -- Some parameters of the request were invalid. + FIRInstanceIDErrorInvalidRequest = 7, +} NS_SWIFT_NAME(InstanceIDError); + +/** + * A class contains the results of InstanceID and token query. + */ +NS_SWIFT_NAME(InstanceIDResult) +@interface FIRInstanceIDResult : NSObject + +/** + * An instanceID uniquely identifies the app instance. + */ +@property(nonatomic, readonly, copy) NSString *instanceID; + +/* + * Returns a Firebase Messaging scoped token for the firebase app. + */ +@property(nonatomic, readonly, copy) NSString *token; + +@end + +/** + * Instance ID provides a unique identifier for each app instance and a mechanism + * to authenticate and authorize actions (for example, sending an FCM message). + * + * Once an InstanceID is generated, the library periodically sends information about the + * application and the device where it's running to the Firebase backend. To stop this. see + * `[FIRInstanceID deleteIDWithHandler:]`. + * + * Instance ID is long lived but, may be reset if the device is not used for + * a long time or the Instance ID service detects a problem. + * If Instance ID is reset, the app will be notified via + * `kFIRInstanceIDTokenRefreshNotification`. + * + * If the Instance ID has become invalid, the app can request a new one and + * send it to the app server. + * To prove ownership of Instance ID and to allow servers to access data or + * services associated with the app, call + * `[FIRInstanceID tokenWithAuthorizedEntity:scope:options:handler]`. + */ +NS_SWIFT_NAME(InstanceID) +@interface FIRInstanceID : NSObject + +/** + * FIRInstanceID. + * + * @return A shared instance of FIRInstanceID. + */ ++ (instancetype)instanceID NS_SWIFT_NAME(instanceID()); + +/** + * Unavailable. Use +instanceID instead. + */ +- (instancetype)init __attribute__((unavailable("Use +instanceID instead."))); + +#pragma mark - Tokens + +/** + * Returns a result of app instance identifier InstanceID and a Firebase Messaging scoped token. + * param handler The callback handler invoked when an app instanceID and a default token + * are generated and returned. If instanceID and token fetching fail for some + * reason the callback is invoked with nil `result` and the appropriate error. + */ +- (void)instanceIDWithHandler:(FIRInstanceIDResultHandler)handler; + +/** + * Returns a Firebase Messaging scoped token for the firebase app. + * + * @return Returns the stored token if the device has registered with Firebase Messaging, otherwise + * returns nil. + */ +- (nullable NSString *)token __deprecated_msg("Use instanceIDWithHandler: instead."); + +/** + * Returns a token that authorizes an Entity (example: cloud service) to perform + * an action on behalf of the application identified by Instance ID. + * + * This is similar to an OAuth2 token except, it applies to the + * application instance instead of a user. + * + * This is an asynchronous call. If the token fetching fails for some reason + * we invoke the completion callback with nil `token` and the appropriate + * error. + * + * This generates an Instance ID if it does not exist yet, which starts periodically sending + * information to the Firebase backend (see `[FIRInstanceID getIDWithHandler:]`). + * + * Note, you can only have one `token` or `deleteToken` call for a given + * authorizedEntity and scope at any point of time. Making another such call with the + * same authorizedEntity and scope before the last one finishes will result in an + * error with code `OperationInProgress`. + * + * @see FIRInstanceID deleteTokenWithAuthorizedEntity:scope:handler: + * + * @param authorizedEntity Entity authorized by the token. + * @param scope Action authorized for authorizedEntity. + * @param options The extra options to be sent with your token request. The + * value for the `apns_token` should be the NSData object + * passed to the UIApplicationDelegate's + * `didRegisterForRemoteNotificationsWithDeviceToken` method. + * The value for `apns_sandbox` should be a boolean (or an + * NSNumber representing a BOOL in Objective C) set to true if + * your app is a debug build, which means that the APNs + * device token is for the sandbox environment. It should be + * set to false otherwise. If the `apns_sandbox` key is not + * provided, an automatically-detected value shall be used. + * @param handler The callback handler which is invoked when the token is + * successfully fetched. In case of success a valid `token` and + * `nil` error are returned. In case of any error the `token` + * is nil and a valid `error` is returned. The valid error + * codes have been documented above. + */ +- (void)tokenWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + options:(nullable NSDictionary *)options + handler:(FIRInstanceIDTokenHandler)handler; + +/** + * Revokes access to a scope (action) for an entity previously + * authorized by `[FIRInstanceID tokenWithAuthorizedEntity:scope:options:handler]`. + * + * This is an asynchronous call. Call this on the main thread since InstanceID lib + * is not thread safe. In case token deletion fails for some reason we invoke the + * `handler` callback passed in with the appropriate error code. + * + * Note, you can only have one `token` or `deleteToken` call for a given + * authorizedEntity and scope at a point of time. Making another such call with the + * same authorizedEntity and scope before the last one finishes will result in an error + * with code `OperationInProgress`. + * + * @param authorizedEntity Entity that must no longer have access. + * @param scope Action that entity is no longer authorized to perform. + * @param handler The handler that is invoked once the unsubscribe call ends. + * In case of error an appropriate error object is returned + * else error is nil. + */ +- (void)deleteTokenWithAuthorizedEntity:(NSString *)authorizedEntity + scope:(NSString *)scope + handler:(FIRInstanceIDDeleteTokenHandler)handler; + +#pragma mark - Identity + +/** + * Asynchronously fetch a stable identifier that uniquely identifies the app + * instance. If the identifier has been revoked or has expired, this method will + * return a new identifier. + * + * Once an InstanceID is generated, the library periodically sends information about the + * application and the device where it's running to the Firebase backend. To stop this. see + * `[FIRInstanceID deleteIDWithHandler:]`. + * + * @param handler The handler to invoke once the identifier has been fetched. + * In case of error an appropriate error object is returned else + * a valid identifier is returned and a valid identifier for the + * application instance. + */ +- (void)getIDWithHandler:(FIRInstanceIDHandler)handler NS_SWIFT_NAME(getID(handler:)); + +/** + * Resets Instance ID and revokes all tokens. + * + * This method also triggers a request to fetch a new Instance ID and Firebase Messaging scope + * token. Please listen to kFIRInstanceIDTokenRefreshNotification when the new ID and token are + * ready. + * + * This stops the periodic sending of data to the Firebase backend that began when the Instance ID + * was generated. No more data is sent until another library calls Instance ID internally again + * (like FCM, RemoteConfig or Analytics) or user explicitly calls Instance ID APIs to get an + * Instance ID and token again. + */ +- (void)deleteIDWithHandler:(FIRInstanceIDDeleteHandler)handler NS_SWIFT_NAME(deleteID(handler:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/InstanceID/Public/FirebaseInstanceID.h b/Firebase/InstanceID/Public/FirebaseInstanceID.h new file mode 100644 index 00000000000..78c9ef1618c --- /dev/null +++ b/Firebase/InstanceID/Public/FirebaseInstanceID.h @@ -0,0 +1,17 @@ +/* + * Copyright 2019 Google + * + * 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 "FIRInstanceID.h" diff --git a/Firebase/Messaging/CHANGELOG.md b/Firebase/Messaging/CHANGELOG.md index a154c32160f..be8cbc927d3 100644 --- a/Firebase/Messaging/CHANGELOG.md +++ b/Firebase/Messaging/CHANGELOG.md @@ -1,3 +1,12 @@ +# 2019-03-19 -- v3.4.0 +- Adding community support for tvOS. (#2428) + +# 2019-03-05 -- v3.3.2 +- Replaced `NSUserDefaults` with `GULUserDefaults` to avoid potential crashes. (#2443) + +# 2019-02-20 -- v3.3.1 +- Internal code cleanup. + # 2019-01-22 -- v3.3.0 - Use the new registerInternalLibrary API to register with FirebaseCore. (#2137) diff --git a/Firebase/Messaging/FIRMessaging.m b/Firebase/Messaging/FIRMessaging.m index 7aa439191ed..d941014169a 100644 --- a/Firebase/Messaging/FIRMessaging.m +++ b/Firebase/Messaging/FIRMessaging.m @@ -47,6 +47,7 @@ #import #import #import +#import #import "NSError+FIRMessaging.h" @@ -141,7 +142,7 @@ @interface FIRMessaging () @implementation FIRMessaging -// File static to support InstanceID tests that call [FIRMessaging messaging] after -// [FIRMessaging messagingForTests]. -static FIRMessaging *sMessaging; - + (FIRMessaging *)messaging { - if (sMessaging != nil) { - return sMessaging; - } FIRApp *defaultApp = [FIRApp defaultApp]; // Missing configure will be logged here. - id messaging = + id instance = FIR_COMPONENT(FIRMessagingInstanceProvider, defaultApp.container); + // We know the instance coming from the container is a FIRMessaging instance, cast it and move on. + FIRMessaging *messaging = (FIRMessaging *)instance; + static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - [(FIRMessaging *)messaging start]; + [messaging start]; }); - sMessaging = (FIRMessaging *)messaging; - return sMessaging; -} - -+ (FIRMessaging *)messagingForTests { - sMessaging = [[FIRMessaging alloc] initWithAnalytics:nil - withInstanceID:[FIRInstanceID instanceID] - withUserDefaults:[NSUserDefaults standardUserDefaults]]; - [sMessaging start]; - return sMessaging; + return messaging; } - (instancetype)initWithAnalytics:(nullable id)analytics withInstanceID:(FIRInstanceID *)instanceID - withUserDefaults:(NSUserDefaults *)defaults { + withUserDefaults:(GULUserDefaults *)defaults { self = [super init]; if (self != nil) { _loggedMessageIDs = [NSMutableSet set]; @@ -227,7 +215,7 @@ + (void)load { id analytics = FIR_COMPONENT(FIRAnalyticsInterop, container); return [[FIRMessaging alloc] initWithAnalytics:analytics withInstanceID:[FIRInstanceID instanceID] - withUserDefaults:[NSUserDefaults standardUserDefaults]]; + withUserDefaults:[GULUserDefaults standardUserDefaults]]; }; FIRComponent *messagingProvider = [FIRComponent componentWithProtocol:@protocol(FIRMessagingInstanceProvider) @@ -279,7 +267,7 @@ - (void)start { withHost:hostname]; [self.reachability start]; - [self setupApplicationSupportSubDirectory]; + [self setupFileManagerSubDirectory]; // setup FIRMessaging objects [self setupRmqManager]; [self setupClient]; @@ -291,10 +279,9 @@ - (void)start { [self setupNotificationListeners]; } -- (void)setupApplicationSupportSubDirectory { - NSString *messagingSubDirectory = kFIRMessagingApplicationSupportSubDirectory; - if (![[self class] hasApplicationSupportSubDirectory:messagingSubDirectory]) { - [[self class] createApplicationSupportSubDirectory:messagingSubDirectory]; +- (void)setupFileManagerSubDirectory { + if (![[self class] hasSubDirectory:kFIRMessagingSubDirectoryName]) { + [[self class] createSubDirectory:kFIRMessagingSubDirectoryName]; } } @@ -322,7 +309,7 @@ - (void)setupNotificationListeners { } - (void)setupReceiver { - self.receiver = [[FIRMessagingReceiver alloc] init]; + self.receiver = [[FIRMessagingReceiver alloc] initWithUserDefaults:self.messagingUserDefaults]; self.receiver.delegate = self; } @@ -479,15 +466,22 @@ - (void)handleIncomingLinkIfNeededFromMessage:(NSDictionary *)message { // Similarly, |application:openURL:sourceApplication:annotation:| will also always be called, due // to the default swizzling done by FIRAAppDelegateProxy in Firebase Analytics } else if ([appDelegate respondsToSelector:openURLWithSourceApplicationSelector]) { +#if TARGET_OS_IOS #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [appDelegate application:application openURL:url sourceApplication:FIRMessagingAppIdentifier() annotation:@{}]; +#pragma clang diagnostic pop +#endif } else if ([appDelegate respondsToSelector:handleOpenURLSelector]) { +#if TARGET_OS_IOS +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [appDelegate application:application handleOpenURL:url]; #pragma clang diagnostic pop +#endif } } @@ -966,8 +960,8 @@ - (void)defaultInstanceIDTokenWasRefreshed:(NSNotification *)notification { #pragma mark - Application Support Directory -+ (BOOL)hasApplicationSupportSubDirectory:(NSString *)subDirectoryName { - NSString *subDirectoryPath = [self pathForApplicationSupportSubDirectory:subDirectoryName]; ++ (BOOL)hasSubDirectory:(NSString *)subDirectoryName { + NSString *subDirectoryPath = [self pathForSubDirectory:subDirectoryName]; BOOL isDirectory; if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath isDirectory:&isDirectory]) { @@ -978,16 +972,16 @@ + (BOOL)hasApplicationSupportSubDirectory:(NSString *)subDirectoryName { return YES; } -+ (NSString *)pathForApplicationSupportSubDirectory:(NSString *)subDirectoryName { - NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, ++ (NSString *)pathForSubDirectory:(NSString *)subDirectoryName { + NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains(FIRMessagingSupportedDirectory(), NSUserDomainMask, YES); - NSString *applicationSupportDirPath = directoryPaths.lastObject; - NSArray *components = @[applicationSupportDirPath, subDirectoryName]; + NSString *dirPath = directoryPaths.lastObject; + NSArray *components = @[dirPath, subDirectoryName]; return [NSString pathWithComponents:components]; } -+ (BOOL)createApplicationSupportSubDirectory:(NSString *)subDirectoryName { - NSString *subDirectoryPath = [self pathForApplicationSupportSubDirectory:subDirectoryName]; ++ (BOOL)createSubDirectory:(NSString *)subDirectoryName { + NSString *subDirectoryPath = [self pathForSubDirectory:subDirectoryName]; BOOL hasSubDirectory; if (![[NSFileManager defaultManager] fileExistsAtPath:subDirectoryPath diff --git a/Firebase/Messaging/FIRMessagingConstants.h b/Firebase/Messaging/FIRMessagingConstants.h index ad0d6c90e4c..9a3ce04c863 100644 --- a/Firebase/Messaging/FIRMessagingConstants.h +++ b/Firebase/Messaging/FIRMessagingConstants.h @@ -44,7 +44,7 @@ FOUNDATION_EXPORT NSString *const kFIRMessagingMessageLinkKey; FOUNDATION_EXPORT NSString *const kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey; -FOUNDATION_EXPORT NSString *const kFIRMessagingApplicationSupportSubDirectory; +FOUNDATION_EXPORT NSString *const kFIRMessagingSubDirectoryName; // Notifications FOUNDATION_EXPORT NSString *const kFIRMessagingCheckinFetchedNotification; diff --git a/Firebase/Messaging/FIRMessagingConstants.m b/Firebase/Messaging/FIRMessagingConstants.m index 8904cc59d01..e50c923647b 100644 --- a/Firebase/Messaging/FIRMessagingConstants.m +++ b/Firebase/Messaging/FIRMessagingConstants.m @@ -38,7 +38,7 @@ NSString *const kFIRMessagingRemoteNotificationsProxyEnabledInfoPlistKey = @"FirebaseAppDelegateProxyEnabled"; -NSString *const kFIRMessagingApplicationSupportSubDirectory = @"Google/FirebaseMessaging"; +NSString *const kFIRMessagingSubDirectoryName = @"Google/FirebaseMessaging"; // Notifications NSString *const kFIRMessagingCheckinFetchedNotification = @"com.google.gcm.notif-checkin-fetched"; diff --git a/Firebase/Messaging/FIRMessagingContextManagerService.m b/Firebase/Messaging/FIRMessagingContextManagerService.m index 8527db1fb19..d177014edf7 100644 --- a/Firebase/Messaging/FIRMessagingContextManagerService.m +++ b/Firebase/Messaging/FIRMessagingContextManagerService.m @@ -130,6 +130,7 @@ + (BOOL)handleContextManagerLocalTimeMessage:(NSDictionary *)message { + (void)scheduleLocalNotificationForMessage:(NSDictionary *)message atDate:(NSDate *)date { +#if TARGET_OS_IOS NSDictionary *apsDictionary = message; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -182,6 +183,7 @@ + (void)scheduleLocalNotificationForMessage:(NSDictionary *)message } [application scheduleLocalNotification:notification]; #pragma clang diagnostic pop +#endif } + (NSDictionary *)parseDataFromMessage:(NSDictionary *)message { diff --git a/Firebase/Messaging/FIRMessagingPubSub.m b/Firebase/Messaging/FIRMessagingPubSub.m index 3f954e8ab5e..0fad4a04d85 100644 --- a/Firebase/Messaging/FIRMessagingPubSub.m +++ b/Firebase/Messaging/FIRMessagingPubSub.m @@ -16,6 +16,8 @@ #import "FIRMessagingPubSub.h" +#import + #import "FIRMessaging.h" #import "FIRMessagingClient.h" #import "FIRMessagingDefines.h" @@ -186,14 +188,14 @@ - (BOOL)pendingTopicsListCanRequestTopicUpdates:(FIRMessagingPendingTopicsList * #pragma mark - Storing Pending Topics - (void)archivePendingTopicsList:(FIRMessagingPendingTopicsList *)topicsList { - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + GULUserDefaults *defaults = [GULUserDefaults standardUserDefaults]; NSData *pendingData = [NSKeyedArchiver archivedDataWithRootObject:topicsList]; [defaults setObject:pendingData forKey:kPendingSubscriptionsListKey]; [defaults synchronize]; } - (void)restorePendingTopicsList { - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + GULUserDefaults *defaults = [GULUserDefaults standardUserDefaults]; NSData *pendingData = [defaults objectForKey:kPendingSubscriptionsListKey]; FIRMessagingPendingTopicsList *subscriptions; @try { diff --git a/Firebase/Messaging/FIRMessagingReceiver.h b/Firebase/Messaging/FIRMessagingReceiver.h index e312420449f..416ee3b6d69 100644 --- a/Firebase/Messaging/FIRMessagingReceiver.h +++ b/Firebase/Messaging/FIRMessagingReceiver.h @@ -17,18 +17,29 @@ #import "FIRMessagingDataMessageManager.h" #import "FIRMessaging.h" +NS_ASSUME_NONNULL_BEGIN + @class FIRMessagingReceiver; +@class GULUserDefaults; @protocol FIRMessagingReceiverDelegate -- (void)receiver:(nonnull FIRMessagingReceiver *)receiver - receivedRemoteMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage; +- (void)receiver:(FIRMessagingReceiver *)receiver + receivedRemoteMessage:(FIRMessagingRemoteMessage *)remoteMessage; @end @interface FIRMessagingReceiver : NSObject +/// Default initializer for creating the messaging receiver. +- (instancetype)initWithUserDefaults:(GULUserDefaults *)defaults NS_DESIGNATED_INITIALIZER; + +/// Use `initWithUserDefaults:` instead. +- (instancetype)init NS_UNAVAILABLE; + @property(nonatomic, weak, nullable) id delegate; /// Whether to use direct channel for direct channel message callback handler in all iOS versions. @property(nonatomic, assign) BOOL useDirectChannel; @end + +NS_ASSUME_NONNULL_END diff --git a/Firebase/Messaging/FIRMessagingReceiver.m b/Firebase/Messaging/FIRMessagingReceiver.m index ac8a6837561..0d947c5f082 100644 --- a/Firebase/Messaging/FIRMessagingReceiver.m +++ b/Firebase/Messaging/FIRMessagingReceiver.m @@ -19,6 +19,7 @@ #import #import +#import #import "FIRMessaging.h" #import "FIRMessagingLogger.h" @@ -36,8 +37,22 @@ static int downstreamMessageID = 0; +@interface FIRMessagingReceiver () +@property(nonatomic, strong) GULUserDefaults *defaults; +@end + @implementation FIRMessagingReceiver +#pragma mark - Initializer + +- (instancetype)initWithUserDefaults:(GULUserDefaults *)defaults { + self = [super init]; + if (self != nil) { + _defaults = defaults; + } + return self; +} + #pragma mark - FIRMessagingDataMessageManager protocol - (void)didReceiveMessage:(NSDictionary *)message withIdentifier:(nullable NSString *)messageID { @@ -152,9 +167,8 @@ + (NSString *)nextMessageID { - (BOOL)useDirectChannel { // Check storage - NSUserDefaults *messagingDefaults = [NSUserDefaults standardUserDefaults]; id shouldUseMessagingDelegate = - [messagingDefaults objectForKey:kFIRMessagingUserDefaultsKeyUseMessagingDelegate]; + [_defaults objectForKey:kFIRMessagingUserDefaultsKeyUseMessagingDelegate]; if (shouldUseMessagingDelegate) { return [shouldUseMessagingDelegate boolValue]; } @@ -170,12 +184,10 @@ - (BOOL)useDirectChannel { } - (void)setUseDirectChannel:(BOOL)useDirectChannel { - NSUserDefaults *messagingDefaults = [NSUserDefaults standardUserDefaults]; BOOL shouldUseMessagingDelegate = [self useDirectChannel]; if (useDirectChannel != shouldUseMessagingDelegate) { - [messagingDefaults setBool:useDirectChannel - forKey:kFIRMessagingUserDefaultsKeyUseMessagingDelegate]; - [messagingDefaults synchronize]; + [_defaults setBool:useDirectChannel forKey:kFIRMessagingUserDefaultsKeyUseMessagingDelegate]; + [_defaults synchronize]; } } diff --git a/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m b/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m index dc6e6c9c9dd..a14db5892a6 100644 --- a/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m +++ b/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m @@ -126,12 +126,16 @@ - (instancetype)initWithDatabaseName:(NSString *)databaseName { self = [super init]; if (self) { _databaseName = [databaseName copy]; +#if TARGET_OS_iOS BOOL didMoveToApplicationSupport = - [self moveToApplicationSupportSubDirectory:kFIRMessagingApplicationSupportSubDirectory]; + [self moveToApplicationSupportSubDirectory:kFIRMessagingSubDirectoryName]; _currentDirectory = didMoveToApplicationSupport ? FIRMessagingRmqDirectoryApplicationSupport : FIRMessagingRmqDirectoryDocuments; +#else + _currentDirectory = FIRMessagingRmqDirectoryApplicationSupport; +#endif [self openDatabase:_databaseName]; } @@ -143,7 +147,7 @@ - (void)dealloc { } - (BOOL)moveToApplicationSupportSubDirectory:(NSString *)subDirectoryName { - NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, + NSArray *directoryPaths = NSSearchPathForDirectoriesInDomains(FIRMessagingSupportedDirectory(), NSUserDomainMask, YES); NSString *applicationSupportDirPath = directoryPaths.lastObject; NSArray *components = @[applicationSupportDirPath, subDirectoryName]; @@ -205,12 +209,12 @@ + (NSString *)pathForDatabase:(NSString *)dbName inDirectory:(FIRMessagingRmqDir break; case FIRMessagingRmqDirectoryApplicationSupport: - paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, + paths = NSSearchPathForDirectoriesInDomains(FIRMessagingSupportedDirectory(), NSUserDomainMask, YES); components = @[ paths.lastObject, - kFIRMessagingApplicationSupportSubDirectory, + kFIRMessagingSubDirectoryName, dbNameWithExtension ]; break; @@ -262,10 +266,10 @@ - (void)removeDatabase { + (void)removeDatabase:(NSString *)dbName { NSString *documentsDirPath = [self pathForDatabase:dbName inDirectory:FIRMessagingRmqDirectoryDocuments]; - NSString *applicationSupportDirPath = + NSString *standardDirPath = [self pathForDatabase:dbName inDirectory:FIRMessagingRmqDirectoryApplicationSupport]; [[NSFileManager defaultManager] removeItemAtPath:documentsDirPath error:nil]; - [[NSFileManager defaultManager] removeItemAtPath:applicationSupportDirPath error:nil]; + [[NSFileManager defaultManager] removeItemAtPath:standardDirPath error:nil]; } - (void)openDatabase:(NSString *)dbName { diff --git a/Firebase/Messaging/FIRMessagingUtilities.h b/Firebase/Messaging/FIRMessagingUtilities.h index 206ff073f7e..58475bf8877 100644 --- a/Firebase/Messaging/FIRMessagingUtilities.h +++ b/Firebase/Messaging/FIRMessagingUtilities.h @@ -55,4 +55,6 @@ FOUNDATION_EXPORT NSString *FIRMessagingAppIdentifier(void); FOUNDATION_EXPORT uint64_t FIRMessagingGetFreeDiskSpaceInMB(void); FOUNDATION_EXPORT UIApplication *FIRMessagingUIApplication(void); +FOUNDATION_EXPORT NSSearchPathDirectory FIRMessagingSupportedDirectory(void); + diff --git a/Firebase/Messaging/FIRMessagingUtilities.m b/Firebase/Messaging/FIRMessagingUtilities.m index fa3a2334b05..743caed37a2 100644 --- a/Firebase/Messaging/FIRMessagingUtilities.m +++ b/Firebase/Messaging/FIRMessagingUtilities.m @@ -186,3 +186,11 @@ uint64_t FIRMessagingGetFreeDiskSpaceInMB(void) { } return [applicationClass sharedApplication]; } + +NSSearchPathDirectory FIRMessagingSupportedDirectory(void) { +#if TARGET_OS_TV + return NSCachesDirectory; +#else + return NSApplicationSupportDirectory; +#endif +} diff --git a/Firebase/Messaging/Public/FIRMessaging.h b/Firebase/Messaging/Public/FIRMessaging.h index d0ccfacf830..5b8e7ad4d2d 100644 --- a/Firebase/Messaging/Public/FIRMessaging.h +++ b/Firebase/Messaging/Public/FIRMessaging.h @@ -321,7 +321,8 @@ NS_SWIFT_NAME(Messaging) * If true, the data message sent by direct channel will be delivered via * `FIRMessagingDelegate messaging(_:didReceive:)` and across all iOS versions. */ -@property(nonatomic, assign) BOOL useMessagingDelegateForDirectChannel; +@property(nonatomic, assign) BOOL useMessagingDelegateForDirectChannel + __deprecated_msg("This is soon to be deprecated. All direct messages will by default delivered in `FIRMessagingDelegate messaging(_:didReceive:)` across all iOS versions"); /** * FIRMessaging diff --git a/Firebase/Storage/CHANGELOG.md b/Firebase/Storage/CHANGELOG.md index dc4c7b0fb4a..14f20c9bda8 100644 --- a/Firebase/Storage/CHANGELOG.md +++ b/Firebase/Storage/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [fixed] `StorageReference.putFile()` now correctly propagates error if file to upload does not exist (#2458, #2350). + # 3.0.3 - [changed] Storage operations can now be scheduled and controlled from any thread (#1302, #1388). - [fixed] Fixed an issue that prevented uploading of files whose names include semicolons. diff --git a/Firebase/Storage/FIRStorageUploadTask.m b/Firebase/Storage/FIRStorageUploadTask.m index 0324974a52f..0aba3ab431b 100644 --- a/Firebase/Storage/FIRStorageUploadTask.m +++ b/Firebase/Storage/FIRStorageUploadTask.m @@ -79,6 +79,13 @@ - (void)enqueue { return; } + NSError *contentValidationError; + if (![strongSelf isContentToUploadValid:&contentValidationError]) { + strongSelf.error = contentValidationError; + [strongSelf finishTaskWithStatus:FIRStorageTaskStatusFailure snapshot:strongSelf.snapshot]; + return; + } + strongSelf.state = FIRStorageTaskStateQueueing; NSMutableURLRequest *request = [strongSelf.baseRequest mutableCopy]; @@ -145,9 +152,8 @@ - (void)enqueue { self.state = FIRStorageTaskStateFailed; self.error = [FIRStorageErrors errorWithServerError:error reference:self.reference]; self.metadata = self->_uploadMetadata; - [self fireHandlersForStatus:FIRStorageTaskStatusFailure snapshot:self.snapshot]; - [self removeAllObservers]; - self->_fetcherCompletion = nil; + + [self finishTaskWithStatus:FIRStorageTaskStatusFailure snapshot:self.snapshot]; return; } @@ -164,9 +170,7 @@ - (void)enqueue { self.error = [FIRStorageErrors errorWithInvalidRequest:data]; } - [self fireHandlersForStatus:FIRStorageTaskStatusSuccess snapshot:self.snapshot]; - [self removeAllObservers]; - self->_fetcherCompletion = nil; + [self finishTaskWithStatus:FIRStorageTaskStatusSuccess snapshot:self.snapshot]; }; #pragma clang diagnostic pop @@ -177,6 +181,37 @@ - (void)enqueue { }]; } +- (void)finishTaskWithStatus:(FIRStorageTaskStatus)status + snapshot:(FIRStorageTaskSnapshot *)snapshot { + [self fireHandlersForStatus:status snapshot:self.snapshot]; + [self removeAllObservers]; + self->_fetcherCompletion = nil; +} + +- (BOOL)isContentToUploadValid:(NSError **)outError { + if (_uploadData != nil) { + return YES; + } + + NSError *fileReachabilityError; + if (![_fileURL checkResourceIsReachableAndReturnError:&fileReachabilityError]) { + if (outError != NULL) { + NSString *description = + [NSString stringWithFormat:@"File at URL: %@ is not reachable.", _fileURL.absoluteString]; + *outError = [NSError errorWithDomain:FIRStorageErrorDomain + code:FIRStorageErrorCodeUnknown + userInfo:@{ + NSUnderlyingErrorKey : fileReachabilityError, + NSLocalizedDescriptionKey : description + }]; + } + + return NO; + } + + return YES; +} + #pragma mark - Upload Management - (void)cancel { diff --git a/FirebaseAnalyticsInterop.podspec b/FirebaseAnalyticsInterop.podspec index c2743634ed9..bea3f9de282 100644 --- a/FirebaseAnalyticsInterop.podspec +++ b/FirebaseAnalyticsInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAnalyticsInterop' - s.version = '1.1.0' + s.version = '1.2.0' s.summary = 'Interfaces that allow other Firebase SDKs to use Analytics functionality.' s.description = <<-DESC diff --git a/FirebaseAuth.podspec b/FirebaseAuth.podspec index d40714e0ac8..9dd85a9c168 100644 --- a/FirebaseAuth.podspec +++ b/FirebaseAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAuth' - s.version = '5.3.0' + s.version = '5.4.1' s.summary = 'The official iOS client for Firebase Authentication (plus community support for macOS and tvOS)' s.description = <<-DESC diff --git a/FirebaseCore.podspec b/FirebaseCore.podspec index 5215f75b0a5..5d72a48d899 100644 --- a/FirebaseCore.podspec +++ b/FirebaseCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCore' - s.version = '5.2.0' + s.version = '5.4.0' s.summary = 'Firebase Core for iOS (plus community support for macOS and tvOS)' s.description = <<-DESC @@ -28,11 +28,12 @@ Firebase Core includes FIRApp and FIROptions which provide central configuration s.public_header_files = 'Firebase/Core/Public/*.h', 'Firebase/Core/Private/*.h' s.private_header_files = 'Firebase/Core/Private/*.h' s.framework = 'Foundation' + s.dependency 'GoogleUtilities/Environment', '~> 5.2' s.dependency 'GoogleUtilities/Logger', '~> 5.2' s.pod_target_xcconfig = { 'GCC_C_LANGUAGE_STANDARD' => 'c99', 'GCC_PREPROCESSOR_DEFINITIONS' => - 'FIRCore_VERSION=' + s.version.to_s + ' Firebase_VERSION=5.16.0', + 'FIRCore_VERSION=' + s.version.to_s + ' Firebase_VERSION=5.19.0', 'OTHER_CFLAGS' => '-fno-autolink' } end diff --git a/FirebaseDatabase.podspec b/FirebaseDatabase.podspec index bed3ef657ab..062db75bd7a 100644 --- a/FirebaseDatabase.podspec +++ b/FirebaseDatabase.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseDatabase' - s.version = '5.1.0' + s.version = '5.1.1' s.summary = 'Firebase Open Source Libraries for iOS (plus community support for macOS and tvOS)' s.description = <<-DESC diff --git a/FirebaseDynamicLinks.podspec b/FirebaseDynamicLinks.podspec index a62a7978b61..067a07e2125 100644 --- a/FirebaseDynamicLinks.podspec +++ b/FirebaseDynamicLinks.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseDynamicLinks' - s.version = '3.4.0' + s.version = '3.4.2' s.summary = 'Firebase DynamicLinks for iOS' s.description = <<-DESC diff --git a/FirebaseFirestore.podspec b/FirebaseFirestore.podspec index 76c7b99224d..644ec427ace 100644 --- a/FirebaseFirestore.podspec +++ b/FirebaseFirestore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFirestore' - s.version = '1.0.0' + s.version = '1.1.0' s.summary = 'Google Cloud Firestore for iOS' s.description = <<-DESC @@ -95,10 +95,19 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, 'Firestore/third_party/abseil-cpp/**/*.cc' ] ss.exclude_files = [ + # Exclude tests and benchmarks from the framework. 'Firestore/third_party/abseil-cpp/**/*_benchmark.cc', 'Firestore/third_party/abseil-cpp/**/*test*.cc', 'Firestore/third_party/abseil-cpp/absl/hash/internal/print_hash_of.cc', - 'Firestore/third_party/abseil-cpp/absl/synchronization/internal/mutex_nonprod.cc', + + # Avoid the debugging package which uses code that isn't portable to + # ARM (see stack_consumption.cc) and uses syscalls not available on + # tvOS (e.g. sigaltstack). + 'Firestore/third_party/abseil-cpp/absl/debugging/**/*.cc', + + # Exclude the synchronization package because it's dead weight: we don't + # write the kind of heavily threaded code that might benefit from it. + 'Firestore/third_party/abseil-cpp/absl/synchronization/**/*.cc', ] ss.library = 'c++' diff --git a/FirebaseFunctions.podspec b/FirebaseFunctions.podspec index dbc8d10e411..a708b44b5b0 100644 --- a/FirebaseFunctions.podspec +++ b/FirebaseFunctions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFunctions' - s.version = '2.2.0' + s.version = '2.4.0' s.summary = 'Cloud Functions for Firebase iOS SDK.' s.description = <<-DESC @@ -16,6 +16,8 @@ iOS SDK for Cloud Functions for Firebase. } s.ios.deployment_target = '8.0' + s.osx.deployment_target = '10.11' + s.tvos.deployment_target = '10.0' s.cocoapods_version = '>= 1.4.0' s.static_framework = true @@ -32,4 +34,14 @@ iOS SDK for Cloud Functions for Firebase. 'GCC_C_LANGUAGE_STANDARD' => 'c99', 'GCC_PREPROCESSOR_DEFINITIONS' => 'FIRFunctions_VERSION=' + s.version.to_s } + + s.test_spec 'unit' do |unit_tests| + unit_tests.source_files = 'Functions/Example/Test*/*.[mh]', 'Example/Shared/FIRAuthInteropFake*' + end + + s.test_spec 'integration' do |int_tests| + int_tests.source_files = 'Functions/Example/IntegrationTests/*.[mh]', + 'Functions/Example/TestUtils/*.[mh]', + 'Example/Shared/FIRAuthInteropFake*' + end end diff --git a/FirebaseInAppMessaging.podspec b/FirebaseInAppMessaging.podspec new file mode 100644 index 00000000000..cf2740a8bf1 --- /dev/null +++ b/FirebaseInAppMessaging.podspec @@ -0,0 +1,39 @@ +Pod::Spec.new do |s| + s.name = 'FirebaseInAppMessaging' + s.version = '0.13.0' + s.summary = 'Firebase In-App Messaging for iOS' + + s.description = <<-DESC +FirebaseInAppMessaging is the headless component of Firebase In-App Messaging on iOS client side. +See more product details at https://firebase.google.com/products/in-app-messaging/ about Firebase In-App Messaging. + DESC + + s.homepage = 'https://firebase.google.com' + s.license = { :type => 'Apache', :file => 'LICENSE' } + s.authors = 'Google, Inc.' + + s.source = { + :git => 'https://github.com/firebase/firebase-ios-sdk.git', + :tag => 'InAppMessaging-' + s.version.to_s + } + s.social_media_url = 'https://twitter.com/Firebase' + s.ios.deployment_target = '8.0' + + s.cocoapods_version = '>= 1.4.0' + s.static_framework = true + s.prefix_header_file = false + + base_dir = "Firebase/InAppMessaging/" + s.source_files = base_dir + '**/*.[mh]' + s.public_header_files = base_dir + 'Public/*.h' + + s.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => + '$(inherited) ' + + 'FIRInAppMessaging_LIB_VERSION=' + String(s.version) + } + + s.dependency 'FirebaseCore' + s.ios.dependency 'FirebaseAnalytics' + s.ios.dependency 'FirebaseAnalyticsInterop' + s.dependency 'FirebaseInstanceID' +end diff --git a/FirebaseInAppMessagingDisplay.podspec b/FirebaseInAppMessagingDisplay.podspec index 0df763d5ff4..50f5eb76725 100644 --- a/FirebaseInAppMessagingDisplay.podspec +++ b/FirebaseInAppMessagingDisplay.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseInAppMessagingDisplay' - s.version = '0.12.2' + s.version = '0.13.1' s.summary = 'Firebase In-App Messaging UI for iOS' s.description = <<-DESC diff --git a/FirebaseInstanceID.podspec b/FirebaseInstanceID.podspec new file mode 100644 index 00000000000..e9b917603d8 --- /dev/null +++ b/FirebaseInstanceID.podspec @@ -0,0 +1,41 @@ +Pod::Spec.new do |s| + s.name = 'FirebaseInstanceID' + s.version = '3.8.0' + s.summary = 'Firebase InstanceID for iOS' + + s.description = <<-DESC +Instance ID provides a unique ID per instance of your iOS apps. In addition to providing +unique IDs for authentication,Instance ID can generate security tokens for use with other +services. + DESC + + s.homepage = 'https://firebase.google.com' + s.license = { :type => 'Apache', :file => 'LICENSE' } + s.authors = 'Google, Inc.' + + s.source = { + :git => 'https://github.com/firebase/firebase-ios-sdk.git', + :tag => 'InstanceID-' + s.version.to_s + } + s.social_media_url = 'https://twitter.com/Firebase' + s.ios.deployment_target = '8.0' + s.tvos.deployment_target = '10.0' + + s.cocoapods_version = '>= 1.4.0' + s.static_framework = true + s.prefix_header_file = false + + base_dir = "Firebase/InstanceID/" + s.source_files = base_dir + '**/*.[mh]' + s.requires_arc = base_dir + '*.m' + s.public_header_files = base_dir + 'Public/*.h' + s.pod_target_xcconfig = { + 'GCC_C_LANGUAGE_STANDARD' => 'c99', + 'GCC_PREPROCESSOR_DEFINITIONS' => + 'FIRInstanceID_LIB_VERSION=' + String(s.version) + } + s.framework = 'Security' + s.dependency 'FirebaseCore', '~> 5.2' + s.dependency 'GoogleUtilities/UserDefaults', '~> 5.2' + s.dependency 'GoogleUtilities/Environment', '~> 5.2' +end diff --git a/FirebaseMessaging.podspec b/FirebaseMessaging.podspec index c03c1ce84a5..b49ff324106 100644 --- a/FirebaseMessaging.podspec +++ b/FirebaseMessaging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMessaging' - s.version = '3.3.0' + s.version = '3.4.0' s.summary = 'Firebase Messaging for iOS' s.description = <<-DESC @@ -20,6 +20,7 @@ device, and it is completely free. } s.social_media_url = 'https://twitter.com/Firebase' s.ios.deployment_target = '8.0' + s.tvos.deployment_target = '10.0' s.cocoapods_version = '>= 1.4.0' s.static_framework = true @@ -39,8 +40,9 @@ device, and it is completely free. s.framework = 'SystemConfiguration' s.dependency 'FirebaseAnalyticsInterop', '~> 1.1' s.dependency 'FirebaseCore', '~> 5.2' - s.dependency 'FirebaseInstanceID', '~> 3.4' - s.dependency 'GoogleUtilities/Reachability', '~> 5.2' - s.dependency 'GoogleUtilities/Environment', '~> 5.2' + s.dependency 'FirebaseInstanceID', '~> 3.6' + s.dependency 'GoogleUtilities/Reachability', '~> 5.3' + s.dependency 'GoogleUtilities/Environment', '~> 5.3' + s.dependency 'GoogleUtilities/UserDefaults', '~> 5.3' s.dependency 'Protobuf', '~> 3.1' end diff --git a/FirebaseStorage.podspec b/FirebaseStorage.podspec index 71a996c030f..d9a9441142c 100644 --- a/FirebaseStorage.podspec +++ b/FirebaseStorage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseStorage' - s.version = '3.1.0' + s.version = '3.1.1' s.summary = 'Firebase Storage for iOS (plus community support for macOS and tvOS)' s.description = <<-DESC diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index 3d9a0a77cc9..1ff2bf5c443 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,4 +1,21 @@ # Unreleased +- [fixed] Fixed the way gRPC certificates are loaded on macOS (#2604). + +# 1.1.0 +- [feature] Added `FieldValue.increment()`, which can be used in + `updateData(_:)` and `setData(_:merge:)` to increment or decrement numeric + field values safely without transactions. +- [changed] Improved performance when querying over documents that contain + subcollections (#2466). +- [changed] Prepared the persistence layer to support collection group queries. + While this feature is not yet available, all schema changes are included + in this release. + +# v1.0.2 +- [changed] Internal improvements. + +# v1.0.1 +- [changed] Internal improvements. # v1.0.0 - [changed] **Breaking change:** The `areTimestampsInSnapshotsEnabled` setting diff --git a/Firestore/Example/App/iOS/Images.xcassets/AppIcon.appiconset/Contents.json b/Firestore/Example/App/iOS/Images.xcassets/AppIcon.appiconset/Contents.json index d7070bc5c02..d8db8d65fd7 100644 --- a/Firestore/Example/App/iOS/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/Firestore/Example/App/iOS/Images.xcassets/AppIcon.appiconset/Contents.json @@ -84,10 +84,15 @@ "idiom" : "ipad", "size" : "83.5x83.5", "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } -} +} \ No newline at end of file diff --git a/Firestore/Example/App/macOS_example/AppDelegate.m b/Firestore/Example/App/macOS_example/AppDelegate.m index 5a852fd41cc..0c753a7eed7 100644 --- a/Firestore/Example/App/macOS_example/AppDelegate.m +++ b/Firestore/Example/App/macOS_example/AppDelegate.m @@ -35,7 +35,6 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { // do the timestamp fix FIRFirestoreSettings *settings = db.settings; - settings.timestampsInSnapshotsEnabled = true; db.settings = settings; // create a doc diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index d988db48a87..64802fa9226 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ 36FD4CE79613D18BC783C55B /* string_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0EE5300F8233D14025EF0456 /* string_apple_test.mm */; }; 3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; }; 4AA4ABE36065DB79CD76DD8D /* Pods_Firestore_Benchmarks_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F694C3CE4B77B3C0FA4BBA53 /* Pods_Firestore_Benchmarks_iOS.framework */; }; + 4D1F46B2DD91198C8867C04C /* xcgmock_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4425A513895DEC60325A139E /* xcgmock_test.mm */; }; 54131E9720ADE679001DF3FF /* string_format_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54131E9620ADE678001DF3FF /* string_format_test.cc */; }; 544129DA21C2DDC800EFB9CC /* common.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D221C2DDC800EFB9CC /* common.pb.cc */; }; 544129DB21C2DDC800EFB9CC /* firestore.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D421C2DDC800EFB9CC /* firestore.pb.cc */; }; @@ -166,17 +167,23 @@ 6E59498D20F55BA800ECD9A5 /* FuzzingResources in Resources */ = {isa = PBXBuildFile; fileRef = 6ED6DEA120F5502700FC6076 /* FuzzingResources */; }; 6E8302E021022309003E1EA3 /* FSTFuzzTestFieldPath.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6E8302DF21022309003E1EA3 /* FSTFuzzTestFieldPath.mm */; }; 6EA39FDE20FE820E008D461F /* FSTFuzzTestSerializer.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6EA39FDD20FE820E008D461F /* FSTFuzzTestSerializer.mm */; }; + 6EC28BB8C38E3FD126F68211 /* delayed_constructor_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = D0A6E9136804A41CEC9D55D4 /* delayed_constructor_test.cc */; }; 6EDD3B4620BF247500C33877 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; 6EDD3B4820BF247500C33877 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; 6EDD3B4920BF247500C33877 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; }; 6EDD3B6020BF25AE00C33877 /* FSTFuzzTestsPrincipal.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6EDD3B5E20BF24D000C33877 /* FSTFuzzTestsPrincipal.mm */; }; 6F3CAC76D918D6B0917EDF92 /* query_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B9C261C26C5D311E1E3C0CB9 /* query_test.cc */; }; 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */; }; + 731541612214AFFA0037F4DC /* query_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 731541602214AFFA0037F4DC /* query_spec_test.json */; }; 73866AA12082B0A5009BB4FF /* FIRArrayTransformTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73866A9F2082B069009BB4FF /* FIRArrayTransformTests.mm */; }; + 73F1F73C2210F3D800E1F692 /* memory_index_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73F1F7392210F3D800E1F692 /* memory_index_manager_test.mm */; }; + 73F1F73D2210F3D800E1F692 /* index_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73F1F73B2210F3D800E1F692 /* index_manager_test.mm */; }; + 73F1F7412211FEF300E1F692 /* leveldb_index_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73F1F7402211FEF300E1F692 /* leveldb_index_manager_test.mm */; }; 73FE5066020EF9B2892C86BF /* hard_assert_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */; }; 84DBE646DCB49305879D3500 /* nanopb_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 353EEE078EF3F39A9B7279F6 /* nanopb_string_test.cc */; }; 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */; }; 8C82D4D3F9AB63E79CC52DC8 /* Pods_Firestore_IntegrationTests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ECEBABC7E7B693BE808A1052 /* Pods_Firestore_IntegrationTests_iOS.framework */; }; + 9794E074439ABE5457E60F35 /* xcgmock_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4425A513895DEC60325A139E /* xcgmock_test.mm */; }; AB356EF7200EA5EB0089B766 /* field_value_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB356EF6200EA5EB0089B766 /* field_value_test.cc */; }; AB380CFB2019388600D97691 /* target_id_generator_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380CF82019382300D97691 /* target_id_generator_test.cc */; }; AB380CFE201A2F4500D97691 /* string_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380CFC201A2EE200D97691 /* string_util_test.cc */; }; @@ -201,7 +208,11 @@ B67BF449216EB43000CA9097 /* create_noop_connectivity_monitor.cc in Sources */ = {isa = PBXBuildFile; fileRef = B67BF448216EB43000CA9097 /* create_noop_connectivity_monitor.cc */; }; B686F2AF2023DDEE0028D6BE /* field_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2AD2023DDB20028D6BE /* field_path_test.cc */; }; B686F2B22025000D0028D6BE /* resource_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2B02024FFD70028D6BE /* resource_path_test.cc */; }; + B68B1E012213A765008977EF /* to_string_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B68B1E002213A764008977EF /* to_string_apple_test.mm */; }; B68FC0E521F6848700A7055C /* watch_change_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B68FC0E421F6848700A7055C /* watch_change_test.mm */; }; + B696858E2214B53900271095 /* to_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B696858D2214B53900271095 /* to_string_test.cc */; }; + B6968590221770F100271095 /* objc_compatibility_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B696858F221770F000271095 /* objc_compatibility_apple_test.mm */; }; + B69CF3F12227386500B281C8 /* hashing_test_apple.mm in Sources */ = {isa = PBXBuildFile; fileRef = B69CF3F02227386500B281C8 /* hashing_test_apple.mm */; }; B6BBE43121262CF400C6A53E /* grpc_stream_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6BBE42F21262CF400C6A53E /* grpc_stream_test.cc */; }; B6D1B68520E2AB1B00B35856 /* exponential_backoff_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D1B68420E2AB1A00B35856 /* exponential_backoff_test.cc */; }; B6D9649121544D4F00EB9CFB /* grpc_connection_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D9649021544D4F00EB9CFB /* grpc_connection_test.cc */; }; @@ -219,6 +230,7 @@ C80B10E79CDD7EF7843C321E /* type_traits_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2A0CF41BA5AED6049B0BEB2C /* type_traits_apple_test.mm */; }; C8D3CE2343E53223E6487F2C /* Pods_Firestore_Example_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5918805E993304321A05E82B /* Pods_Firestore_Example_iOS.framework */; }; CA989C0E6020C372A62B7062 /* testutil.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0352820A3B3BD003E0143 /* testutil.cc */; }; + D5B252EE3F4037405DB1ECE3 /* FIRNumericTransformTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = D5B25E7E7D6873CBA4571841 /* FIRNumericTransformTests.mm */; }; D5B25CBF07F65E885C9D68AB /* perf_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = D5B2593BCB52957D62F1C9D3 /* perf_spec_test.json */; }; D94A1862B8FB778225DB54A1 /* filesystem_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F51859B394D01C0C507282F1 /* filesystem_test.cc */; }; DAFF0CF921E64AC30062958F /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DAFF0CF821E64AC30062958F /* AppDelegate.m */; }; @@ -320,6 +332,7 @@ 3C81DE3772628FE297055662 /* Pods-Firestore_Example_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS/Pods-Firestore_Example_iOS.debug.xcconfig"; sourceTree = ""; }; 3F0992A4B83C60841C52E960 /* Pods-Firestore_Example_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS/Pods-Firestore_Example_iOS.release.xcconfig"; sourceTree = ""; }; 403DBF6EFB541DFD01582AA3 /* path_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = path_test.cc; sourceTree = ""; }; + 4425A513895DEC60325A139E /* xcgmock_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = xcgmock_test.mm; sourceTree = ""; }; 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = hard_assert_test.cc; sourceTree = ""; }; 54131E9620ADE678001DF3FF /* string_format_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = string_format_test.cc; sourceTree = ""; }; 544129D021C2DDC800EFB9CC /* query.pb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = query.pb.h; sourceTree = ""; }; @@ -489,7 +502,12 @@ 6EDD3B5C20BF247500C33877 /* Firestore_FuzzTests_iOS-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Firestore_FuzzTests_iOS-Info.plist"; sourceTree = ""; }; 6EDD3B5E20BF24D000C33877 /* FSTFuzzTestsPrincipal.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTFuzzTestsPrincipal.mm; sourceTree = ""; }; 71719F9E1E33DC2100824A3D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 731541602214AFFA0037F4DC /* query_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = query_spec_test.json; sourceTree = ""; }; 73866A9F2082B069009BB4FF /* FIRArrayTransformTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRArrayTransformTests.mm; sourceTree = ""; }; + 73F1F7392210F3D800E1F692 /* memory_index_manager_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = memory_index_manager_test.mm; sourceTree = ""; }; + 73F1F73A2210F3D800E1F692 /* index_manager_test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = index_manager_test.h; sourceTree = ""; }; + 73F1F73B2210F3D800E1F692 /* index_manager_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = index_manager_test.mm; sourceTree = ""; }; + 73F1F7402211FEF300E1F692 /* leveldb_index_manager_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = leveldb_index_manager_test.mm; sourceTree = ""; }; 79507DF8378D3C42F5B36268 /* string_win_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = string_win_test.cc; sourceTree = ""; }; 84434E57CA72951015FC71BC /* Pods-Firestore_FuzzTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_FuzzTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_FuzzTests_iOS/Pods-Firestore_FuzzTests_iOS.debug.xcconfig"; sourceTree = ""; }; 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -526,8 +544,12 @@ B67BF448216EB43000CA9097 /* create_noop_connectivity_monitor.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = create_noop_connectivity_monitor.cc; sourceTree = ""; }; B686F2AD2023DDB20028D6BE /* field_path_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = field_path_test.cc; sourceTree = ""; }; B686F2B02024FFD70028D6BE /* resource_path_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = resource_path_test.cc; sourceTree = ""; }; + B68B1E002213A764008977EF /* to_string_apple_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = to_string_apple_test.mm; sourceTree = ""; }; B68FC0E421F6848700A7055C /* watch_change_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = watch_change_test.mm; sourceTree = ""; }; + B696858D2214B53900271095 /* to_string_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = to_string_test.cc; sourceTree = ""; }; + B696858F221770F000271095 /* objc_compatibility_apple_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = objc_compatibility_apple_test.mm; sourceTree = ""; }; B69CF05A219B9105004C434D /* FIRFirestore+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FIRFirestore+Testing.h"; sourceTree = ""; }; + B69CF3F02227386500B281C8 /* hashing_test_apple.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = hashing_test_apple.mm; sourceTree = ""; }; B6BBE42F21262CF400C6A53E /* grpc_stream_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = grpc_stream_test.cc; sourceTree = ""; }; B6D1B68420E2AB1A00B35856 /* exponential_backoff_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = exponential_backoff_test.cc; sourceTree = ""; }; B6D9649021544D4F00EB9CFB /* grpc_connection_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = grpc_connection_test.cc; sourceTree = ""; }; @@ -544,10 +566,13 @@ B6FB468A208F9B9100554BA2 /* executor_test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = executor_test.h; sourceTree = ""; }; B79CA87A1A01FC5329031C9B /* Pods_Firestore_FuzzTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_FuzzTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B9C261C26C5D311E1E3C0CB9 /* query_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = query_test.cc; sourceTree = ""; }; + BA6E5B9D53CCF301F58A62D7 /* xcgmock.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = xcgmock.h; sourceTree = ""; }; BB92EB03E3F92485023F64ED /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C8522DE226C467C54E6788D8 /* mutation_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = mutation_test.cc; sourceTree = ""; }; + D0A6E9136804A41CEC9D55D4 /* delayed_constructor_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = delayed_constructor_test.cc; sourceTree = ""; }; D3CC3DC5338DCAF43A211155 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; D5B2593BCB52957D62F1C9D3 /* perf_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = perf_spec_test.json; sourceTree = ""; }; + D5B25E7E7D6873CBA4571841 /* FIRNumericTransformTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRNumericTransformTests.mm; sourceTree = ""; }; DAFF0CF521E64AC30062958F /* macOS_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = macOS_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; DAFF0CF721E64AC30062958F /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; DAFF0CF821E64AC30062958F /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -701,6 +726,8 @@ 5467FB07203E6A44009C9584 /* app_testing.mm */, 54A0352820A3B3BD003E0143 /* testutil.cc */, 54A0352920A3B3BD003E0143 /* testutil.h */, + BA6E5B9D53CCF301F58A62D7 /* xcgmock.h */, + 4425A513895DEC60325A139E /* xcgmock_test.mm */, ); path = testutil; sourceTree = ""; @@ -708,7 +735,6 @@ 546854A720A3681B004BDBD5 /* remote */ = { isa = PBXGroup; children = ( - B68FC0E421F6848700A7055C /* watch_change_test.mm */, 546854A820A36867004BDBD5 /* datastore_test.mm */, B6D1B68420E2AB1A00B35856 /* exponential_backoff_test.cc */, B6D9649021544D4F00EB9CFB /* grpc_connection_test.cc */, @@ -717,6 +743,7 @@ B6D964942163E63900EB9CFB /* grpc_unary_call_test.cc */, 61F72C5520BC48FD001A68CB /* serializer_test.cc */, B66D8995213609EE0086DA0C /* stream_test.mm */, + B68FC0E421F6848700A7055C /* watch_change_test.mm */, ); path = remote; sourceTree = ""; @@ -734,6 +761,7 @@ 548DB928200D59F600E00ABC /* comparison_test.cc */, B67BF448216EB43000CA9097 /* create_noop_connectivity_monitor.cc */, B67BF447216EB42F00CA9097 /* create_noop_connectivity_monitor.h */, + D0A6E9136804A41CEC9D55D4 /* delayed_constructor_test.cc */, B6FB4689208F9B9100554BA2 /* executor_libdispatch_test.mm */, B6FB4687208F9B9100554BA2 /* executor_std_test.cc */, B6FB4688208F9B9100554BA2 /* executor_test.cc */, @@ -745,8 +773,10 @@ ED4B3E3EA0EBF3ED19A07060 /* grpc_stream_tester.h */, 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */, 54511E8D209805F8005BD28F /* hashing_test.cc */, + B69CF3F02227386500B281C8 /* hashing_test_apple.mm */, 54A0353420A3D8CB003E0143 /* iterator_adaptors_test.cc */, 54C2294E1FECABAE007D065B /* log_test.cc */, + B696858F221770F000271095 /* objc_compatibility_apple_test.mm */, AB380D03201BC6E400D97691 /* ordered_code_test.cc */, 403DBF6EFB541DFD01582AA3 /* path_test.cc */, 54740A531FC913E500713A1A /* secure_random_test.cc */, @@ -759,6 +789,8 @@ 54131E9620ADE678001DF3FF /* string_format_test.cc */, AB380CFC201A2EE200D97691 /* string_util_test.cc */, 79507DF8378D3C42F5B36268 /* string_win_test.cc */, + B68B1E002213A764008977EF /* to_string_apple_test.mm */, + B696858D2214B53900271095 /* to_string_test.cc */, 2A0CF41BA5AED6049B0BEB2C /* type_traits_apple_test.mm */, ); path = util; @@ -797,9 +829,13 @@ 54995F70205B6E1A004EFFA0 /* local */ = { isa = PBXGroup; children = ( + 73F1F73A2210F3D800E1F692 /* index_manager_test.h */, + 73F1F73B2210F3D800E1F692 /* index_manager_test.mm */, + 73F1F7402211FEF300E1F692 /* leveldb_index_manager_test.mm */, 54995F6E205B6E12004EFFA0 /* leveldb_key_test.cc */, 332485C4DCC6BA0DBB5E31B7 /* leveldb_util_test.cc */, F8043813A5D16963EC02B182 /* local_serializer_test.cc */, + 73F1F7392210F3D800E1F692 /* memory_index_manager_test.mm */, 132E32997D781B896672D30A /* reference_set_test.cc */, ); path = local; @@ -1258,6 +1294,7 @@ 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */, D5B2593BCB52957D62F1C9D3 /* perf_spec_test.json */, 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */, + 731541602214AFFA0037F4DC /* query_spec_test.json */, 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */, 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */, 54DA12A51F315EE100DD57A1 /* write_spec_test.json */, @@ -1308,6 +1345,7 @@ 5492E06A202154D500B64F25 /* FIRFieldsTests.mm */, 6161B5012047140400A99DBB /* FIRFirestoreSourceTests.mm */, 5492E06B202154D500B64F25 /* FIRListenerRegistrationTests.mm */, + D5B25E7E7D6873CBA4571841 /* FIRNumericTransformTests.mm */, 5492E069202154D500B64F25 /* FIRQueryTests.mm */, 5492E06E202154D600B64F25 /* FIRServerTimestampTests.mm */, 5492E071202154D600B64F25 /* FIRTypeTests.mm */, @@ -1467,7 +1505,7 @@ attributes = { CLASSPREFIX = FIR; LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0720; + LastUpgradeCheck = 1010; ORGANIZATIONNAME = Google; TargetAttributes = { 54C9EDF02040E16300A969CD = { @@ -1574,6 +1612,7 @@ 54DA12AC1F315EE100DD57A1 /* orderby_spec_test.json in Resources */, D5B25CBF07F65E885C9D68AB /* perf_spec_test.json in Resources */, 54DA12AD1F315EE100DD57A1 /* persistence_spec_test.json in Resources */, + 731541612214AFFA0037F4DC /* query_spec_test.json in Resources */, 3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */, 54DA12AE1F315EE100DD57A1 /* resume_token_spec_test.json in Resources */, 54DA12AF1F315EE100DD57A1 /* write_spec_test.json in Resources */, @@ -1616,7 +1655,7 @@ files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS/Pods-Firestore_Example_iOS-frameworks.sh", + "${PODS_ROOT}/Target Support Files/Pods-Firestore_Example_iOS/Pods-Firestore_Example_iOS-frameworks.sh", "${BUILT_PRODUCTS_DIR}/BoringSSL-GRPC-iOS/openssl_grpc.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher-iOS/GTMSessionFetcher.framework", "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", @@ -1639,7 +1678,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS/Pods-Firestore_Example_iOS-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Firestore_Example_iOS/Pods-Firestore_Example_iOS-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 329C25E418360CEF62F6CB2B /* [CP] Embed Pods Frameworks */ = { @@ -1648,7 +1687,7 @@ files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS-frameworks.sh", + "${PODS_ROOT}/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS-frameworks.sh", "${BUILT_PRODUCTS_DIR}/leveldb-library-iOS/leveldb.framework", "${BUILT_PRODUCTS_DIR}/GoogleTest/GoogleTest.framework", "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", @@ -1663,7 +1702,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 4C71ED5B5EF024AEF16B5E55 /* [CP] Embed Pods Frameworks */ = { @@ -1672,7 +1711,7 @@ files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Benchmarks_iOS/Pods-Firestore_Benchmarks_iOS-frameworks.sh", + "${PODS_ROOT}/Target Support Files/Pods-Firestore_Benchmarks_iOS/Pods-Firestore_Benchmarks_iOS-frameworks.sh", "${BUILT_PRODUCTS_DIR}/GoogleBenchmark/GoogleBenchmark.framework", ); name = "[CP] Embed Pods Frameworks"; @@ -1681,7 +1720,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Benchmarks_iOS/Pods-Firestore_Benchmarks_iOS-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Firestore_Benchmarks_iOS/Pods-Firestore_Benchmarks_iOS-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 6A86E48DF663B6AA1CB5BA83 /* [CP] Embed Pods Frameworks */ = { @@ -1689,10 +1728,8 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-macOS_example/Pods-macOS_example-frameworks.sh", + "${PODS_ROOT}/Target Support Files/Pods-macOS_example/Pods-macOS_example-frameworks.sh", "${BUILT_PRODUCTS_DIR}/BoringSSL-GRPC-macOS/openssl_grpc.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher-macOS/GTMSessionFetcher.framework", "${BUILT_PRODUCTS_DIR}/GoogleUtilities-Environment-Logger/GoogleUtilities.framework", @@ -1703,8 +1740,6 @@ "${BUILT_PRODUCTS_DIR}/nanopb-macOS/nanopb.framework", ); name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - ); outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl_grpc.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", @@ -1717,7 +1752,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-macOS_example/Pods-macOS_example-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-macOS_example/Pods-macOS_example-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 6E622C7A20F52C8300B7E93A /* Run Script */ = { @@ -1761,7 +1796,7 @@ files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_FuzzTests_iOS/Pods-Firestore_FuzzTests_iOS-frameworks.sh", + "${PODS_ROOT}/Target Support Files/Pods-Firestore_FuzzTests_iOS/Pods-Firestore_FuzzTests_iOS-frameworks.sh", "${BUILT_PRODUCTS_DIR}/Protobuf-iOS9.0/Protobuf.framework", "${BUILT_PRODUCTS_DIR}/LibFuzzer/LibFuzzer.framework", ); @@ -1772,7 +1807,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_FuzzTests_iOS/Pods-Firestore_FuzzTests_iOS-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Firestore_FuzzTests_iOS/Pods-Firestore_FuzzTests_iOS-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 7C2467DCD3E3E16FB0A737DE /* [CP] Check Pods Manifest.lock */ = { @@ -1780,15 +1815,11 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-macOS_example-checkManifestLockResult.txt", ); @@ -1857,7 +1888,7 @@ files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests_iOS/Pods-Firestore_IntegrationTests_iOS-frameworks.sh", + "${PODS_ROOT}/Target Support Files/Pods-Firestore_IntegrationTests_iOS/Pods-Firestore_IntegrationTests_iOS-frameworks.sh", "${BUILT_PRODUCTS_DIR}/leveldb-library-iOS/leveldb.framework", "${BUILT_PRODUCTS_DIR}/GoogleTest/GoogleTest.framework", "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", @@ -1870,7 +1901,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests_iOS/Pods-Firestore_IntegrationTests_iOS-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Firestore_IntegrationTests_iOS/Pods-Firestore_IntegrationTests_iOS-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; BF6384844477A4F850F0E89F /* [CP] Check Pods Manifest.lock */ = { @@ -1915,7 +1946,7 @@ files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS-frameworks.sh", + "${PODS_ROOT}/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS-frameworks.sh", "${BUILT_PRODUCTS_DIR}/BoringSSL-GRPC-iOS/openssl_grpc.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher-iOS/GTMSessionFetcher.framework", "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", @@ -1938,7 +1969,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -1996,7 +2027,6 @@ 5492E0BA2021555100B64F25 /* FSTDocumentSetTests.mm in Sources */, 5492E0BD2021555100B64F25 /* FSTDocumentTests.mm in Sources */, 5492E03E2021401F00B64F25 /* FSTEventAccumulator.mm in Sources */, - B68FC0E521F6848700A7055C /* watch_change_test.mm in Sources */, 5492E067202154B900B64F25 /* FSTEventManagerTests.mm in Sources */, 5492E0BF2021555100B64F25 /* FSTFieldValueTests.mm in Sources */, 54764FAF1FAA21B90085E60A /* FSTGoogleTestTests.mm in Sources */, @@ -2052,6 +2082,7 @@ ABE6637A201FA81900ED349A /* database_id_test.cc in Sources */, AB38D93020236E21000A432D /* database_info_test.cc in Sources */, 546854AA20A36867004BDBD5 /* datastore_test.mm in Sources */, + 6EC28BB8C38E3FD126F68211 /* delayed_constructor_test.cc in Sources */, 544129DD21C2DDC800EFB9CC /* document.pb.cc in Sources */, B6152AD7202A53CB000E5744 /* document_key_test.cc in Sources */, AB6B908420322E4D00CC290A /* document_test.cc in Sources */, @@ -2076,18 +2107,23 @@ B6D964952163E63900EB9CFB /* grpc_unary_call_test.cc in Sources */, 73FE5066020EF9B2892C86BF /* hard_assert_test.cc in Sources */, 54511E8E209805F8005BD28F /* hashing_test.cc in Sources */, + B69CF3F12227386500B281C8 /* hashing_test_apple.mm in Sources */, 618BBEB020B89AAC00B5BCE7 /* http.pb.cc in Sources */, + 73F1F73D2210F3D800E1F692 /* index_manager_test.mm in Sources */, 54A0353520A3D8CB003E0143 /* iterator_adaptors_test.cc in Sources */, 618BBEAE20B89AAC00B5BCE7 /* latlng.pb.cc in Sources */, + 73F1F7412211FEF300E1F692 /* leveldb_index_manager_test.mm in Sources */, 54995F6F205B6E12004EFFA0 /* leveldb_key_test.cc in Sources */, BEE0294A23AB993E5DE0E946 /* leveldb_util_test.cc in Sources */, 020AFD89BB40E5175838BB76 /* local_serializer_test.cc in Sources */, 54C2294F1FECABAE007D065B /* log_test.cc in Sources */, 618BBEA720B89AAC00B5BCE7 /* maybe_document.pb.cc in Sources */, + 73F1F73C2210F3D800E1F692 /* memory_index_manager_test.mm in Sources */, 618BBEA820B89AAC00B5BCE7 /* mutation.pb.cc in Sources */, 32F022CB75AEE48CDDAF2982 /* mutation_test.cc in Sources */, 84DBE646DCB49305879D3500 /* nanopb_string_test.cc in Sources */, AB6B908820322E8800CC290A /* no_document_test.cc in Sources */, + B6968590221770F100271095 /* objc_compatibility_apple_test.mm in Sources */, AB380D04201BC6E400D97691 /* ordered_code_test.cc in Sources */, 5A080105CCBFDB6BF3F3772D /* path_test.cc in Sources */, 549CCA5920A36E1F00BCEB75 /* precondition_test.cc in Sources */, @@ -2114,12 +2150,16 @@ AB380CFB2019388600D97691 /* target_id_generator_test.cc in Sources */, 54A0352A20A3B3BD003E0143 /* testutil.cc in Sources */, ABF6506C201131F8005F2C74 /* timestamp_test.cc in Sources */, + B68B1E012213A765008977EF /* to_string_apple_test.mm in Sources */, + B696858E2214B53900271095 /* to_string_test.cc in Sources */, ABC1D7E12023A40C00BA84F0 /* token_test.cc in Sources */, 54A0352720A3AED0003E0143 /* transform_operations_test.mm in Sources */, 549CCA5120A36DBC00BCEB75 /* tree_sorted_map_test.cc in Sources */, C80B10E79CDD7EF7843C321E /* type_traits_apple_test.mm in Sources */, ABC1D7DE2023A05300BA84F0 /* user_test.cc in Sources */, + B68FC0E521F6848700A7055C /* watch_change_test.mm in Sources */, 544129DE21C2DDC800EFB9CC /* write.pb.cc in Sources */, + 9794E074439ABE5457E60F35 /* xcgmock_test.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2152,6 +2192,7 @@ 5492E073202154D600B64F25 /* FIRFieldsTests.mm in Sources */, 6161B5032047140C00A99DBB /* FIRFirestoreSourceTests.mm in Sources */, 5492E074202154D600B64F25 /* FIRListenerRegistrationTests.mm in Sources */, + D5B252EE3F4037405DB1ECE3 /* FIRNumericTransformTests.mm in Sources */, 5492E072202154D600B64F25 /* FIRQueryTests.mm in Sources */, 5492E077202154D600B64F25 /* FIRServerTimestampTests.mm in Sources */, 5492E07A202154D600B64F25 /* FIRTypeTests.mm in Sources */, @@ -2166,6 +2207,7 @@ 5492E0442021457E00B64F25 /* XCTestCase+Await.mm in Sources */, EBFC611B1BF195D0EC710AF4 /* app_testing.mm in Sources */, CA989C0E6020C372A62B7062 /* testutil.cc in Sources */, + 4D1F46B2DD91198C8867C04C /* xcgmock_test.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2498,19 +2540,32 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = c99; GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -2540,18 +2595,31 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = YES; ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = c99; + GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -2562,6 +2630,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 8.0; OTHER_CFLAGS = ""; SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -2636,12 +2705,12 @@ HEADER_SEARCH_PATHS = ( "$(inherited)", "\"${PODS_ROOT}/../../..\"", + "\"${PODS_ROOT}/../../../Firestore/Protos/cpp\"", + "\"${PODS_ROOT}/../../../Firestore/Protos/nanopb\"", "\"${PODS_ROOT}/../../../Firestore/third_party/abseil-cpp\"", "\"${PODS_ROOT}/GoogleTest/googlemock/include\"", "\"${PODS_ROOT}/GoogleTest/googletest/include\"", "\"${PODS_ROOT}/leveldb-library/include\"", - "\"${PODS_ROOT}/../../../Firestore/Protos/nanopb\"", - "\"${PODS_ROOT}/../../../Firestore/Protos/cpp\"", "\"${PODS_ROOT}/ProtobufCpp/src\"", ); INFOPLIST_FILE = "Tests/Tests-Info.plist"; @@ -2714,12 +2783,12 @@ HEADER_SEARCH_PATHS = ( "$(inherited)", "\"${PODS_ROOT}/../../..\"", + "\"${PODS_ROOT}/../../../Firestore/Protos/cpp\"", + "\"${PODS_ROOT}/../../../Firestore/Protos/nanopb\"", "\"${PODS_ROOT}/../../../Firestore/third_party/abseil-cpp\"", "\"${PODS_ROOT}/GoogleTest/googlemock/include\"", "\"${PODS_ROOT}/GoogleTest/googletest/include\"", "\"${PODS_ROOT}/leveldb-library/include\"", - "\"${PODS_ROOT}/../../../Firestore/Protos/nanopb\"", - "\"${PODS_ROOT}/../../../Firestore/Protos/cpp\"", "\"${PODS_ROOT}/ProtobufCpp/src\"", ); INFOPLIST_FILE = "Tests/Tests-Info.plist"; @@ -2902,8 +2971,6 @@ "\"leveldb\"", "-framework", "\"nanopb\"", - "-framework", - "\"openssl\"", "-ObjC", ); PRODUCT_BUNDLE_IDENTIFIER = "com.google.Firestore-macOS"; @@ -2978,8 +3045,6 @@ "\"leveldb\"", "-framework", "\"nanopb\"", - "-framework", - "\"openssl\"", "-ObjC", ); PRODUCT_BUNDLE_IDENTIFIER = "com.google.Firestore-macOS"; @@ -3010,8 +3075,9 @@ "$(inherited)", "\"${PODS_ROOT}/../../..\"", "\"${PODS_ROOT}/../../../Firestore/third_party/abseil-cpp\"", - "\"${PODS_ROOT}/leveldb-library/include\"", + "\"${PODS_ROOT}/GoogleTest/googlemock/include\"", "\"${PODS_ROOT}/GoogleTest/googletest/include\"", + "\"${PODS_ROOT}/leveldb-library/include\"", ); INFOPLIST_FILE = "Tests/Tests-Info.plist"; OTHER_LDFLAGS = ( @@ -3047,8 +3113,9 @@ "$(inherited)", "\"${PODS_ROOT}/../../..\"", "\"${PODS_ROOT}/../../../Firestore/third_party/abseil-cpp\"", - "\"${PODS_ROOT}/leveldb-library/include\"", + "\"${PODS_ROOT}/GoogleTest/googlemock/include\"", "\"${PODS_ROOT}/GoogleTest/googletest/include\"", + "\"${PODS_ROOT}/leveldb-library/include\"", ); INFOPLIST_FILE = "Tests/Tests-Info.plist"; OTHER_LDFLAGS = ( diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests_iOS.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests_iOS.xcscheme index 69c49567444..325de9bfba8 100644 --- a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests_iOS.xcscheme +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests_iOS.xcscheme @@ -1,6 +1,6 @@ + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + + + + @@ -50,6 +62,15 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> + + + + + + + + diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests_iOS.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests_iOS.xcscheme index 4e6f82845e2..03e9133eefe 100644 --- a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests_iOS.xcscheme +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests_iOS.xcscheme @@ -1,6 +1,6 @@ '../../' pod 'FirebaseAuthInterop', :path => '../../' diff --git a/Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm b/Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm index 91054ea0dc7..9a45e8c388c 100644 --- a/Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm +++ b/Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm @@ -18,20 +18,27 @@ #import +#include +#include + #import "Firestore/Example/Tests/API/FSTAPIHelpers.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" #import "Firestore/Source/API/FIRDocumentChange+Internal.h" #import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" #import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; -using firebase::firestore::core::DocumentViewChangeType; +using firebase::firestore::core::DocumentViewChange; +using firebase::firestore::core::ViewSnapshot; +using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::DocumentSet; NS_ASSUME_NONNULL_BEGIN @@ -51,15 +58,16 @@ @interface FIRQuerySnapshotTests : XCTestCase @implementation FIRQuerySnapshotTests - (void)testEquals { - FIRQuerySnapshot *foo = FSTTestQuerySnapshot("foo", @{}, @{@"a" : @{@"a" : @1}}, YES, NO); - FIRQuerySnapshot *fooDup = FSTTestQuerySnapshot("foo", @{}, @{@"a" : @{@"a" : @1}}, YES, NO); + FIRQuerySnapshot *foo = FSTTestQuerySnapshot("foo", @{}, @{@"a" : @{@"a" : @1}}, true, false); + FIRQuerySnapshot *fooDup = FSTTestQuerySnapshot("foo", @{}, @{@"a" : @{@"a" : @1}}, true, false); FIRQuerySnapshot *differentPath = - FSTTestQuerySnapshot("bar", @{}, @{@"a" : @{@"a" : @1}}, YES, NO); + FSTTestQuerySnapshot("bar", @{}, @{@"a" : @{@"a" : @1}}, true, false); FIRQuerySnapshot *differentDoc = - FSTTestQuerySnapshot("foo", @{@"a" : @{@"b" : @1}}, @{}, YES, NO); + FSTTestQuerySnapshot("foo", @{@"a" : @{@"b" : @1}}, @{}, true, false); FIRQuerySnapshot *noPendingWrites = - FSTTestQuerySnapshot("foo", @{}, @{@"a" : @{@"a" : @1}}, NO, NO); - FIRQuerySnapshot *fromCache = FSTTestQuerySnapshot("foo", @{}, @{@"a" : @{@"a" : @1}}, YES, YES); + FSTTestQuerySnapshot("foo", @{}, @{@"a" : @{@"a" : @1}}, false, false); + FIRQuerySnapshot *fromCache = + FSTTestQuerySnapshot("foo", @{}, @{@"a" : @{@"a" : @1}}, true, true); XCTAssertEqualObjects(foo, fooDup); XCTAssertNotEqualObjects(foo, differentPath); XCTAssertNotEqualObjects(foo, differentDoc); @@ -80,40 +88,41 @@ - (void)testIncludeMetadataChanges { FSTDocument *doc2Old = FSTTestDoc("foo/baz", 1, @{@"a" : @"b"}, FSTDocumentStateSynced); FSTDocument *doc2New = FSTTestDoc("foo/baz", 1, @{@"a" : @"c"}, FSTDocumentStateSynced); - FSTDocumentSet *oldDocuments = FSTTestDocSet(FSTDocumentComparatorByKey, @[ doc1Old, doc2Old ]); - FSTDocumentSet *newDocuments = FSTTestDocSet(FSTDocumentComparatorByKey, @[ doc2New, doc2New ]); - NSArray *documentChanges = @[ - [FSTDocumentViewChange changeWithDocument:doc1New type:DocumentViewChangeType::kMetadata], - [FSTDocumentViewChange changeWithDocument:doc2New type:DocumentViewChangeType::kModified], - ]; + DocumentSet oldDocuments = FSTTestDocSet(FSTDocumentComparatorByKey, @[ doc1Old, doc2Old ]); + DocumentSet newDocuments = FSTTestDocSet(FSTDocumentComparatorByKey, @[ doc2New, doc2New ]); + std::vector documentChanges{ + DocumentViewChange{doc1New, DocumentViewChange::Type::kMetadata}, + DocumentViewChange{doc2New, DocumentViewChange::Type::kModified}, + }; - FIRFirestore *firestore = FSTTestFirestore(); + Firestore *firestore = FSTTestFirestore().wrapped; FSTQuery *query = FSTTestQuery("foo"); - FSTViewSnapshot *viewSnapshot = [[FSTViewSnapshot alloc] initWithQuery:query - documents:newDocuments - oldDocuments:oldDocuments - documentChanges:documentChanges - fromCache:NO - mutatedKeys:DocumentKeySet {} - syncStateChanged:YES - excludesMetadataChanges:NO]; - FIRSnapshotMetadata *metadata = [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:NO - fromCache:NO]; - FIRQuerySnapshot *snapshot = [FIRQuerySnapshot snapshotWithFirestore:firestore - originalQuery:query - snapshot:viewSnapshot - metadata:metadata]; - - FIRQueryDocumentSnapshot *doc1Snap = [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore - documentKey:doc1New.key - document:doc1New - fromCache:NO - hasPendingWrites:NO]; - FIRQueryDocumentSnapshot *doc2Snap = [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore - documentKey:doc2New.key - document:doc2New - fromCache:NO - hasPendingWrites:NO]; + ViewSnapshot viewSnapshot{query, + newDocuments, + oldDocuments, + std::move(documentChanges), + /*mutated_keys=*/DocumentKeySet{}, + /*from_cache=*/false, + /*sync_state_changed=*/true, + /*excludes_metadata_changes=*/false}; + SnapshotMetadata metadata(/*pending_writes=*/false, /*from_cache=*/false); + FIRQuerySnapshot *snapshot = [[FIRQuerySnapshot alloc] initWithFirestore:firestore + originalQuery:query + snapshot:std::move(viewSnapshot) + metadata:std::move(metadata)]; + + FIRQueryDocumentSnapshot *doc1Snap = + [[FIRQueryDocumentSnapshot alloc] initWithFirestore:firestore + documentKey:doc1New.key + document:doc1New + fromCache:false + hasPendingWrites:false]; + FIRQueryDocumentSnapshot *doc2Snap = + [[FIRQueryDocumentSnapshot alloc] initWithFirestore:firestore + documentKey:doc2New.key + document:doc2New + fromCache:false + hasPendingWrites:false]; NSArray *changesWithoutMetadata = @[ [[FIRDocumentChange alloc] initWithType:FIRDocumentChangeTypeModified diff --git a/Firestore/Example/Tests/API/FIRSnapshotMetadataTests.mm b/Firestore/Example/Tests/API/FIRSnapshotMetadataTests.mm index f705aa76284..442406dca14 100644 --- a/Firestore/Example/Tests/API/FIRSnapshotMetadataTests.mm +++ b/Firestore/Example/Tests/API/FIRSnapshotMetadataTests.mm @@ -28,14 +28,11 @@ @interface FIRSnapshotMetadataTests : XCTestCase @implementation FIRSnapshotMetadataTests - (void)testEquals { - FIRSnapshotMetadata *foo = [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:YES - fromCache:YES]; - FIRSnapshotMetadata *fooDup = [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:YES - fromCache:YES]; - FIRSnapshotMetadata *bar = [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:YES - fromCache:NO]; - FIRSnapshotMetadata *baz = [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:NO - fromCache:YES]; + FIRSnapshotMetadata *foo = [[FIRSnapshotMetadata alloc] initWithPendingWrites:YES fromCache:YES]; + FIRSnapshotMetadata *fooDup = [[FIRSnapshotMetadata alloc] initWithPendingWrites:YES + fromCache:YES]; + FIRSnapshotMetadata *bar = [[FIRSnapshotMetadata alloc] initWithPendingWrites:YES fromCache:NO]; + FIRSnapshotMetadata *baz = [[FIRSnapshotMetadata alloc] initWithPendingWrites:NO fromCache:YES]; XCTAssertEqualObjects(foo, fooDup); XCTAssertNotEqualObjects(foo, bar); XCTAssertNotEqualObjects(foo, baz); @@ -47,6 +44,16 @@ - (void)testEquals { XCTAssertNotEqual([bar hash], [baz hash]); } +- (void)testProperties { + FIRSnapshotMetadata *metadata = [[FIRSnapshotMetadata alloc] initWithPendingWrites:YES + fromCache:NO]; + XCTAssertTrue(metadata.hasPendingWrites); + XCTAssertTrue(metadata.pendingWrites); + + XCTAssertFalse(metadata.isFromCache); + XCTAssertFalse(metadata.fromCache); +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/API/FSTAPIHelpers.h b/Firestore/Example/Tests/API/FSTAPIHelpers.h index 43401f1329a..434b0a0e534 100644 --- a/Firestore/Example/Tests/API/FSTAPIHelpers.h +++ b/Firestore/Example/Tests/API/FSTAPIHelpers.h @@ -65,8 +65,8 @@ FIRQuerySnapshot *FSTTestQuerySnapshot( const char *path, NSDictionary *> *oldDocs, NSDictionary *> *docsToAdd, - BOOL hasPendingWrites, - BOOL fromCache); + bool hasPendingWrites, + bool fromCache); #if __cplusplus } // extern "C" diff --git a/Firestore/Example/Tests/API/FSTAPIHelpers.mm b/Firestore/Example/Tests/API/FSTAPIHelpers.mm index 9de4096a1ac..f374c536cc4 100644 --- a/Firestore/Example/Tests/API/FSTAPIHelpers.mm +++ b/Firestore/Example/Tests/API/FSTAPIHelpers.mm @@ -21,6 +21,8 @@ #import #include +#include +#include #import "Firestore/Example/Tests/Util/FSTHelpers.h" #import "Firestore/Source/API/FIRCollectionReference+Internal.h" @@ -30,16 +32,19 @@ #import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" #import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" namespace testutil = firebase::firestore::testutil; namespace util = firebase::firestore::util; -using firebase::firestore::core::DocumentViewChangeType; +using firebase::firestore::core::DocumentViewChange; +using firebase::firestore::core::ViewSnapshot; +using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::DocumentSet; NS_ASSUME_NONNULL_BEGIN @@ -52,9 +57,9 @@ dispatch_once(&onceToken, ^{ sharedInstance = [[FIRFirestore alloc] initWithProjectID:"abc" database:"abc" - persistenceKey:@"db123" - credentialsProvider:nil - workerQueue:nil + persistenceKey:"db123" + credentialsProvider:nullptr + workerQueue:nullptr firebaseApp:nil]; }); #pragma clang diagnostic pop @@ -70,11 +75,11 @@ data ? FSTTestDoc(path, version, data, hasMutations ? FSTDocumentStateLocalMutations : FSTDocumentStateSynced) : nil; - return [FIRDocumentSnapshot snapshotWithFirestore:FSTTestFirestore() - documentKey:testutil::Key(path) - document:doc - fromCache:fromCache - hasPendingWrites:hasMutations]; + return [[FIRDocumentSnapshot alloc] initWithFirestore:FSTTestFirestore().wrapped + documentKey:testutil::Key(path) + document:doc + fromCache:fromCache + hasPendingWrites:hasMutations]; } FIRCollectionReference *FSTTestCollectionRef(const char *path) { @@ -83,8 +88,13 @@ } FIRDocumentReference *FSTTestDocRef(const char *path) { - return [FIRDocumentReference referenceWithPath:testutil::Resource(path) - firestore:FSTTestFirestore()]; + return [[FIRDocumentReference alloc] initWithPath:testutil::Resource(path) + firestore:FSTTestFirestore().wrapped]; +} + +FIRDocumentReference *FSTTestDocRef(const absl::string_view path) { + return [[FIRDocumentReference alloc] initWithPath:testutil::Resource(path) + firestore:FSTTestFirestore().wrapped]; } /** A convenience method for creating a query snapshots for tests. */ @@ -92,51 +102,45 @@ const char *path, NSDictionary *> *oldDocs, NSDictionary *> *docsToAdd, - BOOL hasPendingWrites, - BOOL fromCache) { - FIRSnapshotMetadata *metadata = - [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:hasPendingWrites fromCache:fromCache]; - FSTDocumentSet *oldDocuments = FSTTestDocSet(FSTDocumentComparatorByKey, @[]); + bool hasPendingWrites, + bool fromCache) { + SnapshotMetadata metadata(hasPendingWrites, fromCache); + DocumentSet oldDocuments = FSTTestDocSet(FSTDocumentComparatorByKey, @[]); DocumentKeySet mutatedKeys; for (NSString *key in oldDocs) { - oldDocuments = [oldDocuments - documentSetByAddingDocument:FSTTestDoc(util::StringFormat("%s/%s", path, key), 1, - oldDocs[key], - hasPendingWrites ? FSTDocumentStateLocalMutations - : FSTDocumentStateSynced)]; + oldDocuments = oldDocuments.insert( + FSTTestDoc(util::StringFormat("%s/%s", path, key), 1, oldDocs[key], + hasPendingWrites ? FSTDocumentStateLocalMutations : FSTDocumentStateSynced)); if (hasPendingWrites) { const std::string documentKey = util::StringFormat("%s/%s", path, key); mutatedKeys = mutatedKeys.insert(testutil::Key(documentKey)); } } - FSTDocumentSet *newDocuments = oldDocuments; - NSArray *documentChanges = [NSArray array]; + DocumentSet newDocuments = oldDocuments; + std::vector documentChanges; for (NSString *key in docsToAdd) { FSTDocument *docToAdd = FSTTestDoc(util::StringFormat("%s/%s", path, key), 1, docsToAdd[key], hasPendingWrites ? FSTDocumentStateLocalMutations : FSTDocumentStateSynced); - newDocuments = [newDocuments documentSetByAddingDocument:docToAdd]; - documentChanges = [documentChanges - arrayByAddingObject:[FSTDocumentViewChange - changeWithDocument:docToAdd - type:DocumentViewChangeType::kAdded]]; + newDocuments = newDocuments.insert(docToAdd); + documentChanges.emplace_back(docToAdd, DocumentViewChange::Type::kAdded); if (hasPendingWrites) { const std::string documentKey = util::StringFormat("%s/%s", path, key); mutatedKeys = mutatedKeys.insert(testutil::Key(documentKey)); } } - FSTViewSnapshot *viewSnapshot = [[FSTViewSnapshot alloc] initWithQuery:FSTTestQuery(path) - documents:newDocuments - oldDocuments:oldDocuments - documentChanges:documentChanges - fromCache:fromCache - mutatedKeys:mutatedKeys - syncStateChanged:YES - excludesMetadataChanges:NO]; - return [FIRQuerySnapshot snapshotWithFirestore:FSTTestFirestore() - originalQuery:FSTTestQuery(path) - snapshot:viewSnapshot - metadata:metadata]; + ViewSnapshot viewSnapshot{FSTTestQuery(path), + newDocuments, + oldDocuments, + std::move(documentChanges), + mutatedKeys, + fromCache, + /*sync_state_changed=*/true, + /*excludes_metadata_changes=*/false}; + return [[FIRQuerySnapshot alloc] initWithFirestore:FSTTestFirestore().wrapped + originalQuery:FSTTestQuery(path) + snapshot:std::move(viewSnapshot) + metadata:std::move(metadata)]; } NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTEventManagerTests.mm b/Firestore/Example/Tests/Core/FSTEventManagerTests.mm index e8f5955c3f1..e0ecfc577d0 100644 --- a/Firestore/Example/Tests/Core/FSTEventManagerTests.mm +++ b/Firestore/Example/Tests/Core/FSTEventManagerTests.mm @@ -19,15 +19,25 @@ #import #import +#include + #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Core/FSTSyncEngine.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" #include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" +using firebase::firestore::core::ViewSnapshot; +using firebase::firestore::core::ViewSnapshotHandler; +using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::DocumentSet; using firebase::firestore::model::OnlineState; +using firebase::firestore::util::StatusOr; NS_ASSUME_NONNULL_BEGIN @@ -51,11 +61,9 @@ @interface FSTEventManagerTests : XCTestCase @implementation FSTEventManagerTests - (FSTQueryListener *)noopListenerForQuery:(FSTQuery *)query { - return [[FSTQueryListener alloc] - initWithQuery:query - options:[FSTListenOptions defaultOptions] - viewSnapshotHandler:^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error){ - }]; + return [[FSTQueryListener alloc] initWithQuery:query + options:ListenOptions::DefaultOptions() + viewSnapshotHandler:[](const StatusOr &) {}]; } - (void)testHandlesManyListenersPerQuery { @@ -91,14 +99,18 @@ - (void)testHandlesUnlistenOnUnknownListenerGracefully { OCMVerifyAll((id)syncEngineMock); } -- (FSTQueryListener *)makeMockListenerForQuery:(FSTQuery *)query - viewSnapshotHandler:(void (^)())handler { - FSTQueryListener *listener = OCMClassMock([FSTQueryListener class]); - OCMStub([listener query]).andReturn(query); - OCMStub([listener queryDidChangeViewSnapshot:[OCMArg any]]).andDo(^(NSInvocation *invocation) { - handler(); - }); - return listener; +- (FSTQueryListener *)queryListenerForQuery:(FSTQuery *)query + withHandler:(ViewSnapshotHandler &&)handler { + return [[FSTQueryListener alloc] initWithQuery:query + options:ListenOptions::DefaultOptions() + viewSnapshotHandler:std::move(handler)]; +} + +- (ViewSnapshot)makeEmptyViewSnapshotWithQuery:(FSTQuery *)query { + DocumentSet emptyDocs{query.comparator}; + // sync_state_changed has to be `true` to prevent an assertion about a meaningless view snapshot. + return ViewSnapshot{ + query, emptyDocs, emptyDocs, {}, DocumentKeySet{}, false, /*sync_state_changed=*/true, false}; } - (void)testNotifiesListenersInTheRightOrder { @@ -106,20 +118,23 @@ - (void)testNotifiesListenersInTheRightOrder { FSTQuery *query2 = FSTTestQuery("bar/baz"); NSMutableArray *eventOrder = [NSMutableArray array]; - FSTQueryListener *listener1 = [self makeMockListenerForQuery:query1 - viewSnapshotHandler:^{ - [eventOrder addObject:@"listener1"]; - }]; + FSTQueryListener *listener1 = + [self queryListenerForQuery:query1 + withHandler:[eventOrder](const StatusOr &) { + [eventOrder addObject:@"listener1"]; + }]; - FSTQueryListener *listener2 = [self makeMockListenerForQuery:query2 - viewSnapshotHandler:^{ - [eventOrder addObject:@"listener2"]; - }]; + FSTQueryListener *listener2 = + [self queryListenerForQuery:query2 + withHandler:[eventOrder](const StatusOr &) { + [eventOrder addObject:@"listener2"]; + }]; - FSTQueryListener *listener3 = [self makeMockListenerForQuery:query1 - viewSnapshotHandler:^{ - [eventOrder addObject:@"listener3"]; - }]; + FSTQueryListener *listener3 = + [self queryListenerForQuery:query1 + withHandler:[eventOrder](const StatusOr &) { + [eventOrder addObject:@"listener3"]; + }]; FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]); FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock]; @@ -130,12 +145,9 @@ - (void)testNotifiesListenersInTheRightOrder { OCMVerify([syncEngineMock listenToQuery:query1]); OCMVerify([syncEngineMock listenToQuery:query2]); - FSTViewSnapshot *snapshot1 = OCMClassMock([FSTViewSnapshot class]); - OCMStub([snapshot1 query]).andReturn(query1); - FSTViewSnapshot *snapshot2 = OCMClassMock([FSTViewSnapshot class]); - OCMStub([snapshot2 query]).andReturn(query2); - - [eventManager handleViewSnapshots:@[ snapshot1, snapshot2 ]]; + ViewSnapshot snapshot1 = [self makeEmptyViewSnapshotWithQuery:query1]; + ViewSnapshot snapshot2 = [self makeEmptyViewSnapshotWithQuery:query2]; + [eventManager handleViewSnapshots:{snapshot1, snapshot2}]; NSArray *expected = @[ @"listener1", @"listener3", @"listener2" ]; XCTAssertEqualObjects(eventOrder, expected); diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm index 12b4f9ecaf0..234b0b82a19 100644 --- a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm +++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm @@ -15,62 +15,80 @@ */ #import + #include +#include +#include #import "Firestore/Source/Core/FSTEventManager.h" #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Core/FSTView.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" #import "Firestore/Source/Util/FSTAsyncQueryListener.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" +#include "Firestore/core/include/firebase/firestore/firestore_errors.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" #include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" +#include "Firestore/core/src/firebase/firestore/util/delayed_constructor.h" #include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" -#include "absl/memory/memory.h" - -using firebase::firestore::core::DocumentViewChangeType; +#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "Firestore/core/test/firebase/firestore/testutil/xcgmock.h" + +using firebase::firestore::FirestoreErrorCode; +using firebase::firestore::core::DocumentViewChange; +using firebase::firestore::core::ViewSnapshot; +using firebase::firestore::core::ViewSnapshotHandler; using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::DocumentSet; using firebase::firestore::model::OnlineState; +using firebase::firestore::remote::TargetChange; +using firebase::firestore::util::DelayedConstructor; using firebase::firestore::util::ExecutorLibdispatch; +using firebase::firestore::util::Status; +using firebase::firestore::util::StatusOr; +using testing::ElementsAre; +using testing::IsEmpty; NS_ASSUME_NONNULL_BEGIN +namespace { + +ViewSnapshot ExcludingMetadataChanges(const ViewSnapshot &snapshot) { + return ViewSnapshot{ + snapshot.query(), + snapshot.documents(), + snapshot.old_documents(), + snapshot.document_changes(), + snapshot.mutated_keys(), + snapshot.from_cache(), + snapshot.sync_state_changed(), + /*excludes_metadata_changes=*/true, + }; +} + +} // namespace + @interface FSTQueryListenerTests : XCTestCase @end @implementation FSTQueryListenerTests { - std::unique_ptr _executor; - FSTListenOptions *_includeMetadataChanges; + DelayedConstructor _executor; + ListenOptions _includeMetadataChanges; } - (void)setUp { - // TODO(varconst): moving this test to C++, it should be possible to store Executor as a value, - // not a pointer, and initialize it in the constructor. - _executor = absl::make_unique( - dispatch_queue_create("FSTQueryListenerTests Queue", DISPATCH_QUEUE_SERIAL)); - _includeMetadataChanges = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES - includeDocumentMetadataChanges:YES - waitForSyncWhenOnline:NO]; -} - -- (FSTViewSnapshot *)setExcludesMetadataChanges:(BOOL)excludesMetadataChanges - snapshot:(FSTViewSnapshot *)snapshot { - return [[FSTViewSnapshot alloc] initWithQuery:snapshot.query - documents:snapshot.documents - oldDocuments:snapshot.oldDocuments - documentChanges:snapshot.documentChanges - fromCache:snapshot.fromCache - mutatedKeys:snapshot.mutatedKeys - syncStateChanged:snapshot.syncStateChanged - excludesMetadataChanges:excludesMetadataChanges]; + _executor.Init(dispatch_queue_create("FSTQueryListenerTests Queue", DISPATCH_QUEUE_SERIAL)); + _includeMetadataChanges = ListenOptions::FromIncludeMetadataChanges(true); } - (void)testRaisesCollectionEvents { - NSMutableArray *accum = [NSMutableArray array]; - NSMutableArray *otherAccum = [NSMutableArray array]; + std::vector accum; + std::vector otherAccum; FSTQuery *query = FSTTestQuery("rooms"); FSTDocument *doc1 = FSTTestDoc("rooms/Eros", 1, @{@"name" : @"Eros"}, FSTDocumentStateSynced); @@ -80,99 +98,92 @@ - (void)testRaisesCollectionEvents { FSTQueryListener *listener = [self listenToQuery:query options:_includeMetadataChanges - accumulatingSnapshots:accum]; - FSTQueryListener *otherListener = [self listenToQuery:query accumulatingSnapshots:otherAccum]; + accumulatingSnapshots:&accum]; + FSTQueryListener *otherListener = [self listenToQuery:query accumulatingSnapshots:&otherAccum]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2prime ], nil); - - FSTDocumentViewChange *change1 = - [FSTDocumentViewChange changeWithDocument:doc1 type:DocumentViewChangeType::kAdded]; - FSTDocumentViewChange *change2 = - [FSTDocumentViewChange changeWithDocument:doc2 type:DocumentViewChangeType::kAdded]; - FSTDocumentViewChange *change3 = - [FSTDocumentViewChange changeWithDocument:doc2prime type:DocumentViewChangeType::kModified]; - FSTDocumentViewChange *change4 = - [FSTDocumentViewChange changeWithDocument:doc2prime type:DocumentViewChangeType::kAdded]; + ViewSnapshot snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt).value(); + ViewSnapshot snap2 = FSTTestApplyChanges(view, @[ doc2prime ], absl::nullopt).value(); + + DocumentViewChange change1{doc1, DocumentViewChange::Type::kAdded}; + DocumentViewChange change2{doc2, DocumentViewChange::Type::kAdded}; + DocumentViewChange change3{doc2prime, DocumentViewChange::Type::kModified}; + DocumentViewChange change4{doc2prime, DocumentViewChange::Type::kAdded}; [listener queryDidChangeViewSnapshot:snap1]; [listener queryDidChangeViewSnapshot:snap2]; [otherListener queryDidChangeViewSnapshot:snap2]; - XCTAssertEqualObjects(accum, (@[ snap1, snap2 ])); - XCTAssertEqualObjects(accum[0].documentChanges, (@[ change1, change2 ])); - XCTAssertEqualObjects(accum[1].documentChanges, (@[ change3 ])); - - FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] - initWithQuery:snap2.query - documents:snap2.documents - oldDocuments:[FSTDocumentSet documentSetWithComparator:snap2.query.comparator] - documentChanges:@[ change1, change4 ] - fromCache:snap2.fromCache - mutatedKeys:snap2.mutatedKeys - syncStateChanged:YES - excludesMetadataChanges:YES]; - XCTAssertEqualObjects(otherAccum, (@[ expectedSnap2 ])); + XC_ASSERT_THAT(accum, ElementsAre(snap1, snap2)); + XC_ASSERT_THAT(accum[0].document_changes(), ElementsAre(change1, change2)); + XC_ASSERT_THAT(accum[1].document_changes(), ElementsAre(change3)); + + ViewSnapshot expectedSnap2{snap2.query(), + snap2.documents(), + /*old_documents=*/DocumentSet{snap2.query().comparator}, + /*document_changes=*/{change1, change4}, + snap2.mutated_keys(), + snap2.from_cache(), + /*sync_state_changed=*/true, + /*excludes_metadata_changes=*/true}; + XC_ASSERT_THAT(otherAccum, ElementsAre(expectedSnap2)); } - (void)testRaisesErrorEvent { - NSMutableArray *accum = [NSMutableArray array]; + __block std::vector accum; FSTQuery *query = FSTTestQuery("rooms/Eros"); FSTQueryListener *listener = [self listenToQuery:query - handler:^(FSTViewSnapshot *snapshot, NSError *error) { - [accum addObject:error]; + handler:^(const StatusOr &maybe_snapshot) { + accum.push_back(maybe_snapshot.status()); }]; - NSError *testError = [NSError errorWithDomain:@"com.google.firestore.test" - code:42 - userInfo:@{@"some" : @"info"}]; + Status testError{FirestoreErrorCode::Unauthenticated, "Some info"}; [listener queryDidError:testError]; - XCTAssertEqualObjects(accum, @[ testError ]); + XC_ASSERT_THAT(accum, ElementsAre(testError)); } - (void)testRaisesEventForEmptyCollectionAfterSync { - NSMutableArray *accum = [NSMutableArray array]; + std::vector accum; FSTQuery *query = FSTTestQuery("rooms"); FSTQueryListener *listener = [self listenToQuery:query options:_includeMetadataChanges - accumulatingSnapshots:accum]; + accumulatingSnapshots:&accum]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], FSTTestTargetChangeMarkCurrent()); + ViewSnapshot snap1 = FSTTestApplyChanges(view, @[], absl::nullopt).value(); + ViewSnapshot snap2 = FSTTestApplyChanges(view, @[], FSTTestTargetChangeMarkCurrent()).value(); [listener queryDidChangeViewSnapshot:snap1]; - XCTAssertEqualObjects(accum, @[]); + XC_ASSERT_THAT(accum, IsEmpty()); [listener queryDidChangeViewSnapshot:snap2]; - XCTAssertEqualObjects(accum, @[ snap2 ]); + XC_ASSERT_THAT(accum, ElementsAre(snap2)); } - (void)testMutingAsyncListenerPreventsAllSubsequentEvents { - NSMutableArray *accum = [NSMutableArray array]; + __block std::vector accum; FSTQuery *query = FSTTestQuery("rooms/Eros"); FSTDocument *doc1 = FSTTestDoc("rooms/Eros", 3, @{@"name" : @"Eros"}, FSTDocumentStateSynced); FSTDocument *doc2 = FSTTestDoc("rooms/Eros", 4, @{@"name" : @"Eros2"}, FSTDocumentStateSynced); - __block FSTAsyncQueryListener *listener = - [[FSTAsyncQueryListener alloc] initWithExecutor:_executor.get() - snapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) { - [accum addObject:snapshot]; - [listener mute]; - }]; + __block FSTAsyncQueryListener *listener = [[FSTAsyncQueryListener alloc] + initWithExecutor:_executor.get() + snapshotHandler:^(const StatusOr &maybe_snapshot) { + accum.push_back(maybe_snapshot.ValueOrDie()); + [listener mute]; + }]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *viewSnapshot1 = FSTTestApplyChanges(view, @[ doc1 ], nil); - FSTViewSnapshot *viewSnapshot2 = FSTTestApplyChanges(view, @[ doc2 ], nil); + ViewSnapshot viewSnapshot1 = FSTTestApplyChanges(view, @[ doc1 ], absl::nullopt).value(); + ViewSnapshot viewSnapshot2 = FSTTestApplyChanges(view, @[ doc2 ], absl::nullopt).value(); - FSTViewSnapshotHandler handler = listener.asyncSnapshotHandler; - handler(viewSnapshot1, nil); - handler(viewSnapshot2, nil); + ViewSnapshotHandler handler = listener.asyncSnapshotHandler; + handler(viewSnapshot1); + handler(viewSnapshot2); // Drain queue XCTestExpectation *expectation = [self expectationWithDescription:@"Queue drained"]; @@ -186,29 +197,29 @@ - (void)testMutingAsyncListenerPreventsAllSubsequentEvents { }]; // We should get the first snapshot but not the second. - XCTAssertEqualObjects(accum, @[ viewSnapshot1 ]); + XC_ASSERT_THAT(accum, ElementsAre(viewSnapshot1)); } - (void)testDoesNotRaiseEventsForMetadataChangesUnlessSpecified { - NSMutableArray *filteredAccum = [NSMutableArray array]; - NSMutableArray *fullAccum = [NSMutableArray array]; + std::vector filteredAccum; + std::vector fullAccum; FSTQuery *query = FSTTestQuery("rooms"); FSTDocument *doc1 = FSTTestDoc("rooms/Eros", 1, @{@"name" : @"Eros"}, FSTDocumentStateSynced); FSTDocument *doc2 = FSTTestDoc("rooms/Hades", 2, @{@"name" : @"Hades"}, FSTDocumentStateSynced); FSTQueryListener *filteredListener = [self listenToQuery:query - accumulatingSnapshots:filteredAccum]; + accumulatingSnapshots:&filteredAccum]; FSTQueryListener *fullListener = [self listenToQuery:query options:_includeMetadataChanges - accumulatingSnapshots:fullAccum]; + accumulatingSnapshots:&fullAccum]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); + ViewSnapshot snap1 = FSTTestApplyChanges(view, @[ doc1 ], absl::nullopt).value(); - FSTTargetChange *ackTarget = FSTTestTargetChangeAckDocuments({doc1.key}); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget); - FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc2 ], nil); + TargetChange ackTarget = FSTTestTargetChangeAckDocuments({doc1.key}); + ViewSnapshot snap2 = FSTTestApplyChanges(view, @[], ackTarget).value(); + ViewSnapshot snap3 = FSTTestApplyChanges(view, @[ doc2 ], absl::nullopt).value(); [filteredListener queryDidChangeViewSnapshot:snap1]; // local event [filteredListener queryDidChangeViewSnapshot:snap2]; // no event @@ -218,16 +229,14 @@ - (void)testDoesNotRaiseEventsForMetadataChangesUnlessSpecified { [fullListener queryDidChangeViewSnapshot:snap2]; // state change event [fullListener queryDidChangeViewSnapshot:snap3]; // doc2 update - XCTAssertEqualObjects(filteredAccum, (@[ - [self setExcludesMetadataChanges:YES snapshot:snap1], - [self setExcludesMetadataChanges:YES snapshot:snap3] - ])); - XCTAssertEqualObjects(fullAccum, (@[ snap1, snap2, snap3 ])); + XC_ASSERT_THAT(filteredAccum, + ElementsAre(ExcludingMetadataChanges(snap1), ExcludingMetadataChanges(snap3))); + XC_ASSERT_THAT(fullAccum, ElementsAre(snap1, snap2, snap3)); } - (void)testRaisesDocumentMetadataEventsOnlyWhenSpecified { - NSMutableArray *filteredAccum = [NSMutableArray array]; - NSMutableArray *fullAccum = [NSMutableArray array]; + std::vector filteredAccum; + std::vector fullAccum; FSTQuery *query = FSTTestQuery("rooms"); FSTDocument *doc1 = @@ -237,29 +246,26 @@ - (void)testRaisesDocumentMetadataEventsOnlyWhenSpecified { FSTTestDoc("rooms/Eros", 1, @{@"name" : @"Eros"}, FSTDocumentStateSynced); FSTDocument *doc3 = FSTTestDoc("rooms/Other", 3, @{@"name" : @"Other"}, FSTDocumentStateSynced); - FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO - includeDocumentMetadataChanges:YES - waitForSyncWhenOnline:NO]; + ListenOptions options( + /*include_query_metadata_changes=*/false, + /*include_document_metadata_changes=*/true, + /*wait_for_sync_when_online=*/false); FSTQueryListener *filteredListener = [self listenToQuery:query - accumulatingSnapshots:filteredAccum]; + accumulatingSnapshots:&filteredAccum]; FSTQueryListener *fullListener = [self listenToQuery:query options:options - accumulatingSnapshots:fullAccum]; + accumulatingSnapshots:&fullAccum]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); - FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); - - FSTDocumentViewChange *change1 = - [FSTDocumentViewChange changeWithDocument:doc1 type:DocumentViewChangeType::kAdded]; - FSTDocumentViewChange *change2 = - [FSTDocumentViewChange changeWithDocument:doc2 type:DocumentViewChangeType::kAdded]; - FSTDocumentViewChange *change3 = - [FSTDocumentViewChange changeWithDocument:doc1Prime type:DocumentViewChangeType::kMetadata]; - FSTDocumentViewChange *change4 = - [FSTDocumentViewChange changeWithDocument:doc3 type:DocumentViewChangeType::kAdded]; + ViewSnapshot snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt).value(); + ViewSnapshot snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], absl::nullopt).value(); + ViewSnapshot snap3 = FSTTestApplyChanges(view, @[ doc3 ], absl::nullopt).value(); + + DocumentViewChange change1{doc1, DocumentViewChange::Type::kAdded}; + DocumentViewChange change2{doc2, DocumentViewChange::Type::kAdded}; + DocumentViewChange change3{doc1Prime, DocumentViewChange::Type::kMetadata}; + DocumentViewChange change4{doc3, DocumentViewChange::Type::kAdded}; [filteredListener queryDidChangeViewSnapshot:snap1]; [filteredListener queryDidChangeViewSnapshot:snap2]; @@ -268,21 +274,19 @@ - (void)testRaisesDocumentMetadataEventsOnlyWhenSpecified { [fullListener queryDidChangeViewSnapshot:snap2]; [fullListener queryDidChangeViewSnapshot:snap3]; - XCTAssertEqualObjects(filteredAccum, (@[ - [self setExcludesMetadataChanges:YES snapshot:snap1], - [self setExcludesMetadataChanges:YES snapshot:snap3] - ])); - XCTAssertEqualObjects(filteredAccum[0].documentChanges, (@[ change1, change2 ])); - XCTAssertEqualObjects(filteredAccum[1].documentChanges, (@[ change4 ])); - - XCTAssertEqualObjects(fullAccum, (@[ snap1, snap2, snap3 ])); - XCTAssertEqualObjects(fullAccum[0].documentChanges, (@[ change1, change2 ])); - XCTAssertEqualObjects(fullAccum[1].documentChanges, (@[ change3 ])); - XCTAssertEqualObjects(fullAccum[2].documentChanges, (@[ change4 ])); + XC_ASSERT_THAT(filteredAccum, + ElementsAre(ExcludingMetadataChanges(snap1), ExcludingMetadataChanges(snap3))); + XC_ASSERT_THAT(filteredAccum[0].document_changes(), ElementsAre(change1, change2)); + XC_ASSERT_THAT(filteredAccum[1].document_changes(), ElementsAre(change4)); + + XC_ASSERT_THAT(fullAccum, ElementsAre(snap1, snap2, snap3)); + XC_ASSERT_THAT(fullAccum[0].document_changes(), ElementsAre(change1, change2)); + XC_ASSERT_THAT(fullAccum[1].document_changes(), ElementsAre(change3)); + XC_ASSERT_THAT(fullAccum[2].document_changes(), ElementsAre(change4)); } - (void)testRaisesQueryMetadataEventsOnlyWhenHasPendingWritesOnTheQueryChanges { - NSMutableArray *fullAccum = [NSMutableArray array]; + std::vector fullAccum; FSTQuery *query = FSTTestQuery("rooms"); FSTDocument *doc1 = @@ -295,41 +299,42 @@ - (void)testRaisesQueryMetadataEventsOnlyWhenHasPendingWritesOnTheQueryChanges { FSTTestDoc("rooms/Hades", 2, @{@"name" : @"Hades"}, FSTDocumentStateSynced); FSTDocument *doc3 = FSTTestDoc("rooms/Other", 3, @{@"name" : @"Other"}, FSTDocumentStateSynced); - FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES - includeDocumentMetadataChanges:NO - waitForSyncWhenOnline:NO]; + ListenOptions options( + /*include_query_metadata_changes=*/true, + /*include_document_metadata_changes=*/false, + /*wait_for_sync_when_online=*/false); FSTQueryListener *fullListener = [self listenToQuery:query options:options - accumulatingSnapshots:fullAccum]; + accumulatingSnapshots:&fullAccum]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); - FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); - FSTViewSnapshot *snap4 = FSTTestApplyChanges(view, @[ doc2Prime ], nil); + ViewSnapshot snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt).value(); + ViewSnapshot snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], absl::nullopt).value(); + ViewSnapshot snap3 = FSTTestApplyChanges(view, @[ doc3 ], absl::nullopt).value(); + ViewSnapshot snap4 = FSTTestApplyChanges(view, @[ doc2Prime ], absl::nullopt).value(); [fullListener queryDidChangeViewSnapshot:snap1]; [fullListener queryDidChangeViewSnapshot:snap2]; // Emits no events. [fullListener queryDidChangeViewSnapshot:snap3]; [fullListener queryDidChangeViewSnapshot:snap4]; // Metadata change event. - FSTViewSnapshot *expectedSnap4 = - [[FSTViewSnapshot alloc] initWithQuery:snap4.query - documents:snap4.documents - oldDocuments:snap3.documents - documentChanges:@[] - fromCache:snap4.fromCache - mutatedKeys:snap4.mutatedKeys - syncStateChanged:snap4.syncStateChanged - excludesMetadataChanges:YES]; // This test excludes document metadata changes - XCTAssertEqualObjects(fullAccum, (@[ - [self setExcludesMetadataChanges:YES snapshot:snap1], - [self setExcludesMetadataChanges:YES snapshot:snap3], expectedSnap4 - ])); + ViewSnapshot expectedSnap4{ + snap4.query(), + snap4.documents(), + snap3.documents(), + /*document_changes=*/{}, + snap4.mutated_keys(), + snap4.from_cache(), + snap4.sync_state_changed(), + /*excludes_metadata_changes=*/true // This test excludes document metadata changes + }; + + XC_ASSERT_THAT(fullAccum, ElementsAre(ExcludingMetadataChanges(snap1), + ExcludingMetadataChanges(snap3), expectedSnap4)); } - (void)testMetadataOnlyDocumentChangesAreFilteredOutWhenIncludeDocumentMetadataChangesIsFalse { - NSMutableArray *filteredAccum = [NSMutableArray array]; + std::vector filteredAccum; FSTQuery *query = FSTTestQuery("rooms"); FSTDocument *doc1 = @@ -340,48 +345,48 @@ - (void)testMetadataOnlyDocumentChangesAreFilteredOutWhenIncludeDocumentMetadata FSTDocument *doc3 = FSTTestDoc("rooms/Other", 3, @{@"name" : @"Other"}, FSTDocumentStateSynced); FSTQueryListener *filteredListener = [self listenToQuery:query - accumulatingSnapshots:filteredAccum]; + accumulatingSnapshots:&filteredAccum]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime, doc3 ], nil); + ViewSnapshot snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt).value(); + ViewSnapshot snap2 = FSTTestApplyChanges(view, @[ doc1Prime, doc3 ], absl::nullopt).value(); - FSTDocumentViewChange *change3 = - [FSTDocumentViewChange changeWithDocument:doc3 type:DocumentViewChangeType::kAdded]; + DocumentViewChange change3{doc3, DocumentViewChange::Type::kAdded}; [filteredListener queryDidChangeViewSnapshot:snap1]; [filteredListener queryDidChangeViewSnapshot:snap2]; - FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] initWithQuery:snap2.query - documents:snap2.documents - oldDocuments:snap1.documents - documentChanges:@[ change3 ] - fromCache:snap2.isFromCache - mutatedKeys:snap2.mutatedKeys - syncStateChanged:snap2.syncStateChanged - excludesMetadataChanges:YES]; - XCTAssertEqualObjects(filteredAccum, - (@[ [self setExcludesMetadataChanges:YES snapshot:snap1], expectedSnap2 ])); + ViewSnapshot expectedSnap2{snap2.query(), + snap2.documents(), + snap1.documents(), + /*document_changes=*/{change3}, + snap2.mutated_keys(), + snap2.from_cache(), + snap2.sync_state_changed(), + /*excludes_metadata_changes=*/true}; + XC_ASSERT_THAT(filteredAccum, ElementsAre(ExcludingMetadataChanges(snap1), expectedSnap2)); } - (void)testWillWaitForSyncIfOnline { - NSMutableArray *events = [NSMutableArray array]; + std::vector events; FSTQuery *query = FSTTestQuery("rooms"); FSTDocument *doc1 = FSTTestDoc("rooms/Eros", 1, @{@"name" : @"Eros"}, FSTDocumentStateSynced); FSTDocument *doc2 = FSTTestDoc("rooms/Hades", 2, @{@"name" : @"Hades"}, FSTDocumentStateSynced); - FSTQueryListener *listener = - [self listenToQuery:query - options:[[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO - includeDocumentMetadataChanges:NO - waitForSyncWhenOnline:YES] - accumulatingSnapshots:events]; + + ListenOptions options( + /*include_query_metadata_changes=*/false, + /*include_document_metadata_changes=*/false, + /*wait_for_sync_when_online=*/true); + FSTQueryListener *listener = [self listenToQuery:query + options:options + accumulatingSnapshots:&events]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); - FSTViewSnapshot *snap3 = - FSTTestApplyChanges(view, @[], FSTTestTargetChangeAckDocuments({doc1.key, doc2.key})); + ViewSnapshot snap1 = FSTTestApplyChanges(view, @[ doc1 ], absl::nullopt).value(); + ViewSnapshot snap2 = FSTTestApplyChanges(view, @[ doc2 ], absl::nullopt).value(); + ViewSnapshot snap3 = + FSTTestApplyChanges(view, @[], FSTTestTargetChangeAckDocuments({doc1.key, doc2.key})).value(); [listener applyChangedOnlineState:OnlineState::Online]; // no event [listener queryDidChangeViewSnapshot:snap1]; @@ -390,38 +395,38 @@ - (void)testWillWaitForSyncIfOnline { [listener queryDidChangeViewSnapshot:snap2]; [listener queryDidChangeViewSnapshot:snap3]; - FSTDocumentViewChange *change1 = - [FSTDocumentViewChange changeWithDocument:doc1 type:DocumentViewChangeType::kAdded]; - FSTDocumentViewChange *change2 = - [FSTDocumentViewChange changeWithDocument:doc2 type:DocumentViewChangeType::kAdded]; - FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] - initWithQuery:snap3.query - documents:snap3.documents - oldDocuments:[FSTDocumentSet documentSetWithComparator:snap3.query.comparator] - documentChanges:@[ change1, change2 ] - fromCache:NO - mutatedKeys:snap3.mutatedKeys - syncStateChanged:YES - excludesMetadataChanges:YES]; - XCTAssertEqualObjects(events, (@[ expectedSnap ])); + DocumentViewChange change1{doc1, DocumentViewChange::Type::kAdded}; + DocumentViewChange change2{doc2, DocumentViewChange::Type::kAdded}; + ViewSnapshot expectedSnap{snap3.query(), + snap3.documents(), + /*old_documents=*/DocumentSet{snap3.query().comparator}, + /*document_changes=*/{change1, change2}, + snap3.mutated_keys(), + /*from_cache=*/false, + /*sync_state_changed=*/true, + /*excludes_metadata_changes=*/true}; + XC_ASSERT_THAT(events, ElementsAre(expectedSnap)); } - (void)testWillRaiseInitialEventWhenGoingOffline { - NSMutableArray *events = [NSMutableArray array]; + std::vector events; FSTQuery *query = FSTTestQuery("rooms"); FSTDocument *doc1 = FSTTestDoc("rooms/Eros", 1, @{@"name" : @"Eros"}, FSTDocumentStateSynced); FSTDocument *doc2 = FSTTestDoc("rooms/Hades", 2, @{@"name" : @"Hades"}, FSTDocumentStateSynced); - FSTQueryListener *listener = - [self listenToQuery:query - options:[[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO - includeDocumentMetadataChanges:NO - waitForSyncWhenOnline:YES] - accumulatingSnapshots:events]; + + ListenOptions options( + /*include_query_metadata_changes=*/false, + /*include_document_metadata_changes=*/false, + /*wait_for_sync_when_online=*/true); + + FSTQueryListener *listener = [self listenToQuery:query + options:options + accumulatingSnapshots:&events]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); + ViewSnapshot snap1 = FSTTestApplyChanges(view, @[ doc1 ], absl::nullopt).value(); + ViewSnapshot snap2 = FSTTestApplyChanges(view, @[ doc2 ], absl::nullopt).value(); [listener applyChangedOnlineState:OnlineState::Online]; // no event [listener queryDidChangeViewSnapshot:snap1]; // no event @@ -430,103 +435,99 @@ - (void)testWillRaiseInitialEventWhenGoingOffline { [listener applyChangedOnlineState:OnlineState::Offline]; // no event [listener queryDidChangeViewSnapshot:snap2]; // another event - FSTDocumentViewChange *change1 = - [FSTDocumentViewChange changeWithDocument:doc1 type:DocumentViewChangeType::kAdded]; - FSTDocumentViewChange *change2 = - [FSTDocumentViewChange changeWithDocument:doc2 type:DocumentViewChangeType::kAdded]; - FSTViewSnapshot *expectedSnap1 = [[FSTViewSnapshot alloc] - initWithQuery:query - documents:snap1.documents - oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator] - documentChanges:@[ change1 ] - fromCache:YES - mutatedKeys:snap1.mutatedKeys - syncStateChanged:YES - excludesMetadataChanges:YES]; - FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] initWithQuery:query - documents:snap2.documents - oldDocuments:snap1.documents - documentChanges:@[ change2 ] - fromCache:YES - mutatedKeys:snap2.mutatedKeys - syncStateChanged:NO - excludesMetadataChanges:YES]; - XCTAssertEqualObjects(events, (@[ expectedSnap1, expectedSnap2 ])); + DocumentViewChange change1{doc1, DocumentViewChange::Type::kAdded}; + DocumentViewChange change2{doc2, DocumentViewChange::Type::kAdded}; + ViewSnapshot expectedSnap1{query, + /*documents=*/snap1.documents(), + /*old_documents=*/DocumentSet{snap1.query().comparator}, + /*document_changes=*/{change1}, + snap1.mutated_keys(), + /*from_cache=*/true, + /*sync_state_changed=*/true, + /*excludes_metadata_changes=*/true}; + + ViewSnapshot expectedSnap2{query, + /*documents=*/snap2.documents(), + /*old_documents=*/snap1.documents(), + /*document_changes=*/{change2}, + snap2.mutated_keys(), + /*from_cache=*/true, + /*sync_state_changed=*/false, + /*excludes_metadata_changes=*/true}; + XC_ASSERT_THAT(events, ElementsAre(expectedSnap1, expectedSnap2)); } - (void)testWillRaiseInitialEventWhenGoingOfflineAndThereAreNoDocs { - NSMutableArray *events = [NSMutableArray array]; + std::vector events; FSTQuery *query = FSTTestQuery("rooms"); FSTQueryListener *listener = [self listenToQuery:query - options:[FSTListenOptions defaultOptions] - accumulatingSnapshots:events]; + options:ListenOptions::DefaultOptions() + accumulatingSnapshots:&events]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); + ViewSnapshot snap1 = FSTTestApplyChanges(view, @[], absl::nullopt).value(); [listener applyChangedOnlineState:OnlineState::Online]; // no event [listener queryDidChangeViewSnapshot:snap1]; // no event [listener applyChangedOnlineState:OnlineState::Offline]; // event - FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] - initWithQuery:query - documents:snap1.documents - oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator] - documentChanges:@[] - fromCache:YES - mutatedKeys:snap1.mutatedKeys - syncStateChanged:YES - excludesMetadataChanges:YES]; - XCTAssertEqualObjects(events, (@[ expectedSnap ])); + ViewSnapshot expectedSnap{query, + /*documents=*/snap1.documents(), + /*old_documents=*/DocumentSet{snap1.query().comparator}, + /*document_changes=*/{}, + snap1.mutated_keys(), + /*from_cache=*/true, + /*sync_state_changed=*/true, + /*excludes_metadata_changes=*/true}; + XC_ASSERT_THAT(events, ElementsAre(expectedSnap)); } - (void)testWillRaiseInitialEventWhenStartingOfflineAndThereAreNoDocs { - NSMutableArray *events = [NSMutableArray array]; + std::vector events; FSTQuery *query = FSTTestQuery("rooms"); FSTQueryListener *listener = [self listenToQuery:query - options:[FSTListenOptions defaultOptions] - accumulatingSnapshots:events]; + options:ListenOptions::DefaultOptions() + accumulatingSnapshots:&events]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); + ViewSnapshot snap1 = FSTTestApplyChanges(view, @[], absl::nullopt).value(); [listener applyChangedOnlineState:OnlineState::Offline]; // no event [listener queryDidChangeViewSnapshot:snap1]; // event - FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc] - initWithQuery:query - documents:snap1.documents - oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator] - documentChanges:@[] - fromCache:YES - mutatedKeys:snap1.mutatedKeys - syncStateChanged:YES - excludesMetadataChanges:YES]; - XCTAssertEqualObjects(events, (@[ expectedSnap ])); + ViewSnapshot expectedSnap{query, + /*documents=*/snap1.documents(), + /*old_documents=*/DocumentSet{snap1.query().comparator}, + /*document_changes=*/{}, + snap1.mutated_keys(), + /*from_cache=*/true, + /*sync_state_changed=*/true, + /*excludes_metadata_changes=*/true}; + XC_ASSERT_THAT(events, ElementsAre(expectedSnap)); } -- (FSTQueryListener *)listenToQuery:(FSTQuery *)query handler:(FSTViewSnapshotHandler)handler { +- (FSTQueryListener *)listenToQuery:(FSTQuery *)query handler:(ViewSnapshotHandler &&)handler { return [[FSTQueryListener alloc] initWithQuery:query - options:[FSTListenOptions defaultOptions] - viewSnapshotHandler:handler]; + options:ListenOptions::DefaultOptions() + viewSnapshotHandler:std::move(handler)]; } - (FSTQueryListener *)listenToQuery:(FSTQuery *)query - options:(FSTListenOptions *)options - accumulatingSnapshots:(NSMutableArray *)values { + options:(ListenOptions)options + accumulatingSnapshots:(std::vector *)values { return [[FSTQueryListener alloc] initWithQuery:query options:options - viewSnapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) { - [values addObject:snapshot]; + viewSnapshotHandler:^(const StatusOr &maybe_snapshot) { + values->push_back(maybe_snapshot.ValueOrDie()); }]; } - (FSTQueryListener *)listenToQuery:(FSTQuery *)query - accumulatingSnapshots:(NSMutableArray *)values { + accumulatingSnapshots:(std::vector *)values { return [self listenToQuery:query - options:[FSTListenOptions defaultOptions] + options:ListenOptions::DefaultOptions() accumulatingSnapshots:values]; } diff --git a/Firestore/Example/Tests/Core/FSTViewSnapshotTest.mm b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.mm index 3847b09dffe..bd13b0ccccb 100644 --- a/Firestore/Example/Tests/Core/FSTViewSnapshotTest.mm +++ b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.mm @@ -14,17 +14,23 @@ * limitations under the License. */ -#import "Firestore/Source/Core/FSTViewSnapshot.h" - #import +#include + #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" -using firebase::firestore::core::DocumentViewChangeType; +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" + +using firebase::firestore::core::DocumentViewChange; +using firebase::firestore::core::DocumentViewChangeSet; +using firebase::firestore::core::ViewSnapshot; +using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::DocumentSet; NS_ASSUME_NONNULL_BEGIN @@ -35,14 +41,14 @@ @implementation FSTViewSnapshotTests - (void)testDocumentChangeConstructor { FSTDocument *doc = FSTTestDoc("a/b", 0, @{}, FSTDocumentStateSynced); - DocumentViewChangeType type = DocumentViewChangeType::kModified; - FSTDocumentViewChange *change = [FSTDocumentViewChange changeWithDocument:doc type:type]; - XCTAssertEqual(change.document, doc); - XCTAssertEqual(change.type, type); + DocumentViewChange::Type type = DocumentViewChange::Type::kModified; + DocumentViewChange change{doc, type}; + XCTAssertEqual(change.document(), doc); + XCTAssertEqual(change.type(), type); } - (void)testTrack { - FSTDocumentViewChangeSet *set = [FSTDocumentViewChangeSet changeSet]; + DocumentViewChangeSet set; FSTDocument *docAdded = FSTTestDoc("a/1", 0, @{}, FSTDocumentStateSynced); FSTDocument *docRemoved = FSTTestDoc("a/2", 0, @{}, FSTDocumentStateSynced); @@ -54,89 +60,73 @@ - (void)testTrack { FSTDocument *docModifiedThenRemoved = FSTTestDoc("b/4", 0, @{}, FSTDocumentStateSynced); FSTDocument *docModifiedThenModified = FSTTestDoc("b/5", 0, @{}, FSTDocumentStateSynced); - [set addChange:[FSTDocumentViewChange changeWithDocument:docAdded - type:DocumentViewChangeType::kAdded]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docRemoved - type:DocumentViewChangeType::kRemoved]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docModified - type:DocumentViewChangeType::kModified]]; - - [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenModified - type:DocumentViewChangeType::kAdded]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenModified - type:DocumentViewChangeType::kModified]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenRemoved - type:DocumentViewChangeType::kAdded]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenRemoved - type:DocumentViewChangeType::kRemoved]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docRemovedThenAdded - type:DocumentViewChangeType::kRemoved]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docRemovedThenAdded - type:DocumentViewChangeType::kAdded]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenRemoved - type:DocumentViewChangeType::kModified]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenRemoved - type:DocumentViewChangeType::kRemoved]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenModified - type:DocumentViewChangeType::kModified]]; - [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenModified - type:DocumentViewChangeType::kModified]]; - - NSArray *changes = [set changes]; - XCTAssertEqual(changes.count, 7); - - XCTAssertEqual(changes[0].document, docAdded); - XCTAssertEqual(changes[0].type, DocumentViewChangeType::kAdded); - - XCTAssertEqual(changes[1].document, docRemoved); - XCTAssertEqual(changes[1].type, DocumentViewChangeType::kRemoved); - - XCTAssertEqual(changes[2].document, docModified); - XCTAssertEqual(changes[2].type, DocumentViewChangeType::kModified); - - XCTAssertEqual(changes[3].document, docAddedThenModified); - XCTAssertEqual(changes[3].type, DocumentViewChangeType::kAdded); - - XCTAssertEqual(changes[4].document, docRemovedThenAdded); - XCTAssertEqual(changes[4].type, DocumentViewChangeType::kModified); - - XCTAssertEqual(changes[5].document, docModifiedThenRemoved); - XCTAssertEqual(changes[5].type, DocumentViewChangeType::kRemoved); - - XCTAssertEqual(changes[6].document, docModifiedThenModified); - XCTAssertEqual(changes[6].type, DocumentViewChangeType::kModified); + set.AddChange(DocumentViewChange{docAdded, DocumentViewChange::Type::kAdded}); + set.AddChange(DocumentViewChange{docRemoved, DocumentViewChange::Type::kRemoved}); + set.AddChange(DocumentViewChange{docModified, DocumentViewChange::Type::kModified}); + set.AddChange(DocumentViewChange{docAddedThenModified, DocumentViewChange::Type::kAdded}); + set.AddChange(DocumentViewChange{docAddedThenModified, DocumentViewChange::Type::kModified}); + set.AddChange(DocumentViewChange{docAddedThenRemoved, DocumentViewChange::Type::kAdded}); + set.AddChange(DocumentViewChange{docAddedThenRemoved, DocumentViewChange::Type::kRemoved}); + set.AddChange(DocumentViewChange{docRemovedThenAdded, DocumentViewChange::Type::kRemoved}); + set.AddChange(DocumentViewChange{docRemovedThenAdded, DocumentViewChange::Type::kAdded}); + set.AddChange(DocumentViewChange{docModifiedThenRemoved, DocumentViewChange::Type::kModified}); + set.AddChange(DocumentViewChange{docModifiedThenRemoved, DocumentViewChange::Type::kRemoved}); + set.AddChange(DocumentViewChange{docModifiedThenModified, DocumentViewChange::Type::kModified}); + set.AddChange(DocumentViewChange{docModifiedThenModified, DocumentViewChange::Type::kModified}); + + std::vector changes = set.GetChanges(); + XCTAssertEqual(changes.size(), 7); + + XCTAssertEqual(changes[0].document(), docAdded); + XCTAssertEqual(changes[0].type(), DocumentViewChange::Type::kAdded); + + XCTAssertEqual(changes[1].document(), docRemoved); + XCTAssertEqual(changes[1].type(), DocumentViewChange::Type::kRemoved); + + XCTAssertEqual(changes[2].document(), docModified); + XCTAssertEqual(changes[2].type(), DocumentViewChange::Type::kModified); + + XCTAssertEqual(changes[3].document(), docAddedThenModified); + XCTAssertEqual(changes[3].type(), DocumentViewChange::Type::kAdded); + + XCTAssertEqual(changes[4].document(), docRemovedThenAdded); + XCTAssertEqual(changes[4].type(), DocumentViewChange::Type::kModified); + + XCTAssertEqual(changes[5].document(), docModifiedThenRemoved); + XCTAssertEqual(changes[5].type(), DocumentViewChange::Type::kRemoved); + + XCTAssertEqual(changes[6].document(), docModifiedThenModified); + XCTAssertEqual(changes[6].type(), DocumentViewChange::Type::kModified); } - (void)testViewSnapshotConstructor { FSTQuery *query = FSTTestQuery("a"); - FSTDocumentSet *documents = [FSTDocumentSet documentSetWithComparator:FSTDocumentComparatorByKey]; - FSTDocumentSet *oldDocuments = documents; - documents = - [documents documentSetByAddingDocument:FSTTestDoc("c/a", 1, @{}, FSTDocumentStateSynced)]; - NSArray *documentChanges = - @[ [FSTDocumentViewChange changeWithDocument:FSTTestDoc("c/a", 1, @{}, FSTDocumentStateSynced) - type:DocumentViewChangeType::kAdded] ]; - - BOOL fromCache = YES; + DocumentSet documents = DocumentSet{FSTDocumentComparatorByKey}; + DocumentSet oldDocuments = documents; + documents = documents.insert(FSTTestDoc("c/a", 1, @{}, FSTDocumentStateSynced)); + std::vector documentChanges{DocumentViewChange{ + FSTTestDoc("c/a", 1, @{}, FSTDocumentStateSynced), DocumentViewChange::Type::kAdded}}; + + bool fromCache = true; DocumentKeySet mutatedKeys; - BOOL syncStateChanged = YES; - - FSTViewSnapshot *snapshot = [[FSTViewSnapshot alloc] initWithQuery:query - documents:documents - oldDocuments:oldDocuments - documentChanges:documentChanges - fromCache:fromCache - mutatedKeys:mutatedKeys - syncStateChanged:syncStateChanged - excludesMetadataChanges:NO]; - - XCTAssertEqual(snapshot.query, query); - XCTAssertEqual(snapshot.documents, documents); - XCTAssertEqual(snapshot.oldDocuments, oldDocuments); - XCTAssertEqual(snapshot.documentChanges, documentChanges); - XCTAssertEqual(snapshot.fromCache, fromCache); - XCTAssertEqual(snapshot.mutatedKeys, mutatedKeys); - XCTAssertEqual(snapshot.syncStateChanged, syncStateChanged); + bool syncStateChanged = true; + + ViewSnapshot snapshot{query, + documents, + oldDocuments, + documentChanges, + mutatedKeys, + fromCache, + syncStateChanged, + /*excludes_metadata_changes=*/false}; + + XCTAssertEqual(snapshot.query(), query); + XCTAssertEqual(snapshot.documents(), documents); + XCTAssertEqual(snapshot.old_documents(), oldDocuments); + XCTAssertEqual(snapshot.document_changes(), documentChanges); + XCTAssertEqual(snapshot.from_cache(), fromCache); + XCTAssertEqual(snapshot.mutated_keys(), mutatedKeys); + XCTAssertEqual(snapshot.sync_state_changed(), syncStateChanged); } @end diff --git a/Firestore/Example/Tests/Core/FSTViewTests.mm b/Firestore/Example/Tests/Core/FSTViewTests.mm index 9173b3ce579..4245d072e6f 100644 --- a/Firestore/Example/Tests/Core/FSTViewTests.mm +++ b/Firestore/Example/Tests/Core/FSTViewTests.mm @@ -18,26 +18,56 @@ #import +#include +#include +#include + #import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" +#include "Firestore/core/test/firebase/firestore/testutil/xcgmock.h" +#include "absl/types/optional.h" namespace testutil = firebase::firestore::testutil; -using firebase::firestore::core::DocumentViewChangeType; +using firebase::firestore::core::DocumentViewChange; +using firebase::firestore::core::ViewSnapshot; using firebase::firestore::model::ResourcePath; using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::DocumentSet; +using testing::ElementsAre; NS_ASSUME_NONNULL_BEGIN +/** + * A custom matcher that verifies that the subject has the same keys as the given documents without + * verifying that the contents are the same. + */ +MATCHER_P(ContainsDocs, expected, "") { + if (expected.size() != arg.size()) { + return false; + } + for (FSTDocument *doc : expected) { + if (!arg.ContainsKey(doc.key)) { + return false; + } + } + return true; +} + +/** Constructs `ContainsDocs` instances with an initializer list. */ +inline ContainsDocsMatcherP> ContainsDocs( + std::vector docs) { + return ContainsDocsMatcherP>(docs); +} + @interface FSTViewTests : XCTestCase @end @@ -59,22 +89,23 @@ - (void)testAddsDocumentsBasedOnQuery { FSTDocument *doc3 = FSTTestDoc("rooms/other/messages/1", 0, @{@"text" : @"msg3"}, FSTDocumentStateSynced); - FSTViewSnapshot *_Nullable snapshot = FSTTestApplyChanges( + absl::optional maybe_snapshot = FSTTestApplyChanges( view, @[ doc1, doc2, doc3 ], FSTTestTargetChangeAckDocuments({doc1.key, doc2.key, doc3.key})); + XCTAssertTrue(maybe_snapshot.has_value()); + ViewSnapshot snapshot = std::move(maybe_snapshot).value(); - XCTAssertEqual(snapshot.query, query); + XCTAssertEqual(snapshot.query(), query); - XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ])); + XC_ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc2)); - XCTAssertEqualObjects( - snapshot.documentChanges, (@[ - [FSTDocumentViewChange changeWithDocument:doc1 type:DocumentViewChangeType::kAdded], - [FSTDocumentViewChange changeWithDocument:doc2 type:DocumentViewChangeType::kAdded] - ])); + XCTAssertTrue(( + snapshot.document_changes() == + std::vector{DocumentViewChange{doc1, DocumentViewChange::Type::kAdded}, + DocumentViewChange{doc2, DocumentViewChange::Type::kAdded}})); - XCTAssertFalse(snapshot.isFromCache); - XCTAssertFalse(snapshot.hasPendingWrites); - XCTAssertTrue(snapshot.syncStateChanged); + XCTAssertFalse(snapshot.from_cache()); + XCTAssertFalse(snapshot.has_pending_writes()); + XCTAssertTrue(snapshot.sync_state_changed()); } - (void)testRemovesDocuments { @@ -89,25 +120,26 @@ - (void)testRemovesDocuments { FSTTestDoc("rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, FSTDocumentStateSynced); // initial state - FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt); // delete doc2, add doc3 - FSTViewSnapshot *snapshot = + absl::optional maybe_snapshot = FSTTestApplyChanges(view, @[ FSTTestDeletedDoc("rooms/eros/messages/2", 0, NO), doc3 ], FSTTestTargetChangeAckDocuments({doc1.key, doc3.key})); + XCTAssertTrue(maybe_snapshot.has_value()); + ViewSnapshot snapshot = std::move(maybe_snapshot).value(); - XCTAssertEqual(snapshot.query, query); + XCTAssertEqual(snapshot.query(), query); - XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); + XC_ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc3)); - XCTAssertEqualObjects( - snapshot.documentChanges, (@[ - [FSTDocumentViewChange changeWithDocument:doc2 type:DocumentViewChangeType::kRemoved], - [FSTDocumentViewChange changeWithDocument:doc3 type:DocumentViewChangeType::kAdded] - ])); + XCTAssertTrue(( + snapshot.document_changes() == + std::vector{DocumentViewChange{doc2, DocumentViewChange::Type::kRemoved}, + DocumentViewChange{doc3, DocumentViewChange::Type::kAdded}})); - XCTAssertFalse(snapshot.isFromCache); - XCTAssertTrue(snapshot.syncStateChanged); + XCTAssertFalse(snapshot.from_cache()); + XCTAssertTrue(snapshot.sync_state_changed()); } - (void)testReturnsNilIfThereAreNoChanges { @@ -120,19 +152,19 @@ - (void)testReturnsNilIfThereAreNoChanges { FSTTestDoc("rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, FSTDocumentStateSynced); // initial state - FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt); // reapply same docs, no changes - FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); - XCTAssertNil(snapshot); + absl::optional snapshot = FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt); + XCTAssertFalse(snapshot.has_value()); } - (void)testDoesNotReturnNilForFirstChanges { FSTQuery *query = [self queryForMessages]; FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[], nil); - XCTAssertNotNil(snapshot); + absl::optional snapshot = FSTTestApplyChanges(view, @[], absl::nullopt); + XCTAssertTrue(snapshot.has_value()); } - (void)testFiltersDocumentsBasedOnQueryWithFilter { @@ -155,21 +187,23 @@ - (void)testFiltersDocumentsBasedOnQueryWithFilter { FSTDocument *doc5 = FSTTestDoc("rooms/eros/messages/5", 0, @{@"sort" : @1}, FSTDocumentStateSynced); - FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4, doc5 ], nil); + absl::optional maybe_snapshot = + FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4, doc5 ], absl::nullopt); + XCTAssertTrue(maybe_snapshot.has_value()); + ViewSnapshot snapshot = std::move(maybe_snapshot).value(); - XCTAssertEqual(snapshot.query, query); + XCTAssertEqual(snapshot.query(), query); - XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc5, doc2 ])); + XC_ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc5, doc2)); - XCTAssertEqualObjects( - snapshot.documentChanges, (@[ - [FSTDocumentViewChange changeWithDocument:doc1 type:DocumentViewChangeType::kAdded], - [FSTDocumentViewChange changeWithDocument:doc5 type:DocumentViewChangeType::kAdded], - [FSTDocumentViewChange changeWithDocument:doc2 type:DocumentViewChangeType::kAdded] - ])); + XCTAssertTrue(( + snapshot.document_changes() == + std::vector{DocumentViewChange{doc1, DocumentViewChange::Type::kAdded}, + DocumentViewChange{doc5, DocumentViewChange::Type::kAdded}, + DocumentViewChange{doc2, DocumentViewChange::Type::kAdded}})); - XCTAssertTrue(snapshot.isFromCache); - XCTAssertTrue(snapshot.syncStateChanged); + XCTAssertTrue(snapshot.from_cache()); + XCTAssertTrue(snapshot.sync_state_changed()); } - (void)testUpdatesDocumentsBasedOnQueryWithFilter { @@ -189,11 +223,12 @@ - (void)testUpdatesDocumentsBasedOnQueryWithFilter { FSTTestDoc("rooms/eros/messages/3", 0, @{@"sort" : @2}, FSTDocumentStateSynced); FSTDocument *doc4 = FSTTestDoc("rooms/eros/messages/4", 0, @{}, FSTDocumentStateSynced); - FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4 ], nil); + ViewSnapshot snapshot = + FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4 ], absl::nullopt).value(); - XCTAssertEqual(snapshot.query, query); + XCTAssertEqual(snapshot.query(), query); - XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); + XC_ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc3)); FSTDocument *newDoc2 = FSTTestDoc("rooms/eros/messages/2", 1, @{@"sort" : @2}, FSTDocumentStateSynced); @@ -202,21 +237,19 @@ - (void)testUpdatesDocumentsBasedOnQueryWithFilter { FSTDocument *newDoc4 = FSTTestDoc("rooms/eros/messages/4", 1, @{@"sort" : @0}, FSTDocumentStateSynced); - snapshot = FSTTestApplyChanges(view, @[ newDoc2, newDoc3, newDoc4 ], nil); + snapshot = FSTTestApplyChanges(view, @[ newDoc2, newDoc3, newDoc4 ], absl::nullopt).value(); - XCTAssertEqual(snapshot.query, query); + XCTAssertEqual(snapshot.query(), query); - XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ newDoc4, doc1, newDoc2 ])); + XC_ASSERT_THAT(snapshot.documents(), ElementsAre(newDoc4, doc1, newDoc2)); - XCTAssertEqualObjects( - snapshot.documentChanges, (@[ - [FSTDocumentViewChange changeWithDocument:doc3 type:DocumentViewChangeType::kRemoved], - [FSTDocumentViewChange changeWithDocument:newDoc4 type:DocumentViewChangeType::kAdded], - [FSTDocumentViewChange changeWithDocument:newDoc2 type:DocumentViewChangeType::kAdded] - ])); + XC_ASSERT_THAT(snapshot.document_changes(), + ElementsAre(DocumentViewChange{doc3, DocumentViewChange::Type::kRemoved}, + DocumentViewChange{newDoc4, DocumentViewChange::Type::kAdded}, + DocumentViewChange{newDoc2, DocumentViewChange::Type::kAdded})); - XCTAssertTrue(snapshot.isFromCache); - XCTAssertFalse(snapshot.syncStateChanged); + XCTAssertTrue(snapshot.from_cache()); + XCTAssertFalse(snapshot.sync_state_changed()); } - (void)testRemovesDocumentsForQueryWithLimit { @@ -232,24 +265,25 @@ - (void)testRemovesDocumentsForQueryWithLimit { FSTTestDoc("rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, FSTDocumentStateSynced); // initial state - FSTTestApplyChanges(view, @[ doc1, doc3 ], nil); + FSTTestApplyChanges(view, @[ doc1, doc3 ], absl::nullopt); // add doc2, which should push out doc3 - FSTViewSnapshot *snapshot = FSTTestApplyChanges( - view, @[ doc2 ], FSTTestTargetChangeAckDocuments({doc1.key, doc2.key, doc3.key})); + ViewSnapshot snapshot = + FSTTestApplyChanges(view, @[ doc2 ], + FSTTestTargetChangeAckDocuments({doc1.key, doc2.key, doc3.key})) + .value(); - XCTAssertEqual(snapshot.query, query); + XCTAssertEqual(snapshot.query(), query); - XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ])); + XC_ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc2)); - XCTAssertEqualObjects( - snapshot.documentChanges, (@[ - [FSTDocumentViewChange changeWithDocument:doc3 type:DocumentViewChangeType::kRemoved], - [FSTDocumentViewChange changeWithDocument:doc2 type:DocumentViewChangeType::kAdded] - ])); + XCTAssertTrue(( + snapshot.document_changes() == + std::vector{DocumentViewChange{doc3, DocumentViewChange::Type::kRemoved}, + DocumentViewChange{doc2, DocumentViewChange::Type::kAdded}})); - XCTAssertFalse(snapshot.isFromCache); - XCTAssertTrue(snapshot.syncStateChanged); + XCTAssertFalse(snapshot.from_cache()); + XCTAssertTrue(snapshot.sync_state_changed()); } - (void)testDoesntReportChangesForDocumentBeyondLimitOfQuery { @@ -269,7 +303,7 @@ - (void)testDoesntReportChangesForDocumentBeyondLimitOfQuery { FSTTestDoc("rooms/eros/messages/4", 0, @{@"num" : @4}, FSTDocumentStateSynced); // initial state - FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); + FSTTestApplyChanges(view, @[ doc1, doc2 ], absl::nullopt); // change doc2 to 5, and add doc3 and doc4. // doc2 will be modified + removed = removed @@ -282,24 +316,24 @@ - (void)testDoesntReportChangesForDocumentBeyondLimitOfQuery { // Verify that all the docs still match. viewDocChanges = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4 ]) previousChanges:viewDocChanges]; - FSTViewSnapshot *snapshot = + absl::optional maybe_snapshot = [view applyChangesToDocuments:viewDocChanges targetChange:FSTTestTargetChangeAckDocuments( {doc1.key, doc2.key, doc3.key, doc4.key})] .snapshot; + XCTAssertTrue(maybe_snapshot.has_value()); + ViewSnapshot snapshot = std::move(maybe_snapshot).value(); - XCTAssertEqual(snapshot.query, query); + XCTAssertEqual(snapshot.query(), query); - XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ])); + XC_ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc3)); - XCTAssertEqualObjects( - snapshot.documentChanges, (@[ - [FSTDocumentViewChange changeWithDocument:doc2 type:DocumentViewChangeType::kRemoved], - [FSTDocumentViewChange changeWithDocument:doc3 type:DocumentViewChangeType::kAdded] - ])); + XC_ASSERT_THAT(snapshot.document_changes(), + ElementsAre(DocumentViewChange{doc2, DocumentViewChange::Type::kRemoved}, + DocumentViewChange{doc3, DocumentViewChange::Type::kAdded})); - XCTAssertFalse(snapshot.isFromCache); - XCTAssertTrue(snapshot.syncStateChanged); + XCTAssertFalse(snapshot.from_cache()); + XCTAssertTrue(snapshot.sync_state_changed()); } - (void)testKeepsTrackOfLimboDocuments { @@ -362,13 +396,6 @@ - (void)testResumingQueryCreatesNoLimbos { XCTAssertEqualObjects(change.limboChanges, @[]); } -- (void)assertDocSet:(FSTDocumentSet *)docSet containsDocs:(NSArray *)docs { - XCTAssertEqual(docs.count, docSet.count); - for (FSTDocument *doc in docs) { - XCTAssertTrue([docSet containsKey:doc.key]); - } -} - - (void)testReturnsNeedsRefillOnDeleteInLimitQuery { FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/0", 0, @{}, FSTDocumentStateSynced); @@ -378,22 +405,22 @@ - (void)testReturnsNeedsRefillOnDeleteInLimitQuery { // Start with a full view. FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, [changes.changeSet changes].count); + XCTAssertEqual(2, changes.changeSet.GetChanges().size()); [view applyChangesToDocuments:changes]; // Remove one of the docs. changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( "rooms/eros/messages/0", 0, NO) ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc2})); XCTAssertTrue(changes.needsRefill); - XCTAssertEqual(1, [changes.changeSet changes].count); + XCTAssertEqual(1, changes.changeSet.GetChanges().size()); // Refill it with just the one doc remaining. changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ]) previousChanges:changes]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc2})); XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(1, [changes.changeSet changes].count); + XCTAssertEqual(1, changes.changeSet.GetChanges().size()); [view applyChangesToDocuments:changes]; } @@ -414,23 +441,23 @@ - (void)testReturnsNeedsRefillOnReorderInLimitQuery { // Start with a full view. FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, [changes.changeSet changes].count); + XCTAssertEqual(2, changes.changeSet.GetChanges().size()); [view applyChangesToDocuments:changes]; // Move one of the docs. doc2 = FSTTestDoc("rooms/eros/messages/1", 1, @{@"order" : @2000}, FSTDocumentStateSynced); changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); XCTAssertTrue(changes.needsRefill); - XCTAssertEqual(1, [changes.changeSet changes].count); + XCTAssertEqual(1, changes.changeSet.GetChanges().size()); // Refill it with all three current docs. changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ]) previousChanges:changes]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc3 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc3})); XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, [changes.changeSet changes].count); + XCTAssertEqual(2, changes.changeSet.GetChanges().size()); [view applyChangesToDocuments:changes]; } @@ -455,17 +482,17 @@ - (void)testDoesntNeedRefillOnReorderWithinLimit { // Start with a full view. FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2, doc3})); XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(3, [changes.changeSet changes].count); + XCTAssertEqual(3, changes.changeSet.GetChanges().size()); [view applyChangesToDocuments:changes]; // Move one of the docs. doc1 = FSTTestDoc("rooms/eros/messages/0", 1, @{@"order" : @3}, FSTDocumentStateSynced); changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc2, doc3, doc1 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc2, doc3, doc1})); XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(1, [changes.changeSet changes].count); + XCTAssertEqual(1, changes.changeSet.GetChanges().size()); [view applyChangesToDocuments:changes]; } @@ -490,17 +517,17 @@ - (void)testDoesntNeedRefillOnReorderAfterLimitQuery { // Start with a full view. FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2, doc3})); XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(3, [changes.changeSet changes].count); + XCTAssertEqual(3, changes.changeSet.GetChanges().size()); [view applyChangesToDocuments:changes]; // Move one of the docs. doc4 = FSTTestDoc("rooms/eros/messages/3", 1, @{@"order" : @6}, FSTDocumentStateSynced); changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc4 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2, doc3})); XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(0, [changes.changeSet changes].count); + XCTAssertEqual(0, changes.changeSet.GetChanges().size()); [view applyChangesToDocuments:changes]; } @@ -513,17 +540,17 @@ - (void)testDoesntNeedRefillForAdditionAfterTheLimit { // Start with a full view. FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, [changes.changeSet changes].count); + XCTAssertEqual(2, changes.changeSet.GetChanges().size()); [view applyChangesToDocuments:changes]; // Add a doc that is past the limit. FSTDocument *doc3 = FSTTestDoc("rooms/eros/messages/2", 1, @{}, FSTDocumentStateSynced); changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(0, [changes.changeSet changes].count); + XCTAssertEqual(0, changes.changeSet.GetChanges().size()); [view applyChangesToDocuments:changes]; } @@ -535,17 +562,17 @@ - (void)testDoesntNeedRefillForDeletionsWhenNotNearTheLimit { FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, [changes.changeSet changes].count); + XCTAssertEqual(2, changes.changeSet.GetChanges().size()); [view applyChangesToDocuments:changes]; // Remove one of the docs. changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( "rooms/eros/messages/1", 0, NO) ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1})); XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(1, [changes.changeSet changes].count); + XCTAssertEqual(1, changes.changeSet.GetChanges().size()); [view applyChangesToDocuments:changes]; } @@ -558,17 +585,17 @@ - (void)testHandlesApplyingIrrelevantDocs { // Start with a full view. FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, [changes.changeSet changes].count); + XCTAssertEqual(2, changes.changeSet.GetChanges().size()); [view applyChangesToDocuments:changes]; // Remove a doc that isn't even in the results. changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc( "rooms/eros/messages/2", 0, NO) ])]; - [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]]; + XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(0, [changes.changeSet changes].count); + XCTAssertEqual(0, changes.changeSet.GetChanges().size()); [view applyChangesToDocuments:changes]; } @@ -647,7 +674,7 @@ - (void)testRaisesHasPendingWritesForPendingMutationsInInitialSnapshot { FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]; FSTViewChange *viewChange = [view applyChangesToDocuments:changes]; - XCTAssertTrue(viewChange.snapshot.hasPendingWrites); + XCTAssertTrue(viewChange.snapshot.value().has_pending_writes()); } - (void)testDoesntRaiseHasPendingWritesForCommittedMutationsInInitialSnapshot { @@ -657,7 +684,7 @@ - (void)testDoesntRaiseHasPendingWritesForCommittedMutationsInInitialSnapshot { FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]; FSTViewChange *viewChange = [view applyChangesToDocuments:changes]; - XCTAssertFalse(viewChange.snapshot.hasPendingWrites); + XCTAssertFalse(viewChange.snapshot.value().has_pending_writes()); } - (void)testSuppressesWriteAcknowledgementIfWatchHasNotCaughtUp { @@ -683,32 +710,24 @@ - (void)testSuppressesWriteAcknowledgementIfWatchHasNotCaughtUp { [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; FSTViewChange *viewChange = [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects( - (@[ - [FSTDocumentViewChange changeWithDocument:doc1 type:DocumentViewChangeType::kAdded], - [FSTDocumentViewChange changeWithDocument:doc2 type:DocumentViewChangeType::kAdded] - ]), - viewChange.snapshot.documentChanges); + XC_ASSERT_THAT(viewChange.snapshot.value().document_changes(), + ElementsAre(DocumentViewChange{doc1, DocumentViewChange::Type::kAdded}, + DocumentViewChange{doc2, DocumentViewChange::Type::kAdded})); changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1Committed, doc2Modified ])]; viewChange = [view applyChangesToDocuments:changes]; // The 'doc1Committed' update is suppressed - XCTAssertEqualObjects( - (@[ [FSTDocumentViewChange changeWithDocument:doc2Modified - type:DocumentViewChangeType::kModified] ]), - viewChange.snapshot.documentChanges); + XC_ASSERT_THAT( + viewChange.snapshot.value().document_changes(), + ElementsAre(DocumentViewChange{doc2Modified, DocumentViewChange::Type::kModified})); changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1Acknowledged, doc2Acknowledged ])]; viewChange = [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects( - (@[ - [FSTDocumentViewChange changeWithDocument:doc1Acknowledged - type:DocumentViewChangeType::kModified], - [FSTDocumentViewChange changeWithDocument:doc2Acknowledged - type:DocumentViewChangeType::kMetadata] - ]), - viewChange.snapshot.documentChanges); + XC_ASSERT_THAT( + viewChange.snapshot.value().document_changes(), + ElementsAre(DocumentViewChange{doc1Acknowledged, DocumentViewChange::Type::kModified}, + DocumentViewChange{doc2Acknowledged, DocumentViewChange::Type::kMetadata})); } @end diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm index 6782563908e..89f6225c621 100644 --- a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm @@ -1040,7 +1040,8 @@ - (void)testUpdateFieldsWithDots { [self writeDocumentRef:doc data:@{@"a.b" : @"old", @"c.d" : @"old"}]; - [self updateDocumentRef:doc data:@{[[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new"}]; + [self updateDocumentRef:doc + data:@{(id)[[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new"}]; XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"]; @@ -1065,8 +1066,8 @@ - (void)testUpdateNestedFields { [self updateDocumentRef:doc data:@{ - @"a.b" : @"new", - [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new" + (id) @"a.b" : @"new", + (id)[[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new" }]; XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"]; diff --git a/Firestore/Example/Tests/Integration/API/FIRFieldsTests.mm b/Firestore/Example/Tests/Integration/API/FIRFieldsTests.mm index f18b698b465..359dd412697 100644 --- a/Firestore/Example/Tests/Integration/API/FIRFieldsTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRFieldsTests.mm @@ -19,6 +19,8 @@ #import +#import "Firestore/core/src/firebase/firestore/util/warnings.h" + #import "Firestore/Source/Core/FSTFirestoreClient.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" @@ -161,8 +163,8 @@ - (void)testFieldsWithSpecialCharsCanBeUpdated { [self writeDocumentRef:doc data:testData]; [self updateDocumentRef:doc data:@{ - [[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]] : @100, - @"c\\slash" : @200 + (id)[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]] : @100, + (id) @"c\\slash" : @200 }]; FIRDocumentSnapshot *result = [self readDocumentForRef:doc]; @@ -266,10 +268,9 @@ - (void)setUp { [super setUp]; // Settings can only be redefined before client is initialized, so this has to happen in setUp. FIRFirestoreSettings *settings = self.db.settings; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" + SUPPRESS_DEPRECATED_DECLARATIONS_BEGIN() settings.timestampsInSnapshotsEnabled = NO; -#pragma clang diagnostic pop + SUPPRESS_END() self.db.settings = settings; } diff --git a/Firestore/Example/Tests/Integration/API/FIRNumericTransformTests.mm b/Firestore/Example/Tests/Integration/API/FIRNumericTransformTests.mm new file mode 100644 index 00000000000..fa742025cd0 --- /dev/null +++ b/Firestore/Example/Tests/Integration/API/FIRNumericTransformTests.mm @@ -0,0 +1,161 @@ +/* + * Copyright 2019 Google + * + * 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 + +#import "Firestore/Source/API/FIRFieldValue+Internal.h" + +#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" +#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" + +double DOUBLE_EPSILON = 0.000001; + +@interface FIRNumericTransformTests : FSTIntegrationTestCase +@end + +@implementation FIRNumericTransformTests { + // A document reference to read and write to. + FIRDocumentReference *_docRef; + + // Accumulator used to capture events during the test. + FSTEventAccumulator *_accumulator; + + // Listener registration for a listener maintained during the course of the test. + id _listenerRegistration; +} + +- (void)setUp { + [super setUp]; + + _docRef = [self documentRef]; + _accumulator = [FSTEventAccumulator accumulatorForTest:self]; + _listenerRegistration = + [_docRef addSnapshotListenerWithIncludeMetadataChanges:YES + listener:_accumulator.valueEventHandler]; + + // Wait for initial nil snapshot to avoid potential races. + FIRDocumentSnapshot *initialSnapshot = [_accumulator awaitEventWithName:@"initial event"]; + XCTAssertFalse(initialSnapshot.exists); +} + +- (void)tearDown { + [_listenerRegistration remove]; + + [super tearDown]; +} + +#pragma mark - Test Helpers + +/** Writes some initial data and consumes the events generated. */ +- (void)writeInitialData:(NSDictionary *)data { + [self writeDocumentRef:_docRef data:data]; + XCTAssertEqualObjects([_accumulator awaitLocalEvent].data, data); + XCTAssertEqualObjects([_accumulator awaitRemoteEvent].data, data); +} + +- (void)expectLocalAndRemoteValue:(int64_t)expectedSum { + FIRDocumentSnapshot *snap = [_accumulator awaitLocalEvent]; + XCTAssertEqualObjects(@(expectedSum), snap[@"sum"]); + snap = [_accumulator awaitRemoteEvent]; + XCTAssertEqualObjects(@(expectedSum), snap[@"sum"]); +} + +- (void)expectApproximateLocalAndRemoteValue:(double)expectedSum { + FIRDocumentSnapshot *snap = [_accumulator awaitLocalEvent]; + XCTAssertEqualWithAccuracy(expectedSum, [snap[@"sum"] doubleValue], DOUBLE_EPSILON); + snap = [_accumulator awaitRemoteEvent]; + XCTAssertEqualWithAccuracy(expectedSum, [snap[@"sum"] doubleValue], DOUBLE_EPSILON); +} + +#pragma mark - Test Cases + +- (void)testCreateDocumentWithIncrement { + [self writeDocumentRef:_docRef + data:@{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1337]}]; + [self expectLocalAndRemoteValue:1337]; +} + +- (void)testMergeOnNonExistingDocumentWithIncrement { + [self mergeDocumentRef:_docRef + data:@{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1337]}]; + [self expectLocalAndRemoteValue:1337]; +} + +- (void)testIntegerIncrementWithExistingInteger { + [self writeInitialData:@{@"sum" : @1337}]; + [self updateDocumentRef:_docRef data:@{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]}]; + [self expectLocalAndRemoteValue:1338]; +} + +- (void)testDoubleIncrementWithExistingDouble { + [self writeInitialData:@{@"sum" : @13.37}]; + [self updateDocumentRef:_docRef + data:@{@"sum" : [FIRFieldValue fieldValueForDoubleIncrement:0.1]}]; + [self expectApproximateLocalAndRemoteValue:13.47]; +} + +- (void)testIntegerIncrementWithExistingDouble { + [self writeInitialData:@{@"sum" : @13.37}]; + [self updateDocumentRef:_docRef data:@{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]}]; + [self expectApproximateLocalAndRemoteValue:14.37]; +} + +- (void)testDoubleIncrementWithExistingInteger { + [self writeInitialData:@{@"sum" : @1337}]; + [self updateDocumentRef:_docRef + data:@{@"sum" : [FIRFieldValue fieldValueForDoubleIncrement:0.1]}]; + [self expectApproximateLocalAndRemoteValue:1337.1]; +} + +- (void)testIntegerIncrementWithExistingString { + [self writeInitialData:@{@"sum" : @"overwrite"}]; + [self updateDocumentRef:_docRef + data:@{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1337]}]; + [self expectLocalAndRemoteValue:1337]; +} + +- (void)testDoubleIncrementWithExistingString { + [self writeInitialData:@{@"sum" : @"overwrite"}]; + [self updateDocumentRef:_docRef + data:@{@"sum" : [FIRFieldValue fieldValueForDoubleIncrement:13.37]}]; + [self expectApproximateLocalAndRemoteValue:13.37]; +} + +- (void)testMultipleDoubleIncrements { + [self writeInitialData:@{@"sum" : @"0.0"}]; + + [self disableNetwork]; + + [_docRef updateData:@{@"sum" : [FIRFieldValue fieldValueForDoubleIncrement:0.1]}]; + [_docRef updateData:@{@"sum" : [FIRFieldValue fieldValueForDoubleIncrement:0.01]}]; + [_docRef updateData:@{@"sum" : [FIRFieldValue fieldValueForDoubleIncrement:0.001]}]; + + FIRDocumentSnapshot *snap = [_accumulator awaitLocalEvent]; + + XCTAssertEqualWithAccuracy(0.1, [snap[@"sum"] doubleValue], DOUBLE_EPSILON); + snap = [_accumulator awaitLocalEvent]; + XCTAssertEqualWithAccuracy(0.11, [snap[@"sum"] doubleValue], DOUBLE_EPSILON); + snap = [_accumulator awaitLocalEvent]; + XCTAssertEqualWithAccuracy(0.111, [snap[@"sum"] doubleValue], DOUBLE_EPSILON); + + [self enableNetwork]; + snap = [_accumulator awaitRemoteEvent]; + XCTAssertEqualWithAccuracy(0.111, [snap[@"sum"] doubleValue], DOUBLE_EPSILON); +} + +@end diff --git a/Firestore/Example/Tests/Integration/API/FIRQueryTests.mm b/Firestore/Example/Tests/Integration/API/FIRQueryTests.mm index da20d92ea26..18555a61334 100644 --- a/Firestore/Example/Tests/Integration/API/FIRQueryTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRQueryTests.mm @@ -20,6 +20,8 @@ #import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" +// TODO(b/116617988): Remove Internal include once CG queries are public. +#import "Firestore/Source/API/FIRFirestore+Internal.h" @interface FIRQueryTests : FSTIntegrationTestCase @end @@ -294,4 +296,112 @@ - (void)testArrayContainsQueries { // of anything else interesting to test. } +- (void)testCollectionGroupQueries { + // Use .document() to get a random collection group name to use but ensure it starts with 'b' + // for predictable ordering. + NSString *collectionGroup = [NSString + stringWithFormat:@"b%@", [[self.db collectionWithPath:@"foo"] documentWithAutoID].documentID]; + + NSArray *docPaths = @[ + @"abc/123/${collectionGroup}/cg-doc1", @"abc/123/${collectionGroup}/cg-doc2", + @"${collectionGroup}/cg-doc3", @"${collectionGroup}/cg-doc4", + @"def/456/${collectionGroup}/cg-doc5", @"${collectionGroup}/virtual-doc/nested-coll/not-cg-doc", + @"x${collectionGroup}/not-cg-doc", @"${collectionGroup}x/not-cg-doc", + @"abc/123/${collectionGroup}x/not-cg-doc", @"abc/123/x${collectionGroup}/not-cg-doc", + @"abc/${collectionGroup}" + ]; + + FIRWriteBatch *batch = [self.db batch]; + for (NSString *docPath in docPaths) { + NSString *path = [docPath stringByReplacingOccurrencesOfString:@"${collectionGroup}" + withString:collectionGroup]; + [batch setData:@{@"x" : @1} forDocument:[self.db documentWithPath:path]]; + } + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRQuerySnapshot *querySnapshot = + [self readDocumentSetForRef:[self.db collectionGroupWithID:collectionGroup]]; + NSArray *ids = FIRQuerySnapshotGetIDs(querySnapshot); + XCTAssertEqualObjects(ids, (@[ @"cg-doc1", @"cg-doc2", @"cg-doc3", @"cg-doc4", @"cg-doc5" ])); +} + +- (void)testCollectionGroupQueriesWithStartAtEndAtWithArbitraryDocumentIDs { + // Use .document() to get a random collection group name to use but ensure it starts with 'b' + // for predictable ordering. + NSString *collectionGroup = [NSString + stringWithFormat:@"b%@", [[self.db collectionWithPath:@"foo"] documentWithAutoID].documentID]; + + NSArray *docPaths = @[ + @"a/a/${collectionGroup}/cg-doc1", @"a/b/a/b/${collectionGroup}/cg-doc2", + @"a/b/${collectionGroup}/cg-doc3", @"a/b/c/d/${collectionGroup}/cg-doc4", + @"a/c/${collectionGroup}/cg-doc5", @"${collectionGroup}/cg-doc6", @"a/b/nope/nope" + ]; + + FIRWriteBatch *batch = [self.db batch]; + for (NSString *docPath in docPaths) { + NSString *path = [docPath stringByReplacingOccurrencesOfString:@"${collectionGroup}" + withString:collectionGroup]; + [batch setData:@{@"x" : @1} forDocument:[self.db documentWithPath:path]]; + } + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRQuerySnapshot *querySnapshot = [self + readDocumentSetForRef:[[[[self.db collectionGroupWithID:collectionGroup] + queryOrderedByFieldPath:[FIRFieldPath documentID]] + queryStartingAfterValues:@[ @"a/b" ]] + queryEndingBeforeValues:@[ + [NSString stringWithFormat:@"a/b/%@/cg-doc3", collectionGroup] + ]]]; + + NSArray *ids = FIRQuerySnapshotGetIDs(querySnapshot); + XCTAssertEqualObjects(ids, (@[ @"cg-doc2" ])); +} + +- (void)testCollectionGroupQueriesWithWhereFiltersOnArbitraryDocumentIDs { + // Use .document() to get a random collection group name to use but ensure it starts with 'b' + // for predictable ordering. + NSString *collectionGroup = [NSString + stringWithFormat:@"b%@", [[self.db collectionWithPath:@"foo"] documentWithAutoID].documentID]; + + NSArray *docPaths = @[ + @"a/a/${collectionGroup}/cg-doc1", @"a/b/a/b/${collectionGroup}/cg-doc2", + @"a/b/${collectionGroup}/cg-doc3", @"a/b/c/d/${collectionGroup}/cg-doc4", + @"a/c/${collectionGroup}/cg-doc5", @"${collectionGroup}/cg-doc6", @"a/b/nope/nope" + ]; + + FIRWriteBatch *batch = [self.db batch]; + for (NSString *docPath in docPaths) { + NSString *path = [docPath stringByReplacingOccurrencesOfString:@"${collectionGroup}" + withString:collectionGroup]; + [batch setData:@{@"x" : @1} forDocument:[self.db documentWithPath:path]]; + } + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; + [batch commitWithCompletion:^(NSError *error) { + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self awaitExpectations]; + + FIRQuerySnapshot *querySnapshot = [self + readDocumentSetForRef:[[[self.db collectionGroupWithID:collectionGroup] + queryWhereFieldPath:[FIRFieldPath documentID] + isGreaterThanOrEqualTo:@"a/b"] + queryWhereFieldPath:[FIRFieldPath documentID] + isLessThan:[NSString stringWithFormat:@"a/b/%@/cg-doc3", + collectionGroup]]]; + + NSArray *ids = FIRQuerySnapshotGetIDs(querySnapshot); + XCTAssertEqualObjects(ids, (@[ @"cg-doc2" ])); +} + @end diff --git a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm index 860c8aac9ee..4af81d26556 100644 --- a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm @@ -220,7 +220,8 @@ - (void)testServerTimestampsWithConsecutiveUpdates { serverTimestampBehavior:FIRServerTimestampBehaviorPrevious], @42); - [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; + // include b=1 to ensure there's a change resulting in a new snapshot. + [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp], @"b" : @1}]; localSnapshot = [_accumulator awaitLocalEvent]; XCTAssertEqualObjects([localSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorPrevious], @@ -263,7 +264,7 @@ - (void)testServerTimestampsPreviousValueFromLocalMutation { - (void)testServerTimestampsWorkViaTransactionSet { [self runTransactionBlock:^(FIRTransaction *transaction) { - [transaction setData:_setData forDocument:_docRef]; + [transaction setData:self->_setData forDocument:self->_docRef]; }]; [self verifySnapshotWithResolvedTimestamps:[_accumulator awaitRemoteEvent]]; @@ -272,7 +273,7 @@ - (void)testServerTimestampsWorkViaTransactionSet { - (void)testServerTimestampsWorkViaTransactionUpdate { [self writeInitialData]; [self runTransactionBlock:^(FIRTransaction *transaction) { - [transaction updateData:_updateData forDocument:_docRef]; + [transaction updateData:self->_updateData forDocument:self->_docRef]; }]; [self verifySnapshotWithResolvedTimestamps:[_accumulator awaitRemoteEvent]]; } @@ -293,7 +294,7 @@ - (void)testServerTimestampsFailViaTransactionUpdateOnNonexistentDocument { XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"]; [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) { - [transaction updateData:_updateData forDocument:_docRef]; + [transaction updateData:self->_updateData forDocument:self->_docRef]; return nil; } completion:^(id result, NSError *error) { diff --git a/Firestore/Example/Tests/Integration/API/FIRValidationTests.mm b/Firestore/Example/Tests/Integration/API/FIRValidationTests.mm index b3a72f43ae7..15cfc004238 100644 --- a/Firestore/Example/Tests/Integration/API/FIRValidationTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRValidationTests.mm @@ -18,10 +18,14 @@ #import +#include + #import "Firestore/Source/API/FIRFieldValue+Internal.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" +// TODO(b/116617988): Remove Internal include once CG queries are public. +#import "Firestore/Source/API/FIRFirestore+Internal.h" // We have tests for passing nil when nil is not supposed to be allowed. So suppress the warnings. #pragma clang diagnostic ignored "-Wnonnull" @@ -226,7 +230,7 @@ - (void)testWritesWithInvalidTypesFail { } - (void)testWritesWithLargeNumbersFail { - NSNumber *num = @((unsigned long long)LONG_MAX + 1); + NSNumber *num = @(static_cast(std::numeric_limits::max()) + 1); NSString *reason = [NSString stringWithFormat:@"NSNumber (%@) is too large (found in field num)", num]; [self expectWrite:@{@"num" : num} toFailWithReason:reason]; @@ -420,6 +424,55 @@ - (void)testQueryCannotBeCreatedFromDocumentsMissingSortValues { FSTAssertThrows([query queryEndingAtDocument:snapshot], reason); } +- (void)testQueriesCannotBeSortedByAnUncommittedServerTimestamp { + __weak FIRCollectionReference *collection = [self collectionRef]; + FIRFirestore *db = [self firestore]; + + [db disableNetworkWithCompletion:[self completionForExpectationWithName:@"Disable network"]]; + [self awaitExpectations]; + + XCTestExpectation *offlineCallbackDone = + [self expectationWithDescription:@"offline callback done"]; + XCTestExpectation *onlineCallbackDone = [self expectationWithDescription:@"online callback done"]; + + [collection addSnapshotListener:^(FIRQuerySnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + + // Skip the initial empty snapshot. + if (snapshot.empty) return; + + XCTAssertEqual(snapshot.count, 1); + FIRQueryDocumentSnapshot *docSnap = snapshot.documents[0]; + + if (snapshot.metadata.pendingWrites) { + // Offline snapshot. Since the server timestamp is uncommitted, we + // shouldn't be able to query by it. + NSString *reason = + @"Invalid query. You are trying to start or end a query using a document for which the " + @"field 'timestamp' is an uncommitted server timestamp. (Since the value of this field " + @"is unknown, you cannot start/end a query with it.)"; + FSTAssertThrows([[[collection queryOrderedByField:@"timestamp"] queryEndingAtDocument:docSnap] + addSnapshotListener:^(FIRQuerySnapshot *, NSError *){ + }], + reason); + [offlineCallbackDone fulfill]; + } else { + // Online snapshot. Since the server timestamp is committed, we should be able to query by it. + [[[collection queryOrderedByField:@"timestamp"] queryEndingAtDocument:docSnap] + addSnapshotListener:^(FIRQuerySnapshot *, NSError *){ + }]; + [onlineCallbackDone fulfill]; + } + }]; + + FIRDocumentReference *document = [collection documentWithAutoID]; + [document setData:@{@"timestamp" : [FIRFieldValue fieldValueForServerTimestamp]}]; + [self awaitExpectations]; + + [db enableNetworkWithCompletion:[self completionForExpectationWithName:@"Enable network"]]; + [self awaitExpectations]; +} + - (void)testQueryBoundMustNotHaveMoreComponentsThanSortOrders { FIRCollectionReference *testCollection = [self collectionRef]; FIRQuery *query = [testCollection queryOrderedByField:@"foo"]; @@ -433,12 +486,19 @@ - (void)testQueryBoundMustNotHaveMoreComponentsThanSortOrders { } - (void)testQueryOrderedByKeyBoundMustBeAStringWithoutSlashes { - FIRCollectionReference *testCollection = [self collectionRef]; - FIRQuery *query = [testCollection queryOrderedByFieldPath:[FIRFieldPath documentID]]; + FIRQuery *query = [[self.db collectionWithPath:@"collection"] + queryOrderedByFieldPath:[FIRFieldPath documentID]]; + FIRQuery *cgQuery = [[self.db collectionGroupWithID:@"collection"] + queryOrderedByFieldPath:[FIRFieldPath documentID]]; FSTAssertThrows([query queryStartingAtValues:@[ @1 ]], @"Invalid query. Expected a string for the document ID."); FSTAssertThrows([query queryStartingAtValues:@[ @"foo/bar" ]], - @"Invalid query. Document ID 'foo/bar' contains a slash."); + @"Invalid query. When querying a collection and ordering by document " + "ID, you must pass a plain document ID, but 'foo/bar' contains a slash."); + FSTAssertThrows([cgQuery queryStartingAtValues:@[ @"foo" ]], + @"Invalid query. When querying a collection group and ordering by " + "document ID, you must pass a value that results in a valid document path, " + "but 'foo' is not because it contains an odd number of segments."); } - (void)testQueryMustNotSpecifyStartingOrEndingPointAfterOrder { @@ -459,8 +519,8 @@ - (void)testQueriesFilteredByDocumentIDMustUseStringsOrDocumentReferences { "document ID, but it was an empty string."; FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@""], reason); - reason = @"Invalid query. When querying by document ID you must provide a valid document ID, " - "but 'foo/bar/baz' contains a '/' character."; + reason = @"Invalid query. When querying a collection by document ID you must provide a " + "plain document ID, but 'foo/bar/baz' contains a '/' character."; FSTAssertThrows( [collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@"foo/bar/baz"], reason); @@ -468,6 +528,14 @@ - (void)testQueriesFilteredByDocumentIDMustUseStringsOrDocumentReferences { "DocumentReference, but it was of type: __NSCFNumber"; FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@1], reason); + reason = @"Invalid query. When querying a collection group by document ID, the value " + "provided must result in a valid document path, but 'foo/bar/baz' is not because it " + "has an odd number of segments."; + FSTAssertThrows( + [[self.db collectionGroupWithID:@"collection"] queryWhereFieldPath:[FIRFieldPath documentID] + isEqualTo:@"foo/bar/baz"], + reason); + reason = @"Invalid query. You can't perform arrayContains queries on document ID since document IDs " "are not arrays."; diff --git a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm index 472b8ac565e..7347e182e33 100644 --- a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm @@ -24,6 +24,14 @@ #import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" +#include "Firestore/core/src/firebase/firestore/util/autoid.h" +#include "Firestore/core/src/firebase/firestore/util/string_apple.h" + +using firebase::firestore::util::CreateAutoId; +using firebase::firestore::util::WrapNSString; + +NS_ASSUME_NONNULL_BEGIN + @interface FIRWriteBatchTests : FSTIntegrationTestCase @end @@ -124,6 +132,52 @@ - (void)testCannotUpdateNonexistentDocuments { XCTAssertFalse(result.exists); } +- (void)testUpdateFieldsWithDots { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc]; + [batch updateData:@{[[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new"} forDocument:doc]; + + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); + }]; + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + +- (void)testUpdateNestedFields { + FIRDocumentReference *doc = [self documentRef]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"]; + FIRWriteBatch *batch = [doc.firestore batch]; + [batch setData:@{@"a" : @{@"b" : @"old"}, @"c" : @{@"d" : @"old"}, @"e" : @{@"f" : @"old"}} + forDocument:doc]; + [batch + updateData:@{@"a.b" : @"new", [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new"} + forDocument:doc]; + [batch commitWithCompletion:^(NSError *_Nullable error) { + XCTAssertNil(error); + [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(snapshot.data, (@{ + @"a" : @{@"b" : @"new"}, + @"c" : @{@"d" : @"new"}, + @"e" : @{@"f" : @"old"} + })); + }]; + [expectation fulfill]; + }]; + + [self awaitExpectations]; +} + - (void)testDeleteDocuments { FIRDocumentReference *doc = [self documentRef]; [self writeDocumentRef:doc data:@{@"foo" : @"bar"}]; @@ -161,6 +215,7 @@ - (void)testBatchesCommitAtomicallyRaisingCorrectEvents { XCTAssertNil(error); [expectation fulfill]; }]; + [self awaitExpectations]; FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; XCTAssertTrue(localSnap.metadata.hasPendingWrites); @@ -192,6 +247,7 @@ - (void)testBatchesFailAtomicallyRaisingCorrectEvents { XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound); [expectation fulfill]; }]; + [self awaitExpectations]; // Local event with the set document. FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; @@ -223,6 +279,7 @@ - (void)testWriteTheSameServerTimestampAcrossWrites { XCTAssertNil(error); [expectation fulfill]; }]; + [self awaitExpectations]; FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; XCTAssertTrue(localSnap.metadata.hasPendingWrites); @@ -254,6 +311,7 @@ - (void)testCanWriteTheSameDocumentMultipleTimes { XCTAssertNil(error); [expectation fulfill]; }]; + [self awaitExpectations]; FIRDocumentSnapshot *localSnap = [accumulator awaitEventWithName:@"local event"]; XCTAssertTrue(localSnap.metadata.hasPendingWrites); @@ -265,50 +323,39 @@ - (void)testCanWriteTheSameDocumentMultipleTimes { XCTAssertEqualObjects(serverSnap.data, (@{@"a" : @1, @"b" : @2, @"when" : when})); } -- (void)testUpdateFieldsWithDots { - FIRDocumentReference *doc = [self documentRef]; +- (void)testCanWriteVeryLargeBatches { + // On Android, SQLite Cursors are limited reading no more than 2 MB per row (despite being able + // to write very large values). This test verifies that the local MutationQueue is not subject + // to this limitation. + + // Create a map containing nearly 1 MB of data. Note that if you use 1024 below this will create + // a document larger than 1 MB, which will be rejected by the backend as too large. + NSString *kb = [@"" stringByPaddingToLength:1000 withString:@"a" startingAtIndex:0]; + NSMutableDictionary *values = [NSMutableDictionary dictionary]; + for (int i = 0; i < 1000; i++) { + values[WrapNSString(CreateAutoId())] = kb; + } - XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"]; + FIRDocumentReference *doc = [self documentRef]; FIRWriteBatch *batch = [doc.firestore batch]; - [batch setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc]; - [batch updateData:@{[[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new"} forDocument:doc]; - - [batch commitWithCompletion:^(NSError *_Nullable error) { - XCTAssertNil(error); - [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"})); - }]; - [expectation fulfill]; - }]; - [self awaitExpectations]; -} - -- (void)testUpdateNestedFields { - FIRDocumentReference *doc = [self documentRef]; + // Write a batch containing 3 copies of the data, creating a ~3 MB batch. Writing to the same + // document in a batch is allowed and so long as the net size of the document is under 1 MB the + // batch is allowed. + [batch setData:values forDocument:doc]; + for (int i = 0; i < 2; i++) { + [batch updateData:values forDocument:doc]; + } - XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"]; - FIRWriteBatch *batch = [doc.firestore batch]; - [batch setData:@{@"a" : @{@"b" : @"old"}, @"c" : @{@"d" : @"old"}, @"e" : @{@"f" : @"old"}} - forDocument:doc]; - [batch - updateData:@{@"a.b" : @"new", [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new"} - forDocument:doc]; + XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"]; [batch commitWithCompletion:^(NSError *_Nullable error) { XCTAssertNil(error); - [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(snapshot.data, (@{ - @"a" : @{@"b" : @"new"}, - @"c" : @{@"d" : @"new"}, - @"e" : @{@"f" : @"old"} - })); - }]; [expectation fulfill]; }]; - [self awaitExpectations]; + + FIRDocumentSnapshot *snap = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(values, snap.data); } // Returns how much memory the test application is currently using, in megabytes (fractional part is @@ -363,3 +410,5 @@ - (void)testReasonableMemoryUsageForLotsOfMutations { } @end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm index 4943d04ab34..e3d3199ef42 100644 --- a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm +++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm @@ -20,6 +20,7 @@ #import #include +#include #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FSTUserDataConverter.h" @@ -29,8 +30,6 @@ #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" -#import "Firestore/Source/Remote/FSTRemoteStore.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" @@ -40,9 +39,12 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/precondition.h" #include "Firestore/core/src/firebase/firestore/remote/datastore.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_store.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" #include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" #include "absl/memory/memory.h" @@ -54,18 +56,18 @@ using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::Precondition; +using firebase::firestore::model::OnlineState; using firebase::firestore::model::TargetId; using firebase::firestore::remote::Datastore; using firebase::firestore::remote::GrpcConnection; +using firebase::firestore::remote::RemoteEvent; +using firebase::firestore::remote::RemoteStore; using firebase::firestore::util::AsyncQueue; using firebase::firestore::util::ExecutorLibdispatch; +using firebase::firestore::util::Status; NS_ASSUME_NONNULL_BEGIN -@interface FSTRemoteStore (Tests) -- (void)addBatchToWritePipeline:(FSTMutationBatch *)batch; -@end - #pragma mark - FSTRemoteStoreEventCapture @interface FSTRemoteStoreEventCapture : NSObject @@ -79,17 +81,17 @@ - (void)expectListenEventWithDescription:(NSString *)description; @property(nonatomic, weak, nullable) XCTestCase *testCase; @property(nonatomic, strong) NSMutableArray *writeEvents; -@property(nonatomic, strong) NSMutableArray *listenEvents; @property(nonatomic, strong) NSMutableArray *writeEventExpectations; @property(nonatomic, strong) NSMutableArray *listenEventExpectations; @end -@implementation FSTRemoteStoreEventCapture +@implementation FSTRemoteStoreEventCapture { + std::vector _listenEvents; +} - (instancetype)initWithTestCase:(XCTestCase *_Nullable)testCase { if (self = [super init]) { _writeEvents = [NSMutableArray array]; - _listenEvents = [NSMutableArray array]; _testCase = testCase; _writeEventExpectations = [NSMutableArray array]; _listenEventExpectations = [NSMutableArray array]; @@ -134,8 +136,8 @@ - (DocumentKeySet)remoteKeysForTarget:(TargetId)targetId { return DocumentKeySet{}; } -- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { - [self.listenEvents addObject:remoteEvent]; +- (void)applyRemoteEvent:(const RemoteEvent &)remoteEvent { + _listenEvents.push_back(remoteEvent); XCTestExpectation *expectation = [self.listenEventExpectations objectAtIndex:0]; [self.listenEventExpectations removeObjectAtIndex:0]; [expectation fulfill]; @@ -160,7 +162,7 @@ @implementation FSTDatastoreTests { DatabaseInfo _databaseInfo; std::shared_ptr _datastore; - FSTRemoteStore *_remoteStore; + std::unique_ptr _remoteStore; } - (void)setUp { @@ -182,17 +184,16 @@ - (void)setUp { _testWorkerQueue = absl::make_unique(absl::make_unique(queue)); _datastore = std::make_shared(_databaseInfo, _testWorkerQueue.get(), &_credentials); - _remoteStore = [[FSTRemoteStore alloc] initWithLocalStore:_localStore - datastore:_datastore - workerQueue:_testWorkerQueue.get()]; + _remoteStore = absl::make_unique(_localStore, _datastore, _testWorkerQueue.get(), + [](OnlineState) {}); - _testWorkerQueue->Enqueue([=] { [_remoteStore start]; }); + _testWorkerQueue->Enqueue([=] { _remoteStore->Start(); }); } - (void)tearDown { XCTestExpectation *completion = [self expectationWithDescription:@"shutdown"]; _testWorkerQueue->Enqueue([=] { - [_remoteStore shutdown]; + _remoteStore->Shutdown(); [completion fulfill]; }); [self awaitExpectations]; @@ -203,8 +204,8 @@ - (void)tearDown { - (void)testCommit { XCTestExpectation *expectation = [self expectationWithDescription:@"commitWithCompletion"]; - _datastore->CommitMutations(@[], ^(NSError *_Nullable error) { - XCTAssertNil(error, @"Failed to commit"); + _datastore->CommitMutations({}, [self, expectation](const Status &status) { + XCTAssertTrue(status.ok(), @"Failed to commit"); [expectation fulfill]; }); @@ -215,17 +216,18 @@ - (void)testStreamingWrite { FSTRemoteStoreEventCapture *capture = [[FSTRemoteStoreEventCapture alloc] initWithTestCase:self]; [capture expectWriteEventWithDescription:@"write mutations"]; - _remoteStore.syncEngine = capture; + _remoteStore->set_sync_engine(capture); FSTSetMutation *mutation = [self setMutation]; FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:23 localWriteTime:[FIRTimestamp timestamp] - mutations:@[ mutation ]]; + baseMutations:{} + mutations:{mutation}]; _testWorkerQueue->Enqueue([=] { - [_remoteStore addBatchToWritePipeline:batch]; + _remoteStore->AddToWritePipeline(batch); // The added batch won't be written immediately because write stream wasn't yet open -- // trigger its opening. - [_remoteStore fillWritePipeline]; + _remoteStore->FillWritePipeline(); }); [self awaitExpectations]; diff --git a/Firestore/Example/Tests/Integration/FSTTransactionTests.mm b/Firestore/Example/Tests/Integration/FSTTransactionTests.mm index 88379143b45..a12db6d2110 100644 --- a/Firestore/Example/Tests/Integration/FSTTransactionTests.mm +++ b/Firestore/Example/Tests/Integration/FSTTransactionTests.mm @@ -404,7 +404,7 @@ - (void)testReadingADocTwiceWithDifferentVersions { // The get itself will fail, because we already read an earlier version of this document. // TODO(klimt): Perhaps we shouldn't fail reads for this, but should wait and fail the // whole transaction? It's an edge-case anyway, as developers shouldn't be reading the same - // do multiple times. But they need to handle read errors anyway. + // doc multiple times. But they need to handle read errors anyway. XCTAssertNotNil(*error); return nil; } diff --git a/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm b/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm index 6c6a91b6621..c4c9915e719 100644 --- a/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm +++ b/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm @@ -20,17 +20,19 @@ #include #include +#include +#include #import "FIRTimestamp.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" #import "Firestore/Source/Local/FSTLRUGarbageCollector.h" -#import "Firestore/Source/Local/FSTMutationQueue.h" #import "Firestore/Source/Local/FSTPersistence.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Util/FSTClasses.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" #include "Firestore/core/src/firebase/firestore/local/query_cache.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" @@ -44,6 +46,7 @@ using firebase::firestore::auth::User; using firebase::firestore::local::LruParams; using firebase::firestore::local::LruResults; +using firebase::firestore::local::MutationQueue; using firebase::firestore::local::QueryCache; using firebase::firestore::local::ReferenceSet; using firebase::firestore::local::RemoteDocumentCache; @@ -64,7 +67,7 @@ @implementation FSTLRUGarbageCollectorTests { id _persistence; QueryCache *_queryCache; RemoteDocumentCache *_documentCache; - id _mutationQueue; + MutationQueue *_mutationQueue; id _lruDelegate; FSTLRUGarbageCollector *_gc; ListenSequenceNumber _initialSequenceNumber; @@ -96,7 +99,7 @@ - (void)newTestResourcesWithLruParams:(LruParams)lruParams { _mutationQueue = [_persistence mutationQueueForUser:_user]; _lruDelegate = (id)_persistence.referenceDelegate; _initialSequenceNumber = _persistence.run("start querycache", [&]() -> ListenSequenceNumber { - [_mutationQueue start]; + _mutationQueue->Start(); _gc = _lruDelegate.gc; return _persistence.currentSequenceNumber; }); @@ -395,7 +398,7 @@ - (void)testRemoveQueriesUpThroughSequenceNumber { XCTAssertEqual(10, removed); // Make sure we removed the even targets with targetID <= 20. _persistence.run("verify remaining targets are > 20 or odd", [&]() { - _queryCache->EnumerateTargets(^(FSTQueryData *queryData, BOOL *stop) { + _queryCache->EnumerateTargets([&](FSTQueryData *queryData) { XCTAssertTrue(queryData.targetID > 20 || queryData.targetID % 2 == 1); }); }); @@ -411,7 +414,7 @@ - (void)testRemoveOrphanedDocuments { // as any documents with pending mutations. std::unordered_set expectedRetained; // we add two mutations later, for now track them in an array. - NSMutableArray *mutations = [NSMutableArray arrayWithCapacity:2]; + std::vector mutations; // Add a target and add two documents to it. The documents are expected to be // retained, since their membership in the target keeps them alive. @@ -425,7 +428,7 @@ - (void)testRemoveOrphanedDocuments { FSTDocument *doc2 = [self cacheADocumentInTransaction]; [self addDocument:doc2.key toTarget:queryData.targetID]; expectedRetained.insert(doc2.key); - [mutations addObject:[self mutationForDocument:doc2.key]]; + mutations.push_back([self mutationForDocument:doc2.key]); }); // Add a second query and register a third document on it @@ -439,7 +442,7 @@ - (void)testRemoveOrphanedDocuments { // cache another document and prepare a mutation on it. _persistence.run("queue a mutation", [&]() { FSTDocument *doc4 = [self cacheADocumentInTransaction]; - [mutations addObject:[self mutationForDocument:doc4.key]]; + mutations.push_back([self mutationForDocument:doc4.key]); expectedRetained.insert(doc4.key); }); @@ -447,7 +450,7 @@ - (void)testRemoveOrphanedDocuments { // serve to keep the mutated documents from being GC'd while the mutations are outstanding. _persistence.run("actually register the mutations", [&]() { FIRTimestamp *writeTime = [FIRTimestamp timestamp]; - [_mutationQueue addMutationBatchWithWriteTime:writeTime mutations:mutations]; + _mutationQueue->AddMutationBatch(writeTime, {}, std::move(mutations)); }); // Mark 5 documents eligible for GC. This simulates documents that were mutated then ack'd. diff --git a/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm index d4d46a496ba..9a48c5b0a17 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm @@ -16,6 +16,7 @@ #import +#include #include #include #include @@ -23,7 +24,6 @@ #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" #import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" #import "Firestore/Source/Local/FSTLevelDB.h" -#import "Firestore/Source/Local/FSTLevelDBMutationQueue.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_migrations.h" @@ -38,6 +38,7 @@ NS_ASSUME_NONNULL_BEGIN using firebase::firestore::FirestoreErrorCode; +using firebase::firestore::local::LevelDbCollectionParentKey; using firebase::firestore::local::LevelDbDocumentMutationKey; using firebase::firestore::local::LevelDbDocumentTargetKey; using firebase::firestore::local::LevelDbMigrations; @@ -335,7 +336,6 @@ - (void)testRemovesMutationBatches { // Verify std::string buffer; LevelDbTransaction transaction(_db.get(), "Verify"); - auto it = transaction.NewIterator(); // verify that we deleted the correct batches XCTAssertTrue(transaction.Get(LevelDbMutationKey::Key("foo", 1), &buffer).IsNotFound()); XCTAssertTrue(transaction.Get(LevelDbMutationKey::Key("foo", 2), &buffer).IsNotFound()); @@ -361,6 +361,60 @@ - (void)testRemovesMutationBatches { } } +- (void)testCreateCollectionParentsIndex { + // This test creates a database with schema version 5 that has a few + // mutations and a few remote documents and then ensures that appropriate + // entries are written to the collectionParentIndex. + std::vector write_paths{"cg1/x", "cg1/y", "cg1/x/cg1/x", "cg2/x", "cg1/x/cg2/x"}; + std::vector remote_doc_paths{"cg1/z", "cg1/y/cg1/x", "cg2/x/cg3/x", + "blah/x/blah/x/cg3/x"}; + std::map> expected_parents{ + {"cg1", {"", "cg1/x", "cg1/y"}}, {"cg2", {"", "cg1/x"}}, {"cg3", {"blah/x/blah/x", "cg2/x"}}}; + + std::string empty_buffer; + LevelDbMigrations::RunMigrations(_db.get(), 5); + { + LevelDbTransaction transaction(_db.get(), "Write Mutations and Remote Documents"); + // Write mutations. + for (auto write_path : write_paths) { + // We "cheat" and only write the DbDocumentMutation index entries, since + // that's all the migration uses. + DocumentKey key = DocumentKey::FromPathString(write_path); + transaction.Put(LevelDbDocumentMutationKey::Key("dummy-uid", key, /*dummy batchId=*/123), + empty_buffer); + } + + // Write remote document entries. + for (auto remote_doc_path : remote_doc_paths) { + DocumentKey key = DocumentKey::FromPathString(remote_doc_path); + transaction.Put(LevelDbRemoteDocumentKey::Key(key), empty_buffer); + } + + transaction.Commit(); + } + + // Migrate to v6 and verify index entries. + LevelDbMigrations::RunMigrations(_db.get(), 6); + { + LevelDbTransaction transaction(_db.get(), "Verify"); + + std::map> actual_parents; + auto index_iterator = transaction.NewIterator(); + std::string index_prefix = LevelDbCollectionParentKey::KeyPrefix(); + LevelDbCollectionParentKey row_key; + for (index_iterator->Seek(index_prefix); index_iterator->Valid(); index_iterator->Next()) { + if (!absl::StartsWith(index_iterator->key(), index_prefix) || + !row_key.Decode(index_iterator->key())) + break; + + std::vector &parents = actual_parents[row_key.collection_id()]; + parents.push_back(row_key.parent().CanonicalString()); + } + + XCTAssertEqual(actual_parents, expected_parents); + } +} + - (void)testCanDowngrade { // First, run all of the migrations LevelDbMigrations::RunMigrations(_db.get()); diff --git a/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm index 917f163c179..d24e24a0818 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm @@ -14,8 +14,6 @@ * limitations under the License. */ -#import "Firestore/Source/Local/FSTLevelDBMutationQueue.h" - #import #include @@ -81,7 +79,7 @@ - (void)setUp { _db = [FSTPersistenceTestHelpers levelDBPersistence]; [_db.referenceDelegate addInMemoryPins:&_additionalReferences]; - self.mutationQueue = [_db mutationQueueForUser:User("user")].mutationQueue; + self.mutationQueue = [_db mutationQueueForUser:User("user")]; self.persistence = _db; self.persistence.run("Setup", [&]() { self.mutationQueue->Start(); }); diff --git a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm index 538f6c61e86..a432f001619 100644 --- a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm @@ -19,6 +19,9 @@ #import #import +#include +#include + #import "Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h" #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" #import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" @@ -79,6 +82,10 @@ - (void)setUp { } - (void)testEncodesMutationBatch { + FSTMutation *base = [[FSTPatchMutation alloc] initWithKey:FSTTestDocKey(@"bar/baz") + fieldMask:FieldMask{testutil::Field("a")} + value:FSTTestObjectValue(@{@"a" : @"b"}) + precondition:Precondition::Exists(true)]; FSTMutation *set = FSTTestSetMutation(@"foo/bar", @{@"a" : @"b", @"num" : @1}); FSTMutation *patch = [[FSTPatchMutation alloc] initWithKey:FSTTestDocKey(@"bar/baz") @@ -89,7 +96,16 @@ - (void)testEncodesMutationBatch { FIRTimestamp *writeTime = [FIRTimestamp timestamp]; FSTMutationBatch *model = [[FSTMutationBatch alloc] initWithBatchID:42 localWriteTime:writeTime - mutations:@[ set, patch, del ]]; + baseMutations:{base} + mutations:{set, patch, del}]; + + GCFSWrite *baseProto = [GCFSWrite message]; + baseProto.update.name = @"projects/p/databases/d/documents/bar/baz"; + [baseProto.update.fields addEntriesFromDictionary:@{ + @"a" : [self.remoteSerializer encodedString:@"b"], + }]; + [baseProto.updateMask.fieldPathsArray addObjectsFromArray:@[ @"a" ]]; + baseProto.currentDocument.exists = YES; GCFSWrite *setProto = [GCFSWrite message]; setProto.update.name = @"projects/p/databases/d/documents/foo/bar"; @@ -116,6 +132,7 @@ - (void)testEncodesMutationBatch { FSTPBWriteBatch *batchProto = [FSTPBWriteBatch message]; batchProto.batchId = 42; + [batchProto.baseWritesArray addObject:baseProto]; [batchProto.writesArray addObjectsFromArray:@[ setProto, patchProto, delProto ]]; batchProto.localWriteTime = writeTimeProto; @@ -123,7 +140,8 @@ - (void)testEncodesMutationBatch { FSTMutationBatch *decoded = [self.serializer decodedMutationBatch:batchProto]; XCTAssertEqual(decoded.batchID, model.batchID); XCTAssertEqualObjects(decoded.localWriteTime, model.localWriteTime); - XCTAssertEqualObjects(decoded.mutations, model.mutations); + FSTAssertEqualVectors(decoded.baseMutations, model.baseMutations); + FSTAssertEqualVectors(decoded.mutations, model.mutations); XCTAssertEqual([decoded keys], [model keys]); } diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm index 98835fad06f..79d906a5d18 100644 --- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm @@ -19,15 +19,17 @@ #import #import +#include +#include + +#import "Firestore/Source/API/FIRFieldValue+Internal.h" #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Local/FSTLocalWriteResult.h" #import "Firestore/Source/Local/FSTPersistence.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" #import "Firestore/Source/Util/FSTClasses.h" #import "Firestore/Example/Tests/Local/FSTLocalStoreTests.h" @@ -37,6 +39,7 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/document_map.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" #include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/remote/watch_change.h" #include "Firestore/core/src/firebase/firestore/util/status.h" @@ -51,6 +54,8 @@ using firebase::firestore::model::MaybeDocumentMap; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; +using firebase::firestore::remote::RemoteEvent; +using firebase::firestore::remote::TestTargetMetadataProvider; using firebase::firestore::remote::WatchChangeAggregator; using firebase::firestore::remote::WatchTargetChange; using firebase::firestore::remote::WatchTargetChangeState; @@ -121,19 +126,21 @@ - (BOOL)isTestBaseClass { } - (void)writeMutation:(FSTMutation *)mutation { - [self writeMutations:@[ mutation ]]; + [self writeMutations:{mutation}]; } -- (void)writeMutations:(NSArray *)mutations { - FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations]; +- (void)writeMutations:(std::vector &&)mutations { + auto mutationsCopy = mutations; + FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:std::move(mutationsCopy)]; XCTAssertNotNil(result); [self.batches addObject:[[FSTMutationBatch alloc] initWithBatchID:result.batchID localWriteTime:[FIRTimestamp timestamp] - mutations:mutations]]; + baseMutations:{} + mutations:std::move(mutations)]]; _lastChanges = result.changes; } -- (void)applyRemoteEvent:(FSTRemoteEvent *)event { +- (void)applyRemoteEvent:(const RemoteEvent &)event { _lastChanges = [self.localStore applyRemoteEvent:event]; } @@ -141,20 +148,26 @@ - (void)notifyLocalViewChanges:(FSTLocalViewChanges *)changes { [self.localStore notifyLocalViewChanges:@[ changes ]]; } -- (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion { +- (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion + transformResult:(id _Nullable)transformResult { FSTMutationBatch *batch = [self.batches firstObject]; [self.batches removeObjectAtIndex:0]; - XCTAssertEqual(batch.mutations.count, 1, @"Acknowledging more than one mutation not supported."); + XCTAssertEqual(batch.mutations.size(), 1, @"Acknowledging more than one mutation not supported."); SnapshotVersion version = testutil::Version(documentVersion); - FSTMutationResult *mutationResult = [[FSTMutationResult alloc] initWithVersion:version - transformResults:nil]; + FSTMutationResult *mutationResult = [[FSTMutationResult alloc] + initWithVersion:version + transformResults:transformResult != nil ? @[ FSTTestFieldValue(transformResult) ] : nil]; FSTMutationBatchResult *result = [FSTMutationBatchResult resultWithBatch:batch commitVersion:version - mutationResults:@[ mutationResult ] + mutationResults:{mutationResult} streamToken:nil]; _lastChanges = [self.localStore acknowledgeBatchWithResult:result]; } +- (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion { + [self acknowledgeMutationWithVersion:documentVersion transformResult:nil]; +} + - (void)rejectMutation { FSTMutationBatch *batch = [self.batches firstObject]; [self.batches removeObjectAtIndex:0]; @@ -220,11 +233,13 @@ - (TargetId)allocateQuery:(FSTQuery *)query { - (void)testMutationBatchKeys { if ([self isTestBaseClass]) return; + FSTMutation *base = FSTTestSetMutation(@"foo/ignore", @{@"foo" : @"bar"}); FSTMutation *set1 = FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}); FSTMutation *set2 = FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}); FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:1 localWriteTime:[FIRTimestamp timestamp] - mutations:@[ set1, set2 ]]; + baseMutations:{base} + mutations:{set1, set2}]; DocumentKeySet keys = [batch keys]; XCTAssertEqual(keys.size(), 2u); } @@ -616,10 +631,10 @@ - (void)testHandlesSetMutationThenPatchMutationThenDocumentThenAckThenAck { - (void)testHandlesSetMutationAndPatchMutationTogether { if ([self isTestBaseClass]) return; - [self writeMutations:@[ + [self writeMutations:{ FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}), - FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {}) - ]]; + FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {}) + }]; FSTAssertChanged( @[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, FSTDocumentStateLocalMutations) ]); @@ -646,11 +661,11 @@ - (void)testHandlesSetMutationThenPatchMutationThenReject { - (void)testHandlesSetMutationsAndPatchMutationOfJustOneTogether { if ([self isTestBaseClass]) return; - [self writeMutations:@[ + [self writeMutations:{ FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}), - FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}), - FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {}) - ]]; + FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}), + FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {}) + }]; FSTAssertChanged((@[ FSTTestDoc("bar/baz", 0, @{@"bar" : @"baz"}, FSTDocumentStateLocalMutations), @@ -840,11 +855,11 @@ - (void)testThrowsAwayDocumentsWithUnknownTargetIDsImmediately { - (void)testCanExecuteDocumentQueries { if ([self isTestBaseClass]) return; - [self.localStore locallyWriteMutations:@[ + [self.localStore locallyWriteMutations:{ FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), - FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), - FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}) - ]]; + FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), + FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}) + }]; FSTQuery *query = FSTTestQuery("foo/bar"); DocumentMap docs = [self.localStore executeQuery:query]; XCTAssertEqualObjects(docMapToArray(docs), @[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, @@ -854,13 +869,13 @@ - (void)testCanExecuteDocumentQueries { - (void)testCanExecuteCollectionQueries { if ([self isTestBaseClass]) return; - [self.localStore locallyWriteMutations:@[ + [self.localStore locallyWriteMutations:{ FSTTestSetMutation(@"fo/bar", @{@"fo" : @"bar"}), - FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), - FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), - FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}), - FSTTestSetMutation(@"fooo/blah", @{@"fooo" : @"blah"}) - ]]; + FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), + FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), + FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}), + FSTTestSetMutation(@"fooo/blah", @{@"fooo" : @"blah"}) + }]; FSTQuery *query = FSTTestQuery("foo"); DocumentMap docs = [self.localStore executeQuery:query]; XCTAssertEqualObjects( @@ -884,7 +899,7 @@ - (void)testCanExecuteMixedCollectionQueries { FSTTestDoc("foo/bar", 20, @{@"a" : @"b"}, FSTDocumentStateSynced), {2}, {})]; - [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]]; + [self.localStore locallyWriteMutations:{ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) }]; DocumentMap docs = [self.localStore executeQuery:query]; XCTAssertEqualObjects(docMapToArray(docs), (@[ @@ -906,11 +921,11 @@ - (void)testPersistsResumeTokens { NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000); WatchTargetChange watchChange{WatchTargetChangeState::Current, {targetID}, resumeToken}; - WatchChangeAggregator aggregator{[FSTTestTargetMetadataProvider - providerWithSingleResultForKey:testutil::Key("foo/bar") - targets:{targetID}]}; + auto metadataProvider = TestTargetMetadataProvider::CreateSingleResultProvider( + testutil::Key("foo/bar"), std::vector{targetID}); + WatchChangeAggregator aggregator{&metadataProvider}; aggregator.HandleTargetChange(watchChange); - FSTRemoteEvent *remoteEvent = aggregator.CreateRemoteEvent(testutil::Version(1000)); + RemoteEvent remoteEvent = aggregator.CreateRemoteEvent(testutil::Version(1000)); [self applyRemoteEvent:remoteEvent]; // Stop listening so that the query should become inactive (but persistent) @@ -939,7 +954,7 @@ - (void)testRemoteDocumentKeysForTarget { applyRemoteEvent:FSTTestAddedRemoteEvent( FSTTestDoc("foo/bar", 20, @{@"a" : @"b"}, FSTDocumentStateSynced), {2})]; - [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]]; + [self.localStore locallyWriteMutations:{ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) }]; DocumentKeySet keys = [self.localStore remoteDocumentKeysForTarget:2]; DocumentKeySet expected{testutil::Key("foo/bar"), testutil::Key("foo/baz")}; @@ -949,6 +964,202 @@ - (void)testRemoteDocumentKeysForTarget { XCTAssertEqual(keys, (DocumentKeySet{testutil::Key("foo/bar"), testutil::Key("foo/baz")})); } +// TODO(mrschmidt): The FieldValue.increment() field transform tests below would probably be +// better implemented as spec tests but currently they don't support transforms. + +- (void)testHandlesSetMutationThenTransformMutationThenTransformMutation { + if ([self isTestBaseClass]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"sum" : @0})]; + FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"sum" : @0}, FSTDocumentStateLocalMutations)); + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"sum" : @0}, FSTDocumentStateLocalMutations) ]); + + [self writeMutation:FSTTestTransformMutation( + @"foo/bar", @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]})]; + FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"sum" : @1}, FSTDocumentStateLocalMutations)); + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"sum" : @1}, FSTDocumentStateLocalMutations) ]); + + [self writeMutation:FSTTestTransformMutation( + @"foo/bar", @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:2]})]; + FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"sum" : @3}, FSTDocumentStateLocalMutations)); + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"sum" : @3}, FSTDocumentStateLocalMutations) ]); +} + +- (void)testHandlesSetMutationThenAckThenTransformMutationThenAckThenTransformMutation { + if ([self isTestBaseClass]) return; + + // Since this test doesn't start a listen, Eager GC removes the documents from the cache as + // soon as the mutation is applied. This creates a lot of special casing in this unit test but + // does not expand its test coverage. + if ([self gcIsEager]) return; + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"sum" : @0})]; + FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"sum" : @0}, FSTDocumentStateLocalMutations)); + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"sum" : @0}, FSTDocumentStateLocalMutations) ]); + + [self acknowledgeMutationWithVersion:1]; + FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"sum" : @0}, FSTDocumentStateCommittedMutations)); + FSTAssertChanged( + @[ FSTTestDoc("foo/bar", 1, @{@"sum" : @0}, FSTDocumentStateCommittedMutations) ]); + + [self writeMutation:FSTTestTransformMutation( + @"foo/bar", @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]})]; + FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"sum" : @1}, FSTDocumentStateLocalMutations)); + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"sum" : @1}, FSTDocumentStateLocalMutations) ]); + + [self acknowledgeMutationWithVersion:2 transformResult:@1]; + FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"sum" : @1}, FSTDocumentStateCommittedMutations)); + FSTAssertChanged( + @[ FSTTestDoc("foo/bar", 2, @{@"sum" : @1}, FSTDocumentStateCommittedMutations) ]); + + [self writeMutation:FSTTestTransformMutation( + @"foo/bar", @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:2]})]; + FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"sum" : @3}, FSTDocumentStateLocalMutations)); + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 2, @{@"sum" : @3}, FSTDocumentStateLocalMutations) ]); +} + +- (void)testHandlesSetMutationThenTransformMutationThenRemoteEventThenTransformMutation { + if ([self isTestBaseClass]) return; + + FSTQuery *query = FSTTestQuery("foo"); + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"sum" : @0})]; + FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"sum" : @0}, FSTDocumentStateLocalMutations)); + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"sum" : @0}, FSTDocumentStateLocalMutations) ]); + + [self + applyRemoteEvent:FSTTestAddedRemoteEvent( + FSTTestDoc("foo/bar", 1, @{@"sum" : @0}, FSTDocumentStateSynced), {2})]; + + [self acknowledgeMutationWithVersion:1]; + FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"sum" : @0}, FSTDocumentStateSynced)); + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"sum" : @0}, FSTDocumentStateSynced) ]); + + [self writeMutation:FSTTestTransformMutation( + @"foo/bar", @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]})]; + FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"sum" : @1}, FSTDocumentStateLocalMutations)); + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"sum" : @1}, FSTDocumentStateLocalMutations) ]); + + // The value in this remote event gets ignored since we still have a pending transform mutation. + [self applyRemoteEvent:FSTTestUpdateRemoteEvent( + FSTTestDoc("foo/bar", 2, @{@"sum" : @0}, FSTDocumentStateSynced), {2}, + {})]; + FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"sum" : @1}, FSTDocumentStateLocalMutations)); + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 2, @{@"sum" : @1}, FSTDocumentStateLocalMutations) ]); + + // Add another increment. Note that we still compute the increment based on the local value. + [self writeMutation:FSTTestTransformMutation( + @"foo/bar", @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:2]})]; + FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"sum" : @3}, FSTDocumentStateLocalMutations)); + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 2, @{@"sum" : @3}, FSTDocumentStateLocalMutations) ]); + + [self acknowledgeMutationWithVersion:3 transformResult:@1]; + FSTAssertContains(FSTTestDoc("foo/bar", 3, @{@"sum" : @3}, FSTDocumentStateLocalMutations)); + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 3, @{@"sum" : @3}, FSTDocumentStateLocalMutations) ]); + + [self acknowledgeMutationWithVersion:4 transformResult:@1339]; + FSTAssertContains( + FSTTestDoc("foo/bar", 4, @{@"sum" : @1339}, FSTDocumentStateCommittedMutations)); + FSTAssertChanged( + @[ FSTTestDoc("foo/bar", 4, @{@"sum" : @1339}, FSTDocumentStateCommittedMutations) ]); +} + +- (void)testHoldsBackOnlyNonIdempotentTransforms { + if ([self isTestBaseClass]) return; + + FSTQuery *query = FSTTestQuery("foo"); + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"sum" : @0, @"array_union" : @[]})]; + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"sum" : @0, @"array_union" : @[]}, + FSTDocumentStateLocalMutations) ]); + + [self acknowledgeMutationWithVersion:1]; + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"sum" : @0, @"array_union" : @[]}, + FSTDocumentStateCommittedMutations) ]); + + [self applyRemoteEvent:FSTTestAddedRemoteEvent( + FSTTestDoc("foo/bar", 1, @{@"sum" : @0, @"array_union" : @[]}, + FSTDocumentStateSynced), + {2})]; + FSTAssertChanged( + @[ FSTTestDoc("foo/bar", 1, @{@"sum" : @0, @"array_union" : @[]}, FSTDocumentStateSynced) ]); + + [self writeMutations:{ + FSTTestTransformMutation(@"foo/bar", + @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]}), + FSTTestTransformMutation( + @"foo/bar", + @{@"array_union" : [FIRFieldValue fieldValueForArrayUnion:@[ @"foo" ]]}) + }]; + + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"sum" : @1, @"array_union" : @[ @"foo" ]}, + FSTDocumentStateLocalMutations) ]); + + // The sum transform is not idempotent and the backend's updated value is ignored. The + // ArrayUnion transform is recomputed and includes the backend value. + [self + applyRemoteEvent:FSTTestUpdateRemoteEvent( + FSTTestDoc("foo/bar", 1, @{@"sum" : @1337, @"array_union" : @[ @"bar" ]}, + FSTDocumentStateSynced), + {2}, {})]; + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"sum" : @1, @"array_union" : @[ @"bar", @"foo" ]}, + FSTDocumentStateLocalMutations) ]); +} + +- (void)testHandlesMergeMutationWithTransformThenRemoteEvent { + if ([self isTestBaseClass]) return; + + FSTQuery *query = FSTTestQuery("foo"); + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self writeMutations:{ + FSTTestPatchMutation("foo/bar", @{}, {firebase::firestore::testutil::Field("sum")}), + FSTTestTransformMutation(@"foo/bar", + @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]}) + }]; + + FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"sum" : @1}, FSTDocumentStateLocalMutations)); + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"sum" : @1}, FSTDocumentStateLocalMutations) ]); + + [self applyRemoteEvent:FSTTestAddedRemoteEvent( + FSTTestDoc("foo/bar", 1, @{@"sum" : @1337}, FSTDocumentStateSynced), + {2})]; + + FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"sum" : @1}, FSTDocumentStateLocalMutations)); + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"sum" : @1}, FSTDocumentStateLocalMutations) ]); +} + +- (void)testHandlesPatchMutationWithTransformThenRemoteEvent { + if ([self isTestBaseClass]) return; + + FSTQuery *query = FSTTestQuery("foo"); + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self writeMutations:{ + FSTTestPatchMutation("foo/bar", @{}, {}), + FSTTestTransformMutation(@"foo/bar", + @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:1]}) + }]; + + FSTAssertNotContains(@"foo/bar"); + FSTAssertChanged(@[ FSTTestDeletedDoc("foo/bar", 0, NO) ]); + + // Note: This test reflects the current behavior, but it may be preferable to replay the + // mutation once we receive the first value from the remote event. + [self applyRemoteEvent:FSTTestAddedRemoteEvent( + FSTTestDoc("foo/bar", 1, @{@"sum" : @1337}, FSTDocumentStateSynced), + {2})]; + + FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"sum" : @1}, FSTDocumentStateLocalMutations)); + FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"sum" : @1}, FSTDocumentStateLocalMutations) ]); +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm index bb2ff6c4018..0d54f4466ec 100644 --- a/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm +++ b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm @@ -14,8 +14,6 @@ * limitations under the License. */ -#import "Firestore/Source/Local/FSTMemoryMutationQueue.h" - #import "Firestore/Source/Local/FSTMemoryPersistence.h" #import "Firestore/Example/Tests/Local/FSTMutationQueueTests.h" @@ -43,7 +41,7 @@ - (void)setUp { self.persistence = [FSTPersistenceTestHelpers eagerGCMemoryPersistence]; [self.persistence.referenceDelegate addInMemoryPins:&_additionalReferences]; - self.mutationQueue = [self.persistence mutationQueueForUser:User("user")].mutationQueue; + self.mutationQueue = [self.persistence mutationQueueForUser:User("user")]; } @end diff --git a/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.mm b/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.mm index c39a15f1fbf..7784056705b 100644 --- a/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.mm +++ b/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.mm @@ -44,7 +44,7 @@ - (void)setUp { self.persistence = [FSTPersistenceTestHelpers eagerGCMemoryPersistence]; HARD_ASSERT(!_cache, "Previous cache not torn down"); - _cache = absl::make_unique(); + _cache = absl::make_unique(self.persistence); } - (RemoteDocumentCache *)remoteDocumentCache { diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm index c975bf70cd8..d0346c2a641 100644 --- a/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm +++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm @@ -19,6 +19,7 @@ #import #include +#include #include #import "Firestore/Source/Core/FSTQuery.h" @@ -50,14 +51,6 @@ - (void)tearDown { [super tearDown]; } -- (void)assertVector:(const std::vector &)actual - matchesExpected:(const std::vector &)expected { - XCTAssertEqual(actual.size(), expected.size(), @"Vector length mismatch"); - for (int i = 0; i < expected.size(); i++) { - XCTAssertEqualObjects(actual[i], expected[i]); - } -} - /** * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for * FSTMutationQueueTests since it is incomplete without the implementations supplied by its @@ -208,7 +201,7 @@ - (void)testAllMutationBatchesAffectingDocumentKey { NSMutableArray *batches = [NSMutableArray array]; for (FSTMutation *mutation in mutations) { FSTMutationBatch *batch = - self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], @[ mutation ]); + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], {}, {mutation}); [batches addObject:batch]; } @@ -216,7 +209,7 @@ - (void)testAllMutationBatchesAffectingDocumentKey { std::vector matches = self.mutationQueue->AllMutationBatchesAffectingDocumentKey(testutil::Key("foo/bar")); - [self assertVector:matches matchesExpected:expected]; + FSTAssertEqualVectors(matches, expected); }); } @@ -235,7 +228,7 @@ - (void)testAllMutationBatchesAffectingDocumentKeys { NSMutableArray *batches = [NSMutableArray array]; for (FSTMutation *mutation in mutations) { FSTMutationBatch *batch = - self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], @[ mutation ]); + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], {}, {mutation}); [batches addObject:batch]; } @@ -248,7 +241,7 @@ - (void)testAllMutationBatchesAffectingDocumentKeys { std::vector matches = self.mutationQueue->AllMutationBatchesAffectingDocumentKeys(keys); - [self assertVector:matches matchesExpected:expected]; + FSTAssertEqualVectors(matches, expected); }); } @@ -256,21 +249,21 @@ - (void)testAllMutationBatchesAffectingDocumentKeys_handlesOverlap { if ([self isTestBaseClass]) return; self.persistence.run("testAllMutationBatchesAffectingDocumentKeys_handlesOverlap", [&]() { - NSArray *group1 = @[ - FSTTestSetMutation(@"foo/bar", @{@"a" : @1}), - FSTTestSetMutation(@"foo/baz", @{@"a" : @1}), - ]; + std::vector group1 = { + FSTTestSetMutation(@"foo/bar", @{@"a" : @1}), + FSTTestSetMutation(@"foo/baz", @{@"a" : @1}), + }; FSTMutationBatch *batch1 = - self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], group1); + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], {}, std::move(group1)); - NSArray *group2 = @[ FSTTestSetMutation(@"food/bar", @{@"a" : @1}) ]; - self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], group2); + std::vector group2 = {FSTTestSetMutation(@"food/bar", @{@"a" : @1})}; + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], {}, std::move(group2)); - NSArray *group3 = @[ - FSTTestSetMutation(@"foo/bar", @{@"b" : @1}), - ]; + std::vector group3 = { + FSTTestSetMutation(@"foo/bar", @{@"b" : @1}), + }; FSTMutationBatch *batch3 = - self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], group3); + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], {}, std::move(group3)); DocumentKeySet keys{ Key("foo/bar"), @@ -281,7 +274,7 @@ - (void)testAllMutationBatchesAffectingDocumentKeys_handlesOverlap { std::vector matches = self.mutationQueue->AllMutationBatchesAffectingDocumentKeys(keys); - [self assertVector:matches matchesExpected:expected]; + FSTAssertEqualVectors(matches, expected); }); } @@ -300,7 +293,7 @@ - (void)testAllMutationBatchesAffectingQuery { NSMutableArray *batches = [NSMutableArray array]; for (FSTMutation *mutation in mutations) { FSTMutationBatch *batch = - self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], @[ mutation ]); + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], {}, {mutation}); [batches addObject:batch]; } @@ -309,7 +302,7 @@ - (void)testAllMutationBatchesAffectingQuery { std::vector matches = self.mutationQueue->AllMutationBatchesAffectingQuery(query); - [self assertVector:matches matchesExpected:expected]; + FSTAssertEqualVectors(matches, expected); }); } @@ -327,7 +320,7 @@ - (void)testRemoveMutationBatches { std::vector found; found = self.mutationQueue->AllMutationBatches(); - [self assertVector:found matchesExpected:batches]; + FSTAssertEqualVectors(found, batches); XCTAssertEqual(found.size(), 9); self.mutationQueue->RemoveMutationBatch(batches[0]); @@ -337,7 +330,7 @@ - (void)testRemoveMutationBatches { XCTAssertEqual([self batchCount], 6); found = self.mutationQueue->AllMutationBatches(); - [self assertVector:found matchesExpected:batches]; + FSTAssertEqualVectors(found, batches); XCTAssertEqual(found.size(), 6); self.mutationQueue->RemoveMutationBatch(batches[0]); @@ -345,7 +338,7 @@ - (void)testRemoveMutationBatches { XCTAssertEqual([self batchCount], 5); found = self.mutationQueue->AllMutationBatches(); - [self assertVector:found matchesExpected:batches]; + FSTAssertEqualVectors(found, batches); XCTAssertEqual(found.size(), 5); self.mutationQueue->RemoveMutationBatch(batches[0]); @@ -357,7 +350,7 @@ - (void)testRemoveMutationBatches { XCTAssertEqual([self batchCount], 3); found = self.mutationQueue->AllMutationBatches(); - [self assertVector:found matchesExpected:batches]; + FSTAssertEqualVectors(found, batches); XCTAssertEqual(found.size(), 3); XCTAssertFalse(self.mutationQueue->IsEmpty()); @@ -404,7 +397,7 @@ - (FSTMutationBatch *)addMutationBatchWithKey:(NSString *)key { FSTSetMutation *mutation = FSTTestSetMutation(key, @{@"a" : @1}); FSTMutationBatch *batch = - self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], @[ mutation ]); + self.mutationQueue->AddMutationBatch([FIRTimestamp timestamp], {}, {mutation}); return batch; } diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.mm b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.mm index c13d967bf89..e58f9535b97 100644 --- a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.mm +++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.mm @@ -172,6 +172,7 @@ - (void)testDocumentsMatchingQuery { // query path. We'll need more tests once we add index support. [self setTestDocumentAtPath:"a/1"]; [self setTestDocumentAtPath:"b/1"]; + [self setTestDocumentAtPath:"b/1/z/1"]; [self setTestDocumentAtPath:"b/2"]; [self setTestDocumentAtPath:"c/1"]; diff --git a/Firestore/Example/Tests/Model/FSTDocumentSetTests.mm b/Firestore/Example/Tests/Model/FSTDocumentSetTests.mm index 05be1b7eeab..edb2334f9b4 100644 --- a/Firestore/Example/Tests/Model/FSTDocumentSetTests.mm +++ b/Firestore/Example/Tests/Model/FSTDocumentSetTests.mm @@ -14,13 +14,19 @@ * limitations under the License. */ -#import "Firestore/Source/Model/FSTDocumentSet.h" - #import -#import "Firestore/Source/Model/FSTDocument.h" +#include #import "Firestore/Example/Tests/Util/FSTHelpers.h" +#import "Firestore/Source/Model/FSTDocument.h" + +// TODO(wilhuff) move to first include once this test filename matches +#include "Firestore/core/src/firebase/firestore/model/document_set.h" +#include "Firestore/core/test/firebase/firestore/testutil/xcgmock.h" + +using firebase::firestore::model::DocumentSet; +using testing::ElementsAre; NS_ASSUME_NONNULL_BEGIN @@ -44,91 +50,93 @@ - (void)setUp { } - (void)testCount { - XCTAssertEqual([FSTTestDocSet(_comp, @[]) count], 0); - XCTAssertEqual([FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]) count], 3); + XCTAssertEqual(FSTTestDocSet(_comp, @[]).size(), 0); + XCTAssertEqual(FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]).size(), 3); } - (void)testHasKey { - FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]); + DocumentSet set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]); - XCTAssertTrue([set containsKey:_doc1.key]); - XCTAssertTrue([set containsKey:_doc2.key]); - XCTAssertFalse([set containsKey:_doc3.key]); + XCTAssertTrue(set.ContainsKey(_doc1.key)); + XCTAssertTrue(set.ContainsKey(_doc2.key)); + XCTAssertFalse(set.ContainsKey(_doc3.key)); } - (void)testDocumentForKey { - FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]); + DocumentSet set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]); - XCTAssertEqualObjects([set documentForKey:_doc1.key], _doc1); - XCTAssertEqualObjects([set documentForKey:_doc2.key], _doc2); - XCTAssertNil([set documentForKey:_doc3.key]); + XCTAssertEqualObjects(set.GetDocument(_doc1.key), _doc1); + XCTAssertEqualObjects(set.GetDocument(_doc2.key), _doc2); + XCTAssertNil(set.GetDocument(_doc3.key)); } - (void)testFirstAndLastDocument { - FSTDocumentSet *set = FSTTestDocSet(_comp, @[]); - XCTAssertNil([set firstDocument]); - XCTAssertNil([set lastDocument]); + DocumentSet set = FSTTestDocSet(_comp, @[]); + XCTAssertNil(set.GetFirstDocument()); + XCTAssertNil(set.GetLastDocument()); set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); - XCTAssertEqualObjects([set firstDocument], _doc3); - XCTAssertEqualObjects([set lastDocument], _doc2); + XCTAssertEqualObjects(set.GetFirstDocument(), _doc3); + XCTAssertEqualObjects(set.GetLastDocument(), _doc2); } - (void)testKeepsDocumentsInTheRightOrder { - FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); - XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, _doc2 ])); + DocumentSet set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + XC_ASSERT_THAT(set, ElementsAre(_doc3, _doc1, _doc2)); } - (void)testDeletes { - FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + DocumentSet set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); - FSTDocumentSet *setWithoutDoc1 = [set documentSetByRemovingKey:_doc1.key]; - XCTAssertEqualObjects([[setWithoutDoc1 documentEnumerator] allObjects], (@[ _doc3, _doc2 ])); - XCTAssertEqual([setWithoutDoc1 count], 2); + DocumentSet setWithoutDoc1 = set.erase(_doc1.key); + XC_ASSERT_THAT(setWithoutDoc1, ElementsAre(_doc3, _doc2)); + XCTAssertEqual(setWithoutDoc1.size(), 2); // Original remains unchanged - XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, _doc2 ])); + XC_ASSERT_THAT(set, ElementsAre(_doc3, _doc1, _doc2)); - FSTDocumentSet *setWithoutDoc3 = [setWithoutDoc1 documentSetByRemovingKey:_doc3.key]; - XCTAssertEqualObjects([[setWithoutDoc3 documentEnumerator] allObjects], (@[ _doc2 ])); - XCTAssertEqual([setWithoutDoc3 count], 1); + DocumentSet setWithoutDoc3 = setWithoutDoc1.erase(_doc3.key); + XC_ASSERT_THAT(setWithoutDoc3, ElementsAre(_doc2)); + XCTAssertEqual(setWithoutDoc3.size(), 1); } - (void)testUpdates { - FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + DocumentSet set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); FSTDocument *doc2Prime = FSTTestDoc("docs/2", 0, @{@"sort" : @9}, FSTDocumentStateSynced); - set = [set documentSetByAddingDocument:doc2Prime]; - XCTAssertEqual([set count], 3); - XCTAssertEqualObjects([set documentForKey:doc2Prime.key], doc2Prime); - XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, doc2Prime ])); + set = set.insert(doc2Prime); + XCTAssertEqual(set.size(), 3); + XCTAssertEqualObjects(set.GetDocument(doc2Prime.key), doc2Prime); + XC_ASSERT_THAT(set, ElementsAre(_doc3, _doc1, doc2Prime)); } - (void)testAddsDocsWithEqualComparisonValues { FSTDocument *doc4 = FSTTestDoc("docs/4", 0, @{@"sort" : @2}, FSTDocumentStateSynced); - FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, doc4 ]); - XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc1, doc4 ])); + DocumentSet set = FSTTestDocSet(_comp, @[ _doc1, doc4 ]); + XC_ASSERT_THAT(set, ElementsAre(_doc1, doc4)); } - (void)testIsEqual { - FSTDocumentSet *set1 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]); - FSTDocumentSet *set2 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]); - XCTAssertEqualObjects(set1, set1); - XCTAssertEqualObjects(set1, set2); - XCTAssertNotEqualObjects(set1, nil); - - FSTDocumentSet *sortedSet1 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); - FSTDocumentSet *sortedSet2 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); - XCTAssertEqualObjects(sortedSet1, sortedSet1); - XCTAssertEqualObjects(sortedSet1, sortedSet2); - XCTAssertNotEqualObjects(sortedSet1, nil); - - FSTDocumentSet *shortSet = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2 ]); - XCTAssertNotEqualObjects(set1, shortSet); - XCTAssertNotEqualObjects(set1, sortedSet1); + DocumentSet empty{FSTDocumentComparatorByKey}; + DocumentSet set1 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]); + DocumentSet set2 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]); + XCTAssertEqual(set1, set1); + XCTAssertEqual(set1, set2); + XCTAssertNotEqual(set1, empty); + + DocumentSet sortedSet1 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + DocumentSet sortedSet2 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]); + XCTAssertEqual(sortedSet1, sortedSet1); + XCTAssertEqual(sortedSet1, sortedSet2); + XCTAssertNotEqual(sortedSet1, empty); + + DocumentSet shortSet = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2 ]); + XCTAssertNotEqual(set1, shortSet); + XCTAssertNotEqual(set1, sortedSet1); } + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Model/FSTFieldValueTests.mm b/Firestore/Example/Tests/Model/FSTFieldValueTests.mm index 7fb501d83f1..76ff572c2c5 100644 --- a/Firestore/Example/Tests/Model/FSTFieldValueTests.mm +++ b/Firestore/Example/Tests/Model/FSTFieldValueTests.mm @@ -54,8 +54,9 @@ } else if ([value isKindOfClass:[FSTDocumentKeyReference class]]) { // We directly convert these here so that the databaseIDs can be different. FSTDocumentKeyReference *reference = (FSTDocumentKeyReference *)value; - wrappedValue = [FSTReferenceValue referenceValue:reference.key - databaseID:reference.databaseID]; + wrappedValue = + [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithDocumentKey:reference.key] + databaseID:reference.databaseID]; } else { wrappedValue = FSTTestFieldValue(value); } @@ -115,7 +116,7 @@ - (void)testWrapsNilAndNSNull { } - (void)testWrapsBooleans { - NSArray *values = @[ @YES, @NO, [NSNumber numberWithChar:1], [NSNumber numberWithChar:0] ]; + NSArray *values = @[ @YES, @NO ]; for (id value in values) { FSTFieldValue *wrapped = FSTTestFieldValue(value); XCTAssertEqualObjects([wrapped class], [FSTBooleanValue class]); @@ -258,7 +259,7 @@ - (void)testWrapResourceNames { for (FSTDocumentKeyReference *value in values) { FSTFieldValue *wrapped = FSTTestFieldValue(value); XCTAssertEqualObjects([wrapped class], [FSTReferenceValue class]); - XCTAssertEqualObjects([wrapped value], value.key); + XCTAssertEqualObjects([wrapped value], [FSTDocumentKey keyWithDocumentKey:value.key]); XCTAssertTrue(*((FSTReferenceValue *)wrapped).databaseID == *(const DatabaseId *)(value.databaseID)); } @@ -459,7 +460,9 @@ - (void)testValueEquality { ], @[ FSTTestFieldValue(FSTTestGeoPoint(1, 0)) ], @[ - [FSTReferenceValue referenceValue:FSTTestDocKey(@"coll/doc1") databaseID:&database_id], + [FSTReferenceValue + referenceValue:[FSTDocumentKey keyWithDocumentKey:FSTTestDocKey(@"coll/doc1")] + databaseID:&database_id], FSTTestFieldValue(FSTTestRef("project", DatabaseId::kDefault, @"coll/doc1")) ], @[ FSTTestRef("project", "(default)", @"coll/doc2") ], diff --git a/Firestore/Example/Tests/Model/FSTMutationTests.mm b/Firestore/Example/Tests/Model/FSTMutationTests.mm index 3ee31a5ed78..6abf37d7336 100644 --- a/Firestore/Example/Tests/Model/FSTMutationTests.mm +++ b/Firestore/Example/Tests/Model/FSTMutationTests.mm @@ -151,6 +151,93 @@ - (void)testAppliesLocalServerTimestampTransformToDocuments { XCTAssertEqualObjects(transformedDoc, expectedDoc); } +- (void)testAppliesIncrementTransformToDocument { + NSDictionary *baseDoc = @{ + @"longPlusLong" : @1, + @"longPlusDouble" : @2, + @"doublePlusLong" : @3.3, + @"doublePlusDouble" : @4.0, + @"longPlusNan" : @5, + @"doublePlusNan" : @6.6, + @"longPlusInfinity" : @7, + @"doublePlusInfinity" : @8.8 + }; + NSDictionary *transform = @{ + @"longPlusLong" : [FIRFieldValue fieldValueForIntegerIncrement:1], + @"longPlusDouble" : [FIRFieldValue fieldValueForDoubleIncrement:2.2], + @"doublePlusLong" : [FIRFieldValue fieldValueForIntegerIncrement:3], + @"doublePlusDouble" : [FIRFieldValue fieldValueForDoubleIncrement:4.4], + @"longPlusNan" : [FIRFieldValue fieldValueForDoubleIncrement:NAN], + @"doublePlusNan" : [FIRFieldValue fieldValueForDoubleIncrement:NAN], + @"longPlusInfinity" : [FIRFieldValue fieldValueForDoubleIncrement:INFINITY], + @"doublePlusInfinity" : [FIRFieldValue fieldValueForDoubleIncrement:INFINITY] + }; + NSDictionary *expected = @{ + @"longPlusLong" : @2L, + @"longPlusDouble" : @4.2, + @"doublePlusLong" : @6.3, + @"doublePlusDouble" : @8.4, + @"longPlusNan" : @(NAN), + @"doublePlusNan" : @(NAN), + @"longPlusInfinity" : @(INFINITY), + @"doublePlusInfinity" : @(INFINITY) + }; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; +} + +- (void)testAppliesIncrementTransformToUnexpectedType { + NSDictionary *baseDoc = @{@"string" : @"zero"}; + NSDictionary *transform = @{@"string" : [FIRFieldValue fieldValueForIntegerIncrement:1]}; + NSDictionary *expected = @{@"string" : @1}; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; +} + +- (void)testAppliesIncrementTransformToMissingField { + NSDictionary *baseDoc = @{}; + NSDictionary *transform = @{@"missing" : [FIRFieldValue fieldValueForIntegerIncrement:1]}; + NSDictionary *expected = @{@"missing" : @1}; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; +} + +- (void)testAppliesIncrementTransformsConsecutively { + NSDictionary *baseDoc = @{@"number" : @1}; + NSDictionary *transform1 = @{@"number" : [FIRFieldValue fieldValueForIntegerIncrement:2]}; + NSDictionary *transform2 = @{@"number" : [FIRFieldValue fieldValueForIntegerIncrement:3]}; + NSDictionary *transform3 = @{@"number" : [FIRFieldValue fieldValueForIntegerIncrement:4]}; + NSDictionary *expected = @{@"number" : @10}; + [self transformBaseDoc:baseDoc + applyTransforms:@[ transform1, transform2, transform3 ] + expecting:expected]; +} + +- (void)testAppliesIncrementWithoutOverflow { + NSDictionary *baseDoc = + @{@"a" : @(LONG_MAX - 1), @"b" : @(LONG_MAX - 1), @"c" : @(LONG_MAX), @"d" : @(LONG_MAX)}; + NSDictionary *transform = @{ + @"a" : [FIRFieldValue fieldValueForIntegerIncrement:1], + @"b" : [FIRFieldValue fieldValueForIntegerIncrement:LONG_MAX], + @"c" : [FIRFieldValue fieldValueForIntegerIncrement:1], + @"d" : [FIRFieldValue fieldValueForIntegerIncrement:LONG_MAX] + }; + NSDictionary *expected = + @{@"a" : @LONG_MAX, @"b" : @LONG_MAX, @"c" : @LONG_MAX, @"d" : @LONG_MAX}; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; +} + +- (void)testAppliesIncrementWithoutUnderflow { + NSDictionary *baseDoc = + @{@"a" : @(LONG_MIN + 1), @"b" : @(LONG_MIN + 1), @"c" : @(LONG_MIN), @"d" : @(LONG_MIN)}; + NSDictionary *transform = @{ + @"a" : [FIRFieldValue fieldValueForIntegerIncrement:-1], + @"b" : [FIRFieldValue fieldValueForIntegerIncrement:LONG_MIN], + @"c" : [FIRFieldValue fieldValueForIntegerIncrement:-1], + @"d" : [FIRFieldValue fieldValueForIntegerIncrement:LONG_MIN] + }; + NSDictionary *expected = + @{@"a" : @(LONG_MIN), @"b" : @(LONG_MIN), @"c" : @(LONG_MIN), @"d" : @(LONG_MIN)}; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; +} + // NOTE: This is more a test of FSTUserDataConverter code than FSTMutation code but we don't have // unit tests for it currently. We could consider removing this test once we have integration tests. - (void)testCreateArrayUnionTransform { @@ -201,28 +288,28 @@ - (void)testAppliesLocalArrayUnionTransformToMissingField { auto baseDoc = @{}; auto transform = @{@"missing" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @2 ]]}; auto expected = @{@"missing" : @[ @1, @2 ]}; - [self transformBaseDoc:baseDoc with:transform expecting:expected]; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; } - (void)testAppliesLocalArrayUnionTransformToNonArrayField { auto baseDoc = @{@"non-array" : @42}; auto transform = @{@"non-array" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @2 ]]}; auto expected = @{@"non-array" : @[ @1, @2 ]}; - [self transformBaseDoc:baseDoc with:transform expecting:expected]; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; } - (void)testAppliesLocalArrayUnionTransformWithNonExistingElements { auto baseDoc = @{@"array" : @[ @1, @3 ]}; auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @2, @4 ]]}; auto expected = @{@"array" : @[ @1, @3, @2, @4 ]}; - [self transformBaseDoc:baseDoc with:transform expecting:expected]; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; } - (void)testAppliesLocalArrayUnionTransformWithExistingElements { auto baseDoc = @{@"array" : @[ @1, @3 ]}; auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @3 ]]}; auto expected = @{@"array" : @[ @1, @3 ]}; - [self transformBaseDoc:baseDoc with:transform expecting:expected]; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; } - (void)testAppliesLocalArrayUnionTransformWithDuplicateExistingElements { @@ -230,7 +317,7 @@ - (void)testAppliesLocalArrayUnionTransformWithDuplicateExistingElements { auto baseDoc = @{@"array" : @[ @1, @2, @2, @3 ]}; auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @2 ]]}; auto expected = @{@"array" : @[ @1, @2, @2, @3 ]}; - [self transformBaseDoc:baseDoc with:transform expecting:expected]; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; } - (void)testAppliesLocalArrayUnionTransformWithDuplicateUnionElements { @@ -238,7 +325,7 @@ - (void)testAppliesLocalArrayUnionTransformWithDuplicateUnionElements { auto baseDoc = @{@"array" : @[ @1, @3 ]}; auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @2, @2 ]]}; auto expected = @{@"array" : @[ @1, @3, @2 ]}; - [self transformBaseDoc:baseDoc with:transform expecting:expected]; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; } - (void)testAppliesLocalArrayUnionTransformWithNonPrimitiveElements { @@ -247,7 +334,7 @@ - (void)testAppliesLocalArrayUnionTransformWithNonPrimitiveElements { auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @{@"a" : @"b"}, @{@"c" : @"d"} ]]}; auto expected = @{@"array" : @[ @1, @{@"a" : @"b"}, @{@"c" : @"d"} ]}; - [self transformBaseDoc:baseDoc with:transform expecting:expected]; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; } - (void)testAppliesLocalArrayUnionTransformWithPartiallyOverlappingElements { @@ -257,35 +344,35 @@ - (void)testAppliesLocalArrayUnionTransformWithPartiallyOverlappingElements { @{@"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @{@"a" : @"b"}, @{@"c" : @"d"} ]]}; auto expected = @{@"array" : @[ @1, @{@"a" : @"b", @"c" : @"d"}, @{@"a" : @"b"}, @{@"c" : @"d"} ]}; - [self transformBaseDoc:baseDoc with:transform expecting:expected]; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; } - (void)testAppliesLocalArrayRemoveTransformToMissingField { auto baseDoc = @{}; auto transform = @{@"missing" : [FIRFieldValue fieldValueForArrayRemove:@[ @1, @2 ]]}; auto expected = @{@"missing" : @[]}; - [self transformBaseDoc:baseDoc with:transform expecting:expected]; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; } - (void)testAppliesLocalArrayRemoveTransformToNonArrayField { auto baseDoc = @{@"non-array" : @42}; auto transform = @{@"non-array" : [FIRFieldValue fieldValueForArrayRemove:@[ @1, @2 ]]}; auto expected = @{@"non-array" : @[]}; - [self transformBaseDoc:baseDoc with:transform expecting:expected]; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; } - (void)testAppliesLocalArrayRemoveTransformWithNonExistingElements { auto baseDoc = @{@"array" : @[ @1, @3 ]}; auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayRemove:@[ @2, @4 ]]}; auto expected = @{@"array" : @[ @1, @3 ]}; - [self transformBaseDoc:baseDoc with:transform expecting:expected]; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; } - (void)testAppliesLocalArrayRemoveTransformWithExistingElements { auto baseDoc = @{@"array" : @[ @1, @2, @3, @4 ]}; auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayRemove:@[ @1, @3 ]]}; auto expected = @{@"array" : @[ @2, @4 ]}; - [self transformBaseDoc:baseDoc with:transform expecting:expected]; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; } - (void)testAppliesLocalArrayRemoveTransformWithNonPrimitiveElements { @@ -294,27 +381,53 @@ - (void)testAppliesLocalArrayRemoveTransformWithNonPrimitiveElements { auto transform = @{@"array" : [FIRFieldValue fieldValueForArrayRemove:@[ @{@"a" : @"b"}, @{@"c" : @"d"} ]]}; auto expected = @{@"array" : @[ @1 ]}; - [self transformBaseDoc:baseDoc with:transform expecting:expected]; + [self transformBaseDoc:baseDoc applyTransform:transform expecting:expected]; } // Helper to test a particular transform scenario. - (void)transformBaseDoc:(NSDictionary *)baseData - with:(NSDictionary *)transformData + applyTransforms:(NSArray *> *)transforms expecting:(NSDictionary *)expectedData { - FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, baseData, FSTDocumentStateSynced); + FSTMaybeDocument *currentDoc = FSTTestDoc("collection/key", 0, baseData, FSTDocumentStateSynced); - FSTMutation *transform = FSTTestTransformMutation(@"collection/key", transformData); - - FSTMaybeDocument *transformedDoc = [transform applyToLocalDocument:baseDoc - baseDocument:baseDoc - localWriteTime:_timestamp]; + for (NSDictionary *transformData in transforms) { + FSTMutation *transform = FSTTestTransformMutation(@"collection/key", transformData); + currentDoc = [transform applyToLocalDocument:currentDoc + baseDocument:currentDoc + localWriteTime:_timestamp]; + } FSTDocument *expectedDoc = [FSTDocument documentWithData:FSTTestObjectValue(expectedData) key:FSTTestDocKey(@"collection/key") version:testutil::Version(0) state:FSTDocumentStateLocalMutations]; - XCTAssertEqualObjects(transformedDoc, expectedDoc); + XCTAssertEqualObjects(currentDoc, expectedDoc); +} + +- (void)transformBaseDoc:(NSDictionary *)baseData + applyTransform:(NSDictionary *)transformData + expecting:(NSDictionary *)expectedData { + [self transformBaseDoc:baseData applyTransforms:@[ transformData ] expecting:expectedData]; +} + +- (void)testAppliesServerAckedIncrementTransformToDocuments { + NSDictionary *docData = @{@"sum" : @1}; + FSTDocument *baseDoc = FSTTestDoc("collection/key", 0, docData, FSTDocumentStateSynced); + + FSTMutation *transform = FSTTestTransformMutation( + @"collection/key", @{@"sum" : [FIRFieldValue fieldValueForIntegerIncrement:2]}); + + FSTMutationResult *mutationResult = + [[FSTMutationResult alloc] initWithVersion:testutil::Version(1) + transformResults:@[ [FSTIntegerValue integerValue:3] ]]; + + FSTMaybeDocument *transformedDoc = [transform applyToRemoteDocument:baseDoc + mutationResult:mutationResult]; + + NSDictionary *expectedData = @{@"sum" : @3}; + XCTAssertEqualObjects(transformedDoc, FSTTestDoc("collection/key", 1, expectedData, + FSTDocumentStateCommittedMutations)); } - (void)testAppliesServerAckedServerTimestampTransformToDocuments { diff --git a/Firestore/Example/Tests/Model/transform_operations_test.mm b/Firestore/Example/Tests/Model/transform_operations_test.mm index 247ea137121..18614136620 100644 --- a/Firestore/Example/Tests/Model/transform_operations_test.mm +++ b/Firestore/Example/Tests/Model/transform_operations_test.mm @@ -41,6 +41,10 @@ Type type() const override { return nil; } + bool idempotent() const override { + return true; + } + bool operator==(const TransformOperation& other) const override { return this == &other; } diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm index b728ee94777..3172d63e812 100644 --- a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm +++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm @@ -14,8 +14,6 @@ * limitations under the License. */ -#import "Firestore/Source/Remote/FSTRemoteEvent.h" - #import #include @@ -46,6 +44,9 @@ using firebase::firestore::remote::DocumentWatchChange; using firebase::firestore::remote::ExistenceFilter; using firebase::firestore::remote::ExistenceFilterWatchChange; +using firebase::firestore::remote::RemoteEvent; +using firebase::firestore::remote::TargetChange; +using firebase::firestore::remote::TestTargetMetadataProvider; using firebase::firestore::remote::WatchChange; using firebase::firestore::remote::WatchChangeAggregator; using firebase::firestore::remote::WatchTargetChange; @@ -92,13 +93,12 @@ @interface FSTRemoteEventTests : XCTestCase @implementation FSTRemoteEventTests { NSData *_resumeToken1; - FSTTestTargetMetadataProvider *_targetMetadataProvider; + TestTargetMetadataProvider _targetMetadataProvider; std::unordered_map _noOutstandingResponses; } - (void)setUp { _resumeToken1 = [@"resume1" dataUsingEncoding:NSUTF8StringEncoding]; - _targetMetadataProvider = [FSTTestTargetMetadataProvider new]; } /** @@ -145,7 +145,7 @@ - (void)setUp { * considered active, or `_noOutstandingResponses` if all targets are already active. * @param existingKeys The set of documents that are considered synced with the test targets as * part of a previous listen. To modify this set during test execution, invoke - * `[_targetMetadataProvider setSyncedKeys:forQueryData:]`. + * `_targetMetadataProvider.SetSyncedKeys()`. * @param watchChanges The watch changes to apply before returning the aggregator. Supported * changes are `DocumentWatchChange` and `WatchTargetChange`. */ @@ -154,7 +154,7 @@ - (void)setUp { outstandingResponses:(const std::unordered_map &)outstandingResponses existingKeys:(DocumentKeySet)existingKeys changes:(const std::vector> &)watchChanges { - WatchChangeAggregator aggregator{_targetMetadataProvider}; + WatchChangeAggregator aggregator{&_targetMetadataProvider}; std::vector targetIDs; for (const auto &kv : targetMap) { @@ -162,7 +162,7 @@ - (void)setUp { FSTQueryData *queryData = kv.second; targetIDs.push_back(targetID); - [_targetMetadataProvider setSyncedKeys:existingKeys forQueryData:queryData]; + _targetMetadataProvider.SetSyncedKeys(existingKeys, queryData); }; for (const auto &kv : outstandingResponses) { @@ -208,7 +208,7 @@ - (void)setUp { * @param watchChanges The watch changes to apply before creating the remote event. Supported * changes are `DocumentWatchChange` and `WatchTargetChange`. */ -- (FSTRemoteEvent *) +- (RemoteEvent) remoteEventAtSnapshotVersion:(FSTTestSnapshotVersion)snapshotVersion targetMap:(std::unordered_map)targetMap outstandingResponses:(const std::unordered_map &)outstandingResponses @@ -223,7 +223,7 @@ - (void)setUp { - (void)testWillAccumulateDocumentAddedAndRemovedEvents { // The target map that contains an entry for every target in this test. If a target ID is - // omitted, the target is considered inactive and FSTTestTargetMetadataProvider will fail on + // omitted, the target is considered inactive and `TestTargetMetadataProvider` will fail on // access. std::unordered_map targetMap{ [self queryDataForTargets:{1, 2, 3, 4, 5, 6}]}; @@ -238,45 +238,43 @@ - (void)testWillAccumulateDocumentAddedAndRemovedEvents { // with the default resume token (`_resumeToken1`). // As `existingDoc` is provided as an existing key, any updates to this document will be treated // as modifications rather than adds. - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:_noOutstandingResponses existingKeys:DocumentKeySet{existingDoc.key} changes:Changes(std::move(change1), std::move(change2))]; - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 2); - XCTAssertEqualObjects(event.documentUpdates.at(existingDoc.key), existingDoc); - XCTAssertEqualObjects(event.documentUpdates.at(newDoc.key), newDoc); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 2); + XCTAssertEqualObjects(event.document_updates().at(existingDoc.key), existingDoc); + XCTAssertEqualObjects(event.document_updates().at(newDoc.key), newDoc); // 'change1' and 'change2' affect six different targets - XCTAssertEqual(event.targetChanges.size(), 6); + XCTAssertEqual(event.target_changes().size(), 6); - FSTTargetChange *targetChange1 = - FSTTestTargetChange(DocumentKeySet{newDoc.key}, DocumentKeySet{existingDoc.key}, - DocumentKeySet{}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{newDoc.key}, + DocumentKeySet{existingDoc.key}, DocumentKeySet{}}; + XCTAssertTrue(event.target_changes().at(1) == targetChange1); - FSTTargetChange *targetChange2 = FSTTestTargetChange( - DocumentKeySet{}, DocumentKeySet{existingDoc.key}, DocumentKeySet{}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2); + TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{}, + DocumentKeySet{existingDoc.key}, DocumentKeySet{}}; + XCTAssertTrue(event.target_changes().at(2) == targetChange2); - FSTTargetChange *targetChange3 = FSTTestTargetChange( - DocumentKeySet{}, DocumentKeySet{existingDoc.key}, DocumentKeySet{}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(3), targetChange3); + TargetChange targetChange3{_resumeToken1, false, DocumentKeySet{}, + DocumentKeySet{existingDoc.key}, DocumentKeySet{}}; + XCTAssertTrue(event.target_changes().at(3) == targetChange3); - FSTTargetChange *targetChange4 = - FSTTestTargetChange(DocumentKeySet{newDoc.key}, DocumentKeySet{}, - DocumentKeySet{existingDoc.key}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(4), targetChange4); + TargetChange targetChange4{_resumeToken1, false, DocumentKeySet{newDoc.key}, DocumentKeySet{}, + DocumentKeySet{existingDoc.key}}; + XCTAssertTrue(event.target_changes().at(4) == targetChange4); - FSTTargetChange *targetChange5 = FSTTestTargetChange( - DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{existingDoc.key}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(5), targetChange5); + TargetChange targetChange5{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{existingDoc.key}}; + XCTAssertTrue(event.target_changes().at(5) == targetChange5); - FSTTargetChange *targetChange6 = FSTTestTargetChange( - DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{existingDoc.key}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(6), targetChange6); + TargetChange targetChange6{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{existingDoc.key}}; + XCTAssertTrue(event.target_changes().at(6) == targetChange6); } - (void)testWillIgnoreEventsForPendingTargets { @@ -292,20 +290,20 @@ - (void)testWillIgnoreEventsForPendingTargets { // We're waiting for the unwatch and watch ack std::unordered_map outstandingResponses{{1, 2}}; - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:outstandingResponses existingKeys:DocumentKeySet {} changes:Changes(std::move(change1), std::move(change2), std::move(change3), std::move(change4))]; - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); // doc1 is ignored because it was part of an inactive target, but doc2 is in the changes // because it become active. - XCTAssertEqual(event.documentUpdates.size(), 1); - XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); + XCTAssertEqual(event.document_updates().size(), 1); + XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2); - XCTAssertEqual(event.targetChanges.size(), 1); + XCTAssertEqual(event.target_changes().size(), 1); } - (void)testWillIgnoreEventsForRemovedTargets { @@ -318,18 +316,18 @@ - (void)testWillIgnoreEventsForRemovedTargets { // We're waiting for the unwatch ack std::unordered_map outstandingResponses{{1, 1}}; - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:outstandingResponses existingKeys:DocumentKeySet {} changes:Changes(std::move(change1), std::move(change2))]; - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); // doc1 is ignored because it was part of an inactive target - XCTAssertEqual(event.documentUpdates.size(), 0); + XCTAssertEqual(event.document_updates().size(), 0); // Target 1 is ignored because it was removed - XCTAssertEqual(event.targetChanges.size(), 0); + XCTAssertEqual(event.target_changes().size(), 0); } - (void)testWillKeepResetMappingEvenWithUpdates { @@ -351,7 +349,7 @@ - (void)testWillKeepResetMappingEvenWithUpdates { // Remove doc2 again, should not show up in reset mapping auto change5 = MakeDocChange({}, {1}, doc2.key, doc2); - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:_noOutstandingResponses @@ -359,18 +357,18 @@ - (void)testWillKeepResetMappingEvenWithUpdates { changes:Changes(std::move(change1), std::move(change2), std::move(change3), std::move(change4), std::move(change5))]; - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 3); - XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); - XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); - XCTAssertEqualObjects(event.documentUpdates.at(doc3.key), doc3); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 3); + XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1); + XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2); + XCTAssertEqualObjects(event.document_updates().at(doc3.key), doc3); - XCTAssertEqual(event.targetChanges.size(), 1); + XCTAssertEqual(event.target_changes().size(), 1); // Only doc3 is part of the new mapping - FSTTargetChange *expectedChange = FSTTestTargetChange( - DocumentKeySet{doc3.key}, DocumentKeySet{}, DocumentKeySet{doc1.key}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(1), expectedChange); + TargetChange expectedChange{_resumeToken1, false, DocumentKeySet{doc3.key}, DocumentKeySet{}, + DocumentKeySet{doc1.key}}; + XCTAssertTrue(event.target_changes().at(1) == expectedChange); } - (void)testWillHandleSingleReset { @@ -385,16 +383,16 @@ - (void)testWillHandleSingleReset { changes:{}]; aggregator.HandleTargetChange(change); - FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); + RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 0); - XCTAssertEqual(event.targetChanges.size(), 1); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 0); + XCTAssertEqual(event.target_changes().size(), 1); // Reset mapping is empty - FSTTargetChange *expectedChange = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, [NSData data], NO); - XCTAssertEqualObjects(event.targetChanges.at(1), expectedChange); + TargetChange expectedChange{ + [NSData data], false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}}; + XCTAssertTrue(event.target_changes().at(1) == expectedChange); } - (void)testWillHandleTargetAddAndRemovalInSameBatch { @@ -406,25 +404,25 @@ - (void)testWillHandleTargetAddAndRemovalInSameBatch { FSTDocument *doc1b = FSTTestDoc("docs/1", 1, @{@"value" : @2}, FSTDocumentStateSynced); auto change2 = MakeDocChange({2}, {1}, doc1b.key, doc1b); - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:_noOutstandingResponses existingKeys:DocumentKeySet{doc1a.key} changes:Changes(std::move(change1), std::move(change2))]; - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 1); - XCTAssertEqualObjects(event.documentUpdates.at(doc1b.key), doc1b); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 1); + XCTAssertEqualObjects(event.document_updates().at(doc1b.key), doc1b); - XCTAssertEqual(event.targetChanges.size(), 2); + XCTAssertEqual(event.target_changes().size(), 2); - FSTTargetChange *targetChange1 = FSTTestTargetChange( - DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1b.key}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{doc1b.key}}; + XCTAssertTrue(event.target_changes().at(1) == targetChange1); - FSTTargetChange *targetChange2 = FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{doc1b.key}, - DocumentKeySet{}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2); + TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{doc1b.key}, + DocumentKeySet{}}; + XCTAssertTrue(event.target_changes().at(2) == targetChange2); } - (void)testTargetCurrentChangeWillMarkTheTargetCurrent { @@ -432,19 +430,19 @@ - (void)testTargetCurrentChangeWillMarkTheTargetCurrent { auto change = MakeTargetChange(WatchTargetChangeState::Current, {1}, _resumeToken1); - FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3 - targetMap:targetMap - outstandingResponses:_noOutstandingResponses - existingKeys:DocumentKeySet {} - changes:Changes(std::move(change))]; + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:Changes(std::move(change))]; - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 0); - XCTAssertEqual(event.targetChanges.size(), 1); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 0); + XCTAssertEqual(event.target_changes().size(), 1); - FSTTargetChange *targetChange = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, YES); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange); + TargetChange targetChange1{_resumeToken1, true, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; + XCTAssertTrue(event.target_changes().at(1) == targetChange1); } - (void)testTargetAddedChangeWillResetPreviousState { @@ -461,7 +459,7 @@ - (void)testTargetAddedChangeWillResetPreviousState { std::unordered_map outstandingResponses{{1, 2}, {2, 1}}; - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:outstandingResponses @@ -470,25 +468,25 @@ - (void)testTargetAddedChangeWillResetPreviousState { std::move(change3), std::move(change4), std::move(change5), std::move(change6))]; - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 2); - XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); - XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 2); + XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1); + XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2); // target 1 and 3 are affected (1 because of re-add), target 2 is not because of remove - XCTAssertEqual(event.targetChanges.size(), 2); + XCTAssertEqual(event.target_changes().size(), 2); // doc1 was before the remove, so it does not show up in the mapping. // Current was before the remove. - FSTTargetChange *targetChange1 = FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{doc2.key}, - DocumentKeySet{}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{doc2.key}, + DocumentKeySet{}}; + XCTAssertTrue(event.target_changes().at(1) == targetChange1); // Doc1 was before the remove // Current was before the remove - FSTTargetChange *targetChange3 = FSTTestTargetChange( - DocumentKeySet{doc1.key}, DocumentKeySet{}, DocumentKeySet{doc2.key}, _resumeToken1, YES); - XCTAssertEqualObjects(event.targetChanges.at(3), targetChange3); + TargetChange targetChange3{_resumeToken1, true, DocumentKeySet{doc1.key}, DocumentKeySet{}, + DocumentKeySet{doc2.key}}; + XCTAssertTrue(event.target_changes().at(3) == targetChange3); } - (void)testNoChangeWillStillMarkTheAffectedTargets { @@ -502,15 +500,15 @@ - (void)testNoChangeWillStillMarkTheAffectedTargets { WatchTargetChange change{WatchTargetChangeState::NoChange, {1}, _resumeToken1}; aggregator.HandleTargetChange(change); - FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); + RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 0); - XCTAssertEqual(event.targetChanges.size(), 1); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 0); + XCTAssertEqual(event.target_changes().size(), 1); - FSTTargetChange *targetChange = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange); + TargetChange targetChange{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; + XCTAssertTrue(event.target_changes().at(1) == targetChange); } - (void)testExistenceFilterMismatchClearsTarget { @@ -528,22 +526,22 @@ - (void)testExistenceFilterMismatchClearsTarget { existingKeys:DocumentKeySet{doc1.key, doc2.key} changes:Changes(std::move(change1), std::move(change2), std::move(change3))]; - FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); + RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 2); - XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); - XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 2); + XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1); + XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2); - XCTAssertEqual(event.targetChanges.size(), 2); + XCTAssertEqual(event.target_changes().size(), 2); - FSTTargetChange *targetChange1 = FSTTestTargetChange( - DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}, DocumentKeySet{}, _resumeToken1, YES); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + TargetChange targetChange1{_resumeToken1, true, DocumentKeySet{}, + DocumentKeySet{doc1.key, doc2.key}, DocumentKeySet{}}; + XCTAssertTrue(event.target_changes().at(1) == targetChange1); - FSTTargetChange *targetChange2 = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2); + TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; + XCTAssertTrue(event.target_changes().at(2) == targetChange2); // The existence filter mismatch will remove the document from target 1, // but not synthesize a document delete. @@ -552,13 +550,13 @@ - (void)testExistenceFilterMismatchClearsTarget { event = aggregator.CreateRemoteEvent(testutil::Version(4)); - FSTTargetChange *targetChange3 = FSTTestTargetChange( - DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}, [NSData data], NO); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange3); + TargetChange targetChange3{ + [NSData data], false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}}; + XCTAssertTrue(event.target_changes().at(1) == targetChange3); - XCTAssertEqual(event.targetChanges.size(), 1); - XCTAssertEqual(event.targetMismatches.size(), 1); - XCTAssertEqual(event.documentUpdates.size(), 0); + XCTAssertEqual(event.target_changes().size(), 1); + XCTAssertEqual(event.target_mismatches().size(), 1); + XCTAssertEqual(event.document_updates().size(), 0); } - (void)testExistenceFilterMismatchRemovesCurrentChanges { @@ -581,18 +579,18 @@ - (void)testExistenceFilterMismatchRemovesCurrentChanges { ExistenceFilterWatchChange existenceFilter{ExistenceFilter{0}, 1}; aggregator.HandleExistenceFilter(existenceFilter); - FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); + RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 1); - XCTAssertEqual(event.targetMismatches.size(), 1); - XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 1); + XCTAssertEqual(event.target_mismatches().size(), 1); + XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1); - XCTAssertEqual(event.targetChanges.size(), 1); + XCTAssertEqual(event.target_changes().size(), 1); - FSTTargetChange *targetChange1 = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, [NSData data], NO); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + TargetChange targetChange1{ + [NSData data], false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}}; + XCTAssertTrue(event.target_changes().at(1) == targetChange1); } - (void)testDocumentUpdate { @@ -609,15 +607,14 @@ - (void)testDocumentUpdate { existingKeys:DocumentKeySet {} changes:Changes(std::move(change1), std::move(change2))]; - FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); + RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 2); - XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); - XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 2); + XCTAssertEqualObjects(event.document_updates().at(doc1.key), doc1); + XCTAssertEqualObjects(event.document_updates().at(doc2.key), doc2); - [_targetMetadataProvider setSyncedKeys:DocumentKeySet{doc1.key, doc2.key} - forQueryData:targetMap[1]]; + _targetMetadataProvider.SetSyncedKeys(DocumentKeySet{doc1.key, doc2.key}, targetMap[1]); FSTDeletedDocument *deletedDoc1 = [FSTDeletedDocument documentWithKey:doc1.key version:testutil::Version(3) @@ -635,22 +632,21 @@ - (void)testDocumentUpdate { event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 3); + XCTAssertEqual(event.snapshot_version(), testutil::Version(3)); + XCTAssertEqual(event.document_updates().size(), 3); // doc1 is replaced - XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), deletedDoc1); + XCTAssertEqualObjects(event.document_updates().at(doc1.key), deletedDoc1); // doc2 is updated - XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), updatedDoc2); + XCTAssertEqualObjects(event.document_updates().at(doc2.key), updatedDoc2); // doc3 is new - XCTAssertEqualObjects(event.documentUpdates.at(doc3.key), doc3); + XCTAssertEqualObjects(event.document_updates().at(doc3.key), doc3); // Target is unchanged - XCTAssertEqual(event.targetChanges.size(), 1); + XCTAssertEqual(event.target_changes().size(), 1); - FSTTargetChange *targetChange = - FSTTestTargetChange(DocumentKeySet{doc3.key}, DocumentKeySet{updatedDoc2.key}, - DocumentKeySet{deletedDoc1.key}, _resumeToken1, NO); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange); + TargetChange targetChange1{_resumeToken1, false, DocumentKeySet{doc3.key}, + DocumentKeySet{updatedDoc2.key}, DocumentKeySet{deletedDoc1.key}}; + XCTAssertTrue(event.target_changes().at(1) == targetChange1); } - (void)testResumeTokensHandledPerTarget { @@ -668,16 +664,16 @@ - (void)testResumeTokensHandledPerTarget { WatchTargetChange change2{WatchTargetChangeState::Current, {2}, resumeToken2}; aggregator.HandleTargetChange(change2); - FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.targetChanges.size(), 2); + RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3)); + XCTAssertEqual(event.target_changes().size(), 2); - FSTTargetChange *targetChange1 = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, YES); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + TargetChange targetChange1{_resumeToken1, true, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; + XCTAssertTrue(event.target_changes().at(1) == targetChange1); - FSTTargetChange *targetChange2 = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, resumeToken2, YES); - XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2); + TargetChange targetChange2{resumeToken2, true, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; + XCTAssertTrue(event.target_changes().at(2) == targetChange2); } - (void)testLastResumeTokenWins { @@ -699,16 +695,16 @@ - (void)testLastResumeTokenWins { WatchTargetChange change3{WatchTargetChangeState::NoChange, {2}, resumeToken3}; aggregator.HandleTargetChange(change3); - FSTRemoteEvent *event = aggregator.CreateRemoteEvent(testutil::Version(3)); - XCTAssertEqual(event.targetChanges.size(), 2); + RemoteEvent event = aggregator.CreateRemoteEvent(testutil::Version(3)); + XCTAssertEqual(event.target_changes().size(), 2); - FSTTargetChange *targetChange1 = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, resumeToken2, YES); - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + TargetChange targetChange1{resumeToken2, true, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; + XCTAssertTrue(event.target_changes().at(1) == targetChange1); - FSTTargetChange *targetChange2 = - FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, resumeToken3, NO); - XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2); + TargetChange targetChange2{resumeToken3, false, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; + XCTAssertTrue(event.target_changes().at(2) == targetChange2); } - (void)testSynthesizeDeletes { @@ -716,18 +712,17 @@ - (void)testSynthesizeDeletes { DocumentKey limboKey = testutil::Key("coll/limbo"); auto resolveLimboTarget = MakeTargetChange(WatchTargetChangeState::Current, {1}); - FSTRemoteEvent *event = - [self remoteEventAtSnapshotVersion:3 - targetMap:targetMap - outstandingResponses:_noOutstandingResponses - existingKeys:DocumentKeySet {} - changes:Changes(std::move(resolveLimboTarget))]; + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:Changes(std::move(resolveLimboTarget))]; FSTDeletedDocument *expected = [FSTDeletedDocument documentWithKey:limboKey - version:event.snapshotVersion + version:event.snapshot_version() hasCommittedMutations:NO]; - XCTAssertEqualObjects(event.documentUpdates.at(limboKey), expected); - XCTAssertTrue(event.limboDocumentChanges.contains(limboKey)); + XCTAssertEqualObjects(event.document_updates().at(limboKey), expected); + XCTAssertTrue(event.limbo_document_changes().contains(limboKey)); } - (void)testDoesntSynthesizeDeletesForWrongState { @@ -735,14 +730,14 @@ - (void)testDoesntSynthesizeDeletesForWrongState { auto wrongState = MakeTargetChange(WatchTargetChangeState::NoChange, {1}); - FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3 - targetMap:targetMap - outstandingResponses:_noOutstandingResponses - existingKeys:DocumentKeySet {} - changes:Changes(std::move(wrongState))]; + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:Changes(std::move(wrongState))]; - XCTAssertEqual(event.documentUpdates.size(), 0); - XCTAssertEqual(event.limboDocumentChanges.size(), 0); + XCTAssertEqual(event.document_updates().size(), 0); + XCTAssertEqual(event.limbo_document_changes().size(), 0); } - (void)testDoesntSynthesizeDeletesForExistingDoc { @@ -750,15 +745,15 @@ - (void)testDoesntSynthesizeDeletesForExistingDoc { auto hasDocument = MakeTargetChange(WatchTargetChangeState::Current, {3}); - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:_noOutstandingResponses existingKeys:DocumentKeySet{FSTTestDocKey(@"coll/limbo")} changes:Changes(std::move(hasDocument))]; - XCTAssertEqual(event.documentUpdates.size(), 0); - XCTAssertEqual(event.limboDocumentChanges.size(), 0); + XCTAssertEqual(event.document_updates().size(), 0); + XCTAssertEqual(event.limbo_document_changes().size(), 0); } - (void)testSeparatesDocumentUpdates { @@ -777,7 +772,7 @@ - (void)testSeparatesDocumentUpdates { FSTDeletedDocument *missingDoc = FSTTestDeletedDoc("docs/missing", 1, NO); auto missingDocChange = MakeDocChange({}, {1}, missingDoc.key, missingDoc); - FSTRemoteEvent *event = [self + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:_noOutstandingResponses @@ -786,11 +781,10 @@ - (void)testSeparatesDocumentUpdates { std::move(deletedDocChange), std::move(missingDocChange))]; - FSTTargetChange *targetChange = - FSTTestTargetChange(DocumentKeySet{newDoc.key}, DocumentKeySet{existingDoc.key}, - DocumentKeySet{deletedDoc.key}, _resumeToken1, NO); + TargetChange targetChange2{_resumeToken1, false, DocumentKeySet{newDoc.key}, + DocumentKeySet{existingDoc.key}, DocumentKeySet{deletedDoc.key}}; - XCTAssertEqualObjects(event.targetChanges.at(1), targetChange); + XCTAssertTrue(event.target_changes().at(1) == targetChange2); } - (void)testTracksLimboDocuments { @@ -809,7 +803,7 @@ - (void)testTracksLimboDocuments { auto docChange3 = MakeDocChange({1}, {}, doc3.key, doc3); auto targetsChange = MakeTargetChange(WatchTargetChangeState::Current, {1, 2}); - FSTRemoteEvent *event = + RemoteEvent event = [self remoteEventAtSnapshotVersion:3 targetMap:targetMap outstandingResponses:_noOutstandingResponses @@ -817,7 +811,7 @@ - (void)testTracksLimboDocuments { changes:Changes(std::move(docChange1), std::move(docChange2), std::move(docChange3), std::move(targetsChange))]; - DocumentKeySet limboDocChanges = event.limboDocumentChanges; + DocumentKeySet limboDocChanges = event.limbo_document_changes(); // Doc1 is in both limbo and non-limbo targets, therefore not tracked as limbo XCTAssertFalse(limboDocChanges.contains(doc1.key)); // Doc2 is only in the limbo target, so is tracked as a limbo document diff --git a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm index aa9a3cc997a..c3215086081 100644 --- a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm +++ b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm @@ -386,7 +386,7 @@ - (void)testEncodesPatchMutation { "docs/1", @{@"a" : @"b", @"num" : @1, @"some.de\\\\ep.th\\ing'" : @2}, {}); GCFSWrite *proto = [GCFSWrite message]; proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; - proto.updateMask = [self.serializer encodedFieldMask:mutation.fieldMask]; + proto.updateMask = [self.serializer encodedFieldMask:*(mutation.fieldMask)]; proto.currentDocument.exists = YES; [self assertRoundTripForMutation:mutation proto:proto]; diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h index 9d58e3416bb..cc1e4335d59 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h @@ -18,6 +18,7 @@ #include #include +#include #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" @@ -39,8 +40,8 @@ class MockDatastore : public Datastore { util::AsyncQueue* worker_queue, auth::CredentialsProvider* credentials); - std::shared_ptr CreateWatchStream(id delegate) override; - std::shared_ptr CreateWriteStream(id delegate) override; + std::shared_ptr CreateWatchStream(WatchStreamCallback* callback) override; + std::shared_ptr CreateWriteStream(WriteStreamCallback* callback) override; /** * A count of the total number of requests sent to the watch stream since the beginning of the @@ -77,12 +78,12 @@ class MockDatastore : public Datastore { /** * Returns the next write that was "sent to the backend", failing if there are no queued sent */ - NSArray* NextSentWrite(); + std::vector NextSentWrite(); /** Returns the number of writes that have been sent to the backend but not waited on yet. */ int WritesSent() const; /** Injects a write ack as though it had come from the backend in response to a write. */ - void AckWrite(const model::SnapshotVersion& version, NSArray* results); + void AckWrite(const model::SnapshotVersion& version, std::vector results); /** Injects a stream failure as though it had come from the backend. */ void FailWrite(const util::Status& error); diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm index f8b6a2d6f87..ab25c01e567 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm @@ -25,7 +25,6 @@ #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" -#import "Firestore/Source/Remote/FSTStream.h" #include "Firestore/core/src/firebase/firestore/auth/credentials_provider.h" #include "Firestore/core/src/firebase/firestore/auth/empty_credentials_provider.h" @@ -70,11 +69,11 @@ CredentialsProvider* credentials_provider, FSTSerializerBeta* serializer, GrpcConnection* grpc_connection, - id delegate, + WatchStreamCallback* callback, MockDatastore* datastore) - : WatchStream{worker_queue, credentials_provider, serializer, grpc_connection, delegate}, + : WatchStream{worker_queue, credentials_provider, serializer, grpc_connection, callback}, datastore_{datastore}, - delegate_{delegate} { + callback_{callback} { } const std::unordered_map& ActiveTargets() const { @@ -84,7 +83,7 @@ void Start() override { HARD_ASSERT(!open_, "Trying to start already started watch stream"); open_ = true; - [delegate_ watchStreamDidOpen]; + callback_->OnWatchStreamOpen(); } void Stop() override { @@ -118,7 +117,7 @@ void UnwatchTargetId(model::TargetId target_id) override { void FailStream(const Status& error) { open_ = false; - [delegate_ watchStreamWasInterruptedWithError:error]; + callback_->OnWatchStreamClose(error); } void WriteWatchChange(const WatchChange& change, SnapshotVersion snap) { @@ -145,14 +144,14 @@ void WriteWatchChange(const WatchChange& change, SnapshotVersion snap) { } } - [delegate_ watchStreamDidChange:change snapshotVersion:snap]; + callback_->OnWatchStreamChange(change, snap); } private: bool open_ = false; std::unordered_map active_targets_; MockDatastore* datastore_ = nullptr; - id delegate_ = nullptr; + WatchStreamCallback* callback_ = nullptr; }; class MockWriteStream : public WriteStream { @@ -161,18 +160,18 @@ void WriteWatchChange(const WatchChange& change, SnapshotVersion snap) { CredentialsProvider* credentials_provider, FSTSerializerBeta* serializer, GrpcConnection* grpc_connection, - id delegate, + WriteStreamCallback* callback, MockDatastore* datastore) - : WriteStream{worker_queue, credentials_provider, serializer, grpc_connection, delegate}, + : WriteStream{worker_queue, credentials_provider, serializer, grpc_connection, callback}, datastore_{datastore}, - delegate_{delegate} { + callback_{callback} { } void Start() override { HARD_ASSERT(!open_, "Trying to start already started write stream"); open_ = true; sent_mutations_ = {}; - [delegate_ writeStreamDidOpen]; + callback_->OnWriteStreamOpen(); } void Stop() override { @@ -194,32 +193,32 @@ bool IsOpen() const override { void WriteHandshake() override { datastore_->IncrementWriteStreamRequests(); SetHandshakeComplete(); - [delegate_ writeStreamDidCompleteHandshake]; + callback_->OnWriteStreamHandshakeComplete(); } - void WriteMutations(NSArray* mutations) override { + void WriteMutations(const std::vector& mutations) override { datastore_->IncrementWriteStreamRequests(); sent_mutations_.push(mutations); } /** Injects a write ack as though it had come from the backend in response to a write. */ - void AckWrite(const SnapshotVersion& commitVersion, NSArray* results) { - [delegate_ writeStreamDidReceiveResponseWithVersion:commitVersion mutationResults:results]; + void AckWrite(const SnapshotVersion& commitVersion, std::vector results) { + callback_->OnWriteStreamMutationResult(commitVersion, std::move(results)); } /** Injects a failed write response as though it had come from the backend. */ void FailStream(const Status& error) { open_ = false; - [delegate_ writeStreamWasInterruptedWithError:error]; + callback_->OnWriteStreamClose(error); } /** * Returns the next write that was "sent to the backend", failing if there are no queued sent */ - NSArray* NextSentWrite() { + std::vector NextSentWrite() { HARD_ASSERT(!sent_mutations_.empty(), "Writes need to happen before you can call NextSentWrite."); - NSArray* result = std::move(sent_mutations_.front()); + std::vector result = std::move(sent_mutations_.front()); sent_mutations_.pop(); return result; } @@ -234,9 +233,9 @@ int sent_mutations_count() const { private: bool open_ = false; - std::queue*> sent_mutations_; + std::queue> sent_mutations_; MockDatastore* datastore_ = nullptr; - id delegate_ = nullptr; + WriteStreamCallback* callback_ = nullptr; }; MockDatastore::MockDatastore(const core::DatabaseInfo& database_info, @@ -248,20 +247,20 @@ int sent_mutations_count() const { credentials_{credentials} { } -std::shared_ptr MockDatastore::CreateWatchStream(id delegate) { +std::shared_ptr MockDatastore::CreateWatchStream(WatchStreamCallback* callback) { watch_stream_ = std::make_shared( worker_queue_, credentials_, [[FSTSerializerBeta alloc] initWithDatabaseID:&database_info_->database_id()], - grpc_connection(), delegate, this); + grpc_connection(), callback, this); return watch_stream_; } -std::shared_ptr MockDatastore::CreateWriteStream(id delegate) { +std::shared_ptr MockDatastore::CreateWriteStream(WriteStreamCallback* callback) { write_stream_ = std::make_shared( worker_queue_, credentials_, [[FSTSerializerBeta alloc] initWithDatabaseID:&database_info_->database_id()], - grpc_connection(), delegate, this); + grpc_connection(), callback, this); return write_stream_; } @@ -282,7 +281,7 @@ int sent_mutations_count() const { return watch_stream_->IsOpen(); } -NSArray* MockDatastore::NextSentWrite() { +std::vector MockDatastore::NextSentWrite() { return write_stream_->NextSentWrite(); } @@ -290,8 +289,9 @@ int sent_mutations_count() const { return write_stream_->sent_mutations_count(); } -void MockDatastore::AckWrite(const SnapshotVersion& version, NSArray* results) { - write_stream_->AckWrite(version, results); +void MockDatastore::AckWrite(const SnapshotVersion& version, + std::vector results) { + write_stream_->AckWrite(version, std::move(results)); } void MockDatastore::FailWrite(const Status& error) { diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm index 266cda72610..fde73c3fcbf 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm @@ -39,6 +39,7 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" #include "Firestore/core/src/firebase/firestore/remote/existence_filter.h" @@ -46,17 +47,20 @@ #include "Firestore/core/src/firebase/firestore/util/async_queue.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" +#include "Firestore/core/src/firebase/firestore/util/objc_compatibility.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" namespace testutil = firebase::firestore::testutil; namespace util = firebase::firestore::util; +namespace objc = util::objc; using firebase::firestore::FirestoreErrorCode; using firebase::firestore::auth::User; -using firebase::firestore::core::DocumentViewChangeType; +using firebase::firestore::core::DocumentViewChange; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::ResourcePath; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; using firebase::firestore::remote::ExistenceFilter; @@ -168,7 +172,10 @@ - (nullable FSTQuery *)parseQuery:(id)querySpec { } else if ([querySpec isKindOfClass:[NSDictionary class]]) { NSDictionary *queryDict = (NSDictionary *)querySpec; NSString *path = queryDict[@"path"]; - __block FSTQuery *query = FSTTestQuery(util::MakeString(path)); + ResourcePath resource_path = ResourcePath::FromString(util::MakeString(path)); + NSString *_Nullable collectionGroup = queryDict[@"collectionGroup"]; + __block FSTQuery *query = [FSTQuery queryWithPath:resource_path + collectionGroup:collectionGroup]; if (queryDict[@"limit"]) { NSNumber *limit = queryDict[@"limit"]; query = [query queryBySettingLimit:limit.integerValue]; @@ -200,7 +207,7 @@ - (SnapshotVersion)parseVersion:(NSNumber *_Nullable)version { return testutil::Version(version.longLongValue); } -- (FSTDocumentViewChange *)parseChange:(NSDictionary *)jsonDoc ofType:(DocumentViewChangeType)type { +- (DocumentViewChange)parseChange:(NSDictionary *)jsonDoc ofType:(DocumentViewChange::Type)type { NSNumber *version = jsonDoc[@"version"]; NSDictionary *options = jsonDoc[@"options"]; FSTDocumentState documentState = [options[@"hasLocalMutations"] isEqualToNumber:@YES] @@ -212,7 +219,7 @@ - (FSTDocumentViewChange *)parseChange:(NSDictionary *)jsonDoc ofType:(DocumentV XCTAssert([jsonDoc[@"key"] isKindOfClass:[NSString class]]); FSTDocument *doc = FSTTestDoc(util::MakeString((NSString *)jsonDoc[@"key"]), version.longLongValue, jsonDoc[@"value"], documentState); - return [FSTDocumentViewChange changeWithDocument:doc type:type]; + return DocumentViewChange{doc, type}; } #pragma mark - Methods for doing the steps of the spec test. @@ -363,7 +370,7 @@ - (void)doWriteAck:(NSDictionary *)spec { FSTMutationResult *mutationResult = [[FSTMutationResult alloc] initWithVersion:version transformResults:nil]; - [self.driver receiveWriteAckWithVersion:version mutationResults:@[ mutationResult ]]; + [self.driver receiveWriteAckWithVersion:version mutationResults:{mutationResult}]; } - (void)doFailWrite:(NSDictionary *)spec { @@ -497,35 +504,39 @@ - (void)validateEvent:(FSTQueryEvent *)actual matches:(NSDictionary *)expected { XCTAssertNotNil(actual.error); XCTAssertEqual(actual.error.code, [expected[@"errorCode"] integerValue]); } else { - NSMutableArray *expectedChanges = [NSMutableArray array]; + std::vector expectedChanges; NSMutableArray *removed = expected[@"removed"]; for (NSDictionary *changeSpec in removed) { - [expectedChanges addObject:[self parseChange:changeSpec - ofType:DocumentViewChangeType::kRemoved]]; + expectedChanges.push_back([self parseChange:changeSpec + ofType:DocumentViewChange::Type::kRemoved]); } NSMutableArray *added = expected[@"added"]; for (NSDictionary *changeSpec in added) { - [expectedChanges addObject:[self parseChange:changeSpec - ofType:DocumentViewChangeType::kAdded]]; + expectedChanges.push_back([self parseChange:changeSpec + ofType:DocumentViewChange::Type::kAdded]); } NSMutableArray *modified = expected[@"modified"]; for (NSDictionary *changeSpec in modified) { - [expectedChanges addObject:[self parseChange:changeSpec - ofType:DocumentViewChangeType::kModified]]; + expectedChanges.push_back([self parseChange:changeSpec + ofType:DocumentViewChange::Type::kModified]); } NSMutableArray *metadata = expected[@"metadata"]; for (NSDictionary *changeSpec in metadata) { - [expectedChanges addObject:[self parseChange:changeSpec - ofType:DocumentViewChangeType::kMetadata]]; + expectedChanges.push_back([self parseChange:changeSpec + ofType:DocumentViewChange::Type::kMetadata]); + } + + XCTAssertEqual(actual.viewSnapshot.value().document_changes().size(), expectedChanges.size()); + for (size_t i = 0; i != expectedChanges.size(); ++i) { + XCTAssertTrue((actual.viewSnapshot.value().document_changes()[i] == expectedChanges[i])); } - XCTAssertEqualObjects(actual.viewSnapshot.documentChanges, expectedChanges); BOOL expectedHasPendingWrites = expected[@"hasPendingWrites"] ? [expected[@"hasPendingWrites"] boolValue] : NO; BOOL expectedIsFromCache = expected[@"fromCache"] ? [expected[@"fromCache"] boolValue] : NO; - XCTAssertEqual(actual.viewSnapshot.hasPendingWrites, expectedHasPendingWrites, + XCTAssertEqual(actual.viewSnapshot.value().has_pending_writes(), expectedHasPendingWrites, @"hasPendingWrites"); - XCTAssertEqual(actual.viewSnapshot.isFromCache, expectedIsFromCache, @"isFromCache"); + XCTAssertEqual(actual.viewSnapshot.value().from_cache(), expectedIsFromCache, @"isFromCache"); } } @@ -682,16 +693,8 @@ - (void)validateActiveTargets { actualTargets.erase(targetID); } - if (!actualTargets.empty()) { - // Converting to an Objective-C class is a quick-and-dirty way to get - // a readable debug description of the context of the map. - NSMutableDictionary *actualTargetsDictionary = [NSMutableDictionary dictionary]; - for (const auto &kv : actualTargets) { - actualTargetsDictionary[@(kv.first)] = kv.second; - } - XCTAssertTrue(actualTargets.empty(), "Unexpected active targets: %@", - [actualTargetsDictionary description]); - } + XCTAssertTrue(actualTargets.empty(), "Unexpected active targets: %@", + objc::Description(actualTargets)); } - (void)runSpecTestSteps:(NSArray *)steps config:(NSDictionary *)config { diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h index 0622aa4290e..a849c3308af 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h @@ -18,10 +18,10 @@ #include #include - -#import "Firestore/Source/Remote/FSTRemoteStore.h" +#include #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @@ -34,7 +34,6 @@ @class FSTMutationResult; @class FSTQuery; @class FSTQueryData; -@class FSTViewSnapshot; @protocol FSTPersistence; NS_ASSUME_NONNULL_BEGIN @@ -45,8 +44,11 @@ NS_ASSUME_NONNULL_BEGIN */ @interface FSTQueryEvent : NSObject @property(nonatomic, strong) FSTQuery *query; -@property(nonatomic, strong, nullable) FSTViewSnapshot *viewSnapshot; @property(nonatomic, strong, nullable) NSError *error; + +- (const absl::optional &)viewSnapshot; +- (void)setViewSnapshot:(absl::optional)snapshot; + @end /** Holds an outstanding write and its result. */ @@ -86,7 +88,7 @@ typedef std::unordered_map +@interface FSTSyncEngineTestDriver : NSObject /** * Initializes the underlying FSTSyncEngine with the given local persistence implementation and @@ -195,9 +197,9 @@ typedef std::unordered_map *)mutationResults; +- (FSTOutstandingWrite *) + receiveWriteAckWithVersion:(const firebase::firestore::model::SnapshotVersion &)commitVersion + mutationResults:(std::vector)mutationResults; /** * A count of the mutations written to the write stream by the FSTSyncEngine, but not yet @@ -285,7 +287,7 @@ typedef std::unordered_map #include +#include #include #include +#include #import "Firestore/Source/Core/FSTEventManager.h" #import "Firestore/Source/Core/FSTQuery.h" @@ -39,11 +41,16 @@ #include "Firestore/core/src/firebase/firestore/core/database_info.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_store.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/src/firebase/firestore/util/error_apple.h" #include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" #include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "Firestore/core/src/firebase/firestore/util/string_format.h" +#include "Firestore/core/src/firebase/firestore/util/to_string.h" #include "absl/memory/memory.h" using firebase::firestore::FirestoreErrorCode; @@ -51,6 +58,7 @@ using firebase::firestore::auth::HashUser; using firebase::firestore::auth::User; using firebase::firestore::core::DatabaseInfo; +using firebase::firestore::core::ViewSnapshot; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; @@ -58,21 +66,38 @@ using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; using firebase::firestore::remote::MockDatastore; +using firebase::firestore::remote::RemoteStore; using firebase::firestore::remote::WatchChange; using firebase::firestore::util::AsyncQueue; using firebase::firestore::util::TimerId; using firebase::firestore::util::ExecutorLibdispatch; +using firebase::firestore::util::MakeNSError; using firebase::firestore::util::MakeString; using firebase::firestore::util::Status; +using firebase::firestore::util::StatusOr; +using firebase::firestore::util::StringFormat; +using firebase::firestore::util::ToString; +using firebase::firestore::util::WrapNSString; NS_ASSUME_NONNULL_BEGIN -@implementation FSTQueryEvent +@implementation FSTQueryEvent { + absl::optional _maybeViewSnapshot; +} + +- (const absl::optional &)viewSnapshot { + return _maybeViewSnapshot; +} + +- (void)setViewSnapshot:(absl::optional)snapshot { + _maybeViewSnapshot = std::move(snapshot); +} - (NSString *)description { // The Query is also included in the view, so we skip it. - return [NSString stringWithFormat:@"", - self.viewSnapshot, self.error]; + std::string str = StringFormat("", + ToString(_maybeViewSnapshot), self.error); + return WrapNSString(str); } @end @@ -85,7 +110,6 @@ @interface FSTSyncEngineTestDriver () #pragma mark - Parts of the Firestore system that the spec tests need to control. @property(nonatomic, strong, readonly) FSTEventManager *eventManager; -@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore; @property(nonatomic, strong, readonly) FSTLocalStore *localStore; @property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; @property(nonatomic, strong, readonly) id persistence; @@ -112,6 +136,8 @@ @interface FSTSyncEngineTestDriver () @implementation FSTSyncEngineTestDriver { std::unique_ptr _workerQueue; + std::unique_ptr _remoteStore; + std::unordered_map _expectedActiveTargets; // ivar is declared as mutable. @@ -153,18 +179,19 @@ - (instancetype)initWithPersistence:(id)persistence _datastore = std::make_shared(_databaseInfo, _workerQueue.get(), &_credentialProvider); - _remoteStore = [[FSTRemoteStore alloc] initWithLocalStore:_localStore - datastore:_datastore - workerQueue:_workerQueue.get()]; + _remoteStore = absl::make_unique( + _localStore, _datastore, _workerQueue.get(), [self](OnlineState onlineState) { + [self.syncEngine applyChangedOnlineState:onlineState]; + [self.eventManager applyChangedOnlineState:onlineState]; + }); + ; _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore - remoteStore:_remoteStore + remoteStore:_remoteStore.get() initialUser:initialUser]; - _remoteStore.syncEngine = _syncEngine; + _remoteStore->set_sync_engine(_syncEngine); _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine]; - _remoteStore.onlineStateDelegate = self; - // Set up internal event tracking for the spec tests. NSMutableArray *events = [NSMutableArray array]; _eventHandler = ^(FSTQueryEvent *e) { @@ -203,15 +230,10 @@ - (void)drainQueue { return _currentUser; } -- (void)applyChangedOnlineState:(OnlineState)onlineState { - [self.syncEngine applyChangedOnlineState:onlineState]; - [self.eventManager applyChangedOnlineState:onlineState]; -} - - (void)start { _workerQueue->EnqueueBlocking([&] { [self.localStore start]; - [self.remoteStore start]; + _remoteStore->Start(); }); } @@ -223,15 +245,15 @@ - (void)validateUsage { - (void)shutdown { _workerQueue->EnqueueBlocking([&] { - [self.remoteStore shutdown]; + _remoteStore->Shutdown(); [self.persistence shutdown]; }); } - (void)validateNextWriteSent:(FSTMutation *)expectedWrite { - NSArray *request = _datastore->NextSentWrite(); + std::vector request = _datastore->NextSentWrite(); // Make sure the write went through the pipe like we expected it to. - HARD_ASSERT(request.count == 1, "Only single mutation requests are supported at the moment"); + HARD_ASSERT(request.size() == 1, "Only single mutation requests are supported at the moment"); FSTMutation *actualWrite = request[0]; HARD_ASSERT([actualWrite isEqual:expectedWrite], "Mock datastore received write %s but first outstanding mutation was %s", actualWrite, @@ -255,13 +277,13 @@ - (void)disableNetwork { _workerQueue->EnqueueBlocking([&] { // Make sure to execute all writes that are currently queued. This allows us // to assert on the total number of requests sent before shutdown. - [self.remoteStore fillWritePipeline]; - [self.remoteStore disableNetwork]; + _remoteStore->FillWritePipeline(); + _remoteStore->DisableNetwork(); }); } - (void)enableNetwork { - _workerQueue->EnqueueBlocking([&] { [self.remoteStore enableNetwork]; }); + _workerQueue->EnqueueBlocking([&] { _remoteStore->EnableNetwork(); }); } - (void)runTimer:(TimerId)timerID { @@ -275,12 +297,13 @@ - (void)changeUser:(const User &)user { - (FSTOutstandingWrite *)receiveWriteAckWithVersion:(const SnapshotVersion &)commitVersion mutationResults: - (NSArray *)mutationResults { + (std::vector)mutationResults { FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject; [[self currentOutstandingWrites] removeObjectAtIndex:0]; [self validateNextWriteSent:write.write]; - _workerQueue->EnqueueBlocking([&] { _datastore->AckWrite(commitVersion, mutationResults); }); + _workerQueue->EnqueueBlocking( + [&] { _datastore->AckWrite(commitVersion, std::move(mutationResults)); }); return write; } @@ -326,17 +349,19 @@ - (FSTOutstandingWrite *)receiveWriteError:(int)errorCode - (TargetId)addUserListenerWithQuery:(FSTQuery *)query { // TODO(dimond): Allow customizing listen options in spec tests // TODO(dimond): Change spec tests to verify isFromCache on snapshots - FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES - includeDocumentMetadataChanges:YES - waitForSyncWhenOnline:NO]; + ListenOptions options = ListenOptions::FromIncludeMetadataChanges(true); FSTQueryListener *listener = [[FSTQueryListener alloc] initWithQuery:query options:options - viewSnapshotHandler:^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error) { + viewSnapshotHandler:[self, query](const StatusOr &maybe_snapshot) { FSTQueryEvent *event = [[FSTQueryEvent alloc] init]; event.query = query; - event.viewSnapshot = snapshot; - event.error = error; + if (maybe_snapshot.ok()) { + [event setViewSnapshot:maybe_snapshot.ValueOrDie()]; + } else { + event.error = MakeNSError(maybe_snapshot.status()); + } + [self.events addObject:event]; }]; self.queryListeners[query] = listener; @@ -357,7 +382,7 @@ - (void)writeUserMutation:(FSTMutation *)mutation { [[self currentOutstandingWrites] addObject:write]; LOG_DEBUG("sending a user write."); _workerQueue->EnqueueBlocking([=] { - [self.syncEngine writeMutations:@[ mutation ] + [self.syncEngine writeMutations:{mutation} completion:^(NSError *_Nullable error) { LOG_DEBUG("A callback was called with error: %s", error); write.done = YES; diff --git a/Firestore/Example/Tests/SpecTests/json/query_spec_test.json b/Firestore/Example/Tests/SpecTests/json/query_spec_test.json new file mode 100644 index 00000000000..4ca89f24cba --- /dev/null +++ b/Firestore/Example/Tests/SpecTests/json/query_spec_test.json @@ -0,0 +1,1170 @@ +{ + "Collection Group query": { + "describeName": "Queries:", + "itName": "Collection Group query", + "tags": [], + "config": { + "useGarbageCollection": true, + "numClients": 1 + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "cg/1", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "cg/1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "cg/1", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "version": 1000, + "targetIds": [] + }, + "expect": [ + { + "query": { + "path": "cg/1", + "filters": [], + "orderBys": [] + }, + "added": [ + { + "key": "cg/1", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userListen": [ + 4, + { + "path": "cg/2", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "cg/1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "cg/2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "cg/2", + "version": 1000, + "value": { + "val": 2 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "version": 1000, + "targetIds": [] + }, + "expect": [ + { + "query": { + "path": "cg/2", + "filters": [], + "orderBys": [] + }, + "added": [ + { + "key": "cg/2", + "version": 1000, + "value": { + "val": 2 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userListen": [ + 6, + { + "path": "not-cg/nope/cg/3", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "cg/1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "cg/2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "6": { + "query": { + "path": "not-cg/nope/cg/3", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 6 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "not-cg/nope/cg/3", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "targets": [ + 6 + ] + } + }, + { + "watchCurrent": [ + [ + 6 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "version": 1000, + "targetIds": [] + }, + "expect": [ + { + "query": { + "path": "not-cg/nope/cg/3", + "filters": [], + "orderBys": [] + }, + "added": [ + { + "key": "not-cg/nope/cg/3", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userListen": [ + 8, + { + "path": "not-cg/nope", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "cg/1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "cg/2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "6": { + "query": { + "path": "not-cg/nope/cg/3", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "8": { + "query": { + "path": "not-cg/nope", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 8 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "not-cg/nope", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "targets": [ + 8 + ] + } + }, + { + "watchCurrent": [ + [ + 8 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "version": 1000, + "targetIds": [] + }, + "expect": [ + { + "query": { + "path": "not-cg/nope", + "filters": [], + "orderBys": [] + }, + "added": [ + { + "key": "not-cg/nope", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userListen": [ + 10, + { + "path": "cg/1/not-cg/nope", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "cg/1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "cg/2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "6": { + "query": { + "path": "not-cg/nope/cg/3", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "8": { + "query": { + "path": "not-cg/nope", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "10": { + "query": { + "path": "cg/1/not-cg/nope", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 10 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "cg/1/not-cg/nope", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "targets": [ + 10 + ] + } + }, + { + "watchCurrent": [ + [ + 10 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "version": 1000, + "targetIds": [] + }, + "expect": [ + { + "query": { + "path": "cg/1/not-cg/nope", + "filters": [], + "orderBys": [] + }, + "added": [ + { + "key": "cg/1/not-cg/nope", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userListen": [ + 12, + { + "path": "", + "collectionGroup": "cg", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "cg/1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "cg/2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "6": { + "query": { + "path": "not-cg/nope/cg/3", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "8": { + "query": { + "path": "not-cg/nope", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "10": { + "query": { + "path": "cg/1/not-cg/nope", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "12": { + "query": { + "path": "", + "collectionGroup": "cg", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "", + "collectionGroup": "cg", + "filters": [], + "orderBys": [] + }, + "added": [ + { + "key": "cg/1", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + }, + { + "key": "cg/2", + "version": 1000, + "value": { + "val": 2 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + }, + { + "key": "not-cg/nope/cg/3", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "userListen": [ + 14, + { + "path": "", + "collectionGroup": "cg", + "filters": [ + [ + "val", + "==", + 1 + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "cg/1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "cg/2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "6": { + "query": { + "path": "not-cg/nope/cg/3", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "8": { + "query": { + "path": "not-cg/nope", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "10": { + "query": { + "path": "cg/1/not-cg/nope", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "12": { + "query": { + "path": "", + "collectionGroup": "cg", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "14": { + "query": { + "path": "", + "collectionGroup": "cg", + "filters": [ + [ + "val", + "==", + 1 + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "", + "collectionGroup": "cg", + "filters": [ + [ + "val", + "==", + 1 + ] + ], + "orderBys": [] + }, + "added": [ + { + "key": "cg/1", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + }, + { + "key": "not-cg/nope/cg/3", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Collection Group query with mutations": { + "describeName": "Queries:", + "itName": "Collection Group query with mutations", + "tags": [], + "config": { + "useGarbageCollection": true, + "numClients": 1 + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "cg/1", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "cg/1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "cg/1", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "version": 1000, + "targetIds": [] + }, + "expect": [ + { + "query": { + "path": "cg/1", + "filters": [], + "orderBys": [] + }, + "added": [ + { + "key": "cg/1", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userListen": [ + 4, + { + "path": "not-cg/nope", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "cg/1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "not-cg/nope", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "not-cg/nope", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "version": 1000, + "targetIds": [] + }, + "expect": [ + { + "query": { + "path": "not-cg/nope", + "filters": [], + "orderBys": [] + }, + "added": [ + { + "key": "not-cg/nope", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userSet": [ + "cg/2", + { + "val": 2 + } + ] + }, + { + "userSet": [ + "not-cg/nope/cg/3", + { + "val": 1 + } + ] + }, + { + "userSet": [ + "not-cg2/nope", + { + "val": 1 + } + ] + }, + { + "userListen": [ + 6, + { + "path": "", + "collectionGroup": "cg", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "cg/1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "not-cg/nope", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "6": { + "query": { + "path": "", + "collectionGroup": "cg", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "", + "collectionGroup": "cg", + "filters": [], + "orderBys": [] + }, + "added": [ + { + "key": "cg/1", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + }, + { + "key": "cg/2", + "version": 0, + "value": { + "val": 2 + }, + "options": { + "hasLocalMutations": true, + "hasCommittedMutations": false + } + }, + { + "key": "not-cg/nope/cg/3", + "version": 0, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": true, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + }, + { + "userListen": [ + 8, + { + "path": "", + "collectionGroup": "cg", + "filters": [ + [ + "val", + "==", + 1 + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "cg/1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "not-cg/nope", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "6": { + "query": { + "path": "", + "collectionGroup": "cg", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "8": { + "query": { + "path": "", + "collectionGroup": "cg", + "filters": [ + [ + "val", + "==", + 1 + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "", + "collectionGroup": "cg", + "filters": [ + [ + "val", + "==", + 1 + ] + ], + "orderBys": [] + }, + "added": [ + { + "key": "cg/1", + "version": 1000, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + }, + { + "key": "not-cg/nope/cg/3", + "version": 0, + "value": { + "val": 1 + }, + "options": { + "hasLocalMutations": true, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": true + } + ] + } + ] + } +} diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h index a276ef4900b..a56b9b7eac1 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.h +++ b/Firestore/Example/Tests/Util/FSTHelpers.h @@ -17,39 +17,49 @@ #import #include +#include #include #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/model/document_map.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "absl/strings/string_view.h" +#include "absl/types/optional.h" @class FIRGeoPoint; @class FSTDeleteMutation; @class FSTDeletedDocument; @class FSTDocument; @class FSTDocumentKeyReference; -@class FSTDocumentSet; @class FSTFieldValue; @class FSTFilter; @class FSTLocalViewChanges; @class FSTPatchMutation; @class FSTQuery; -@class FSTRemoteEvent; @class FSTSetMutation; @class FSTSortOrder; -@class FSTTargetChange; @class FIRTimestamp; @class FSTTransformMutation; @class FSTView; -@class FSTViewSnapshot; @class FSTObjectValue; +namespace firebase { +namespace firestore { +namespace remote { + +class RemoteEvent; + +} // namespace remote +} // namespace firestore +} // namespace firebase + NS_ASSUME_NONNULL_BEGIN #define FSTAssertIsKindOfClass(value, classType) \ @@ -72,9 +82,9 @@ NS_ASSUME_NONNULL_BEGIN NSComparisonResult result = [left compare:right]; \ NSComparisonResult inverseResult = [right compare:left]; \ XCTAssertEqual(result, expected, @"comparing %@ with %@ at (%lu, %lu)", left, right, \ - i, j); \ + (unsigned long)i, (unsigned long)j); \ XCTAssertEqual(inverseResult, -expected, @"comparing %@ with %@ at (%lu, %lu)", right, \ - left, j, i); \ + left, (unsigned long)j, (unsigned long)i); \ } \ } \ } \ @@ -131,52 +141,67 @@ inline NSString *FSTRemoveExceptionPrefix(NSString *exception) { XCTAssertTrue(didThrow, ##__VA_ARGS__); \ } while (0) -/** - * An implementation of FSTTargetMetadataProvider that provides controlled access to the - * `FSTTargetMetadataProvider` callbacks. Any target accessed via these callbacks must be - * registered beforehand via the factory methods or via `setSyncedKeys:forQueryData:`. - */ -@interface FSTTestTargetMetadataProvider : NSObject - -/** - * Creates an FSTTestTargetMetadataProvider that behaves as if there's an established listen for - * each of the given targets, where each target has previously seen query results containing just - * the given documentKey. - * - * Internally this means that the `remoteKeysForTarget` callback for these targets will return just - * the documentKey and that the provided targets will be returned as active from the - * `queryDataForTarget` target. - */ -+ (instancetype) - providerWithSingleResultForKey:(firebase::firestore::model::DocumentKey)documentKey - targets: - (const std::vector &)targets; - -+ (instancetype) - providerWithSingleResultForKey:(firebase::firestore::model::DocumentKey)documentKey - listenTargets: - (const std::vector &)listenTargets - limboTargets: - (const std::vector &)limboTargets; +// Helper to compare vectors containing Objective-C objects. +#define FSTAssertEqualVectors(v1, v2) \ + do { \ + XCTAssertEqual(v1.size(), v2.size(), @"Vector length mismatch"); \ + for (size_t i = 0; i < v1.size(); i++) { \ + XCTAssertEqualObjects(v1[i], v2[i]); \ + } \ + } while (0) /** - * Creates an FSTTestTargetMetadataProvider that behaves as if there's an established listen for - * each of the given targets, where each target has not seen any previous document. - * - * Internally this means that the `remoteKeysForTarget` callback for these targets will return an - * empty set of document keys and that the provided targets will be returned as active from the - * `queryDataForTarget` target. + * An implementation of `TargetMetadataProvider` that provides controlled access to the + * `TargetMetadataProvider` callbacks. Any target accessed via these callbacks must be + * registered beforehand via the factory methods or via `setSyncedKeys:forQueryData:`. */ -+ (instancetype) - providerWithEmptyResultForKey:(firebase::firestore::model::DocumentKey)documentKey - targets: - (const std::vector &)targets; - -/** Sets or replaces the local state for the provided query data. */ -- (void)setSyncedKeys:(firebase::firestore::model::DocumentKeySet)keys - forQueryData:(FSTQueryData *)queryData; - -@end +namespace firebase { +namespace firestore { +namespace remote { + +class TestTargetMetadataProvider : public TargetMetadataProvider { + public: + /** + * Creates a `TestTargetMetadataProvider` that behaves as if there's an established listen for + * each of the given targets, where each target has previously seen query results containing just + * the given `document_key`. + * + * Internally this means that the `GetRemoteKeysForTarget` callback for these targets will return + * just the `document_key` and that the provided targets will be returned as active from the + * `GetQueryDataForTarget` target. + */ + static TestTargetMetadataProvider CreateSingleResultProvider( + model::DocumentKey document_key, const std::vector &targets); + static TestTargetMetadataProvider CreateSingleResultProvider( + model::DocumentKey document_key, + const std::vector &targets, + const std::vector &limbo_targets); + + /** + * Creates an `TestTargetMetadataProvider` that behaves as if there's an established listen for + * each of the given targets, where each target has not seen any previous document. + * + * Internally this means that the `GetRemoteKeysForTarget` callback for these targets will return + * an empty set of document keys and that the provided targets will be returned as active from the + * `GetQueryDataForTarget` target. + */ + static TestTargetMetadataProvider CreateEmptyResultProvider( + const model::DocumentKey &document_key, const std::vector &targets); + + /** Sets or replaces the local state for the provided query data. */ + void SetSyncedKeys(model::DocumentKeySet keys, FSTQueryData *query_data); + + model::DocumentKeySet GetRemoteKeysForTarget(model::TargetId target_id) const override; + FSTQueryData *GetQueryDataForTarget(model::TargetId target_id) const override; + + private: + std::unordered_map synced_keys_; + std::unordered_map query_data_; +}; + +} // namespace remote +} // namespace firestore +} // namespace firebase /** Creates a new FIRTimestamp from components. Note that year, month, and day are all one-based. */ FIRTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second); @@ -250,15 +275,17 @@ FSTSortOrder *FSTTestOrderBy(const absl::string_view field, NSString *direction) NSComparator FSTTestDocComparator(const absl::string_view fieldPath); /** - * Creates a FSTDocumentSet based on the given comparator, initially containing the given + * Creates a DocumentSet based on the given comparator, initially containing the given * documents. */ -FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray *docs); +firebase::firestore::model::DocumentSet FSTTestDocSet(NSComparator comp, + NSArray *docs); /** Computes changes to the view with the docs and then applies them and returns the snapshot. */ -FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view, - NSArray *docs, - FSTTargetChange *_Nullable targetChange); +absl::optional FSTTestApplyChanges( + FSTView *view, + NSArray *docs, + const absl::optional &targetChange); /** Creates a set mutation for the document key at the given path. */ FSTSetMutation *FSTTestSetMutation(NSString *path, NSDictionary *values); @@ -283,17 +310,17 @@ FSTDeleteMutation *FSTTestDeleteMutation(NSString *path); firebase::firestore::model::MaybeDocumentMap FSTTestDocUpdates(NSArray *docs); /** Creates a remote event that inserts a new document. */ -FSTRemoteEvent *FSTTestAddedRemoteEvent( +firebase::firestore::remote::RemoteEvent FSTTestAddedRemoteEvent( FSTMaybeDocument *doc, const std::vector &addedToTargets); /** Creates a remote event with changes to a document. */ -FSTRemoteEvent *FSTTestUpdateRemoteEvent( +firebase::firestore::remote::RemoteEvent FSTTestUpdateRemoteEvent( FSTMaybeDocument *doc, const std::vector &updatedInTargets, const std::vector &removedFromTargets); /** Creates a remote event with changes to a document. Allows for identifying limbo targets */ -FSTRemoteEvent *FSTTestUpdateRemoteEventWithLimboTargets( +firebase::firestore::remote::RemoteEvent FSTTestUpdateRemoteEventWithLimboTargets( FSTMaybeDocument *doc, const std::vector &updatedInTargets, const std::vector &removedFromTargets, @@ -305,17 +332,11 @@ FSTLocalViewChanges *FSTTestViewChanges(firebase::firestore::model::TargetId tar NSArray *removedKeys); /** Creates a test target change that acks all 'docs' and marks the target as CURRENT */ -FSTTargetChange *FSTTestTargetChangeAckDocuments(firebase::firestore::model::DocumentKeySet docs); +firebase::firestore::remote::TargetChange FSTTestTargetChangeAckDocuments( + firebase::firestore::model::DocumentKeySet docs); /** Creates a test target change that marks the target as CURRENT */ -FSTTargetChange *FSTTestTargetChangeMarkCurrent(); - -/** Creates a test target change. */ -FSTTargetChange *FSTTestTargetChange(firebase::firestore::model::DocumentKeySet added, - firebase::firestore::model::DocumentKeySet modified, - firebase::firestore::model::DocumentKeySet removed, - NSData *resumeToken, - BOOL current); +firebase::firestore::remote::TargetChange FSTTestTargetChangeMarkCurrent(); /** Creates a resume token to match the given snapshot version. */ NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion watchSnapshot); diff --git a/Firestore/Example/Tests/Util/FSTHelpers.mm b/Firestore/Example/Tests/Util/FSTHelpers.mm index ffd04dd8c1b..db77d439c9e 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.mm +++ b/Firestore/Example/Tests/Util/FSTHelpers.mm @@ -23,26 +23,23 @@ #include #include #include -#include #include -#include #import "Firestore/Source/API/FIRFieldPath+Internal.h" #import "Firestore/Source/API/FSTUserDataConverter.h" #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Core/FSTView.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" #import "Firestore/Source/Local/FSTLocalViewChanges.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" #include "Firestore/core/src/firebase/firestore/model/field_mask.h" #include "Firestore/core/src/firebase/firestore/model/field_transform.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" @@ -58,9 +55,11 @@ namespace testutil = firebase::firestore::testutil; namespace util = firebase::firestore::util; using firebase::firestore::core::ParsedUpdateData; +using firebase::firestore::core::ViewSnapshot; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::DocumentSet; using firebase::firestore::model::FieldMask; using firebase::firestore::model::FieldPath; using firebase::firestore::model::FieldTransform; @@ -73,6 +72,8 @@ using firebase::firestore::model::TargetId; using firebase::firestore::model::TransformOperation; using firebase::firestore::remote::DocumentWatchChange; +using firebase::firestore::remote::RemoteEvent; +using firebase::firestore::remote::TargetChange; using firebase::firestore::remote::WatchChangeAggregator; NS_ASSUME_NONNULL_BEGIN @@ -236,10 +237,10 @@ NSComparator FSTTestDocComparator(const absl::string_view fieldPath) { return [query comparator]; } -FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray *docs) { - FSTDocumentSet *docSet = [FSTDocumentSet documentSetWithComparator:comp]; +DocumentSet FSTTestDocSet(NSComparator comp, NSArray *docs) { + DocumentSet docSet{comp}; for (FSTDocument *doc in docs) { - docSet = [docSet documentSetByAddingDocument:doc]; + docSet = docSet.insert(doc); } return docSet; } @@ -269,10 +270,11 @@ NSComparator FSTTestDocComparator(const absl::string_view fieldPath) { DocumentKey key = testutil::Key(path); FieldMask mask(merge ? std::set(updateMask.begin(), updateMask.end()) : fieldMaskPaths); - return [[FSTPatchMutation alloc] initWithKey:key - fieldMask:mask - value:objectValue - precondition:Precondition::Exists(true)]; + return [[FSTPatchMutation alloc] + initWithKey:key + fieldMask:mask + value:objectValue + precondition:merge ? Precondition::None() : Precondition::Exists(true)]; } FSTTransformMutation *FSTTestTransformMutation(NSString *path, NSDictionary *data) { @@ -297,123 +299,117 @@ MaybeDocumentMap FSTTestDocUpdates(NSArray *docs) { return updates; } -FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view, - NSArray *docs, - FSTTargetChange *_Nullable targetChange) { - return [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(docs)] - targetChange:targetChange] - .snapshot; -} - -@implementation FSTTestTargetMetadataProvider { - std::unordered_map _syncedKeys; - std::unordered_map _queryData; -} - -+ (instancetype)providerWithSingleResultForKey:(DocumentKey)documentKey - listenTargets:(const std::vector &)listenTargets - limboTargets:(const std::vector &)limboTargets { - FSTTestTargetMetadataProvider *metadataProvider = [FSTTestTargetMetadataProvider new]; - FSTQuery *query = [FSTQuery queryWithPath:documentKey.path()]; - - for (TargetId targetID : listenTargets) { - FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query - targetID:targetID - listenSequenceNumber:0 - purpose:FSTQueryPurposeListen]; - [metadataProvider setSyncedKeys:DocumentKeySet{documentKey} forQueryData:queryData]; +absl::optional FSTTestApplyChanges(FSTView *view, + NSArray *docs, + const absl::optional &targetChange) { + FSTViewChange *change = + [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(docs)] + targetChange:targetChange]; + return std::move(change.snapshot); +} + +namespace firebase { +namespace firestore { +namespace remote { + +TestTargetMetadataProvider TestTargetMetadataProvider::CreateSingleResultProvider( + DocumentKey document_key, + const std::vector &listen_targets, + const std::vector &limbo_targets) { + TestTargetMetadataProvider metadata_provider; + FSTQuery *query = [FSTQuery queryWithPath:document_key.path()]; + + for (TargetId target_id : listen_targets) { + FSTQueryData *query_data = [[FSTQueryData alloc] initWithQuery:query + targetID:target_id + listenSequenceNumber:0 + purpose:FSTQueryPurposeListen]; + metadata_provider.SetSyncedKeys(DocumentKeySet{document_key}, query_data); } - for (TargetId targetID : limboTargets) { - FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query - targetID:targetID - listenSequenceNumber:0 - purpose:FSTQueryPurposeLimboResolution]; - [metadataProvider setSyncedKeys:DocumentKeySet{documentKey} forQueryData:queryData]; + for (TargetId target_id : limbo_targets) { + FSTQueryData *query_data = [[FSTQueryData alloc] initWithQuery:query + targetID:target_id + listenSequenceNumber:0 + purpose:FSTQueryPurposeLimboResolution]; + metadata_provider.SetSyncedKeys(DocumentKeySet{document_key}, query_data); } - return metadataProvider; + return metadata_provider; } -+ (instancetype)providerWithSingleResultForKey:(DocumentKey)documentKey - targets:(const std::vector &)targets { - return [self providerWithSingleResultForKey:documentKey listenTargets:targets limboTargets:{}]; +TestTargetMetadataProvider TestTargetMetadataProvider::CreateSingleResultProvider( + DocumentKey document_key, const std::vector &targets) { + return CreateSingleResultProvider(document_key, targets, /*limbo_targets=*/{}); } -+ (instancetype)providerWithEmptyResultForKey:(DocumentKey)documentKey - targets:(const std::vector &)targets { - FSTTestTargetMetadataProvider *metadataProvider = [FSTTestTargetMetadataProvider new]; - FSTQuery *query = [FSTQuery queryWithPath:documentKey.path()]; +TestTargetMetadataProvider TestTargetMetadataProvider::CreateEmptyResultProvider( + const DocumentKey &document_key, const std::vector &targets) { + TestTargetMetadataProvider metadata_provider; + FSTQuery *query = [FSTQuery queryWithPath:document_key.path()]; - for (TargetId targetID : targets) { - FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query - targetID:targetID - listenSequenceNumber:0 - purpose:FSTQueryPurposeListen]; - [metadataProvider setSyncedKeys:DocumentKeySet {} forQueryData:queryData]; + for (TargetId target_id : targets) { + FSTQueryData *query_data = [[FSTQueryData alloc] initWithQuery:query + targetID:target_id + listenSequenceNumber:0 + purpose:FSTQueryPurposeListen]; + metadata_provider.SetSyncedKeys(DocumentKeySet{}, query_data); } - return metadataProvider; + return metadata_provider; } -- (void)setSyncedKeys:(DocumentKeySet)keys forQueryData:(FSTQueryData *)queryData { - _syncedKeys[queryData.targetID] = keys; - _queryData[queryData.targetID] = queryData; +void TestTargetMetadataProvider::SetSyncedKeys(DocumentKeySet keys, FSTQueryData *query_data) { + synced_keys_[query_data.targetID] = keys; + query_data_[query_data.targetID] = query_data; } -- (DocumentKeySet)remoteKeysForTarget:(TargetId)targetID { - auto it = _syncedKeys.find(targetID); - HARD_ASSERT(it != _syncedKeys.end(), "Cannot process unknown target %s", targetID); +DocumentKeySet TestTargetMetadataProvider::GetRemoteKeysForTarget(TargetId target_id) const { + auto it = synced_keys_.find(target_id); + HARD_ASSERT(it != synced_keys_.end(), "Cannot process unknown target %s", target_id); return it->second; } -- (nullable FSTQueryData *)queryDataForTarget:(TargetId)targetID { - auto it = _queryData.find(targetID); - HARD_ASSERT(it != _queryData.end(), "Cannot process unknown target %s", targetID); +FSTQueryData *TestTargetMetadataProvider::GetQueryDataForTarget(TargetId target_id) const { + auto it = query_data_.find(target_id); + HARD_ASSERT(it != query_data_.end(), "Cannot process unknown target %s", target_id); return it->second; } -@end +} // namespace remote +} // namespace firestore +} // namespace firebase + +using firebase::firestore::remote::TestTargetMetadataProvider; -FSTRemoteEvent *FSTTestAddedRemoteEvent(FSTMaybeDocument *doc, - const std::vector &addedToTargets) { +RemoteEvent FSTTestAddedRemoteEvent(FSTMaybeDocument *doc, + const std::vector &addedToTargets) { HARD_ASSERT(![doc isKindOfClass:[FSTDocument class]] || ![(FSTDocument *)doc hasLocalMutations], "Docs from remote updates shouldn't have local changes."); DocumentWatchChange change{addedToTargets, {}, doc.key, doc}; - WatchChangeAggregator aggregator{ - [FSTTestTargetMetadataProvider providerWithEmptyResultForKey:doc.key targets:addedToTargets]}; + auto metadataProvider = + TestTargetMetadataProvider::CreateEmptyResultProvider(doc.key, addedToTargets); + WatchChangeAggregator aggregator{&metadataProvider}; aggregator.HandleDocumentChange(change); return aggregator.CreateRemoteEvent(doc.version); } -FSTTargetChange *FSTTestTargetChangeMarkCurrent() { - return [[FSTTargetChange alloc] initWithResumeToken:[NSData data] - current:YES - addedDocuments:DocumentKeySet {} - modifiedDocuments:DocumentKeySet {} - removedDocuments:DocumentKeySet{}]; -} - -FSTTargetChange *FSTTestTargetChangeAckDocuments(DocumentKeySet docs) { - return [[FSTTargetChange alloc] initWithResumeToken:[NSData data] - current:YES - addedDocuments:docs - modifiedDocuments:DocumentKeySet {} - removedDocuments:DocumentKeySet{}]; +TargetChange FSTTestTargetChangeMarkCurrent() { + return {[NSData data], + /*current=*/true, + /*added_documents=*/DocumentKeySet{}, + /*modified_documents=*/DocumentKeySet{}, + /*removed_documents=*/DocumentKeySet{}}; } -FSTTargetChange *FSTTestTargetChange(DocumentKeySet added, - DocumentKeySet modified, - DocumentKeySet removed, - NSData *resumeToken, - BOOL current) { - return [[FSTTargetChange alloc] initWithResumeToken:resumeToken - current:current - addedDocuments:added - modifiedDocuments:modified - removedDocuments:removed]; +TargetChange FSTTestTargetChangeAckDocuments(DocumentKeySet docs) { + return {[NSData data], + /*current=*/true, + /*added_documents*/ std::move(docs), + /*modified_documents*/ DocumentKeySet{}, + /*removed_documents*/ DocumentKeySet{}}; } -FSTRemoteEvent *FSTTestUpdateRemoteEventWithLimboTargets( +RemoteEvent FSTTestUpdateRemoteEventWithLimboTargets( FSTMaybeDocument *doc, const std::vector &updatedInTargets, const std::vector &removedFromTargets, @@ -425,17 +421,16 @@ - (nullable FSTQueryData *)queryDataForTarget:(TargetId)targetID { std::vector listens = updatedInTargets; listens.insert(listens.end(), removedFromTargets.begin(), removedFromTargets.end()); - WatchChangeAggregator aggregator{[FSTTestTargetMetadataProvider - providerWithSingleResultForKey:doc.key - listenTargets:listens - limboTargets:limboTargets]}; + auto metadataProvider = + TestTargetMetadataProvider::CreateSingleResultProvider(doc.key, listens, limboTargets); + WatchChangeAggregator aggregator{&metadataProvider}; aggregator.HandleDocumentChange(change); return aggregator.CreateRemoteEvent(doc.version); } -FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc, - const std::vector &updatedInTargets, - const std::vector &removedFromTargets) { +RemoteEvent FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc, + const std::vector &updatedInTargets, + const std::vector &removedFromTargets) { return FSTTestUpdateRemoteEventWithLimboTargets(doc, updatedInTargets, removedFromTargets, {}); } diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm index cd08ec3756d..fe6cf3a166b 100644 --- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm @@ -193,7 +193,7 @@ - (FIRFirestore *)firestoreWithProjectID:(NSString *)projectID { FIRFirestore *firestore = [[FIRFirestore alloc] initWithProjectID:util::MakeString(projectID) database:DatabaseId::kDefault - persistenceKey:persistenceKey + persistenceKey:util::MakeString(persistenceKey) credentialsProvider:std::move(credentials_provider) workerQueue:std::move(workerQueue) firebaseApp:app]; diff --git a/Firestore/Source/API/FIRCollectionReference.mm b/Firestore/Source/API/FIRCollectionReference.mm index 76f441359eb..3156ef42a63 100644 --- a/Firestore/Source/API/FIRCollectionReference.mm +++ b/Firestore/Source/API/FIRCollectionReference.mm @@ -15,11 +15,13 @@ */ #import "FIRCollectionReference.h" -#import "FIRFirestore.h" + +#include #include "Firestore/core/src/firebase/firestore/util/autoid.h" #import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRQuery+Internal.h" #import "Firestore/Source/API/FIRQuery_Init.h" #import "Firestore/Source/Core/FSTQuery.h" @@ -99,7 +101,8 @@ - (FIRDocumentReference *_Nullable)parent { return nil; } else { DocumentKey key{parentPath}; - return [FIRDocumentReference referenceWithKey:key firestore:self.firestore]; + return [[FIRDocumentReference alloc] initWithKey:std::move(key) + firestore:self.firestore.wrapped]; } } @@ -112,8 +115,9 @@ - (FIRDocumentReference *)documentWithPath:(NSString *)documentPath { FSTThrowInvalidArgument(@"Document path cannot be nil."); } const ResourcePath subPath = ResourcePath::FromString(util::MakeString(documentPath)); - const ResourcePath path = self.query.path.Append(subPath); - return [FIRDocumentReference referenceWithPath:path firestore:self.firestore]; + ResourcePath path = self.query.path.Append(subPath); + return [[FIRDocumentReference alloc] initWithPath:std::move(path) + firestore:self.firestore.wrapped]; } - (FIRDocumentReference *)addDocumentWithData:(NSDictionary *)data { @@ -129,8 +133,8 @@ - (FIRDocumentReference *)addDocumentWithData:(NSDictionary *)da } - (FIRDocumentReference *)documentWithAutoID { - const DocumentKey key{self.query.path.Append(CreateAutoId())}; - return [FIRDocumentReference referenceWithKey:key firestore:self.firestore]; + DocumentKey key{self.query.path.Append(CreateAutoId())}; + return [[FIRDocumentReference alloc] initWithKey:std::move(key) firestore:self.firestore.wrapped]; } @end diff --git a/Firestore/Source/API/FIRDocumentChange+Internal.h b/Firestore/Source/API/FIRDocumentChange+Internal.h index 2aaced1e000..c1c26904db8 100644 --- a/Firestore/Source/API/FIRDocumentChange+Internal.h +++ b/Firestore/Source/API/FIRDocumentChange+Internal.h @@ -16,8 +16,10 @@ #import "FIRDocumentChange.h" +#include "Firestore/core/src/firebase/firestore/api/firestore.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" + @class FIRFirestore; -@class FSTViewSnapshot; NS_ASSUME_NONNULL_BEGIN @@ -25,9 +27,10 @@ NS_ASSUME_NONNULL_BEGIN @interface FIRDocumentChange (Internal) /** Calculates the array of FIRDocumentChange's based on the given FSTViewSnapshot. */ -+ (NSArray *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot - includeMetadataChanges:(BOOL)includeMetadataChanges - firestore:(FIRFirestore *)firestore; ++ (NSArray *) + documentChangesForSnapshot:(const firebase::firestore::core::ViewSnapshot &)snapshot + includeMetadataChanges:(bool)includeMetadataChanges + firestore:(firebase::firestore::api::Firestore *)firestore; @end diff --git a/Firestore/Source/API/FIRDocumentChange.mm b/Firestore/Source/API/FIRDocumentChange.mm index a01b424be79..1a5bccd20bb 100644 --- a/Firestore/Source/API/FIRDocumentChange.mm +++ b/Firestore/Source/API/FIRDocumentChange.mm @@ -17,14 +17,18 @@ #import "FIRDocumentChange.h" #import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -using firebase::firestore::core::DocumentViewChangeType; +using firebase::firestore::api::Firestore; +using firebase::firestore::core::DocumentViewChange; +using firebase::firestore::core::ViewSnapshot; +using firebase::firestore::model::DocumentSet; NS_ASSUME_NONNULL_BEGIN @@ -39,39 +43,39 @@ - (instancetype)initWithType:(FIRDocumentChangeType)type @implementation FIRDocumentChange (Internal) -+ (FIRDocumentChangeType)documentChangeTypeForChange:(FSTDocumentViewChange *)change { - switch (change.type) { - case DocumentViewChangeType::kAdded: ++ (FIRDocumentChangeType)documentChangeTypeForChange:(const DocumentViewChange &)change { + switch (change.type()) { + case DocumentViewChange::Type::kAdded: return FIRDocumentChangeTypeAdded; - case DocumentViewChangeType::kModified: - case DocumentViewChangeType::kMetadata: + case DocumentViewChange::Type::kModified: + case DocumentViewChange::Type::kMetadata: return FIRDocumentChangeTypeModified; - case DocumentViewChangeType::kRemoved: + case DocumentViewChange::Type::kRemoved: return FIRDocumentChangeTypeRemoved; } - HARD_FAIL("Unknown DocumentViewChangeTyp: %s", change.type); + HARD_FAIL("Unknown DocumentViewChange::Type: %s", change.type()); } -+ (NSArray *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot - includeMetadataChanges:(BOOL)includeMetadataChanges - firestore:(FIRFirestore *)firestore { - if (snapshot.oldDocuments.isEmpty) { ++ (NSArray *)documentChangesForSnapshot:(const ViewSnapshot &)snapshot + includeMetadataChanges:(bool)includeMetadataChanges + firestore:(Firestore *)firestore { + if (snapshot.old_documents().empty()) { // Special case the first snapshot because index calculation is easy and fast. Also all changes // on the first snapshot are adds so there are also no metadata-only changes to filter out. FSTDocument *_Nullable lastDocument = nil; NSUInteger index = 0; NSMutableArray *changes = [NSMutableArray array]; - for (FSTDocumentViewChange *change in snapshot.documentChanges) { - FIRQueryDocumentSnapshot *document = [FIRQueryDocumentSnapshot - snapshotWithFirestore:firestore - documentKey:change.document.key - document:change.document - fromCache:snapshot.isFromCache - hasPendingWrites:snapshot.mutatedKeys.contains(change.document.key)]; - HARD_ASSERT(change.type == DocumentViewChangeType::kAdded, + for (const DocumentViewChange &change : snapshot.document_changes()) { + FIRQueryDocumentSnapshot *document = [[FIRQueryDocumentSnapshot alloc] + initWithFirestore:firestore + documentKey:change.document().key + document:change.document() + fromCache:snapshot.from_cache() + hasPendingWrites:snapshot.mutated_keys().contains(change.document().key)]; + HARD_ASSERT(change.type() == DocumentViewChange::Type::kAdded, "Invalid event type for first snapshot"); - HARD_ASSERT(!lastDocument || snapshot.query.comparator(lastDocument, change.document) == + HARD_ASSERT(!lastDocument || snapshot.query().comparator(lastDocument, change.document()) == NSOrderedAscending, "Got added events in wrong order"); [changes addObject:[[FIRDocumentChange alloc] initWithType:FIRDocumentChangeTypeAdded @@ -83,30 +87,30 @@ + (FIRDocumentChangeType)documentChangeTypeForChange:(FSTDocumentViewChange *)ch } else { // A DocumentSet that is updated incrementally as changes are applied to use to lookup the index // of a document. - FSTDocumentSet *indexTracker = snapshot.oldDocuments; + DocumentSet indexTracker = snapshot.old_documents(); NSMutableArray *changes = [NSMutableArray array]; - for (FSTDocumentViewChange *change in snapshot.documentChanges) { - if (!includeMetadataChanges && change.type == DocumentViewChangeType::kMetadata) { + for (const DocumentViewChange &change : snapshot.document_changes()) { + if (!includeMetadataChanges && change.type() == DocumentViewChange::Type::kMetadata) { continue; } - FIRQueryDocumentSnapshot *document = [FIRQueryDocumentSnapshot - snapshotWithFirestore:firestore - documentKey:change.document.key - document:change.document - fromCache:snapshot.isFromCache - hasPendingWrites:snapshot.mutatedKeys.contains(change.document.key)]; - - NSUInteger oldIndex = NSNotFound; - NSUInteger newIndex = NSNotFound; - if (change.type != DocumentViewChangeType::kAdded) { - oldIndex = [indexTracker indexOfKey:change.document.key]; - HARD_ASSERT(oldIndex != NSNotFound, "Index for document not found"); - indexTracker = [indexTracker documentSetByRemovingKey:change.document.key]; + FIRQueryDocumentSnapshot *document = [[FIRQueryDocumentSnapshot alloc] + initWithFirestore:firestore + documentKey:change.document().key + document:change.document() + fromCache:snapshot.from_cache() + hasPendingWrites:snapshot.mutated_keys().contains(change.document().key)]; + + size_t oldIndex = DocumentSet::npos; + size_t newIndex = DocumentSet::npos; + if (change.type() != DocumentViewChange::Type::kAdded) { + oldIndex = indexTracker.IndexOf(change.document().key); + HARD_ASSERT(oldIndex != DocumentSet::npos, "Index for document not found"); + indexTracker = indexTracker.erase(change.document().key); } - if (change.type != DocumentViewChangeType::kRemoved) { - indexTracker = [indexTracker documentSetByAddingDocument:change.document]; - newIndex = [indexTracker indexOfKey:change.document.key]; + if (change.type() != DocumentViewChange::Type::kRemoved) { + indexTracker = indexTracker.insert(change.document()); + newIndex = indexTracker.IndexOf(change.document().key); } [FIRDocumentChange documentChangeTypeForChange:change]; FIRDocumentChangeType type = [FIRDocumentChange documentChangeTypeForChange:change]; diff --git a/Firestore/Source/API/FIRDocumentReference+Internal.h b/Firestore/Source/API/FIRDocumentReference+Internal.h index eb078ca535d..aa12e97f784 100644 --- a/Firestore/Source/API/FIRDocumentReference+Internal.h +++ b/Firestore/Source/API/FIRDocumentReference+Internal.h @@ -16,19 +16,28 @@ #import "FIRDocumentReference.h" +#include "Firestore/core/src/firebase/firestore/api/document_reference.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" NS_ASSUME_NONNULL_BEGIN +@interface FIRDocumentReference (/* Init */) + +- (instancetype)initWithReference:(firebase::firestore::api::DocumentReference &&)reference + NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithPath:(firebase::firestore::model::ResourcePath)path + firestore:(firebase::firestore::api::Firestore *)firestore; + +- (instancetype)initWithKey:(firebase::firestore::model::DocumentKey)key + firestore:(firebase::firestore::api::Firestore *)firestore; + +@end + /** Internal FIRDocumentReference API we don't want exposed in our public header files. */ @interface FIRDocumentReference (Internal) -+ (instancetype)referenceWithPath:(const firebase::firestore::model::ResourcePath &)path - firestore:(FIRFirestore *)firestore; -+ (instancetype)referenceWithKey:(firebase::firestore::model::DocumentKey)key - firestore:(FIRFirestore *)firestore; - - (const firebase::firestore::model::DocumentKey &)key; @end diff --git a/Firestore/Source/API/FIRDocumentReference.mm b/Firestore/Source/API/FIRDocumentReference.mm index 0222843c718..d11501ba436 100644 --- a/Firestore/Source/API/FIRDocumentReference.mm +++ b/Firestore/Source/API/FIRDocumentReference.mm @@ -21,7 +21,6 @@ #import "FIRFirestoreErrors.h" #import "FIRFirestoreSource.h" -#import "FIRSnapshotMetadata.h" #import "Firestore/Source/API/FIRCollectionReference+Internal.h" #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" @@ -29,120 +28,134 @@ #import "Firestore/Source/API/FIRListenerRegistration+Internal.h" #import "Firestore/Source/API/FSTUserDataConverter.h" #import "Firestore/Source/Core/FSTEventManager.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Util/FSTAsyncQueryListener.h" #import "Firestore/Source/Util/FSTUsageValidation.h" +#include "Firestore/core/src/firebase/firestore/api/document_reference.h" +#include "Firestore/core/src/firebase/firestore/api/document_snapshot.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" #include "Firestore/core/src/firebase/firestore/model/precondition.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/error_apple.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "Firestore/core/src/firebase/firestore/util/statusor_callback.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; +using firebase::firestore::api::DocumentReference; +using firebase::firestore::api::DocumentSnapshot; +using firebase::firestore::api::Firestore; using firebase::firestore::core::ParsedSetData; using firebase::firestore::core::ParsedUpdateData; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::Precondition; using firebase::firestore::model::ResourcePath; +using firebase::firestore::util::Status; +using firebase::firestore::util::StatusOr; +using firebase::firestore::util::StatusOrCallback; NS_ASSUME_NONNULL_BEGIN #pragma mark - FIRDocumentReference -@interface FIRDocumentReference () -- (instancetype)initWithKey:(DocumentKey)key - firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; -@end - @implementation FIRDocumentReference { - DocumentKey _key; + DocumentReference _documentReference; } -- (instancetype)initWithKey:(DocumentKey)key firestore:(FIRFirestore *)firestore { +- (instancetype)initWithReference:(DocumentReference &&)reference { if (self = [super init]) { - _key = std::move(key); - _firestore = firestore; + _documentReference = std::move(reference); } return self; } +- (instancetype)initWithPath:(ResourcePath)path firestore:(Firestore *)firestore { + if (path.size() % 2 != 0) { + FSTThrowInvalidArgument(@"Invalid document reference. Document references must have an even " + "number of segments, but %s has %zu", + path.CanonicalString().c_str(), path.size()); + } + return [self initWithKey:DocumentKey{std::move(path)} firestore:firestore]; +} + +- (instancetype)initWithKey:(DocumentKey)key firestore:(Firestore *)firestore { + DocumentReference delegate{std::move(key), firestore}; + return [self initWithReference:std::move(delegate)]; +} + #pragma mark - NSObject Methods - (BOOL)isEqual:(nullable id)other { if (other == self) return YES; if (![[other class] isEqual:[self class]]) return NO; - return [self isEqualToReference:other]; -} - -- (BOOL)isEqualToReference:(nullable FIRDocumentReference *)reference { - if (self == reference) return YES; - if (reference == nil) return NO; - return [self.firestore isEqual:reference.firestore] && self.key == reference.key; + return _documentReference == static_cast(other)->_documentReference; } - (NSUInteger)hash { - NSUInteger hash = [self.firestore hash]; - hash = hash * 31u + self.key.Hash(); - return hash; + return _documentReference.Hash(); } #pragma mark - Public Methods +@dynamic firestore; + +- (FIRFirestore *)firestore { + return [FIRFirestore recoverFromFirestore:_documentReference.firestore()]; +} + - (NSString *)documentID { - return util::WrapNSString(self.key.path().last_segment()); + return util::WrapNSString(_documentReference.document_id()); } - (FIRCollectionReference *)parent { - return [FIRCollectionReference referenceWithPath:self.key.path().PopLast() + return [FIRCollectionReference referenceWithPath:_documentReference.key().path().PopLast() firestore:self.firestore]; } - (NSString *)path { - return util::WrapNSString(self.key.path().CanonicalString()); + return util::WrapNSString(_documentReference.Path()); } - (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { if (!collectionPath) { FSTThrowInvalidArgument(@"Collection path cannot be nil."); } - const ResourcePath subPath = ResourcePath::FromString(util::MakeString(collectionPath)); - const ResourcePath path = self.key.path().Append(subPath); + + ResourcePath subPath = ResourcePath::FromString(util::MakeString(collectionPath)); + ResourcePath path = _documentReference.key().path().Append(subPath); return [FIRCollectionReference referenceWithPath:path firestore:self.firestore]; } - (void)setData:(NSDictionary *)documentData { - return [self setData:documentData merge:NO completion:nil]; + [self setData:documentData merge:NO completion:nil]; } - (void)setData:(NSDictionary *)documentData merge:(BOOL)merge { - return [self setData:documentData merge:merge completion:nil]; + [self setData:documentData merge:merge completion:nil]; } - (void)setData:(NSDictionary *)documentData mergeFields:(NSArray *)mergeFields { - return [self setData:documentData mergeFields:mergeFields completion:nil]; + [self setData:documentData mergeFields:mergeFields completion:nil]; } - (void)setData:(NSDictionary *)documentData completion:(nullable void (^)(NSError *_Nullable error))completion { - return [self setData:documentData merge:NO completion:completion]; + [self setData:documentData merge:NO completion:completion]; } - (void)setData:(NSDictionary *)documentData merge:(BOOL)merge completion:(nullable void (^)(NSError *_Nullable error))completion { - ParsedSetData parsed = merge ? [self.firestore.dataConverter parsedMergeData:documentData - fieldMask:nil] - : [self.firestore.dataConverter parsedSetData:documentData]; - return [self.firestore.client - writeMutations:std::move(parsed).ToMutations(self.key, Precondition::None()) - completion:completion]; + auto dataConverter = self.firestore.dataConverter; + ParsedSetData parsed = merge ? [dataConverter parsedMergeData:documentData fieldMask:nil] + : [dataConverter parsedSetData:documentData]; + _documentReference.SetData( + std::move(parsed).ToMutations(_documentReference.key(), Precondition::None()), completion); } - (void)setData:(NSDictionary *)documentData @@ -150,98 +163,38 @@ - (void)setData:(NSDictionary *)documentData completion:(nullable void (^)(NSError *_Nullable error))completion { ParsedSetData parsed = [self.firestore.dataConverter parsedMergeData:documentData fieldMask:mergeFields]; - return [self.firestore.client - writeMutations:std::move(parsed).ToMutations(self.key, Precondition::None()) - completion:completion]; + _documentReference.SetData( + std::move(parsed).ToMutations(_documentReference.key(), Precondition::None()), completion); } - (void)updateData:(NSDictionary *)fields { - return [self updateData:fields completion:nil]; + [self updateData:fields completion:nil]; } - (void)updateData:(NSDictionary *)fields completion:(nullable void (^)(NSError *_Nullable error))completion { ParsedUpdateData parsed = [self.firestore.dataConverter parsedUpdateData:fields]; - return [self.firestore.client - writeMutations:std::move(parsed).ToMutations(self.key, Precondition::Exists(true)) - completion:completion]; + _documentReference.UpdateData( + std::move(parsed).ToMutations(_documentReference.key(), Precondition::Exists(true)), + completion); } - (void)deleteDocument { - return [self deleteDocumentWithCompletion:nil]; + [self deleteDocumentWithCompletion:nil]; } - (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { - FSTDeleteMutation *mutation = [[FSTDeleteMutation alloc] initWithKey:self.key - precondition:Precondition::None()]; - return [self.firestore.client writeMutations:@[ mutation ] completion:completion]; + _documentReference.DeleteDocument(completion); } -- (void)getDocumentWithCompletion:(void (^)(FIRDocumentSnapshot *_Nullable document, - NSError *_Nullable error))completion { - return [self getDocumentWithSource:FIRFirestoreSourceDefault completion:completion]; +- (void)getDocumentWithCompletion:(FIRDocumentSnapshotBlock)completion { + _documentReference.GetDocument(FIRFirestoreSourceDefault, + [self wrapDocumentSnapshotBlock:completion]); } - (void)getDocumentWithSource:(FIRFirestoreSource)source - completion:(void (^)(FIRDocumentSnapshot *_Nullable document, - NSError *_Nullable error))completion { - if (source == FIRFirestoreSourceCache) { - [self.firestore.client getDocumentFromLocalCache:self completion:completion]; - return; - } - - FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES - includeDocumentMetadataChanges:YES - waitForSyncWhenOnline:YES]; - - dispatch_semaphore_t registered = dispatch_semaphore_create(0); - __block id listenerRegistration; - FIRDocumentSnapshotBlock listener = ^(FIRDocumentSnapshot *snapshot, NSError *error) { - if (error) { - completion(nil, error); - return; - } - - // Remove query first before passing event to user to avoid user actions affecting the - // now stale query. - dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); - [listenerRegistration remove]; - - if (!snapshot.exists && snapshot.metadata.fromCache) { - // TODO(dimond): Reconsider how to raise missing documents when offline. - // If we're online and the document doesn't exist then we call the completion with - // a document with document.exists set to false. If we're offline however, we call the - // completion handler with an error. Two options: - // 1) Cache the negative response from the server so we can deliver that even when you're - // offline. - // 2) Actually call the completion handler with an error if the document doesn't exist when - // you are offline. - completion(nil, - [NSError errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeUnavailable - userInfo:@{ - NSLocalizedDescriptionKey : - @"Failed to get document because the client is offline.", - }]); - } else if (snapshot.exists && snapshot.metadata.fromCache && - source == FIRFirestoreSourceServer) { - completion(nil, - [NSError errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeUnavailable - userInfo:@{ - NSLocalizedDescriptionKey : - @"Failed to get document from server. (However, this " - @"document does exist in the local cache. Run again " - @"without setting source to FIRFirestoreSourceServer to " - @"retrieve the cached document.)" - }]); - } else { - completion(snapshot, nil); - } - }; - - listenerRegistration = [self addSnapshotListenerInternalWithOptions:options listener:listener]; - dispatch_semaphore_signal(registered); + completion:(FIRDocumentSnapshotBlock)completion { + _documentReference.GetDocument(source, [self wrapDocumentSnapshotBlock:completion]); } - (id)addSnapshotListener:(FIRDocumentSnapshotBlock)listener { @@ -251,57 +204,28 @@ - (void)getDocumentWithSource:(FIRFirestoreSource)source - (id) addSnapshotListenerWithIncludeMetadataChanges:(BOOL)includeMetadataChanges listener:(FIRDocumentSnapshotBlock)listener { - FSTListenOptions *options = - [self internalOptionsForIncludeMetadataChanges:includeMetadataChanges]; + ListenOptions options = ListenOptions::FromIncludeMetadataChanges(includeMetadataChanges); return [self addSnapshotListenerInternalWithOptions:options listener:listener]; } -- (id) - addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions - listener:(FIRDocumentSnapshotBlock)listener { - FIRFirestore *firestore = self.firestore; - FSTQuery *query = [FSTQuery queryWithPath:self.key.path()]; - const DocumentKey key = self.key; +- (id)addSnapshotListenerInternalWithOptions:(ListenOptions)internalOptions + listener:(FIRDocumentSnapshotBlock) + listener { + return _documentReference.AddSnapshotListener([self wrapDocumentSnapshotBlock:listener], + std::move(internalOptions)); +} - FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { - if (error) { - listener(nil, error); - return; +- (StatusOrCallback)wrapDocumentSnapshotBlock:(FIRDocumentSnapshotBlock)block { + FIRFirestore *firestore = self.firestore; + return [block, firestore](StatusOr maybe_snapshot) { + if (maybe_snapshot.ok()) { + FIRDocumentSnapshot *result = + [[FIRDocumentSnapshot alloc] initWithSnapshot:std::move(maybe_snapshot).ValueOrDie()]; + block(result, nil); + } else { + block(nil, util::MakeNSError(maybe_snapshot.status())); } - - HARD_ASSERT(snapshot.documents.count <= 1, "Too many document returned on a document query"); - FSTDocument *document = [snapshot.documents documentForKey:key]; - - BOOL hasPendingWrites = document - ? snapshot.mutatedKeys.contains(key) - : NO; // We don't raise `hasPendingWrites` for deleted documents. - - FIRDocumentSnapshot *result = [FIRDocumentSnapshot snapshotWithFirestore:firestore - documentKey:key - document:document - fromCache:snapshot.fromCache - hasPendingWrites:hasPendingWrites]; - listener(result, nil); }; - - FSTAsyncQueryListener *asyncListener = - [[FSTAsyncQueryListener alloc] initWithExecutor:self.firestore.client.userExecutor - snapshotHandler:snapshotHandler]; - - FSTQueryListener *internalListener = - [firestore.client listenToQuery:query - options:internalOptions - viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; - return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client - asyncListener:asyncListener - internalListener:internalListener]; -} - -/** Converts the public API options object to the internal options object. */ -- (FSTListenOptions *)internalOptionsForIncludeMetadataChanges:(BOOL)includeMetadataChanges { - return [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:includeMetadataChanges - includeDocumentMetadataChanges:includeMetadataChanges - waitForSyncWhenOnline:NO]; } @end @@ -310,21 +234,8 @@ - (FSTListenOptions *)internalOptionsForIncludeMetadataChanges:(BOOL)includeMeta @implementation FIRDocumentReference (Internal) -+ (instancetype)referenceWithPath:(const ResourcePath &)path firestore:(FIRFirestore *)firestore { - if (path.size() % 2 != 0) { - FSTThrowInvalidArgument(@"Invalid document reference. Document references must have an even " - "number of segments, but %s has %zu", - path.CanonicalString().c_str(), path.size()); - } - return [FIRDocumentReference referenceWithKey:DocumentKey{path} firestore:firestore]; -} - -+ (instancetype)referenceWithKey:(DocumentKey)key firestore:(FIRFirestore *)firestore { - return [[FIRDocumentReference alloc] initWithKey:std::move(key) firestore:firestore]; -} - -- (const firebase::firestore::model::DocumentKey &)key { - return _key; +- (const DocumentKey &)key { + return _documentReference.key(); } @end diff --git a/Firestore/Source/API/FIRDocumentSnapshot+Internal.h b/Firestore/Source/API/FIRDocumentSnapshot+Internal.h index fdc5ac8a3d5..b371b6a03cc 100644 --- a/Firestore/Source/API/FIRDocumentSnapshot+Internal.h +++ b/Firestore/Source/API/FIRDocumentSnapshot+Internal.h @@ -16,22 +16,40 @@ #import "FIRDocumentSnapshot.h" +#include "Firestore/core/src/firebase/firestore/api/document_snapshot.h" +#include "Firestore/core/src/firebase/firestore/api/snapshot_metadata.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" @class FIRFirestore; @class FSTDocument; +using firebase::firestore::api::DocumentSnapshot; +using firebase::firestore::api::Firestore; +using firebase::firestore::api::SnapshotMetadata; +using firebase::firestore::model::DocumentKey; + NS_ASSUME_NONNULL_BEGIN +@interface FIRDocumentSnapshot (/* Init */) + +- (instancetype)initWithSnapshot:(DocumentSnapshot &&)snapshot NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithFirestore:(Firestore *)firestore + documentKey:(DocumentKey)documentKey + document:(nullable FSTDocument *)document + metadata:(SnapshotMetadata)metadata; + +- (instancetype)initWithFirestore:(Firestore *)firestore + documentKey:(DocumentKey)documentKey + document:(nullable FSTDocument *)document + fromCache:(bool)fromCache + hasPendingWrites:(bool)hasPendingWrites; + +@end + /** Internal FIRDocumentSnapshot API we don't want exposed in our public header files. */ @interface FIRDocumentSnapshot (Internal) -+ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore - documentKey:(firebase::firestore::model::DocumentKey)documentKey - document:(nullable FSTDocument *)document - fromCache:(BOOL)fromCache - hasPendingWrites:(BOOL)pendingWrites; - @property(nonatomic, strong, readonly, nullable) FSTDocument *internalDocument; @end diff --git a/Firestore/Source/API/FIRDocumentSnapshot.mm b/Firestore/Source/API/FIRDocumentSnapshot.mm index f8ac7eaac72..eb210d96e02 100644 --- a/Firestore/Source/API/FIRDocumentSnapshot.mm +++ b/Firestore/Source/API/FIRDocumentSnapshot.mm @@ -18,6 +18,8 @@ #include +#include "Firestore/core/src/firebase/firestore/util/warnings.h" + #import "FIRFirestoreSettings.h" #import "Firestore/Source/API/FIRDocumentReference+Internal.h" @@ -28,89 +30,72 @@ #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Util/FSTUsageValidation.h" +#include "Firestore/core/src/firebase/firestore/api/document_snapshot.h" +#include "Firestore/core/src/firebase/firestore/api/firestore.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; +using firebase::firestore::api::DocumentSnapshot; +using firebase::firestore::api::Firestore; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKey; +using firebase::firestore::util::WrapNSString; NS_ASSUME_NONNULL_BEGIN -/** Converts a public FIRServerTimestampBehavior into its internal equivalent. */ -static FSTServerTimestampBehavior InternalServerTimestampBehavor( - FIRServerTimestampBehavior behavior) { +namespace { + +/** + * Converts a public FIRServerTimestampBehavior into its internal equivalent. + */ +ServerTimestampBehavior InternalServerTimestampBehavior(FIRServerTimestampBehavior behavior) { switch (behavior) { case FIRServerTimestampBehaviorNone: - return FSTServerTimestampBehaviorNone; + return ServerTimestampBehavior::None; case FIRServerTimestampBehaviorEstimate: - return FSTServerTimestampBehaviorEstimate; + return ServerTimestampBehavior::Estimate; case FIRServerTimestampBehaviorPrevious: - return FSTServerTimestampBehaviorPrevious; + return ServerTimestampBehavior::Previous; default: HARD_FAIL("Unexpected server timestamp option: %s", behavior); } } -@interface FIRDocumentSnapshot () - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - documentKey:(DocumentKey)documentKey - document:(nullable FSTDocument *)document - fromCache:(BOOL)fromCache - hasPendingWrites:(BOOL)pendingWrites NS_DESIGNATED_INITIALIZER; - -- (const DocumentKey &)internalKey; - -@property(nonatomic, strong, readonly) FIRFirestore *firestore; -@property(nonatomic, strong, readonly, nullable) FSTDocument *internalDocument; -@property(nonatomic, assign, readonly) BOOL fromCache; -@property(nonatomic, assign, readonly) BOOL pendingWrites; - -@end - -@implementation FIRDocumentSnapshot (Internal) - -+ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore - documentKey:(DocumentKey)documentKey - document:(nullable FSTDocument *)document - fromCache:(BOOL)fromCache - hasPendingWrites:(BOOL)pendingWrites { - return [[[self class] alloc] initWithFirestore:firestore - documentKey:std::move(documentKey) - document:document - fromCache:fromCache - hasPendingWrites:pendingWrites]; -} - -@end +} // namespace @implementation FIRDocumentSnapshot { + DocumentSnapshot _snapshot; + FIRSnapshotMetadata *_cachedMetadata; - DocumentKey _internalKey; } -@dynamic metadata; - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - documentKey:(DocumentKey)documentKey - document:(nullable FSTDocument *)document - fromCache:(BOOL)fromCache - hasPendingWrites:(BOOL)pendingWrites { +- (instancetype)initWithSnapshot:(DocumentSnapshot &&)snapshot { if (self = [super init]) { - _firestore = firestore; - _internalKey = std::move(documentKey); - _internalDocument = document; - _fromCache = fromCache; - _pendingWrites = pendingWrites; + _snapshot = std::move(snapshot); } return self; } -- (const DocumentKey &)internalKey { - return _internalKey; +- (instancetype)initWithFirestore:(Firestore *)firestore + documentKey:(DocumentKey)documentKey + document:(nullable FSTDocument *)document + metadata:(SnapshotMetadata)metadata { + DocumentSnapshot wrapped{firestore, std::move(documentKey), document, std::move(metadata)}; + return [self initWithSnapshot:std::move(wrapped)]; +} + +- (instancetype)initWithFirestore:(Firestore *)firestore + documentKey:(DocumentKey)documentKey + document:(nullable FSTDocument *)document + fromCache:(bool)fromCache + hasPendingWrites:(bool)hasPendingWrites { + return [self initWithFirestore:firestore + documentKey:std::move(documentKey) + document:document + metadata:SnapshotMetadata(hasPendingWrites, fromCache)]; } // NSObject Methods @@ -119,46 +104,36 @@ - (BOOL)isEqual:(nullable id)other { // self class could be FIRDocumentSnapshot or subtype. So we compare with base type explicitly. if (![other isKindOfClass:[FIRDocumentSnapshot class]]) return NO; - return [self isEqualToSnapshot:other]; -} - -- (BOOL)isEqualToSnapshot:(nullable FIRDocumentSnapshot *)snapshot { - if (self == snapshot) return YES; - if (snapshot == nil) return NO; - - return [self.firestore isEqual:snapshot.firestore] && self.internalKey == snapshot.internalKey && - (self.internalDocument == snapshot.internalDocument || - [self.internalDocument isEqual:snapshot.internalDocument]) && - self.pendingWrites == snapshot.pendingWrites && self.fromCache == snapshot.fromCache; + return _snapshot == static_cast(other)->_snapshot; } - (NSUInteger)hash { - NSUInteger hash = [self.firestore hash]; - hash = hash * 31u + self.internalKey.Hash(); - hash = hash * 31u + [self.internalDocument hash]; - hash = hash * 31u + (_pendingWrites ? 1 : 0); - hash = hash * 31u + (self.fromCache ? 1 : 0); - return hash; + return _snapshot.Hash(); } @dynamic exists; - (BOOL)exists { - return _internalDocument != nil; + return _snapshot.exists(); +} + +- (FSTDocument *)internalDocument { + return _snapshot.internal_document(); } - (FIRDocumentReference *)reference { - return [FIRDocumentReference referenceWithKey:self.internalKey firestore:self.firestore]; + return [[FIRDocumentReference alloc] initWithReference:_snapshot.CreateReference()]; } - (NSString *)documentID { - return util::WrapNSString(self.internalKey.path().last_segment()); + return WrapNSString(_snapshot.document_id()); } +@dynamic metadata; + - (FIRSnapshotMetadata *)metadata { if (!_cachedMetadata) { - _cachedMetadata = [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:_pendingWrites - fromCache:self.fromCache]; + _cachedMetadata = [[FIRSnapshotMetadata alloc] initWithMetadata:_snapshot.metadata()]; } return _cachedMetadata; } @@ -170,9 +145,8 @@ - (FIRSnapshotMetadata *)metadata { - (nullable NSDictionary *)dataWithServerTimestampBehavior: (FIRServerTimestampBehavior)serverTimestampBehavior { FSTFieldValueOptions *options = [self optionsForServerTimestampBehavior:serverTimestampBehavior]; - return self.internalDocument == nil - ? nil - : [self convertedObject:[self.internalDocument data] options:options]; + FSTObjectValue *data = _snapshot.GetData(); + return data == nil ? nil : [self convertedObject:data options:options]; } - (nullable id)valueForField:(id)field { @@ -182,7 +156,6 @@ - (nullable id)valueForField:(id)field { - (nullable id)valueForField:(id)field serverTimestampBehavior:(FIRServerTimestampBehavior)serverTimestampBehavior { FIRFieldPath *fieldPath; - if ([field isKindOfClass:[NSString class]]) { fieldPath = [FIRFieldPath pathWithDotSeparatedString:field]; } else if ([field isKindOfClass:[FIRFieldPath class]]) { @@ -191,25 +164,24 @@ - (nullable id)valueForField:(id)field FSTThrowInvalidArgument(@"Subscript key must be an NSString or FIRFieldPath."); } - FSTFieldValue *fieldValue = [[self.internalDocument data] valueForPath:fieldPath.internalValue]; + FSTFieldValue *fieldValue = _snapshot.GetValue(fieldPath.internalValue); FSTFieldValueOptions *options = [self optionsForServerTimestampBehavior:serverTimestampBehavior]; return fieldValue == nil ? nil : [self convertedValue:fieldValue options:options]; } +- (nullable id)objectForKeyedSubscript:(id)key { + return [self valueForField:key]; +} + - (FSTFieldValueOptions *)optionsForServerTimestampBehavior: (FIRServerTimestampBehavior)serverTimestampBehavior { - FSTServerTimestampBehavior internalBehavior = - InternalServerTimestampBehavor(serverTimestampBehavior); -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" + SUPPRESS_DEPRECATED_DECLARATIONS_BEGIN() return [[FSTFieldValueOptions alloc] - initWithServerTimestampBehavior:internalBehavior - timestampsInSnapshotsEnabled:self.firestore.settings.timestampsInSnapshotsEnabled]; -#pragma clang diagnostic pop -} - -- (nullable id)objectForKeyedSubscript:(id)key { - return [self valueForField:key]; + initWithServerTimestampBehavior:InternalServerTimestampBehavior(serverTimestampBehavior) + timestampsInSnapshotsEnabled:_snapshot.firestore() + ->settings() + .timestampsInSnapshotsEnabled]; + SUPPRESS_END() } - (id)convertedValue:(FSTFieldValue *)value options:(FSTFieldValueOptions *)options { @@ -220,7 +192,7 @@ - (id)convertedValue:(FSTFieldValue *)value options:(FSTFieldValueOptions *)opti } else if ([value isKindOfClass:[FSTReferenceValue class]]) { FSTReferenceValue *ref = (FSTReferenceValue *)value; const DatabaseId *refDatabase = ref.databaseID; - const DatabaseId *database = self.firestore.databaseID; + const DatabaseId *database = &_snapshot.firestore()->database_id(); if (*refDatabase != *database) { // TODO(b/32073923): Log this as a proper warning. NSLog(@"WARNING: Document %@ contains a document reference within a different database " @@ -231,7 +203,7 @@ - (id)convertedValue:(FSTFieldValue *)value options:(FSTFieldValueOptions *)opti database->database_id().c_str()); } DocumentKey key = [[ref valueWithOptions:options] key]; - return [FIRDocumentReference referenceWithKey:key firestore:self.firestore]; + return [[FIRDocumentReference alloc] initWithKey:key firestore:_snapshot.firestore()]; } else { return [value valueWithOptions:options]; } @@ -259,31 +231,8 @@ - (id)convertedValue:(FSTFieldValue *)value options:(FSTFieldValueOptions *)opti @end -@interface FIRQueryDocumentSnapshot () - -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - documentKey:(DocumentKey)documentKey - document:(FSTDocument *)document - fromCache:(BOOL)fromCache - hasPendingWrites:(BOOL)pendingWrites NS_DESIGNATED_INITIALIZER; - -@end - @implementation FIRQueryDocumentSnapshot -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - documentKey:(DocumentKey)documentKey - document:(FSTDocument *)document - fromCache:(BOOL)fromCache - hasPendingWrites:(BOOL)pendingWrites { - self = [super initWithFirestore:firestore - documentKey:std::move(documentKey) - document:document - fromCache:fromCache - hasPendingWrites:pendingWrites]; - return self; -} - - (NSDictionary *)data { NSDictionary *data = [super data]; HARD_ASSERT(data, "Document in a QueryDocumentSnapshot should exist"); diff --git a/Firestore/Source/API/FIRFieldValue+Internal.h b/Firestore/Source/API/FIRFieldValue+Internal.h index 1618cd4cdc3..7e0193db39c 100644 --- a/Firestore/Source/API/FIRFieldValue+Internal.h +++ b/Firestore/Source/API/FIRFieldValue+Internal.h @@ -54,4 +54,10 @@ NS_ASSUME_NONNULL_BEGIN @property(strong, nonatomic, readonly) NSArray *elements; @end +/** FIRFieldValue class for number increments. */ +@interface FSTNumericIncrementFieldValue : FIRFieldValue +- (instancetype)init NS_UNAVAILABLE; +@property(strong, nonatomic, readonly) NSNumber *operand; +@end + NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldValue.mm b/Firestore/Source/API/FIRFieldValue.mm index 5f7fbd753a1..884b1879529 100644 --- a/Firestore/Source/API/FIRFieldValue.mm +++ b/Firestore/Source/API/FIRFieldValue.mm @@ -122,6 +122,28 @@ - (NSString *)methodName { @end +#pragma mark - FSTNumericIncrementFieldValue + +/* FieldValue class for increment() transforms. */ +@interface FSTNumericIncrementFieldValue () +- (instancetype)initWithOperand:(NSNumber *)operand; +@end + +@implementation FSTNumericIncrementFieldValue +- (instancetype)initWithOperand:(NSNumber *)operand; +{ + if (self = [super initPrivate]) { + _operand = operand; + } + return self; +} + +- (NSString *)methodName { + return @"FieldValue.increment()"; +} + +@end + #pragma mark - FIRFieldValue @implementation FIRFieldValue @@ -147,6 +169,14 @@ + (instancetype)fieldValueForArrayRemove:(NSArray *)elements { return [[FSTArrayRemoveFieldValue alloc] initWithElements:elements]; } ++ (instancetype)fieldValueForDoubleIncrement:(double)d { + return [[FSTNumericIncrementFieldValue alloc] initWithOperand:@(d)]; +} + ++ (instancetype)fieldValueForIntegerIncrement:(int64_t)l { + return [[FSTNumericIncrementFieldValue alloc] initWithOperand:@(l)]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestore+Internal.h b/Firestore/Source/API/FIRFirestore+Internal.h index 0878237316a..06d5b7d008a 100644 --- a/Firestore/Source/API/FIRFirestore+Internal.h +++ b/Firestore/Source/API/FIRFirestore+Internal.h @@ -19,13 +19,13 @@ #include #include +#include "Firestore/core/src/firebase/firestore/api/firestore.h" #include "Firestore/core/src/firebase/firestore/auth/credentials_provider.h" -#include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "absl/strings/string_view.h" NS_ASSUME_NONNULL_BEGIN +@class FIRApp; @class FSTFirestoreClient; @class FSTUserDataConverter; @@ -38,20 +38,34 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype) initWithProjectID:(std::string)projectID database:(std::string)database - persistenceKey:(NSString *)persistenceKey + persistenceKey:(std::string)persistenceKey credentialsProvider: (std::unique_ptr)credentialsProvider workerQueue:(std::unique_ptr)workerQueue firebaseApp:(FIRApp *)app; - @end /** Internal FIRFirestore API we don't want exposed in our public header files. */ @interface FIRFirestore (Internal) +// TODO(b/116617988): Move this to FIRFirestore.h and update CHANGELOG.md once backend support is +// ready. +#pragma mark - Collection Group Queries +/** + * Creates and returns a new `Query` that includes all documents in the database that are contained + * in a collection or subcollection with the given collectionID. + * + * @param collectionID Identifies the collections to query over. Every collection or subcollection + * with this ID as the last segment of its path will be included. Cannot contain a slash. + * @return The created `Query`. + */ +- (FIRQuery *)collectionGroupWithID:(NSString *)collectionID NS_SWIFT_NAME(collectionGroup(_:)); + /** Checks to see if logging is is globally enabled for the Firestore client. */ + (BOOL)isLoggingEnabled; ++ (FIRFirestore *)recoverFromFirestore:(firebase::firestore::api::Firestore *)firestore; + /** * Shutdown this `FIRFirestore`, releasing all resources (abandoning any outstanding writes, * removing all listens, closing all network connections, etc.). @@ -61,7 +75,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion NS_SWIFT_NAME(shutdown(completion:)); -- (firebase::firestore::util::AsyncQueue *)workerQueue; +@property(nonatomic, assign, readonly) firebase::firestore::api::Firestore *wrapped; + +@property(nonatomic, assign, readonly) firebase::firestore::util::AsyncQueue *workerQueue; // FIRFirestore ownes the DatabaseId instance. @property(nonatomic, assign, readonly) const firebase::firestore::model::DatabaseId *databaseID; diff --git a/Firestore/Source/API/FIRFirestore.mm b/Firestore/Source/API/FIRFirestore.mm index f716a7dafb8..e4761b1f7ba 100644 --- a/Firestore/Source/API/FIRFirestore.mm +++ b/Firestore/Source/API/FIRFirestore.mm @@ -26,38 +26,30 @@ #include #include -#import "FIRFirestoreSettings.h" -#import "Firestore/Source/API/FIRCollectionReference+Internal.h" +#import "FIRFirestore.h" + #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRTransaction+Internal.h" -#import "Firestore/Source/API/FIRWriteBatch+Internal.h" #import "Firestore/Source/API/FSTFirestoreComponent.h" #import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" #import "Firestore/Source/Util/FSTUsageValidation.h" +#include "Firestore/core/src/firebase/firestore/api/firestore.h" #include "Firestore/core/src/firebase/firestore/auth/credentials_provider.h" -#include "Firestore/core/src/firebase/firestore/auth/firebase_credentials_provider_apple.h" #include "Firestore/core/src/firebase/firestore/core/database_info.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" -#include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/delayed_constructor.h" #include "Firestore/core/src/firebase/firestore/util/log.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" -#include "absl/memory/memory.h" namespace util = firebase::firestore::util; +using firebase::firestore::api::DocumentReference; +using firebase::firestore::api::Firestore; using firebase::firestore::auth::CredentialsProvider; -using firebase::firestore::auth::FirebaseCredentialsProvider; -using firebase::firestore::core::DatabaseInfo; using firebase::firestore::model::DatabaseId; -using firebase::firestore::model::ResourcePath; -using util::AsyncQueue; -using util::Executor; -using util::ExecutorLibdispatch; +using firebase::firestore::util::AsyncQueue; +using firebase::firestore::util::DelayedConstructor; NS_ASSUME_NONNULL_BEGIN @@ -65,32 +57,14 @@ #pragma mark - FIRFirestore -@interface FIRFirestore () { - /** The actual owned DatabaseId instance is allocated in FIRFirestore. */ - DatabaseId _databaseID; - std::unique_ptr _credentialsProvider; -} - -@property(nonatomic, strong) NSString *persistenceKey; +@interface FIRFirestore () -// Note that `client` is updated after initialization, but marking this readwrite would generate an -// incorrect setter (since we make the assignment to `client` inside an `@synchronized` block. -@property(nonatomic, strong, readonly) FSTFirestoreClient *client; @property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter; @end @implementation FIRFirestore { - // Ownership will be transferred to `FSTFirestoreClient` as soon as the client is created. - std::unique_ptr _workerQueue; - - // All guarded by @synchronized(self) - FIRFirestoreSettings *_settings; - FSTFirestoreClient *_client; -} - -- (AsyncQueue *)workerQueue { - return [_client workerQueue]; + DelayedConstructor _firestore; } + (NSMutableDictionary *)instances { @@ -165,12 +139,16 @@ + (instancetype)firestoreForApp:(FIRApp *)app database:(NSString *)database { - (instancetype)initWithProjectID:(std::string)projectID database:(std::string)database - persistenceKey:(NSString *)persistenceKey + persistenceKey:(std::string)persistenceKey credentialsProvider:(std::unique_ptr)credentialsProvider workerQueue:(std::unique_ptr)workerQueue firebaseApp:(FIRApp *)app { if (self = [super init]) { - _databaseID = DatabaseId{std::move(projectID), std::move(database)}; + _firestore.Init(std::move(projectID), std::move(database), std::move(persistenceKey), + std::move(credentialsProvider), std::move(workerQueue), (__bridge void *)self); + + _app = app; + FSTPreConverterBlock block = ^id _Nullable(id _Nullable input) { if ([input isKindOfClass:[FIRDocumentReference class]]) { FIRDocumentReference *documentReference = (FIRDocumentReference *)input; @@ -180,67 +158,26 @@ - (instancetype)initWithProjectID:(std::string)projectID return input; } }; - _dataConverter = [[FSTUserDataConverter alloc] initWithDatabaseID:&_databaseID + + _dataConverter = [[FSTUserDataConverter alloc] initWithDatabaseID:&_firestore->database_id() preConverter:block]; - _persistenceKey = persistenceKey; - _credentialsProvider = std::move(credentialsProvider); - _workerQueue = std::move(workerQueue); - _app = app; - _settings = [[FIRFirestoreSettings alloc] init]; } return self; } - (FIRFirestoreSettings *)settings { - @synchronized(self) { - // Disallow mutation of our internal settings - return [_settings copy]; - } + return _firestore->settings(); } - (void)setSettings:(FIRFirestoreSettings *)settings { - @synchronized(self) { - // As a special exception, don't throw if the same settings are passed repeatedly. This should - // make it more friendly to create a Firestore instance. - if (_client && ![_settings isEqual:settings]) { - FSTThrowInvalidUsage(@"FIRIllegalStateException", - @"Firestore instance has already been started and its settings can no " - "longer be changed. You can only set settings before calling any " - "other methods on a Firestore instance."); - } - _settings = [settings copy]; - } + _firestore->set_settings(settings); } /** * Ensures that the FirestoreClient is configured and returns it. */ - (FSTFirestoreClient *)client { - [self ensureClientConfigured]; - return _client; -} - -- (void)ensureClientConfigured { - @synchronized(self) { - if (!_client) { - // These values are validated elsewhere; this is just double-checking: - HARD_ASSERT(_settings.host, "FirestoreSettings.host cannot be nil."); - HARD_ASSERT(_settings.dispatchQueue, "FirestoreSettings.dispatchQueue cannot be nil."); - - const DatabaseInfo database_info(*self.databaseID, util::MakeString(_persistenceKey), - util::MakeString(_settings.host), _settings.sslEnabled); - - std::unique_ptr userExecutor = - absl::make_unique(_settings.dispatchQueue); - - HARD_ASSERT(_workerQueue, "Expected non-null _workerQueue"); - _client = [FSTFirestoreClient clientWithDatabaseInfo:database_info - settings:_settings - credentialsProvider:_credentialsProvider.get() - userExecutor:std::move(userExecutor) - workerQueue:std::move(_workerQueue)]; - } - } + return _firestore->client(); } - (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { @@ -252,9 +189,7 @@ - (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { collectionPath); } - [self ensureClientConfigured]; - const ResourcePath path = ResourcePath::FromString(util::MakeString(collectionPath)); - return [FIRCollectionReference referenceWithPath:path firestore:self]; + return _firestore->GetCollection(util::MakeString(collectionPath)); } - (FIRDocumentReference *)documentWithPath:(NSString *)documentPath { @@ -265,9 +200,12 @@ - (FIRDocumentReference *)documentWithPath:(NSString *)documentPath { FSTThrowInvalidArgument(@"Invalid path (%@). Paths must not contain // in them.", documentPath); } - [self ensureClientConfigured]; - const ResourcePath path = ResourcePath::FromString(util::MakeString(documentPath)); - return [FIRDocumentReference referenceWithPath:path firestore:self]; + DocumentReference documentReference = _firestore->GetDocument(util::MakeString(documentPath)); + return [[FIRDocumentReference alloc] initWithReference:std::move(documentReference)]; +} + +- (FIRWriteBatch *)batch { + return _firestore->GetBatch(); } - (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **))updateBlock @@ -275,35 +213,14 @@ - (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **)) completion: (void (^)(id _Nullable result, NSError *_Nullable error))completion { // We wrap the function they provide in order to use internal implementation classes for - // FSTTransaction, and to run the user callback block on the proper queue. + // transaction, and to run the user callback block on the proper queue. if (!updateBlock) { FSTThrowInvalidArgument(@"Transaction block cannot be nil."); } else if (!completion) { FSTThrowInvalidArgument(@"Transaction completion block cannot be nil."); } - FSTTransactionBlock wrappedUpdate = - ^(FSTTransaction *internalTransaction, - void (^internalCompletion)(id _Nullable, NSError *_Nullable)) { - FIRTransaction *transaction = - [FIRTransaction transactionWithFSTTransaction:internalTransaction firestore:self]; - dispatch_async(queue, ^{ - NSError *_Nullable error = nil; - id _Nullable result = updateBlock(transaction, &error); - if (error) { - // Force the result to be nil in the case of an error, in case the user set both. - result = nil; - } - internalCompletion(result, error); - }); - }; - [self.client transactionWithRetries:5 updateBlock:wrappedUpdate completion:completion]; -} - -- (FIRWriteBatch *)batch { - [self ensureClientConfigured]; - - return [FIRWriteBatch writeBatchWithFirestore:self]; + _firestore->RunTransaction(updateBlock, queue, completion); } - (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **error))updateBlock @@ -320,38 +237,56 @@ - (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **er completion:completion]; } -- (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { - if (!_client) { - if (completion) { - // We should be dispatching the callback on the user dispatch queue but if the client is nil - // here that queue was never created. - completion(nil); - } - } else { - [_client shutdownWithCompletion:completion]; - } -} - -+ (BOOL)isLoggingEnabled { - return FIRIsLoggableLevel(FIRLoggerLevelDebug, NO); -} - + (void)enableLogging:(BOOL)logging { FIRSetLoggerLevel(logging ? FIRLoggerLevelDebug : FIRLoggerLevelNotice); } - (void)enableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { - [self ensureClientConfigured]; - [self.client enableNetworkWithCompletion:completion]; + _firestore->EnableNetwork(completion); } - (void)disableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable))completion { - [self ensureClientConfigured]; - [self.client disableNetworkWithCompletion:completion]; + _firestore->DisableNetwork(completion); +} + +@end + +@implementation FIRFirestore (Internal) + +- (Firestore *)wrapped { + return _firestore.get(); +} + +- (AsyncQueue *)workerQueue { + return _firestore->worker_queue(); } - (const DatabaseId *)databaseID { - return &_databaseID; + return &_firestore->database_id(); +} + ++ (BOOL)isLoggingEnabled { + return FIRIsLoggableLevel(FIRLoggerLevelDebug, NO); +} + ++ (FIRFirestore *)recoverFromFirestore:(Firestore *)firestore { + return (__bridge FIRFirestore *)firestore->extension(); +} + +- (FIRQuery *)collectionGroupWithID:(NSString *)collectionID { + if (!collectionID) { + FSTThrowInvalidArgument(@"Collection ID cannot be nil."); + } + if ([collectionID containsString:@"/"]) { + FSTThrowInvalidArgument( + @"Invalid collection ID (%@). Collection IDs must not contain / in them.", collectionID); + } + + return _firestore->GetCollectionGroup(collectionID); +} + +- (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + _firestore->Shutdown(completion); } @end diff --git a/Firestore/Source/API/FIRFirestoreSettings.mm b/Firestore/Source/API/FIRFirestoreSettings.mm index b2747128048..3dd7dbda29b 100644 --- a/Firestore/Source/API/FIRFirestoreSettings.mm +++ b/Firestore/Source/API/FIRFirestoreSettings.mm @@ -14,6 +14,8 @@ * limitations under the License. */ +#include "Firestore/core/src/firebase/firestore/util/warnings.h" + #import "FIRFirestoreSettings.h" #import "Firestore/Source/Util/FSTUsageValidation.h" @@ -49,15 +51,14 @@ - (BOOL)isEqual:(id)other { } FIRFirestoreSettings *otherSettings = (FIRFirestoreSettings *)other; + SUPPRESS_DEPRECATED_DECLARATIONS_BEGIN() return [self.host isEqual:otherSettings.host] && self.isSSLEnabled == otherSettings.isSSLEnabled && self.dispatchQueue == otherSettings.dispatchQueue && self.isPersistenceEnabled == otherSettings.isPersistenceEnabled && -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" self.timestampsInSnapshotsEnabled == otherSettings.timestampsInSnapshotsEnabled && -#pragma clang diagnostic pop self.cacheSizeBytes == otherSettings.cacheSizeBytes; + SUPPRESS_END() } - (NSUInteger)hash { @@ -65,10 +66,9 @@ - (NSUInteger)hash { result = 31 * result + (self.isSSLEnabled ? 1231 : 1237); // Ignore the dispatchQueue to avoid having to deal with sizeof(dispatch_queue_t). result = 31 * result + (self.isPersistenceEnabled ? 1231 : 1237); -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" + SUPPRESS_DEPRECATED_DECLARATIONS_BEGIN() result = 31 * result + (self.timestampsInSnapshotsEnabled ? 1231 : 1237); -#pragma clang diagnostic pop + SUPPRESS_END() result = 31 * result + (NSUInteger)self.cacheSizeBytes; return result; } @@ -79,10 +79,9 @@ - (id)copyWithZone:(nullable NSZone *)zone { copy.sslEnabled = _sslEnabled; copy.dispatchQueue = _dispatchQueue; copy.persistenceEnabled = _persistenceEnabled; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" + SUPPRESS_DEPRECATED_DECLARATIONS_BEGIN() copy.timestampsInSnapshotsEnabled = _timestampsInSnapshotsEnabled; -#pragma clang diagnostic pop + SUPPRESS_END() copy.cacheSizeBytes = _cacheSizeBytes; return copy; } diff --git a/Firestore/Source/API/FIRQuery.mm b/Firestore/Source/API/FIRQuery.mm index e4d2f7066cd..9b46040bda4 100644 --- a/Firestore/Source/API/FIRQuery.mm +++ b/Firestore/Source/API/FIRQuery.mm @@ -16,12 +16,15 @@ #import "FIRQuery.h" +#include + #import "FIRDocumentReference.h" #import "FIRFirestoreErrors.h" #import "FIRFirestoreSource.h" #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" #import "Firestore/Source/API/FIRFieldPath+Internal.h" +#import "Firestore/Source/API/FIRFieldValue+Internal.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRListenerRegistration+Internal.h" #import "Firestore/Source/API/FIRQuery+Internal.h" @@ -40,13 +43,19 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/util/error_apple.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; +using firebase::firestore::core::ViewSnapshot; +using firebase::firestore::core::ViewSnapshotHandler; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::FieldPath; using firebase::firestore::model::ResourcePath; +using firebase::firestore::util::MakeNSError; +using firebase::firestore::util::StatusOr; NS_ASSUME_NONNULL_BEGIN @@ -109,10 +118,10 @@ - (void)getDocumentsWithSource:(FIRFirestoreSource)source return; } - FSTListenOptions *listenOptions = - [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES - includeDocumentMetadataChanges:YES - waitForSyncWhenOnline:YES]; + ListenOptions listenOptions( + /*include_query_metadata_changes=*/true, + /*include_document_metadata_changes=*/true, + /*wait_for_sync_when_online=*/true); dispatch_semaphore_t registered = dispatch_semaphore_create(0); __block id listenerRegistration; @@ -155,41 +164,40 @@ - (void)getDocumentsWithSource:(FIRFirestoreSource)source - (id) addSnapshotListenerWithIncludeMetadataChanges:(BOOL)includeMetadataChanges listener:(FIRQuerySnapshotBlock)listener { - auto options = [self internalOptionsForIncludeMetadataChanges:includeMetadataChanges]; + auto options = ListenOptions::FromIncludeMetadataChanges(includeMetadataChanges); return [self addSnapshotListenerInternalWithOptions:options listener:listener]; } -- (id) - addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions - listener:(FIRQuerySnapshotBlock)listener { - FIRFirestore *firestore = self.firestore; +- (id)addSnapshotListenerInternalWithOptions:(ListenOptions)internalOptions + listener: + (FIRQuerySnapshotBlock)listener { + Firestore *firestore = self.firestore.wrapped; FSTQuery *query = self.query; - FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { - if (error) { - listener(nil, error); + ViewSnapshotHandler snapshotHandler = [listener, firestore, + query](const StatusOr &maybe_snapshot) { + if (!maybe_snapshot.status().ok()) { + listener(nil, MakeNSError(maybe_snapshot.status())); return; } + ViewSnapshot snapshot = maybe_snapshot.ValueOrDie(); + SnapshotMetadata metadata(snapshot.has_pending_writes(), snapshot.from_cache()); - FIRSnapshotMetadata *metadata = - [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:snapshot.hasPendingWrites - fromCache:snapshot.fromCache]; - - listener([FIRQuerySnapshot snapshotWithFirestore:firestore - originalQuery:query - snapshot:snapshot - metadata:metadata], + listener([[FIRQuerySnapshot alloc] initWithFirestore:firestore + originalQuery:query + snapshot:std::move(snapshot) + metadata:std::move(metadata)], nil); }; FSTAsyncQueryListener *asyncListener = [[FSTAsyncQueryListener alloc] initWithExecutor:self.firestore.client.userExecutor - snapshotHandler:snapshotHandler]; + snapshotHandler:std::move(snapshotHandler)]; FSTQueryListener *internalListener = - [firestore.client listenToQuery:query - options:internalOptions - viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; + [firestore->client() listenToQuery:query + options:internalOptions + viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client asyncListener:asyncListener internalListener:internalListener]; @@ -461,20 +469,32 @@ - (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator } if ([value isKindOfClass:[NSString class]]) { NSString *documentKey = (NSString *)value; - if ([documentKey containsString:@"/"]) { - FSTThrowInvalidArgument(@"Invalid query. When querying by document ID you must provide " - "a valid document ID, but '%@' contains a '/' character.", - documentKey); - } else if (documentKey.length == 0) { + if (documentKey.length == 0) { FSTThrowInvalidArgument(@"Invalid query. When querying by document ID you must provide " "a valid document ID, but it was an empty string."); } - ResourcePath path = self.query.path.Append([documentKey UTF8String]); - fieldValue = [FSTReferenceValue referenceValue:DocumentKey{path} - databaseID:self.firestore.databaseID]; + if (![self.query isCollectionGroupQuery] && [documentKey containsString:@"/"]) { + FSTThrowInvalidArgument( + @"Invalid query. When querying a collection by document ID you must provide " + "a plain document ID, but '%@' contains a '/' character.", + documentKey); + } + ResourcePath path = + self.query.path.Append(ResourcePath::FromString([documentKey UTF8String])); + if (!DocumentKey::IsDocumentKey(path)) { + FSTThrowInvalidArgument( + @"Invalid query. When querying a collection group by document ID, " + "the value provided must result in a valid document path, but '%s' is not because it " + "has an odd number of segments.", + path.CanonicalString().c_str()); + } + fieldValue = + [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithDocumentKey:DocumentKey{path}] + databaseID:self.firestore.databaseID]; } else if ([value isKindOfClass:[FIRDocumentReference class]]) { FIRDocumentReference *ref = (FIRDocumentReference *)value; - fieldValue = [FSTReferenceValue referenceValue:ref.key databaseID:self.firestore.databaseID]; + fieldValue = [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithDocumentKey:ref.key] + databaseID:self.firestore.databaseID]; } else { FSTThrowInvalidArgument(@"Invalid query. When querying by document ID you must provide a " "valid string or DocumentReference, but it was of type: %@", @@ -550,7 +570,9 @@ - (void)validateOrderByField:(const FieldPath &)orderByField * Note that the FSTBound will always include the key of the document and the position will be * unambiguous. * - * Will throw if the document does not contain all fields of the order by of the query. + * Will throw if the document does not contain all fields of the order by of + * the query or if any of the fields in the order by are an uncommitted server + * timestamp. */ - (FSTBound *)boundFromSnapshot:(FIRDocumentSnapshot *)snapshot isBefore:(BOOL)isBefore { if (![snapshot exists]) { @@ -568,11 +590,20 @@ - (FSTBound *)boundFromSnapshot:(FIRDocumentSnapshot *)snapshot isBefore:(BOOL)i // orders), multiple documents could match the position, yielding duplicate results. for (FSTSortOrder *sortOrder in self.query.sortOrders) { if (sortOrder.field == FieldPath::KeyFieldPath()) { - [components addObject:[FSTReferenceValue referenceValue:document.key - databaseID:self.firestore.databaseID]]; + [components addObject:[FSTReferenceValue + referenceValue:[FSTDocumentKey keyWithDocumentKey:document.key] + databaseID:self.firestore.databaseID]]; } else { FSTFieldValue *value = [document fieldForPath:sortOrder.field]; - if (value != nil) { + + if ([value isKindOfClass:[FSTServerTimestampValue class]]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using a " + "document for which the field '%s' is an uncommitted server " + "timestamp. (Since the value of this field is unknown, you cannot " + "start/end a query with it.)", + sortOrder.field.CanonicalString().c_str()); + } else if (value != nil) { [components addObject:value]; } else { FSTThrowInvalidUsage(@"InvalidQueryException", @@ -605,13 +636,26 @@ - (FSTBound *)boundFromFieldValues:(NSArray *)fieldValues isBefore:(BOOL)isB @"Invalid query. Expected a string for the document ID."); } NSString *documentID = (NSString *)rawValue; - if ([documentID containsString:@"/"]) { - FSTThrowInvalidUsage(@"InvalidQueryException", - @"Invalid query. Document ID '%@' contains a slash.", documentID); + if (![self.query isCollectionGroupQuery] && [documentID containsString:@"/"]) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. When querying a collection and ordering by document ID, " + "you must pass a plain document ID, but '%@' contains a slash.", + documentID); + } + ResourcePath path = self.query.path.Append(ResourcePath::FromString([documentID UTF8String])); + if (!DocumentKey::IsDocumentKey(path)) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. When querying a collection group and ordering by document ID, " + "you must pass a value that results in a valid document path, but '%s' " + "is not because it contains an odd number of segments.", + path.CanonicalString().c_str()); } - const DocumentKey key{self.query.path.Append([documentID UTF8String])}; - [components addObject:[FSTReferenceValue referenceValue:key - databaseID:self.firestore.databaseID]]; + DocumentKey key{path}; + [components + addObject:[FSTReferenceValue referenceValue:[FSTDocumentKey keyWithDocumentKey:key] + databaseID:self.firestore.databaseID]]; } else { FSTFieldValue *fieldValue = [self.firestore.dataConverter parsedQueryValue:rawValue]; [components addObject:fieldValue]; @@ -621,13 +665,6 @@ - (FSTBound *)boundFromFieldValues:(NSArray *)fieldValues isBefore:(BOOL)isB return [FSTBound boundWithPosition:components isBefore:isBefore]; } -/** Converts the public API options object to the internal options object. */ -- (FSTListenOptions *)internalOptionsForIncludeMetadataChanges:(BOOL)includeMetadataChanges { - return [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:includeMetadataChanges - includeDocumentMetadataChanges:includeMetadataChanges - waitForSyncWhenOnline:NO]; -} - @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuerySnapshot+Internal.h b/Firestore/Source/API/FIRQuerySnapshot+Internal.h index 3a1e9dbf609..96589d1a3dd 100644 --- a/Firestore/Source/API/FIRQuerySnapshot+Internal.h +++ b/Firestore/Source/API/FIRQuerySnapshot+Internal.h @@ -16,21 +16,31 @@ #import "FIRQuerySnapshot.h" +#include "Firestore/core/src/firebase/firestore/api/firestore.h" +#include "Firestore/core/src/firebase/firestore/api/query_snapshot.h" +#include "Firestore/core/src/firebase/firestore/api/snapshot_metadata.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" + @class FIRFirestore; @class FIRSnapshotMetadata; -@class FSTDocumentSet; @class FSTQuery; -@class FSTViewSnapshot; + +using firebase::firestore::api::Firestore; +using firebase::firestore::api::QuerySnapshot; +using firebase::firestore::api::SnapshotMetadata; +using firebase::firestore::core::ViewSnapshot; NS_ASSUME_NONNULL_BEGIN /** Internal FIRQuerySnapshot API we don't want exposed in our public header files. */ -@interface FIRQuerySnapshot (Internal) +@interface FIRQuerySnapshot (/* Init */) + +- (instancetype)initWithSnapshot:(QuerySnapshot &&)snapshot NS_DESIGNATED_INITIALIZER; -+ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore - originalQuery:(FSTQuery *)query - snapshot:(FSTViewSnapshot *)snapshot - metadata:(FIRSnapshotMetadata *)metadata; +- (instancetype)initWithFirestore:(Firestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(ViewSnapshot &&)snapshot + metadata:(SnapshotMetadata)metadata; @end diff --git a/Firestore/Source/API/FIRQuerySnapshot.mm b/Firestore/Source/API/FIRQuerySnapshot.mm index c7b48eb1510..52a4810575c 100644 --- a/Firestore/Source/API/FIRQuerySnapshot.mm +++ b/Firestore/Source/API/FIRQuerySnapshot.mm @@ -14,49 +14,36 @@ * limitations under the License. */ +#include + #import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" -#import "FIRFirestore.h" #import "FIRSnapshotMetadata.h" #import "Firestore/Source/API/FIRDocumentChange+Internal.h" #import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRQuery+Internal.h" +#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Source/Util/FSTUsageValidation.h" -NS_ASSUME_NONNULL_BEGIN - -@interface FIRQuerySnapshot () +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" +#include "Firestore/core/src/firebase/firestore/util/delayed_constructor.h" -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - originalQuery:(FSTQuery *)query - snapshot:(FSTViewSnapshot *)snapshot - metadata:(FIRSnapshotMetadata *)metadata; - -@property(nonatomic, strong, readonly) FIRFirestore *firestore; -@property(nonatomic, strong, readonly) FSTQuery *originalQuery; -@property(nonatomic, strong, readonly) FSTViewSnapshot *snapshot; - -@end +using firebase::firestore::api::Firestore; +using firebase::firestore::api::QuerySnapshot; +using firebase::firestore::core::ViewSnapshot; +using firebase::firestore::util::DelayedConstructor; -@implementation FIRQuerySnapshot (Internal) +NS_ASSUME_NONNULL_BEGIN -+ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore - originalQuery:(FSTQuery *)query - snapshot:(FSTViewSnapshot *)snapshot - metadata:(FIRSnapshotMetadata *)metadata { - return [[FIRQuerySnapshot alloc] initWithFirestore:firestore - originalQuery:query - snapshot:snapshot - metadata:metadata]; -} +@implementation FIRQuerySnapshot { + DelayedConstructor _snapshot; -@end + FIRSnapshotMetadata *_cached_metadata; -@implementation FIRQuerySnapshot { // Cached value of the documents property. NSArray *_documents; @@ -65,78 +52,64 @@ @implementation FIRQuerySnapshot { BOOL _documentChangesIncludeMetadataChanges; } -- (instancetype)initWithFirestore:(FIRFirestore *)firestore - originalQuery:(FSTQuery *)query - snapshot:(FSTViewSnapshot *)snapshot - metadata:(FIRSnapshotMetadata *)metadata { +- (instancetype)initWithSnapshot:(QuerySnapshot &&)snapshot { if (self = [super init]) { - _firestore = firestore; - _originalQuery = query; - _snapshot = snapshot; - _metadata = metadata; - _documentChangesIncludeMetadataChanges = NO; + _snapshot.Init(std::move(snapshot)); } return self; } +- (instancetype)initWithFirestore:(Firestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(ViewSnapshot &&)snapshot + metadata:(SnapshotMetadata)metadata { + QuerySnapshot wrapped(firestore, query, std::move(snapshot), std::move(metadata)); + return [self initWithSnapshot:std::move(wrapped)]; +} + // NSObject Methods - (BOOL)isEqual:(nullable id)other { - if (other == self) return YES; - if (![[other class] isEqual:[self class]]) return NO; + if (![other isKindOfClass:[FIRQuerySnapshot class]]) return NO; - return [self isEqualToSnapshot:other]; + FIRQuerySnapshot *otherSnapshot = other; + return *_snapshot == *(otherSnapshot->_snapshot); } -- (BOOL)isEqualToSnapshot:(nullable FIRQuerySnapshot *)snapshot { - if (self == snapshot) return YES; - if (snapshot == nil) return NO; +- (NSUInteger)hash { + return _snapshot->Hash(); +} - return [self.firestore isEqual:snapshot.firestore] && - [self.originalQuery isEqual:snapshot.originalQuery] && - [self.snapshot isEqual:snapshot.snapshot] && [self.metadata isEqual:snapshot.metadata]; +- (FIRQuery *)query { + FIRFirestore *firestore = [FIRFirestore recoverFromFirestore:_snapshot->firestore()]; + return [FIRQuery referenceWithQuery:_snapshot->internal_query() firestore:firestore]; } -- (NSUInteger)hash { - NSUInteger hash = [self.firestore hash]; - hash = hash * 31u + [self.originalQuery hash]; - hash = hash * 31u + [self.snapshot hash]; - hash = hash * 31u + [self.metadata hash]; - return hash; +- (FIRSnapshotMetadata *)metadata { + if (!_cached_metadata) { + _cached_metadata = [[FIRSnapshotMetadata alloc] initWithMetadata:_snapshot->metadata()]; + } + return _cached_metadata; } @dynamic empty; -- (FIRQuery *)query { - return [FIRQuery referenceWithQuery:self.originalQuery firestore:self.firestore]; -} - - (BOOL)isEmpty { - return self.snapshot.documents.isEmpty; + return _snapshot->empty(); } // This property is exposed as an NSInteger instead of an NSUInteger since (as of Xcode 8.1) // Swift bridges NSUInteger as UInt, and we want to avoid forcing Swift users to cast their ints // where we can. See cr/146959032 for additional context. - (NSInteger)count { - return self.snapshot.documents.count; + return static_cast(_snapshot->size()); } - (NSArray *)documents { if (!_documents) { - FSTDocumentSet *documentSet = self.snapshot.documents; - FIRFirestore *firestore = self.firestore; - BOOL fromCache = self.metadata.fromCache; - NSMutableArray *result = [NSMutableArray array]; - for (FSTDocument *document in documentSet.documentEnumerator) { - [result - addObject:[FIRQueryDocumentSnapshot - snapshotWithFirestore:firestore - documentKey:document.key - document:document - fromCache:fromCache - hasPendingWrites:self.snapshot.mutatedKeys.contains(document.key)]]; - } + _snapshot->ForEachDocument([&result](DocumentSnapshot snapshot) { + [result addObject:[[FIRQueryDocumentSnapshot alloc] initWithSnapshot:std::move(snapshot)]]; + }); _documents = result; } @@ -149,16 +122,16 @@ - (NSInteger)count { - (NSArray *)documentChangesWithIncludeMetadataChanges: (BOOL)includeMetadataChanges { - if (includeMetadataChanges && self.snapshot.excludesMetadataChanges) { + if (includeMetadataChanges && _snapshot->view_snapshot().excludes_metadata_changes()) { FSTThrowInvalidArgument( @"To include metadata changes with your document changes, you must call " @"addSnapshotListener(includeMetadataChanges: true)."); } if (!_documentChanges || _documentChangesIncludeMetadataChanges != includeMetadataChanges) { - _documentChanges = [FIRDocumentChange documentChangesForSnapshot:self.snapshot + _documentChanges = [FIRDocumentChange documentChangesForSnapshot:_snapshot->view_snapshot() includeMetadataChanges:includeMetadataChanges - firestore:self.firestore]; + firestore:_snapshot->firestore()]; _documentChangesIncludeMetadataChanges = includeMetadataChanges; } return _documentChanges; diff --git a/Firestore/Source/API/FIRSnapshotMetadata+Internal.h b/Firestore/Source/API/FIRSnapshotMetadata+Internal.h index d3265cd0959..6c5c6997bff 100644 --- a/Firestore/Source/API/FIRSnapshotMetadata+Internal.h +++ b/Firestore/Source/API/FIRSnapshotMetadata+Internal.h @@ -18,11 +18,17 @@ #import +#include "Firestore/core/src/firebase/firestore/api/snapshot_metadata.h" + +using firebase::firestore::api::SnapshotMetadata; + NS_ASSUME_NONNULL_BEGIN -@interface FIRSnapshotMetadata (Internal) +@interface FIRSnapshotMetadata (/* Init */) + +- (instancetype)initWithMetadata:(SnapshotMetadata)metadata NS_DESIGNATED_INITIALIZER; -+ (instancetype)snapshotMetadataWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache; +- (instancetype)initWithPendingWrites:(bool)pendingWrites fromCache:(bool)fromCache; @end diff --git a/Firestore/Source/API/FIRSnapshotMetadata.mm b/Firestore/Source/API/FIRSnapshotMetadata.mm index 27747cee260..87fa45efa23 100644 --- a/Firestore/Source/API/FIRSnapshotMetadata.mm +++ b/Firestore/Source/API/FIRSnapshotMetadata.mm @@ -16,53 +16,49 @@ #import "FIRSnapshotMetadata.h" -#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FIRSnapshotMetadata () +#include -- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache; +#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" -@end +#include "Firestore/core/src/firebase/firestore/api/snapshot_metadata.h" -@implementation FIRSnapshotMetadata (Internal) +NS_ASSUME_NONNULL_BEGIN -+ (instancetype)snapshotMetadataWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache { - return [[FIRSnapshotMetadata alloc] initWithPendingWrites:pendingWrites fromCache:fromCache]; +@implementation FIRSnapshotMetadata { + SnapshotMetadata _metadata; } -@end - -@implementation FIRSnapshotMetadata - -- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache { +- (instancetype)initWithMetadata:(SnapshotMetadata)metadata { if (self = [super init]) { - _pendingWrites = pendingWrites; - _fromCache = fromCache; + _metadata = std::move(metadata); } return self; } +- (instancetype)initWithPendingWrites:(bool)pendingWrites fromCache:(bool)fromCache { + SnapshotMetadata wrapped(pendingWrites, fromCache); + return [self initWithMetadata:std::move(wrapped)]; +} + // NSObject Methods - (BOOL)isEqual:(nullable id)other { if (other == self) return YES; - if (![[other class] isEqual:[self class]]) return NO; + if (![other isKindOfClass:[FIRSnapshotMetadata class]]) return NO; - return [self isEqualToMetadata:other]; + FIRSnapshotMetadata *otherMetadata = other; + return _metadata == otherMetadata->_metadata; } -- (BOOL)isEqualToMetadata:(nullable FIRSnapshotMetadata *)metadata { - if (self == metadata) return YES; - if (metadata == nil) return NO; +- (NSUInteger)hash { + return _metadata.Hash(); +} - return self.pendingWrites == metadata.pendingWrites && self.fromCache == metadata.fromCache; +- (BOOL)hasPendingWrites { + return _metadata.pending_writes(); } -- (NSUInteger)hash { - NSUInteger hash = self.pendingWrites ? 1 : 0; - hash = hash * 31u + (self.fromCache ? 1 : 0); - return hash; +- (BOOL)isFromCache { + return _metadata.from_cache(); } @end diff --git a/Firestore/Source/API/FIRTransaction+Internal.h b/Firestore/Source/API/FIRTransaction+Internal.h index 8fd3f65e333..32abd3f21c9 100644 --- a/Firestore/Source/API/FIRTransaction+Internal.h +++ b/Firestore/Source/API/FIRTransaction+Internal.h @@ -16,12 +16,16 @@ #import "FIRTransaction.h" +#include + +#include "Firestore/core/src/firebase/firestore/core/transaction.h" + @class FIRFirestore; -@class FSTTransaction; @interface FIRTransaction (Internal) -+ (instancetype)transactionWithFSTTransaction:(FSTTransaction *)transaction - firestore:(FIRFirestore *)firestore; ++ (instancetype)transactionWithInternalTransaction: + (std::shared_ptr)transaction + firestore:(FIRFirestore *)firestore; @end diff --git a/Firestore/Source/API/FIRTransaction.mm b/Firestore/Source/API/FIRTransaction.mm index 9f12aa0dba1..b0d0f9fee9f 100644 --- a/Firestore/Source/API/FIRTransaction.mm +++ b/Firestore/Source/API/FIRTransaction.mm @@ -16,21 +16,28 @@ #import "FIRTransaction.h" +#include #include +#include #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRTransaction+Internal.h" #import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTTransaction.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Util/FSTUsageValidation.h" +#include "Firestore/core/src/firebase/firestore/core/transaction.h" +#include "Firestore/core/src/firebase/firestore/util/error_apple.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" using firebase::firestore::core::ParsedSetData; using firebase::firestore::core::ParsedUpdateData; +using firebase::firestore::core::Transaction; +using firebase::firestore::util::MakeNSError; +using firebase::firestore::util::Status; NS_ASSUME_NONNULL_BEGIN @@ -38,29 +45,30 @@ @interface FIRTransaction () -- (instancetype)initWithTransaction:(FSTTransaction *)transaction +- (instancetype)initWithTransaction:(std::shared_ptr)transaction firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; -@property(nonatomic, strong, readonly) FSTTransaction *internalTransaction; @property(nonatomic, strong, readonly) FIRFirestore *firestore; @end @implementation FIRTransaction (Internal) -+ (instancetype)transactionWithFSTTransaction:(FSTTransaction *)transaction - firestore:(FIRFirestore *)firestore { - return [[FIRTransaction alloc] initWithTransaction:transaction firestore:firestore]; ++ (instancetype)transactionWithInternalTransaction:(std::shared_ptr)transaction + firestore:(FIRFirestore *)firestore { + return [[FIRTransaction alloc] initWithTransaction:std::move(transaction) firestore:firestore]; } @end -@implementation FIRTransaction +@implementation FIRTransaction { + std::shared_ptr _internalTransaction; +} -- (instancetype)initWithTransaction:(FSTTransaction *)transaction +- (instancetype)initWithTransaction:(std::shared_ptr)transaction firestore:(FIRFirestore *)firestore { self = [super init]; if (self) { - _internalTransaction = transaction; + _internalTransaction = std::move(transaction); _firestore = firestore; } return self; @@ -77,7 +85,7 @@ - (FIRTransaction *)setData:(NSDictionary *)data [self validateReference:document]; ParsedSetData parsed = merge ? [self.firestore.dataConverter parsedMergeData:data fieldMask:nil] : [self.firestore.dataConverter parsedSetData:data]; - [self.internalTransaction setData:std::move(parsed) forDocument:document.key]; + _internalTransaction->Set(document.key, std::move(parsed)); return self; } @@ -86,7 +94,7 @@ - (FIRTransaction *)setData:(NSDictionary *)data mergeFields:(NSArray *)mergeFields { [self validateReference:document]; ParsedSetData parsed = [self.firestore.dataConverter parsedMergeData:data fieldMask:mergeFields]; - [self.internalTransaction setData:std::move(parsed) forDocument:document.key]; + _internalTransaction->Set(document.key, std::move(parsed)); return self; } @@ -94,13 +102,13 @@ - (FIRTransaction *)updateData:(NSDictionary *)fields forDocument:(FIRDocumentReference *)document { [self validateReference:document]; ParsedUpdateData parsed = [self.firestore.dataConverter parsedUpdateData:fields]; - [self.internalTransaction updateData:std::move(parsed) forDocument:document.key]; + _internalTransaction->Update(document.key, std::move(parsed)); return self; } - (FIRTransaction *)deleteDocument:(FIRDocumentReference *)document { [self validateReference:document]; - [self.internalTransaction deleteDocument:document.key]; + _internalTransaction->Delete(document.key); return self; } @@ -108,38 +116,37 @@ - (void)getDocument:(FIRDocumentReference *)document completion:(void (^)(FIRDocumentSnapshot *_Nullable document, NSError *_Nullable error))completion { [self validateReference:document]; - [self.internalTransaction - lookupDocumentsForKeys:{document.key} - completion:^(NSArray *_Nullable documents, - NSError *_Nullable error) { - if (error) { - completion(nil, error); - return; - } - HARD_ASSERT(documents.count == 1, - "Mismatch in docs returned from document lookup."); - FSTMaybeDocument *internalDoc = documents.firstObject; - if ([internalDoc isKindOfClass:[FSTDeletedDocument class]]) { - FIRDocumentSnapshot *doc = - [FIRDocumentSnapshot snapshotWithFirestore:self.firestore - documentKey:document.key - document:nil - fromCache:NO - hasPendingWrites:NO]; - completion(doc, nil); - } else if ([internalDoc isKindOfClass:[FSTDocument class]]) { - FIRDocumentSnapshot *doc = - [FIRDocumentSnapshot snapshotWithFirestore:self.firestore - documentKey:internalDoc.key - document:(FSTDocument *)internalDoc - fromCache:NO - hasPendingWrites:NO]; - completion(doc, nil); - } else { - HARD_FAIL("BatchGetDocumentsRequest returned unexpected document type: %s", - NSStringFromClass([internalDoc class])); - } - }]; + _internalTransaction->Lookup( + {document.key}, [self, document, completion](const std::vector &documents, + const Status &status) { + if (!status.ok()) { + completion(nil, MakeNSError(status)); + return; + } + + HARD_ASSERT(documents.size() == 1, "Mismatch in docs returned from document lookup."); + FSTMaybeDocument *internalDoc = documents.front(); + if ([internalDoc isKindOfClass:[FSTDeletedDocument class]]) { + FIRDocumentSnapshot *doc = + [[FIRDocumentSnapshot alloc] initWithFirestore:self.firestore.wrapped + documentKey:document.key + document:nil + fromCache:false + hasPendingWrites:false]; + completion(doc, nil); + } else if ([internalDoc isKindOfClass:[FSTDocument class]]) { + FIRDocumentSnapshot *doc = + [[FIRDocumentSnapshot alloc] initWithFirestore:self.firestore.wrapped + documentKey:internalDoc.key + document:(FSTDocument *)internalDoc + fromCache:false + hasPendingWrites:false]; + completion(doc, nil); + } else { + HARD_FAIL("BatchGetDocumentsRequest returned unexpected document type: %s", + NSStringFromClass([internalDoc class])); + } + }); } - (FIRDocumentSnapshot *_Nullable)getDocument:(FIRDocumentReference *)document diff --git a/Firestore/Source/API/FIRWriteBatch.mm b/Firestore/Source/API/FIRWriteBatch.mm index 8ce58d3077e..484b8b0f4e8 100644 --- a/Firestore/Source/API/FIRWriteBatch.mm +++ b/Firestore/Source/API/FIRWriteBatch.mm @@ -16,7 +16,9 @@ #import "FIRWriteBatch.h" +#include #include +#include #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" @@ -41,7 +43,6 @@ @interface FIRWriteBatch () - (instancetype)initWithFirestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; @property(nonatomic, strong, readonly) FIRFirestore *firestore; -@property(nonatomic, strong, readonly) NSMutableArray *mutations; @property(nonatomic, assign) BOOL committed; @end @@ -54,13 +55,14 @@ + (instancetype)writeBatchWithFirestore:(FIRFirestore *)firestore { @end -@implementation FIRWriteBatch +@implementation FIRWriteBatch { + std::vector _mutations; +} - (instancetype)initWithFirestore:(FIRFirestore *)firestore { self = [super init]; if (self) { _firestore = firestore; - _mutations = [NSMutableArray array]; } return self; } @@ -75,10 +77,13 @@ - (FIRWriteBatch *)setData:(NSDictionary *)data merge:(BOOL)merge { [self verifyNotCommitted]; [self validateReference:document]; + ParsedSetData parsed = merge ? [self.firestore.dataConverter parsedMergeData:data fieldMask:nil] : [self.firestore.dataConverter parsedSetData:data]; - [self.mutations - addObjectsFromArray:std::move(parsed).ToMutations(document.key, Precondition::None())]; + std::vector append_mutations = + std::move(parsed).ToMutations(document.key, Precondition::None()); + std::move(append_mutations.begin(), append_mutations.end(), std::back_inserter(_mutations)); + return self; } @@ -87,9 +92,12 @@ - (FIRWriteBatch *)setData:(NSDictionary *)data mergeFields:(NSArray *)mergeFields { [self verifyNotCommitted]; [self validateReference:document]; + ParsedSetData parsed = [self.firestore.dataConverter parsedMergeData:data fieldMask:mergeFields]; - [self.mutations - addObjectsFromArray:std::move(parsed).ToMutations(document.key, Precondition::None())]; + std::vector append_mutations = + std::move(parsed).ToMutations(document.key, Precondition::None()); + std::move(append_mutations.begin(), append_mutations.end(), std::back_inserter(_mutations)); + return self; } @@ -97,17 +105,22 @@ - (FIRWriteBatch *)updateData:(NSDictionary *)fields forDocument:(FIRDocumentReference *)document { [self verifyNotCommitted]; [self validateReference:document]; + ParsedUpdateData parsed = [self.firestore.dataConverter parsedUpdateData:fields]; - [self.mutations - addObjectsFromArray:std::move(parsed).ToMutations(document.key, Precondition::Exists(true))]; + std::vector append_mutations = + std::move(parsed).ToMutations(document.key, Precondition::Exists(true)); + std::move(append_mutations.begin(), append_mutations.end(), std::back_inserter(_mutations)); + return self; } - (FIRWriteBatch *)deleteDocument:(FIRDocumentReference *)document { [self verifyNotCommitted]; [self validateReference:document]; - [self.mutations addObject:[[FSTDeleteMutation alloc] initWithKey:document.key - precondition:Precondition::None()]]; + + _mutations.push_back([[FSTDeleteMutation alloc] initWithKey:document.key + precondition:Precondition::None()]); + ; return self; } @@ -118,7 +131,7 @@ - (void)commit { - (void)commitWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { [self verifyNotCommitted]; self.committed = TRUE; - [self.firestore.client writeMutations:self.mutations completion:completion]; + [self.firestore.client writeMutations:std::move(_mutations) completion:completion]; } - (void)verifyNotCommitted { diff --git a/Firestore/Source/API/FSTFirestoreComponent.mm b/Firestore/Source/API/FSTFirestoreComponent.mm index 4791c121442..2d676d96c4d 100644 --- a/Firestore/Source/API/FSTFirestoreComponent.mm +++ b/Firestore/Source/API/FSTFirestoreComponent.mm @@ -31,6 +31,7 @@ #import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/Util/FSTUsageValidation.h" #include "Firestore/core/include/firebase/firestore/firestore_version.h" +#include "Firestore/core/src/firebase/firestore/api/firestore.h" #include "Firestore/core/src/firebase/firestore/auth/credentials_provider.h" #include "Firestore/core/src/firebase/firestore/auth/firebase_credentials_provider_apple.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" @@ -39,6 +40,7 @@ #include "absl/memory/memory.h" namespace util = firebase::firestore::util; +using firebase::firestore::api::Firestore; using firebase::firestore::auth::CredentialsProvider; using firebase::firestore::auth::FirebaseCredentialsProvider; using util::AsyncQueue; @@ -90,15 +92,14 @@ - (FIRFirestore *)firestoreForDatabase:(NSString *)database { auto workerQueue = absl::make_unique(std::move(executor)); id auth = FIR_COMPONENT(FIRAuthInterop, self.app.container); - std::unique_ptr credentials_provider = - absl::make_unique(self.app, auth); + auto credentialsProvider = absl::make_unique(self.app, auth); - NSString *persistenceKey = self.app.name; - NSString *projectID = self.app.options.projectID; - firestore = [[FIRFirestore alloc] initWithProjectID:util::MakeString(projectID) + std::string projectID = util::MakeString(self.app.options.projectID); + std::string persistenceKey = util::MakeString(self.app.name); + firestore = [[FIRFirestore alloc] initWithProjectID:std::move(projectID) database:util::MakeString(database) - persistenceKey:persistenceKey - credentialsProvider:std::move(credentials_provider) + persistenceKey:std::move(persistenceKey) + credentialsProvider:std::move(credentialsProvider) workerQueue:std::move(workerQueue) firebaseApp:self.app]; _instances[key] = firestore; diff --git a/Firestore/Source/API/FSTUserDataConverter.mm b/Firestore/Source/API/FSTUserDataConverter.mm index 5b981f645f5..0f0abd80114 100644 --- a/Firestore/Source/API/FSTUserDataConverter.mm +++ b/Firestore/Source/API/FSTUserDataConverter.mm @@ -58,6 +58,7 @@ using firebase::firestore::model::FieldMask; using firebase::firestore::model::FieldPath; using firebase::firestore::model::FieldTransform; +using firebase::firestore::model::NumericIncrementTransform; using firebase::firestore::model::Precondition; using firebase::firestore::model::ServerTimestampTransform; using firebase::firestore::model::TransformOperation; @@ -342,6 +343,15 @@ - (void)parseSentinelFieldValue:(FIRFieldValue *)fieldValue context:(ParseContex std::move(parsedElements)); context.AddToFieldTransforms(*context.path(), std::move(array_remove)); + } else if ([fieldValue isKindOfClass:[FSTNumericIncrementFieldValue class]]) { + FSTNumericIncrementFieldValue *numericIncrementFieldValue = + (FSTNumericIncrementFieldValue *)fieldValue; + FSTNumberValue *operand = + (FSTNumberValue *)[self parsedQueryValue:numericIncrementFieldValue.operand]; + auto numeric_increment = absl::make_unique(operand); + + context.AddToFieldTransforms(*context.path(), std::move(numeric_increment)); + } else { HARD_FAIL("Unknown FIRFieldValue type: %s", NSStringFromClass([fieldValue class])); } @@ -455,7 +465,8 @@ - (nullable FSTFieldValue *)parseScalarValue:(nullable id)input context:(ParseCo self.databaseID->project_id().c_str(), self.databaseID->database_id().c_str(), context.FieldDescription().c_str()); } - return [FSTReferenceValue referenceValue:reference.key databaseID:self.databaseID]; + return [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithDocumentKey:reference.key] + databaseID:self.databaseID]; } else { FSTThrowInvalidArgument(@"Unsupported type: %@%s", NSStringFromClass([input class]), diff --git a/Firestore/Source/Core/FSTEventManager.h b/Firestore/Source/Core/FSTEventManager.h index 67951b91984..4b9d7b800f8 100644 --- a/Firestore/Source/Core/FSTEventManager.h +++ b/Firestore/Source/Core/FSTEventManager.h @@ -16,36 +16,17 @@ #import -#import "Firestore/Source/Core/FSTViewSnapshot.h" -#import "Firestore/Source/Remote/FSTRemoteStore.h" - +#include "Firestore/core/src/firebase/firestore/core/listen_options.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" @class FSTQuery; @class FSTSyncEngine; NS_ASSUME_NONNULL_BEGIN -#pragma mark - FSTListenOptions - -@interface FSTListenOptions : NSObject - -+ (instancetype)defaultOptions; - -- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges - includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges - waitForSyncWhenOnline:(BOOL)waitForSyncWhenOnline - NS_DESIGNATED_INITIALIZER; - -- (instancetype)init NS_UNAVAILABLE; - -@property(nonatomic, assign, readonly) BOOL includeQueryMetadataChanges; - -@property(nonatomic, assign, readonly) BOOL includeDocumentMetadataChanges; - -@property(nonatomic, assign, readonly) BOOL waitForSyncWhenOnline; - -@end +using firebase::firestore::core::ListenOptions; #pragma mark - FSTQueryListener @@ -56,13 +37,14 @@ NS_ASSUME_NONNULL_BEGIN @interface FSTQueryListener : NSObject - (instancetype)initWithQuery:(FSTQuery *)query - options:(FSTListenOptions *)options - viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler NS_DESIGNATED_INITIALIZER; + options:(ListenOptions)options + viewSnapshotHandler:(firebase::firestore::core::ViewSnapshotHandler &&)viewSnapshotHandler + NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; -- (void)queryDidChangeViewSnapshot:(FSTViewSnapshot *)snapshot; -- (void)queryDidError:(NSError *)error; +- (void)queryDidChangeViewSnapshot:(firebase::firestore::core::ViewSnapshot)snapshot; +- (void)queryDidError:(const firebase::firestore::util::Status &)error; - (void)applyChangedOnlineState:(firebase::firestore::model::OnlineState)onlineState; @property(nonatomic, strong, readonly) FSTQuery *query; @@ -75,7 +57,7 @@ NS_ASSUME_NONNULL_BEGIN * EventManager is responsible for mapping queries to query event emitters. It handles "fan-out." * (Identical queries will re-use the same watch on the backend.) */ -@interface FSTEventManager : NSObject +@interface FSTEventManager : NSObject + (instancetype)eventManagerWithSyncEngine:(FSTSyncEngine *)syncEngine; @@ -84,6 +66,8 @@ NS_ASSUME_NONNULL_BEGIN - (firebase::firestore::model::TargetId)addListener:(FSTQueryListener *)listener; - (void)removeListener:(FSTQueryListener *)listener; +- (void)applyChangedOnlineState:(firebase::firestore::model::OnlineState)onlineState; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTEventManager.mm b/Firestore/Source/Core/FSTEventManager.mm index b67a4699c9c..b3f2130f919 100644 --- a/Firestore/Source/Core/FSTEventManager.mm +++ b/Firestore/Source/Core/FSTEventManager.mm @@ -16,50 +16,27 @@ #import "Firestore/Source/Core/FSTEventManager.h" +#include +#include + #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Core/FSTSyncEngine.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" +#include "Firestore/core/src/firebase/firestore/util/error_apple.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" - -using firebase::firestore::core::DocumentViewChangeType; -using firebase::firestore::model::OnlineState; -using firebase::firestore::model::TargetId; +#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "absl/types/optional.h" NS_ASSUME_NONNULL_BEGIN -#pragma mark - FSTListenOptions - -@implementation FSTListenOptions - -+ (instancetype)defaultOptions { - static FSTListenOptions *defaultOptions; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - defaultOptions = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO - includeDocumentMetadataChanges:NO - waitForSyncWhenOnline:NO]; - }); - return defaultOptions; -} - -- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges - includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges - waitForSyncWhenOnline:(BOOL)waitForSyncWhenOnline { - if (self = [super init]) { - _includeQueryMetadataChanges = includeQueryMetadataChanges; - _includeDocumentMetadataChanges = includeDocumentMetadataChanges; - _waitForSyncWhenOnline = waitForSyncWhenOnline; - } - return self; -} - -- (instancetype)init { - HARD_FAIL("FSTListenOptions init not supported"); - return nil; -} - -@end +using firebase::firestore::core::DocumentViewChange; +using firebase::firestore::core::ViewSnapshot; +using firebase::firestore::core::ViewSnapshotHandler; +using firebase::firestore::model::OnlineState; +using firebase::firestore::model::TargetId; +using firebase::firestore::util::MakeStatus; +using firebase::firestore::util::Status; #pragma mark - FSTQueryListenersInfo @@ -68,12 +45,25 @@ - (instancetype)init { * EventManager. */ @interface FSTQueryListenersInfo : NSObject -@property(nonatomic, strong, nullable, readwrite) FSTViewSnapshot *viewSnapshot; @property(nonatomic, assign, readwrite) TargetId targetID; @property(nonatomic, strong, readonly) NSMutableArray *listeners; + +- (const absl::optional &)viewSnapshot; +- (void)setViewSnapshot:(const absl::optional &)snapshot; + @end -@implementation FSTQueryListenersInfo +@implementation FSTQueryListenersInfo { + absl::optional _viewSnapshot; +} + +- (const absl::optional &)viewSnapshot { + return _viewSnapshot; +} +- (void)setViewSnapshot:(const absl::optional &)snapshot { + _viewSnapshot = snapshot; +} + - (instancetype)init { if (self = [super init]) { _listeners = [NSMutableArray array]; @@ -88,12 +78,10 @@ - (instancetype)init { @interface FSTQueryListener () /** The last received view snapshot. */ -@property(nonatomic, strong, nullable) FSTViewSnapshot *snapshot; - -@property(nonatomic, strong, readonly) FSTListenOptions *options; +- (const absl::optional &)snapshot; /** - * Initial snapshots (e.g. from cache) may not be propagated to the FSTViewSnapshotHandler. + * Initial snapshots (e.g. from cache) may not be propagated to the ViewSnapshotHandler. * This flag is set to YES once we've actually raised an event. */ @property(nonatomic, assign, readwrite) BOOL raisedInitialEvent; @@ -101,45 +89,54 @@ @interface FSTQueryListener () /** The last online state this query listener got. */ @property(nonatomic, assign, readwrite) OnlineState onlineState; -/** The FSTViewSnapshotHandler associated with this query listener. */ -@property(nonatomic, copy, nullable) FSTViewSnapshotHandler viewSnapshotHandler; - @end -@implementation FSTQueryListener +@implementation FSTQueryListener { + ListenOptions _options; + + absl::optional _snapshot; + + /** The ViewSnapshotHandler associated with this query listener. */ + ViewSnapshotHandler _viewSnapshotHandler; +} - (instancetype)initWithQuery:(FSTQuery *)query - options:(FSTListenOptions *)options - viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler { + options:(ListenOptions)options + viewSnapshotHandler:(ViewSnapshotHandler &&)viewSnapshotHandler { if (self = [super init]) { _query = query; - _options = options; - _viewSnapshotHandler = viewSnapshotHandler; + _options = std::move(options); + _viewSnapshotHandler = std::move(viewSnapshotHandler); _raisedInitialEvent = NO; } return self; } -- (void)queryDidChangeViewSnapshot:(FSTViewSnapshot *)snapshot { - HARD_ASSERT(snapshot.documentChanges.count > 0 || snapshot.syncStateChanged, +- (const absl::optional &)snapshot { + return _snapshot; +} + +- (void)queryDidChangeViewSnapshot:(ViewSnapshot)snapshot { + HARD_ASSERT(!snapshot.document_changes().empty() || snapshot.sync_state_changed(), "We got a new snapshot with no changes?"); - if (!self.options.includeDocumentMetadataChanges) { + if (!_options.include_document_metadata_changes()) { // Remove the metadata-only changes. - NSMutableArray *changes = [NSMutableArray array]; - for (FSTDocumentViewChange *change in snapshot.documentChanges) { - if (change.type != DocumentViewChangeType::kMetadata) { - [changes addObject:change]; + std::vector changes; + for (const DocumentViewChange &change : snapshot.document_changes()) { + if (change.type() != DocumentViewChange::Type::kMetadata) { + changes.push_back(change); } } - snapshot = [[FSTViewSnapshot alloc] initWithQuery:snapshot.query - documents:snapshot.documents - oldDocuments:snapshot.oldDocuments - documentChanges:changes - fromCache:snapshot.fromCache - mutatedKeys:snapshot.mutatedKeys - syncStateChanged:snapshot.syncStateChanged - excludesMetadataChanges:YES]; + + snapshot = ViewSnapshot{snapshot.query(), + snapshot.documents(), + snapshot.old_documents(), + std::move(changes), + snapshot.mutated_keys(), + snapshot.from_cache(), + snapshot.sync_state_changed(), + /*excludes_metadata_changes=*/true}; } if (!self.raisedInitialEvent) { @@ -147,31 +144,31 @@ - (void)queryDidChangeViewSnapshot:(FSTViewSnapshot *)snapshot { [self raiseInitialEventForSnapshot:snapshot]; } } else if ([self shouldRaiseEventForSnapshot:snapshot]) { - self.viewSnapshotHandler(snapshot, nil); + _viewSnapshotHandler(snapshot); } - self.snapshot = snapshot; + _snapshot = std::move(snapshot); } -- (void)queryDidError:(NSError *)error { - self.viewSnapshotHandler(nil, error); +- (void)queryDidError:(const Status &)error { + _viewSnapshotHandler(error); } - (void)applyChangedOnlineState:(OnlineState)onlineState { self.onlineState = onlineState; - if (self.snapshot && !self.raisedInitialEvent && - [self shouldRaiseInitialEventForSnapshot:self.snapshot onlineState:onlineState]) { - [self raiseInitialEventForSnapshot:self.snapshot]; + if (_snapshot.has_value() && !self.raisedInitialEvent && + [self shouldRaiseInitialEventForSnapshot:_snapshot.value() onlineState:onlineState]) { + [self raiseInitialEventForSnapshot:_snapshot.value()]; } } -- (BOOL)shouldRaiseInitialEventForSnapshot:(FSTViewSnapshot *)snapshot +- (BOOL)shouldRaiseInitialEventForSnapshot:(const ViewSnapshot &)snapshot onlineState:(OnlineState)onlineState { HARD_ASSERT(!self.raisedInitialEvent, "Determining whether to raise initial event, but already had first event."); // Always raise the first event when we're synced - if (!snapshot.fromCache) { + if (!snapshot.from_cache()) { return YES; } @@ -180,27 +177,27 @@ - (BOOL)shouldRaiseInitialEventForSnapshot:(FSTViewSnapshot *)snapshot BOOL maybeOnline = onlineState != OnlineState::Offline; // Don't raise the event if we're online, aren't synced yet (checked // above) and are waiting for a sync. - if (self.options.waitForSyncWhenOnline && maybeOnline) { - HARD_ASSERT(snapshot.fromCache, "Waiting for sync, but snapshot is not from cache."); + if (_options.wait_for_sync_when_online() && maybeOnline) { + HARD_ASSERT(snapshot.from_cache(), "Waiting for sync, but snapshot is not from cache."); return NO; } // Raise data from cache if we have any documents or we are offline - return !snapshot.documents.isEmpty || onlineState == OnlineState::Offline; + return !snapshot.documents().empty() || onlineState == OnlineState::Offline; } -- (BOOL)shouldRaiseEventForSnapshot:(FSTViewSnapshot *)snapshot { +- (BOOL)shouldRaiseEventForSnapshot:(const ViewSnapshot &)snapshot { // We don't need to handle includeDocumentMetadataChanges here because the Metadata only changes // have already been stripped out if needed. At this point the only changes we will see are the // ones we should propagate. - if (snapshot.documentChanges.count > 0) { + if (!snapshot.document_changes().empty()) { return YES; } - BOOL hasPendingWritesChanged = - self.snapshot && self.snapshot.hasPendingWrites != snapshot.hasPendingWrites; - if (snapshot.syncStateChanged || hasPendingWritesChanged) { - return self.options.includeQueryMetadataChanges; + BOOL hasPendingWritesChanged = _snapshot.has_value() && _snapshot.value().has_pending_writes() != + snapshot.has_pending_writes(); + if (snapshot.sync_state_changed() || hasPendingWritesChanged) { + return _options.include_query_metadata_changes(); } // Generally we should have hit one of the cases above, but it's possible to get here if there @@ -208,15 +205,13 @@ - (BOOL)shouldRaiseEventForSnapshot:(FSTViewSnapshot *)snapshot { return NO; } -- (void)raiseInitialEventForSnapshot:(FSTViewSnapshot *)snapshot { +- (void)raiseInitialEventForSnapshot:(const ViewSnapshot &)snapshot { HARD_ASSERT(!self.raisedInitialEvent, "Trying to raise initial events for second time"); - snapshot = [FSTViewSnapshot snapshotForInitialDocuments:snapshot.documents - query:snapshot.query - mutatedKeys:snapshot.mutatedKeys - fromCache:snapshot.fromCache - excludesMetadataChanges:snapshot.excludesMetadataChanges]; + ViewSnapshot modifiedSnapshot = ViewSnapshot::FromInitialDocuments( + snapshot.query(), snapshot.documents(), snapshot.mutated_keys(), snapshot.from_cache(), + snapshot.excludes_metadata_changes()); self.raisedInitialEvent = YES; - self.viewSnapshotHandler(snapshot, nil); + _viewSnapshotHandler(modifiedSnapshot); } @end @@ -264,8 +259,8 @@ - (TargetId)addListener:(FSTQueryListener *)listener { [listener applyChangedOnlineState:self.onlineState]; - if (queryInfo.viewSnapshot) { - [listener queryDidChangeViewSnapshot:queryInfo.viewSnapshot]; + if (queryInfo.viewSnapshot.has_value()) { + [listener queryDidChangeViewSnapshot:queryInfo.viewSnapshot.value()]; } if (firstListen) { @@ -290,15 +285,15 @@ - (void)removeListener:(FSTQueryListener *)listener { } } -- (void)handleViewSnapshots:(NSArray *)viewSnapshots { - for (FSTViewSnapshot *viewSnapshot in viewSnapshots) { - FSTQuery *query = viewSnapshot.query; +- (void)handleViewSnapshots:(std::vector &&)viewSnapshots { + for (ViewSnapshot &viewSnapshot : viewSnapshots) { + FSTQuery *query = viewSnapshot.query(); FSTQueryListenersInfo *queryInfo = self.queries[query]; if (queryInfo) { for (FSTQueryListener *listener in queryInfo.listeners) { [listener queryDidChangeViewSnapshot:viewSnapshot]; } - queryInfo.viewSnapshot = viewSnapshot; + [queryInfo setViewSnapshot:std::move(viewSnapshot)]; } } } @@ -307,7 +302,7 @@ - (void)handleError:(NSError *)error forQuery:(FSTQuery *)query { FSTQueryListenersInfo *queryInfo = self.queries[query]; if (queryInfo) { for (FSTQueryListener *listener in queryInfo.listeners) { - [listener queryDidError:error]; + [listener queryDidError:MakeStatus(error)]; } } diff --git a/Firestore/Source/Core/FSTFirestoreClient.h b/Firestore/Source/Core/FSTFirestoreClient.h index f4d850dda6d..629eb4b520d 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.h +++ b/Firestore/Source/Core/FSTFirestoreClient.h @@ -17,16 +17,20 @@ #import #include +#include #import "Firestore/Source/Core/FSTTypes.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" -#import "Firestore/Source/Remote/FSTRemoteStore.h" +#include "Firestore/core/src/firebase/firestore/api/document_reference.h" +#include "Firestore/core/src/firebase/firestore/api/document_snapshot.h" #include "Firestore/core/src/firebase/firestore/auth/credentials_provider.h" #include "Firestore/core/src/firebase/firestore/core/database_info.h" +#include "Firestore/core/src/firebase/firestore/core/listen_options.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" #include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "Firestore/core/src/firebase/firestore/util/statusor_callback.h" @class FIRDocumentReference; @class FIRDocumentSnapshot; @@ -36,7 +40,6 @@ @class FSTDatabaseID; @class FSTDatabaseInfo; @class FSTDocument; -@class FSTListenOptions; @class FSTMutation; @class FSTQuery; @class FSTQueryListener; @@ -44,12 +47,14 @@ NS_ASSUME_NONNULL_BEGIN +using firebase::firestore::core::ListenOptions; + /** * FirestoreClient is a top-level class that constructs and owns all of the pieces of the client * SDK architecture. It is responsible for creating the worker queue that is shared by all of the * other components in the system. */ -@interface FSTFirestoreClient : NSObject +@interface FSTFirestoreClient : NSObject /** * Creates and returns a FSTFirestoreClient with the given parameters. @@ -77,8 +82,9 @@ NS_ASSUME_NONNULL_BEGIN /** Starts listening to a query. */ - (FSTQueryListener *)listenToQuery:(FSTQuery *)query - options:(FSTListenOptions *)options - viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler; + options:(ListenOptions)options + viewSnapshotHandler: + (firebase::firestore::core::ViewSnapshotHandler &&)viewSnapshotHandler; /** Stops listening to a query previously listened to. */ - (void)removeListener:(FSTQueryListener *)listener; @@ -87,9 +93,9 @@ NS_ASSUME_NONNULL_BEGIN * Retrieves a document from the cache via the indicated completion. If the doc * doesn't exist, an error will be sent to the completion. */ -- (void)getDocumentFromLocalCache:(FIRDocumentReference *)doc - completion:(void (^)(FIRDocumentSnapshot *_Nullable document, - NSError *_Nullable error))completion; +- (void)getDocumentFromLocalCache:(const firebase::firestore::api::DocumentReference &)doc + completion:(firebase::firestore::util::StatusOrCallback< + firebase::firestore::api::DocumentSnapshot> &&)completion; /** * Retrieves a (possibly empty) set of documents from the cache via the @@ -100,7 +106,7 @@ NS_ASSUME_NONNULL_BEGIN NSError *_Nullable error))completion; /** Write mutations. completion will be notified when it's written to the backend. */ -- (void)writeMutations:(NSArray *)mutations +- (void)writeMutations:(std::vector &&)mutations completion:(nullable FSTVoidErrorBlock)completion; /** Tries to execute the transaction in updateBlock up to retries times. */ diff --git a/Firestore/Source/Core/FSTFirestoreClient.mm b/Firestore/Source/Core/FSTFirestoreClient.mm index 9f6b33df461..4bd38fafc58 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.mm +++ b/Firestore/Source/Core/FSTFirestoreClient.mm @@ -25,13 +25,13 @@ #import "FIRFirestoreSettings.h" #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRQuery+Internal.h" #import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" #import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" #import "Firestore/Source/Core/FSTEventManager.h" #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Core/FSTSyncEngine.h" -#import "Firestore/Source/Core/FSTTransaction.h" #import "Firestore/Source/Core/FSTView.h" #import "Firestore/Source/Local/FSTLRUGarbageCollector.h" #import "Firestore/Source/Local/FSTLevelDB.h" @@ -39,24 +39,32 @@ #import "Firestore/Source/Local/FSTLocalStore.h" #import "Firestore/Source/Local/FSTMemoryPersistence.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" -#import "Firestore/Source/Remote/FSTRemoteStore.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" #import "Firestore/Source/Util/FSTClasses.h" #include "Firestore/core/src/firebase/firestore/auth/credentials_provider.h" #include "Firestore/core/src/firebase/firestore/core/database_info.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" #include "Firestore/core/src/firebase/firestore/remote/datastore.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_store.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" +#include "absl/memory/memory.h" namespace util = firebase::firestore::util; +using firebase::firestore::FirestoreErrorCode; +using firebase::firestore::api::DocumentReference; +using firebase::firestore::api::DocumentSnapshot; using firebase::firestore::auth::CredentialsProvider; using firebase::firestore::auth::User; using firebase::firestore::core::DatabaseInfo; +using firebase::firestore::core::ViewSnapshot; +using firebase::firestore::core::ViewSnapshotHandler; using firebase::firestore::local::LruParams; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKeySet; @@ -64,11 +72,15 @@ using firebase::firestore::model::MaybeDocumentMap; using firebase::firestore::model::OnlineState; using firebase::firestore::remote::Datastore; +using firebase::firestore::remote::RemoteStore; using firebase::firestore::util::Path; using firebase::firestore::util::Status; using firebase::firestore::util::AsyncQueue; using firebase::firestore::util::DelayedOperation; using firebase::firestore::util::Executor; +using firebase::firestore::util::Status; +using firebase::firestore::util::StatusOr; +using firebase::firestore::util::StatusOrCallback; using firebase::firestore::util::TimerId; NS_ASSUME_NONNULL_BEGIN @@ -93,7 +105,6 @@ - (instancetype)initWithDatabaseInfo:(const DatabaseInfo &)databaseInfo @property(nonatomic, strong, readonly) FSTEventManager *eventManager; @property(nonatomic, strong, readonly) id persistence; @property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; -@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore; @property(nonatomic, strong, readonly) FSTLocalStore *localStore; // Does not own the CredentialsProvider instance. @@ -110,6 +121,8 @@ @implementation FSTFirestoreClient { */ std::unique_ptr _workerQueue; + std::unique_ptr _remoteStore; + std::unique_ptr _userExecutor; std::chrono::milliseconds _initialGcDelay; std::chrono::milliseconds _regularGcDelay; @@ -226,25 +239,23 @@ - (void)initializeWithUser:(const User &)user settings:(FIRFirestoreSettings *)s auto datastore = std::make_shared(*self.databaseInfo, _workerQueue.get(), _credentialsProvider); - _remoteStore = [[FSTRemoteStore alloc] initWithLocalStore:_localStore - datastore:std::move(datastore) - workerQueue:_workerQueue.get()]; + _remoteStore = absl::make_unique( + _localStore, std::move(datastore), _workerQueue.get(), + [self](OnlineState onlineState) { [self.syncEngine applyChangedOnlineState:onlineState]; }); _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore - remoteStore:_remoteStore + remoteStore:_remoteStore.get() initialUser:user]; _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine]; // Setup wiring for remote store. - _remoteStore.syncEngine = _syncEngine; - - _remoteStore.onlineStateDelegate = self; + _remoteStore->set_sync_engine(_syncEngine); // NOTE: RemoteStore depends on LocalStore (for persisting stream tokens, refilling mutation // queue, etc.) so must be started after LocalStore. [_localStore start]; - [_remoteStore start]; + _remoteStore->Start(); } /** @@ -267,13 +278,9 @@ - (void)credentialDidChangeWithUser:(const User &)user { [self.syncEngine credentialDidChangeWithUser:user]; } -- (void)applyChangedOnlineState:(OnlineState)onlineState { - [self.syncEngine applyChangedOnlineState:onlineState]; -} - - (void)disableNetworkWithCompletion:(nullable FSTVoidErrorBlock)completion { _workerQueue->Enqueue([self, completion] { - [self.remoteStore disableNetwork]; + _remoteStore->DisableNetwork(); if (completion) { self->_userExecutor->Execute([=] { completion(nil); }); } @@ -282,7 +289,7 @@ - (void)disableNetworkWithCompletion:(nullable FSTVoidErrorBlock)completion { - (void)enableNetworkWithCompletion:(nullable FSTVoidErrorBlock)completion { _workerQueue->Enqueue([self, completion] { - [self.remoteStore enableNetwork]; + _remoteStore->EnableNetwork(); if (completion) { self->_userExecutor->Execute([=] { completion(nil); }); } @@ -297,7 +304,7 @@ - (void)shutdownWithCompletion:(nullable FSTVoidErrorBlock)completion { if (self->_lruCallback) { self->_lruCallback.Cancel(); } - [self.remoteStore shutdown]; + _remoteStore->Shutdown(); [self.persistence shutdown]; if (completion) { self->_userExecutor->Execute([=] { completion(nil); }); @@ -306,11 +313,12 @@ - (void)shutdownWithCompletion:(nullable FSTVoidErrorBlock)completion { } - (FSTQueryListener *)listenToQuery:(FSTQuery *)query - options:(FSTListenOptions *)options - viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler { - FSTQueryListener *listener = [[FSTQueryListener alloc] initWithQuery:query - options:options - viewSnapshotHandler:viewSnapshotHandler]; + options:(ListenOptions)options + viewSnapshotHandler:(ViewSnapshotHandler &&)viewSnapshotHandler { + FSTQueryListener *listener = + [[FSTQueryListener alloc] initWithQuery:query + options:std::move(options) + viewSnapshotHandler:std::move(viewSnapshotHandler)]; _workerQueue->Enqueue([self, listener] { [self.eventManager addListener:listener]; }); @@ -321,41 +329,30 @@ - (void)removeListener:(FSTQueryListener *)listener { _workerQueue->Enqueue([self, listener] { [self.eventManager removeListener:listener]; }); } -- (void)getDocumentFromLocalCache:(FIRDocumentReference *)doc - completion:(void (^)(FIRDocumentSnapshot *_Nullable document, - NSError *_Nullable error))completion { +- (void)getDocumentFromLocalCache:(const DocumentReference &)doc + completion:(StatusOrCallback &&)completion { _workerQueue->Enqueue([self, doc, completion] { - FSTMaybeDocument *maybeDoc = [self.localStore readDocument:doc.key]; - FIRDocumentSnapshot *_Nullable result = nil; - NSError *_Nullable error = nil; + FSTMaybeDocument *maybeDoc = [self.localStore readDocument:doc.key()]; + StatusOr maybe_snapshot; if ([maybeDoc isKindOfClass:[FSTDocument class]]) { FSTDocument *document = (FSTDocument *)maybeDoc; - result = [FIRDocumentSnapshot snapshotWithFirestore:doc.firestore - documentKey:doc.key - document:document - fromCache:YES - hasPendingWrites:document.hasLocalMutations]; + maybe_snapshot = DocumentSnapshot{doc.firestore(), doc.key(), document, + /*from_cache=*/true, + /*has_pending_writes=*/document.hasLocalMutations}; } else if ([maybeDoc isKindOfClass:[FSTDeletedDocument class]]) { - result = [FIRDocumentSnapshot snapshotWithFirestore:doc.firestore - documentKey:doc.key - document:nil - fromCache:YES - hasPendingWrites:NO]; + maybe_snapshot = DocumentSnapshot{doc.firestore(), doc.key(), nil, + /*from_cache=*/true, + /*has_pending_writes=*/false}; } else { - error = [NSError errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeUnavailable - userInfo:@{ - NSLocalizedDescriptionKey : - @"Failed to get document from cache. (However, this document " - @"may exist on the server. Run again without setting source to " - @"FIRFirestoreSourceCache to attempt to retrieve the document " - @"from the server.)", - }]; + maybe_snapshot = Status{FirestoreErrorCode::Unavailable, + "Failed to get document from cache. (However, this document " + "may exist on the server. Run again without setting source to " + "FIRFirestoreSourceCache to attempt to retrieve the document "}; } if (completion) { - self->_userExecutor->Execute([=] { completion(result, error); }); + self->_userExecutor->Execute([=] { completion(std::move(maybe_snapshot)); }); } }); } @@ -372,16 +369,15 @@ - (void)getDocumentsFromLocalCache:(FIRQuery *)query FSTViewChange *viewChange = [view applyChangesToDocuments:viewDocChanges]; HARD_ASSERT(viewChange.limboChanges.count == 0, "View returned limbo documents during local-only query execution."); + HARD_ASSERT(viewChange.snapshot.has_value(), "Expected a snapshot"); - FSTViewSnapshot *snapshot = viewChange.snapshot; - FIRSnapshotMetadata *metadata = - [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:snapshot.hasPendingWrites - fromCache:snapshot.fromCache]; + ViewSnapshot snapshot = std::move(viewChange.snapshot).value(); + SnapshotMetadata metadata(snapshot.has_pending_writes(), snapshot.from_cache()); - FIRQuerySnapshot *result = [FIRQuerySnapshot snapshotWithFirestore:query.firestore - originalQuery:query.query - snapshot:snapshot - metadata:metadata]; + FIRQuerySnapshot *result = [[FIRQuerySnapshot alloc] initWithFirestore:query.firestore.wrapped + originalQuery:query.query + snapshot:std::move(snapshot) + metadata:std::move(metadata)]; if (completion) { self->_userExecutor->Execute([=] { completion(result, nil); }); @@ -389,15 +385,16 @@ - (void)getDocumentsFromLocalCache:(FIRQuery *)query }); } -- (void)writeMutations:(NSArray *)mutations +- (void)writeMutations:(std::vector &&)mutations completion:(nullable FSTVoidErrorBlock)completion { - _workerQueue->Enqueue([self, mutations, completion] { - if (mutations.count == 0) { + // TODO(c++14): move `mutations` into lambda (C++14). + _workerQueue->Enqueue([self, mutations, completion]() mutable { + if (mutations.empty()) { if (completion) { self->_userExecutor->Execute([=] { completion(nil); }); } } else { - [self.syncEngine writeMutations:mutations + [self.syncEngine writeMutations:std::move(mutations) completion:^(NSError *error) { // Dispatch the result back onto the user dispatch queue. if (completion) { diff --git a/Firestore/Source/Core/FSTQuery.h b/Firestore/Source/Core/FSTQuery.h index 17c781b2838..851da923d53 100644 --- a/Firestore/Source/Core/FSTQuery.h +++ b/Firestore/Source/Core/FSTQuery.h @@ -174,6 +174,7 @@ typedef NS_ENUM(NSInteger, FSTRelationFilterOperator) { * Initializes a query with all of its components directly. */ - (instancetype)initWithPath:(firebase::firestore::model::ResourcePath)path + collectionGroup:(nullable NSString *)collectionGroup filterBy:(NSArray *)filters orderBy:(NSArray *)sortOrders limit:(NSInteger)limit @@ -188,6 +189,18 @@ typedef NS_ENUM(NSInteger, FSTRelationFilterOperator) { */ + (instancetype)queryWithPath:(firebase::firestore::model::ResourcePath)path; +/** + * Creates and returns a new FSTQuery. + * + * @param path The path to the location to be queried over. Must currently be + * empty in the case of a collection group query. + * @param collectionGroup The collection group to be queried over. nil if this + * is not a collection group query. + * @return A new instance of FSTQuery. + */ ++ (instancetype)queryWithPath:(firebase::firestore::model::ResourcePath)path + collectionGroup:(nullable NSString *)collectionGroup; + /** * Returns the list of ordering constraints that were explicitly requested on the query by the * user. @@ -244,9 +257,19 @@ typedef NS_ENUM(NSInteger, FSTRelationFilterOperator) { */ - (instancetype)queryByAddingEndAt:(FSTBound *)bound; +/** + * Helper to convert a collection group query into a collection query at a specific path. This is + * used when executing collection group queries, since we have to split the query into a set of + * collection queries at multiple paths. + */ +- (instancetype)collectionQueryAtPath:(firebase::firestore::model::ResourcePath)path; + /** Returns YES if the receiver is query for a specific document. */ - (BOOL)isDocumentQuery; +/** Returns YES if the receiver is a collection group query. */ +- (BOOL)isCollectionGroupQuery; + /** Returns YES if the @a document matches the constraints of the receiver. */ - (BOOL)matchesDocument:(FSTDocument *)document; @@ -266,6 +289,9 @@ typedef NS_ENUM(NSInteger, FSTRelationFilterOperator) { /** The base path of the query. */ - (const firebase::firestore::model::ResourcePath &)path; +/** The collection group of the query. */ +@property(nonatomic, nullable, strong, readonly) NSString *collectionGroup; + /** The filters on the documents returned by the query. */ @property(nonatomic, strong, readonly) NSArray *filters; diff --git a/Firestore/Source/Core/FSTQuery.mm b/Firestore/Source/Core/FSTQuery.mm index 76598d9c0d5..d057c87c438 100644 --- a/Firestore/Source/Core/FSTQuery.mm +++ b/Firestore/Source/Core/FSTQuery.mm @@ -535,21 +535,6 @@ @interface FSTQuery () { ResourcePath _path; } -/** - * Initializes the receiver with the given query constraints. - * - * @param path The base path of the query. - * @param filters Filters specify which documents to include in the results. - * @param sortOrders The fields and directions to sort the results. - * @param limit If not NSNotFound, only this many results will be returned. - */ -- (instancetype)initWithPath:(ResourcePath)path - filterBy:(NSArray *)filters - orderBy:(NSArray *)sortOrders - limit:(NSInteger)limit - startAt:(nullable FSTBound *)startAtBound - endAt:(nullable FSTBound *)endAtBound NS_DESIGNATED_INITIALIZER; - /** A list of fields given to sort by. This does not include the implicit key sort at the end. */ @property(nonatomic, strong, readonly) NSArray *explicitSortOrders; @@ -563,7 +548,13 @@ @implementation FSTQuery #pragma mark - Constructors + (instancetype)queryWithPath:(ResourcePath)path { + return [FSTQuery queryWithPath:std::move(path) collectionGroup:nil]; +} + ++ (instancetype)queryWithPath:(ResourcePath)path + collectionGroup:(nullable NSString *)collectionGroup { return [[FSTQuery alloc] initWithPath:std::move(path) + collectionGroup:collectionGroup filterBy:@[] orderBy:@[] limit:NSNotFound @@ -572,6 +563,7 @@ + (instancetype)queryWithPath:(ResourcePath)path { } - (instancetype)initWithPath:(ResourcePath)path + collectionGroup:(nullable NSString *)collectionGroup filterBy:(NSArray *)filters orderBy:(NSArray *)sortOrders limit:(NSInteger)limit @@ -579,6 +571,7 @@ - (instancetype)initWithPath:(ResourcePath)path endAt:(nullable FSTBound *)endAtBound { if (self = [super init]) { _path = std::move(path); + _collectionGroup = collectionGroup; _filters = filters; _explicitSortOrders = sortOrders; _limit = limit; @@ -662,7 +655,7 @@ - (NSArray *)sortOrders { } - (instancetype)queryByAddingFilter:(FSTFilter *)filter { - HARD_ASSERT(!DocumentKey::IsDocumentKey(_path), "No filtering allowed for document query"); + HARD_ASSERT(![self isDocumentQuery], "No filtering allowed for document query"); const FieldPath *newInequalityField = nullptr; if ([filter isKindOfClass:[FSTRelationFilter class]] && @@ -675,6 +668,7 @@ - (instancetype)queryByAddingFilter:(FSTFilter *)filter { "Query must only have one inequality field."); return [[FSTQuery alloc] initWithPath:self.path + collectionGroup:self.collectionGroup filterBy:[self.filters arrayByAddingObject:filter] orderBy:self.explicitSortOrders limit:self.limit @@ -683,10 +677,11 @@ - (instancetype)queryByAddingFilter:(FSTFilter *)filter { } - (instancetype)queryByAddingSortOrder:(FSTSortOrder *)sortOrder { - HARD_ASSERT(!DocumentKey::IsDocumentKey(_path), "No ordering is allowed for a document query."); + HARD_ASSERT(![self isDocumentQuery], "No ordering is allowed for a document query."); // TODO(klimt): Validate that the same key isn't added twice. return [[FSTQuery alloc] initWithPath:self.path + collectionGroup:self.collectionGroup filterBy:self.filters orderBy:[self.explicitSortOrders arrayByAddingObject:sortOrder] limit:self.limit @@ -696,6 +691,7 @@ - (instancetype)queryByAddingSortOrder:(FSTSortOrder *)sortOrder { - (instancetype)queryBySettingLimit:(NSInteger)limit { return [[FSTQuery alloc] initWithPath:self.path + collectionGroup:self.collectionGroup filterBy:self.filters orderBy:self.explicitSortOrders limit:limit @@ -705,6 +701,7 @@ - (instancetype)queryBySettingLimit:(NSInteger)limit { - (instancetype)queryByAddingStartAt:(FSTBound *)bound { return [[FSTQuery alloc] initWithPath:self.path + collectionGroup:self.collectionGroup filterBy:self.filters orderBy:self.explicitSortOrders limit:self.limit @@ -714,6 +711,7 @@ - (instancetype)queryByAddingStartAt:(FSTBound *)bound { - (instancetype)queryByAddingEndAt:(FSTBound *)bound { return [[FSTQuery alloc] initWithPath:self.path + collectionGroup:self.collectionGroup filterBy:self.filters orderBy:self.explicitSortOrders limit:self.limit @@ -721,13 +719,28 @@ - (instancetype)queryByAddingEndAt:(FSTBound *)bound { endAt:bound]; } +- (instancetype)collectionQueryAtPath:(firebase::firestore::model::ResourcePath)path { + return [[FSTQuery alloc] initWithPath:path + collectionGroup:nil + filterBy:self.filters + orderBy:self.explicitSortOrders + limit:self.limit + startAt:self.startAt + endAt:self.endAt]; +} + - (BOOL)isDocumentQuery { - return DocumentKey::IsDocumentKey(_path) && self.filters.count == 0; + return DocumentKey::IsDocumentKey(_path) && !self.collectionGroup && self.filters.count == 0; +} + +- (BOOL)isCollectionGroupQuery { + return self.collectionGroup != nil; } - (BOOL)matchesDocument:(FSTDocument *)document { - return [self pathMatchesDocument:document] && [self orderByMatchesDocument:document] && - [self filtersMatchDocument:document] && [self boundsMatchDocument:document]; + return [self pathAndCollectionGroupMatchDocument:document] && + [self orderByMatchesDocument:document] && [self filtersMatchDocument:document] && + [self boundsMatchDocument:document]; } - (NSComparator)comparator { @@ -787,6 +800,10 @@ - (NSString *)canonicalID { NSMutableString *canonicalID = [NSMutableString string]; [canonicalID appendFormat:@"%s", _path.CanonicalString().c_str()]; + if (self.collectionGroup) { + [canonicalID appendFormat:@"|cg:%@", self.collectionGroup]; + } + // Add filters. [canonicalID appendString:@"|f:"]; for (FSTFilter *predicate in self.filters) { @@ -819,16 +836,24 @@ - (NSString *)canonicalID { #pragma mark - Private methods - (BOOL)isEqualToQuery:(FSTQuery *)other { - return self.path == other.path && self.limit == other.limit && - [self.filters isEqual:other.filters] && [self.sortOrders isEqual:other.sortOrders] && + return self.path == other.path && + (self.collectionGroup == other.collectionGroup || + [self.collectionGroup isEqual:other.collectionGroup]) && + self.limit == other.limit && [self.filters isEqual:other.filters] && + [self.sortOrders isEqual:other.sortOrders] && (self.startAt == other.startAt || [self.startAt isEqual:other.startAt]) && (self.endAt == other.endAt || [self.endAt isEqual:other.endAt]); } -/* Returns YES if the document matches the path for the receiver. */ -- (BOOL)pathMatchesDocument:(FSTDocument *)document { +/* Returns YES if the document matches the path and collection group for the receiver. */ +- (BOOL)pathAndCollectionGroupMatchDocument:(FSTDocument *)document { const ResourcePath &documentPath = document.key.path(); - if (DocumentKey::IsDocumentKey(_path)) { + if (self.collectionGroup) { + // NOTE: self.path is currently always empty since we don't expose Collection Group queries + // rooted at a document path yet. + return document.key.HasCollectionId(util::MakeString(self.collectionGroup)) && + self.path.IsPrefixOf(documentPath); + } else if (DocumentKey::IsDocumentKey(_path)) { // Exact match for document queries. return self.path == documentPath; } else { diff --git a/Firestore/Source/Core/FSTSyncEngine.h b/Firestore/Source/Core/FSTSyncEngine.h index b4c4af93c22..0409498c24d 100644 --- a/Firestore/Source/Core/FSTSyncEngine.h +++ b/Firestore/Source/Core/FSTSyncEngine.h @@ -16,19 +16,18 @@ #import +#include + #import "Firestore/Source/Core/FSTTypes.h" -#import "Firestore/Source/Remote/FSTRemoteStore.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_store.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" @class FSTLocalStore; @class FSTMutation; @class FSTQuery; -@class FSTRemoteEvent; -@class FSTRemoteStore; -@class FSTViewSnapshot; using firebase::firestore::model::OnlineState; @@ -41,7 +40,7 @@ NS_ASSUME_NONNULL_BEGIN * new view snapshots or errors. */ @protocol FSTSyncEngineDelegate -- (void)handleViewSnapshots:(NSArray *)viewSnapshots; +- (void)handleViewSnapshots:(std::vector &&)viewSnapshots; - (void)handleError:(NSError *)error forQuery:(FSTQuery *)query; - (void)applyChangedOnlineState:(OnlineState)onlineState; @end @@ -64,7 +63,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithLocalStore:(FSTLocalStore *)localStore - remoteStore:(FSTRemoteStore *)remoteStore + remoteStore:(firebase::firestore::remote::RemoteStore *)remoteStore initialUser:(const firebase::firestore::auth::User &)user NS_DESIGNATED_INITIALIZER; @@ -75,7 +74,7 @@ NS_ASSUME_NONNULL_BEGIN /** * Initiates a new listen. The FSTLocalStore will be queried for initial data and the listen will - * be sent to the FSTRemoteStore to get remote data. The registered FSTSyncEngineDelegate will be + * be sent to the `RemoteStore` to get remote data. The registered FSTSyncEngineDelegate will be * notified of resulting view snapshots and/or listen errors. * * @return the target ID assigned to the query. @@ -91,7 +90,8 @@ NS_ASSUME_NONNULL_BEGIN * write caused. The provided completion block will be called once the write has been acked or * rejected by the backend (or failed locally for any other reason). */ -- (void)writeMutations:(NSArray *)mutations completion:(FSTVoidErrorBlock)completion; +- (void)writeMutations:(std::vector &&)mutations + completion:(FSTVoidErrorBlock)completion; /** * Runs the given transaction block up to retries times and then calls completion. diff --git a/Firestore/Source/Core/FSTSyncEngine.mm b/Firestore/Source/Core/FSTSyncEngine.mm index 57ec5434316..b00982fea36 100644 --- a/Firestore/Source/Core/FSTSyncEngine.mm +++ b/Firestore/Source/Core/FSTSyncEngine.mm @@ -17,36 +17,43 @@ #import "Firestore/Source/Core/FSTSyncEngine.h" #include +#include #include #include #include +#include #import "FIRFirestoreErrors.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTTransaction.h" #import "Firestore/Source/Core/FSTView.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" #import "Firestore/Source/Local/FSTLocalStore.h" #import "Firestore/Source/Local/FSTLocalViewChanges.h" #import "Firestore/Source/Local/FSTLocalWriteResult.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/core/target_id_generator.h" +#include "Firestore/core/src/firebase/firestore/core/transaction.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_map.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" +#include "Firestore/core/src/firebase/firestore/util/error_apple.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "absl/types/optional.h" using firebase::firestore::auth::HashUser; using firebase::firestore::auth::User; using firebase::firestore::core::TargetIdGenerator; +using firebase::firestore::core::Transaction; +using firebase::firestore::core::ViewSnapshot; using firebase::firestore::local::ReferenceSet; using firebase::firestore::model::BatchId; using firebase::firestore::model::DocumentKey; @@ -57,7 +64,12 @@ using firebase::firestore::model::OnlineState; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; +using firebase::firestore::remote::RemoteEvent; +using firebase::firestore::remote::RemoteStore; +using firebase::firestore::remote::TargetChange; using firebase::firestore::util::AsyncQueue; +using firebase::firestore::util::MakeNSError; +using firebase::firestore::util::Status; NS_ASSUME_NONNULL_BEGIN @@ -147,9 +159,6 @@ @interface FSTSyncEngine () /** The local store, used to persist mutations and cached documents. */ @property(nonatomic, strong, readonly) FSTLocalStore *localStore; -/** The remote store for sending writes, watches, etc. to the backend. */ -@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore; - /** FSTQueryViews for all active queries, indexed by query. */ @property(nonatomic, strong, readonly) NSMutableDictionary *queryViewsByQuery; @@ -157,6 +166,9 @@ @interface FSTSyncEngine () @end @implementation FSTSyncEngine { + /** The remote store for sending writes, watches, etc. to the backend. */ + RemoteStore *_remoteStore; + /** Used for creating the TargetId for the listens used to resolve limbo documents. */ TargetIdGenerator _targetIdGenerator; @@ -186,7 +198,7 @@ @implementation FSTSyncEngine { } - (instancetype)initWithLocalStore:(FSTLocalStore *)localStore - remoteStore:(FSTRemoteStore *)remoteStore + remoteStore:(RemoteStore *)remoteStore initialUser:(const User &)initialUser { if (self = [super init]) { _localStore = localStore; @@ -205,14 +217,14 @@ - (TargetId)listenToQuery:(FSTQuery *)query { HARD_ASSERT(self.queryViewsByQuery[query] == nil, "We already listen to query: %s", query); FSTQueryData *queryData = [self.localStore allocateQuery:query]; - FSTViewSnapshot *viewSnapshot = [self initializeViewAndComputeSnapshotForQueryData:queryData]; - [self.syncEngineDelegate handleViewSnapshots:@[ viewSnapshot ]]; + ViewSnapshot viewSnapshot = [self initializeViewAndComputeSnapshotForQueryData:queryData]; + [self.syncEngineDelegate handleViewSnapshots:{viewSnapshot}]; - [self.remoteStore listenToTargetWithQueryData:queryData]; + _remoteStore->Listen(queryData); return queryData.targetID; } -- (FSTViewSnapshot *)initializeViewAndComputeSnapshotForQueryData:(FSTQueryData *)queryData { +- (ViewSnapshot)initializeViewAndComputeSnapshotForQueryData:(FSTQueryData *)queryData { DocumentMap docs = [self.localStore executeQuery:queryData.query]; DocumentKeySet remoteKeys = [self.localStore remoteDocumentKeysForTarget:queryData.targetID]; @@ -230,7 +242,9 @@ - (FSTViewSnapshot *)initializeViewAndComputeSnapshotForQueryData:(FSTQueryData self.queryViewsByQuery[queryData.query] = queryView; _queryViewsByTarget[queryData.targetID] = queryView; - return viewChange.snapshot; + HARD_ASSERT(viewChange.snapshot.has_value(), + "applyChangesToDocuments for new view should always return a snapshot"); + return viewChange.snapshot.value(); } - (void)stopListeningToQuery:(FSTQuery *)query { @@ -240,19 +254,19 @@ - (void)stopListeningToQuery:(FSTQuery *)query { HARD_ASSERT(queryView, "Trying to stop listening to a query not found"); [self.localStore releaseQuery:query]; - [self.remoteStore stopListeningToTargetID:queryView.targetID]; + _remoteStore->StopListening(queryView.targetID); [self removeAndCleanupQuery:queryView]; } -- (void)writeMutations:(NSArray *)mutations +- (void)writeMutations:(std::vector &&)mutations completion:(FSTVoidErrorBlock)completion { [self assertDelegateExistsForSelector:_cmd]; - FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations]; + FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:std::move(mutations)]; [self addMutationCompletionBlock:completion batchID:result.batchID]; - [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:result.changes remoteEvent:nil]; - [self.remoteStore fillWritePipeline]; + [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:result.changes remoteEvent:absl::nullopt]; + _remoteStore->FillWritePipeline(); } - (void)addMutationCompletionBlock:(FSTVoidErrorBlock)completion batchID:(BatchId)batchID { @@ -283,7 +297,8 @@ - (void)transactionWithRetries:(int)retries completion:(FSTVoidIDErrorBlock)completion { workerQueue->VerifyIsCurrentQueue(); HARD_ASSERT(retries >= 0, "Got negative number of retries for transaction"); - FSTTransaction *transaction = [self.remoteStore transaction]; + + std::shared_ptr transaction = _remoteStore->CreateTransaction(); updateBlock(transaction, ^(id _Nullable result, NSError *_Nullable error) { workerQueue->Enqueue( [self, retries, workerQueue, updateBlock, completion, transaction, result, error] { @@ -291,11 +306,13 @@ - (void)transactionWithRetries:(int)retries completion(nil, error); return; } - [transaction commitWithCompletion:^(NSError *_Nullable transactionError) { - if (!transactionError) { + transaction->Commit([self, retries, workerQueue, updateBlock, completion, + result](const Status &status) { + if (status.ok()) { completion(result, nil); return; } + // TODO(b/35201829): Only retry on real transaction failures. if (retries == 0) { NSError *wrappedError = @@ -303,7 +320,7 @@ - (void)transactionWithRetries:(int)retries code:FIRFirestoreErrorCodeFailedPrecondition userInfo:@{ NSLocalizedDescriptionKey : @"Transaction failed all retries.", - NSUnderlyingErrorKey : transactionError + NSUnderlyingErrorKey : MakeNSError(status) }]; completion(nil, wrappedError); return; @@ -313,34 +330,34 @@ - (void)transactionWithRetries:(int)retries workerQueue:workerQueue updateBlock:updateBlock completion:completion]; - }]; + }); }); }); } -- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { +- (void)applyRemoteEvent:(const RemoteEvent &)remoteEvent { [self assertDelegateExistsForSelector:_cmd]; // Update `receivedDocument` as appropriate for any limbo targets. - for (const auto &entry : remoteEvent.targetChanges) { + for (const auto &entry : remoteEvent.target_changes()) { TargetId targetID = entry.first; - FSTTargetChange *change = entry.second; + const TargetChange &change = entry.second; const auto iter = _limboResolutionsByTarget.find(targetID); if (iter != _limboResolutionsByTarget.end()) { LimboResolution &limboResolution = iter->second; // Since this is a limbo resolution lookup, it's for a single document and it could be // added, modified, or removed, but not a combination. - HARD_ASSERT(change.addedDocuments.size() + change.modifiedDocuments.size() + - change.removedDocuments.size() <= + HARD_ASSERT(change.added_documents().size() + change.modified_documents().size() + + change.removed_documents().size() <= 1, "Limbo resolution for single document contains multiple changes."); - if (change.addedDocuments.size() > 0) { + if (change.added_documents().size() > 0) { limboResolution.document_received = true; - } else if (change.modifiedDocuments.size() > 0) { + } else if (change.modified_documents().size() > 0) { HARD_ASSERT(limboResolution.document_received, "Received change for limbo target document without add."); - } else if (change.removedDocuments.size() > 0) { + } else if (change.removed_documents().size() > 0) { HARD_ASSERT(limboResolution.document_received, "Received remove for limbo target document without add."); limboResolution.document_received = false; @@ -355,18 +372,18 @@ - (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { } - (void)applyChangedOnlineState:(OnlineState)onlineState { - NSMutableArray *newViewSnapshots = [NSMutableArray array]; + __block std::vector newViewSnapshots; [self.queryViewsByQuery enumerateKeysAndObjectsUsingBlock:^(FSTQuery *query, FSTQueryView *queryView, BOOL *stop) { FSTViewChange *viewChange = [queryView.view applyChangedOnlineState:onlineState]; HARD_ASSERT(viewChange.limboChanges.count == 0, "OnlineState should not affect limbo documents."); - if (viewChange.snapshot) { - [newViewSnapshots addObject:viewChange.snapshot]; + if (viewChange.snapshot.has_value()) { + newViewSnapshots.push_back(std::move(viewChange.snapshot.value())); } }]; - [self.syncEngineDelegate handleViewSnapshots:newViewSnapshots]; + [self.syncEngineDelegate handleViewSnapshots:std::move(newViewSnapshots)]; [self.syncEngineDelegate applyChangedOnlineState:onlineState]; } @@ -390,13 +407,8 @@ - (void)rejectListenWithTargetID:(const TargetId)targetID error:(NSError *)error version:SnapshotVersion::None() hasCommittedMutations:NO]; DocumentKeySet limboDocuments = DocumentKeySet{doc.key}; - FSTRemoteEvent *event = [[FSTRemoteEvent alloc] initWithSnapshotVersion:SnapshotVersion::None() - targetChanges:{} - targetMismatches:{} - documentUpdates:{ - { limboKey, doc } - } - limboDocuments:std::move(limboDocuments)]; + RemoteEvent event{SnapshotVersion::None(), /*target_changes=*/{}, /*target_mismatches=*/{}, + /*document_updates=*/{{limboKey, doc}}, std::move(limboDocuments)}; [self applyRemoteEvent:event]; } else { auto found = _queryViewsByTarget.find(targetID); @@ -422,7 +434,7 @@ - (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult { [self processUserCallbacksForBatchID:batchResult.batch.batchID error:nil]; MaybeDocumentMap changes = [self.localStore acknowledgeBatchWithResult:batchResult]; - [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:nil]; + [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:absl::nullopt]; } - (void)rejectFailedWriteWithBatchID:(BatchId)batchID error:(NSError *)error { @@ -439,7 +451,7 @@ - (void)rejectFailedWriteWithBatchID:(BatchId)batchID error:(NSError *)error { // consistently happen before listen events. [self processUserCallbacksForBatchID:batchID error:error]; - [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:nil]; + [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:absl::nullopt]; } - (void)processUserCallbacksForBatchID:(BatchId)batchID error:(NSError *_Nullable)error { @@ -481,8 +493,9 @@ - (void)removeAndCleanupQuery:(FSTQueryView *)queryView { * Computes a new snapshot from the changes and calls the registered callback with the new snapshot. */ - (void)emitNewSnapshotsAndNotifyLocalStoreWithChanges:(const MaybeDocumentMap &)changes - remoteEvent:(FSTRemoteEvent *_Nullable)remoteEvent { - NSMutableArray *newSnapshots = [NSMutableArray array]; + remoteEvent:(const absl::optional &) + maybeRemoteEvent { + __block std::vector newSnapshots; NSMutableArray *documentChangesInAllViews = [NSMutableArray array]; [self.queryViewsByQuery @@ -498,10 +511,11 @@ - (void)emitNewSnapshotsAndNotifyLocalStoreWithChanges:(const MaybeDocumentMap & previousChanges:viewDocChanges]; } - FSTTargetChange *_Nullable targetChange = nil; - if (remoteEvent) { - auto it = remoteEvent.targetChanges.find(queryView.targetID); - if (it != remoteEvent.targetChanges.end()) { + absl::optional targetChange; + if (maybeRemoteEvent.has_value()) { + const RemoteEvent &remoteEvent = maybeRemoteEvent.value(); + auto it = remoteEvent.target_changes().find(queryView.targetID); + if (it != remoteEvent.target_changes().end()) { targetChange = it->second; } } @@ -511,16 +525,16 @@ - (void)emitNewSnapshotsAndNotifyLocalStoreWithChanges:(const MaybeDocumentMap & [self updateTrackedLimboDocumentsWithChanges:viewChange.limboChanges targetID:queryView.targetID]; - if (viewChange.snapshot) { - [newSnapshots addObject:viewChange.snapshot]; + if (viewChange.snapshot.has_value()) { + newSnapshots.push_back(viewChange.snapshot.value()); FSTLocalViewChanges *docChanges = - [FSTLocalViewChanges changesForViewSnapshot:viewChange.snapshot + [FSTLocalViewChanges changesForViewSnapshot:viewChange.snapshot.value() withTargetID:queryView.targetID]; [documentChangesInAllViews addObject:docChanges]; } }]; - [self.syncEngineDelegate handleViewSnapshots:newSnapshots]; + [self.syncEngineDelegate handleViewSnapshots:std::move(newSnapshots)]; [self.localStore notifyLocalViewChanges:documentChangesInAllViews]; } @@ -561,7 +575,7 @@ - (void)trackLimboChange:(FSTLimboDocumentChange *)limboChange { listenSequenceNumber:kIrrelevantSequenceNumber purpose:FSTQueryPurposeLimboResolution]; _limboResolutionsByTarget.emplace(limboTargetID, LimboResolution{key}); - [self.remoteStore listenToTargetWithQueryData:queryData]; + _remoteStore->Listen(queryData); _limboTargetsByKey[key] = limboTargetID; } } @@ -573,7 +587,7 @@ - (void)removeLimboTargetForKey:(const DocumentKey &)key { return; } TargetId limboTargetID = iter->second; - [self.remoteStore stopListeningToTargetID:limboTargetID]; + _remoteStore->StopListening(limboTargetID); _limboTargetsByKey.erase(key); _limboResolutionsByTarget.erase(limboTargetID); } @@ -591,11 +605,11 @@ - (void)credentialDidChangeWithUser:(const firebase::firestore::auth::User &)use if (userChanged) { // Notify local store and emit any resulting events from swapping out the mutation queue. MaybeDocumentMap changes = [self.localStore userDidChange:user]; - [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:nil]; + [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:absl::nullopt]; } // Notify remote store so it can restart its streams. - [self.remoteStore credentialDidChange]; + _remoteStore->HandleCredentialChange(); } - (DocumentKeySet)remoteKeysForTarget:(TargetId)targetId { diff --git a/Firestore/Source/Core/FSTTransaction.h b/Firestore/Source/Core/FSTTransaction.h deleted file mode 100644 index 352fa7a4f55..00000000000 --- a/Firestore/Source/Core/FSTTransaction.h +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#include - -#import "Firestore/Source/Core/FSTTypes.h" - -#include "Firestore/core/src/firebase/firestore/core/user_data.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/remote/datastore.h" - -@class FSTMaybeDocument; -@class FSTObjectValue; -@class FSTParsedUpdateData; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTTransaction - -/** Provides APIs to use in a transaction context. */ -@interface FSTTransaction : NSObject - -/** Creates a new transaction object, which can only be used for one transaction attempt. **/ -+ (instancetype)transactionWithDatastore:(firebase::firestore::remote::Datastore *)datastore; - -/** - * Takes a set of keys and asynchronously attempts to fetch all the documents from the backend, - * ignoring any local changes. - */ -- (void)lookupDocumentsForKeys:(const std::vector &)keys - completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion; - -/** - * Stores mutation for the given key and set data, to be committed when commitWithCompletion is - * called. - */ -- (void)setData:(firebase::firestore::core::ParsedSetData &&)data - forDocument:(const firebase::firestore::model::DocumentKey &)key; - -/** - * Stores mutations for the given key and update data, to be committed when commitWithCompletion - * is called. - */ -- (void)updateData:(firebase::firestore::core::ParsedUpdateData &&)data - forDocument:(const firebase::firestore::model::DocumentKey &)key; - -/** - * Stores a delete mutation for the given key, to be committed when commitWithCompletion is called. - */ -- (void)deleteDocument:(const firebase::firestore::model::DocumentKey &)key; - -/** - * Attempts to commit the mutations set on this transaction. Calls the given completion block when - * finished. Once this is called, no other mutations or commits are allowed on the transaction. - */ -- (void)commitWithCompletion:(FSTVoidErrorBlock)completion; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTransaction.mm b/Firestore/Source/Core/FSTTransaction.mm deleted file mode 100644 index 2ee220705cf..00000000000 --- a/Firestore/Source/Core/FSTTransaction.mm +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTTransaction.h" - -#include -#include -#include - -#import "FIRFirestoreErrors.h" -#import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Util/FSTUsageValidation.h" - -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" -#include "Firestore/core/src/firebase/firestore/model/precondition.h" -#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" -#include "Firestore/core/src/firebase/firestore/remote/datastore.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" - -using firebase::firestore::core::ParsedSetData; -using firebase::firestore::core::ParsedUpdateData; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::Precondition; -using firebase::firestore::model::SnapshotVersion; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::remote::Datastore; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTTransaction - -@interface FSTTransaction () -@property(nonatomic, strong, readonly) NSMutableArray *mutations; -@property(nonatomic, assign) BOOL commitCalled; -/** - * An error that may have occurred as a consequence of a write. If set, needs to be raised in the - * completion handler instead of trying to commit. - */ -@property(nonatomic, strong, nullable) NSError *lastWriteError; -@end - -@implementation FSTTransaction { - Datastore *_datastore; - std::map _readVersions; -} - -+ (instancetype)transactionWithDatastore:(Datastore *)datastore { - return [[FSTTransaction alloc] initWithDatastore:datastore]; -} - -- (instancetype)initWithDatastore:(Datastore *)datastore { - self = [super init]; - if (self) { - _datastore = datastore; - _mutations = [NSMutableArray array]; - _commitCalled = NO; - } - return self; -} - -/** - * Every time a document is read, this should be called to record its version. If we read two - * different versions of the same document, this will return an error through its out parameter. - * When the transaction is committed, the versions recorded will be set as preconditions on the - * writes sent to the backend. - */ -- (BOOL)recordVersionForDocument:(FSTMaybeDocument *)doc error:(NSError **)error { - HARD_ASSERT(error != nil, "nil error parameter"); - *error = nil; - SnapshotVersion docVersion; - if ([doc isKindOfClass:[FSTDocument class]]) { - docVersion = doc.version; - } else if ([doc isKindOfClass:[FSTDeletedDocument class]]) { - // For deleted docs, we must record an explicit no version to build the right precondition - // when writing. - docVersion = SnapshotVersion::None(); - } else { - HARD_FAIL("Unexpected document type in transaction: %s", NSStringFromClass([doc class])); - } - - if (_readVersions.find(doc.key) == _readVersions.end()) { - _readVersions[doc.key] = docVersion; - return YES; - } else { - if (error) { - *error = [NSError errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeFailedPrecondition - userInfo:@{ - NSLocalizedDescriptionKey : - @"A document cannot be read twice within a single transaction." - }]; - } - return NO; - } -} - -- (void)lookupDocumentsForKeys:(const std::vector &)keys - completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion { - [self ensureCommitNotCalled]; - if (self.mutations.count) { - FSTThrowInvalidUsage(@"FIRIllegalStateException", - @"All reads in a transaction must be done before any writes."); - } - _datastore->LookupDocuments( - keys, ^(NSArray *_Nullable documents, NSError *_Nullable error) { - if (error) { - completion(nil, error); - return; - } - for (FSTMaybeDocument *doc in documents) { - NSError *recordError = nil; - if (![self recordVersionForDocument:doc error:&recordError]) { - completion(nil, recordError); - return; - } - } - completion(documents, nil); - }); -} - -/** Stores mutations to be written when commitWithCompletion is called. */ -- (void)writeMutations:(NSArray *)mutations { - [self ensureCommitNotCalled]; - [self.mutations addObjectsFromArray:mutations]; -} - -/** - * Returns version of this doc when it was read in this transaction as a precondition, or no - * precondition if it was not read. - */ -- (Precondition)preconditionForDocumentKey:(const DocumentKey &)key { - const auto iter = _readVersions.find(key); - if (iter == _readVersions.end()) { - return Precondition::None(); - } else { - return Precondition::UpdateTime(iter->second); - } -} - -/** - * Returns the precondition for a document if the operation is an update, based on the provided - * UpdateOptions. Will return none precondition if an error occurred, in which case it sets the - * error parameter. - */ -- (Precondition)preconditionForUpdateWithDocumentKey:(const DocumentKey &)key - error:(NSError **)error { - const auto iter = _readVersions.find(key); - if (iter == _readVersions.end()) { - // Document was not read, so we just use the preconditions for an update. - return Precondition::Exists(true); - } - - const SnapshotVersion &version = iter->second; - if (version == SnapshotVersion::None()) { - // The document was read, but doesn't exist. - // Return an error because the precondition is impossible - if (error) { - *error = [NSError - errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeAborted - userInfo:@{ - NSLocalizedDescriptionKey : @"Can't update a document that doesn't exist." - }]; - } - return Precondition::None(); - } else { - // Document exists, just base precondition on document update time. - return Precondition::UpdateTime(version); - } -} - -- (void)setData:(ParsedSetData &&)data forDocument:(const DocumentKey &)key { - [self writeMutations:std::move(data).ToMutations(key, [self preconditionForDocumentKey:key])]; -} - -- (void)updateData:(ParsedUpdateData &&)data forDocument:(const DocumentKey &)key { - NSError *error = nil; - const Precondition precondition = [self preconditionForUpdateWithDocumentKey:key error:&error]; - if (precondition.IsNone()) { - HARD_ASSERT(error, "Got nil precondition, but error was not set"); - self.lastWriteError = error; - } else { - [self writeMutations:std::move(data).ToMutations(key, precondition)]; - } -} - -- (void)deleteDocument:(const DocumentKey &)key { - [self writeMutations:@[ [[FSTDeleteMutation alloc] - initWithKey:key - precondition:[self preconditionForDocumentKey:key]] ]]; - // Since the delete will be applied before all following writes, we need to ensure that the - // precondition for the next write will be exists without timestamp. - _readVersions[key] = SnapshotVersion::None(); -} - -- (void)commitWithCompletion:(FSTVoidErrorBlock)completion { - [self ensureCommitNotCalled]; - // Once commitWithCompletion is called once, mark this object so it can't be used again. - self.commitCalled = YES; - - // If there was an error writing, raise that error now - if (self.lastWriteError) { - completion(self.lastWriteError); - return; - } - - // Make a list of read documents that haven't been written. - DocumentKeySet unwritten; - for (const auto &kv : _readVersions) { - unwritten = unwritten.insert(kv.first); - }; - // For each mutation, note that the doc was written. - for (FSTMutation *mutation in self.mutations) { - unwritten = unwritten.erase(mutation.key); - } - if (!unwritten.empty()) { - // TODO(klimt): This is a temporary restriction, until "verify" is supported on the backend. - completion([NSError - errorWithDomain:FIRFirestoreErrorDomain - code:FIRFirestoreErrorCodeFailedPrecondition - userInfo:@{ - NSLocalizedDescriptionKey : @"Every document read in a transaction must also be " - @"written in that transaction." - }]); - } else { - _datastore->CommitMutations(self.mutations, ^(NSError *_Nullable error) { - if (error) { - completion(error); - } else { - completion(nil); - } - }); - } -} - -- (void)ensureCommitNotCalled { - if (self.commitCalled) { - FSTThrowInvalidUsage( - @"FIRIllegalStateException", - @"A transaction object cannot be used after its update block has completed."); - } -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTypes.h b/Firestore/Source/Core/FSTTypes.h index 32e421e1b78..86c1e94395c 100644 --- a/Firestore/Source/Core/FSTTypes.h +++ b/Firestore/Source/Core/FSTTypes.h @@ -16,10 +16,21 @@ #import +#include + +namespace firebase { +namespace firestore { +namespace core { + +class Transaction; + +} +} +} + NS_ASSUME_NONNULL_BEGIN @class FSTMaybeDocument; -@class FSTTransaction; /** * FSTVoidBlock is a block that's called when a specific event happens but that otherwise has @@ -53,7 +64,8 @@ typedef void (^FSTVoidMaybeDocumentArrayErrorBlock)( * transaction. * @param completion To be called by the block once the user's code is finished. */ -typedef void (^FSTTransactionBlock)(FSTTransaction *transaction, - void (^completion)(id _Nullable, NSError *_Nullable)); +typedef void (^FSTTransactionBlock)( + std::shared_ptr transaction, + void (^completion)(id _Nullable, NSError *_Nullable)); NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTView.h b/Firestore/Source/Core/FSTView.h index 1a25cd4ea7d..aed87321503 100644 --- a/Firestore/Source/Core/FSTView.h +++ b/Firestore/Source/Core/FSTView.h @@ -16,16 +16,25 @@ #import +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/document_map.h" #include "Firestore/core/src/firebase/firestore/model/types.h" -@class FSTDocumentSet; -@class FSTDocumentViewChangeSet; +#include "absl/types/optional.h" + +namespace firebase { +namespace firestore { +namespace remote { + +class TargetChange; + +} // namespace remote +} // namespace firestore +} // namespace firebase + @class FSTQuery; -@class FSTTargetChange; -@class FSTViewSnapshot; NS_ASSUME_NONNULL_BEGIN @@ -39,10 +48,10 @@ NS_ASSUME_NONNULL_BEGIN - (const firebase::firestore::model::DocumentKeySet &)mutatedKeys; /** The new set of docs that should be in the view. */ -@property(nonatomic, strong, readonly) FSTDocumentSet *documentSet; +- (const firebase::firestore::model::DocumentSet &)documentSet; /** The diff of this these docs with the previous set of docs. */ -@property(nonatomic, strong, readonly) FSTDocumentViewChangeSet *changeSet; +- (const firebase::firestore::core::DocumentViewChangeSet &)changeSet; /** * Whether the set of documents passed in was not sufficient to calculate the new state of the view @@ -79,7 +88,7 @@ typedef NS_ENUM(NSInteger, FSTLimboDocumentChangeType) { - (id)init __attribute__((unavailable("Use a static constructor method."))); -@property(nonatomic, strong, readonly, nullable) FSTViewSnapshot *snapshot; +- (absl::optional &)snapshot; @property(nonatomic, strong, readonly) NSArray *limboChanges; @end @@ -139,8 +148,10 @@ typedef NS_ENUM(NSInteger, FSTLimboDocumentChangeType) { * @param targetChange A target change to apply for computing limbo docs and sync state. * @return A new FSTViewChange with the given docs, changes, and sync state. */ -- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges - targetChange:(nullable FSTTargetChange *)targetChange; +- (FSTViewChange *) + applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges + targetChange: + (const absl::optional &)targetChange; /** * Applies an OnlineState change to the view, potentially generating an FSTViewChange if the diff --git a/Firestore/Source/Core/FSTView.mm b/Firestore/Source/Core/FSTView.mm index d400544bd58..079c547ad27 100644 --- a/Firestore/Source/Core/FSTView.mm +++ b/Firestore/Source/Core/FSTView.mm @@ -16,50 +16,83 @@ #import "Firestore/Source/Core/FSTView.h" +#include #include +#include #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Source/Model/FSTFieldValue.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" +#include "Firestore/core/src/firebase/firestore/util/delayed_constructor.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -using firebase::firestore::core::DocumentViewChangeType; +using firebase::firestore::core::DocumentViewChange; +using firebase::firestore::core::DocumentViewChangeSet; +using firebase::firestore::core::SyncState; +using firebase::firestore::core::ViewSnapshot; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::DocumentSet; using firebase::firestore::model::MaybeDocumentMap; using firebase::firestore::model::OnlineState; +using firebase::firestore::remote::TargetChange; +using firebase::firestore::util::DelayedConstructor; NS_ASSUME_NONNULL_BEGIN +namespace { + +int GetDocumentViewChangeTypePosition(DocumentViewChange::Type changeType) { + switch (changeType) { + case DocumentViewChange::Type::kRemoved: + return 0; + case DocumentViewChange::Type::kAdded: + return 1; + case DocumentViewChange::Type::kModified: + return 2; + case DocumentViewChange::Type::kMetadata: + // A metadata change is converted to a modified change at the public API layer. Since we sort + // by document key and then change type, metadata and modified changes must be sorted + // equivalently. + return 2; + } + HARD_FAIL("Unknown DocumentViewChange::Type %s", changeType); +} + +} // namespace + #pragma mark - FSTViewDocumentChanges /** The result of applying a set of doc changes to a view. */ @interface FSTViewDocumentChanges () -- (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet - changeSet:(FSTDocumentViewChangeSet *)changeSet +- (instancetype)initWithDocumentSet:(DocumentSet)documentSet + changeSet:(DocumentViewChangeSet &&)changeSet needsRefill:(BOOL)needsRefill mutatedKeys:(DocumentKeySet)mutatedKeys NS_DESIGNATED_INITIALIZER; @end @implementation FSTViewDocumentChanges { + DelayedConstructor _documentSet; DocumentKeySet _mutatedKeys; + DocumentViewChangeSet _changeSet; } -- (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet - changeSet:(FSTDocumentViewChangeSet *)changeSet +- (instancetype)initWithDocumentSet:(DocumentSet)documentSet + changeSet:(DocumentViewChangeSet &&)changeSet needsRefill:(BOOL)needsRefill mutatedKeys:(DocumentKeySet)mutatedKeys { self = [super init]; if (self) { - _documentSet = documentSet; - _changeSet = changeSet; + _documentSet.Init(std::move(documentSet)); + _changeSet = std::move(changeSet); _needsRefill = needsRefill; _mutatedKeys = std::move(mutatedKeys); } @@ -70,6 +103,14 @@ - (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet return _mutatedKeys; } +- (const firebase::firestore::model::DocumentSet &)documentSet { + return *_documentSet; +} + +- (const firebase::firestore::core::DocumentViewChangeSet &)changeSet { + return _changeSet; +} + @end #pragma mark - FSTLimboDocumentChange @@ -117,7 +158,7 @@ - (BOOL)isEqual:(id)other { - (NSUInteger)hash { NSUInteger hash = self.type; - hash = hash * 31u + [self.key hash]; + hash = hash * 31u + self.key.Hash(); return hash; } @@ -127,44 +168,47 @@ - (NSUInteger)hash { @interface FSTViewChange () -+ (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot ++ (FSTViewChange *)changeWithSnapshot:(absl::optional &&)snapshot limboChanges:(NSArray *)limboChanges; -- (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot +- (instancetype)initWithSnapshot:(absl::optional &&)snapshot limboChanges:(NSArray *)limboChanges NS_DESIGNATED_INITIALIZER; @end -@implementation FSTViewChange +@implementation FSTViewChange { + absl::optional _snapshot; +} -+ (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot ++ (FSTViewChange *)changeWithSnapshot:(absl::optional &&)snapshot limboChanges:(NSArray *)limboChanges { - return [[self alloc] initWithSnapshot:snapshot limboChanges:limboChanges]; + return [[self alloc] initWithSnapshot:std::move(snapshot) limboChanges:limboChanges]; } -- (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot +- (instancetype)initWithSnapshot:(absl::optional &&)snapshot limboChanges:(NSArray *)limboChanges { self = [super init]; if (self) { - _snapshot = snapshot; + _snapshot = std::move(snapshot); _limboChanges = limboChanges; } return self; } +- (absl::optional &)snapshot { + return _snapshot; +} + @end #pragma mark - FSTView -static NSComparisonResult FSTCompareDocumentViewChangeTypes(DocumentViewChangeType c1, - DocumentViewChangeType c2); - @interface FSTView () @property(nonatomic, strong, readonly) FSTQuery *query; -@property(nonatomic, assign) FSTSyncState syncState; +@property(nonatomic, assign) firebase::firestore::core::SyncState syncState; /** * A flag whether the view is current with the backend. A view is considered current after it @@ -173,11 +217,11 @@ @interface FSTView () */ @property(nonatomic, assign, getter=isCurrent) BOOL current; -@property(nonatomic, strong) FSTDocumentSet *documentSet; - @end @implementation FSTView { + DelayedConstructor _documentSet; + /** Documents included in the remote target. */ DocumentKeySet _syncedDocuments; @@ -192,7 +236,7 @@ - (instancetype)initWithQuery:(FSTQuery *)query remoteDocuments:(DocumentKeySet) self = [super init]; if (self) { _query = query; - _documentSet = [FSTDocumentSet documentSetWithComparator:query.comparator]; + _documentSet.Init(query.comparator); _syncedDocuments = std::move(remoteDocuments); } return self; @@ -209,13 +253,15 @@ - (FSTViewDocumentChanges *)computeChangesWithDocuments:(const MaybeDocumentMap - (FSTViewDocumentChanges *)computeChangesWithDocuments:(const MaybeDocumentMap &)docChanges previousChanges: (nullable FSTViewDocumentChanges *)previousChanges { - FSTDocumentViewChangeSet *changeSet = - previousChanges ? previousChanges.changeSet : [FSTDocumentViewChangeSet changeSet]; - FSTDocumentSet *oldDocumentSet = previousChanges ? previousChanges.documentSet : self.documentSet; + DocumentViewChangeSet changeSet; + if (previousChanges) { + changeSet = previousChanges.changeSet; + } + DocumentSet oldDocumentSet = previousChanges ? previousChanges.documentSet : *_documentSet; DocumentKeySet newMutatedKeys = previousChanges ? previousChanges.mutatedKeys : _mutatedKeys; DocumentKeySet oldMutatedKeys = _mutatedKeys; - FSTDocumentSet *newDocumentSet = oldDocumentSet; + DocumentSet newDocumentSet = oldDocumentSet; BOOL needsRefill = NO; // Track the last doc in a (full) limit. This is necessary, because some update (a delete, or an @@ -227,21 +273,22 @@ - (FSTViewDocumentChanges *)computeChangesWithDocuments:(const MaybeDocumentMap // Note that this should never get used in a refill (when previousChanges is set), because there // will only be adds -- no deletes or updates. FSTDocument *_Nullable lastDocInLimit = - (self.query.limit && oldDocumentSet.count == self.query.limit) ? oldDocumentSet.lastDocument - : nil; + (self.query.limit != NSNotFound && oldDocumentSet.size() == self.query.limit) + ? oldDocumentSet.GetLastDocument() + : nil; for (const auto &kv : docChanges) { const DocumentKey &key = kv.first; FSTMaybeDocument *maybeNewDoc = kv.second; - FSTDocument *_Nullable oldDoc = [oldDocumentSet documentForKey:key]; + FSTDocument *_Nullable oldDoc = oldDocumentSet.GetDocument(key); FSTDocument *_Nullable newDoc = nil; if ([maybeNewDoc isKindOfClass:[FSTDocument class]]) { newDoc = (FSTDocument *)maybeNewDoc; } if (newDoc) { - HARD_ASSERT(key == newDoc.key, "Mismatching key in document changes: %s != %s", key, - newDoc.key.ToString()); + HARD_ASSERT(key == newDoc.key, "Mismatching key in document changes: %s != %s", + key.ToString(), newDoc.key.ToString()); if (![self.query matchesDocument:newDoc]) { newDoc = nil; } @@ -261,9 +308,7 @@ - (FSTViewDocumentChanges *)computeChangesWithDocuments:(const MaybeDocumentMap BOOL docsEqual = [oldDoc.data isEqual:newDoc.data]; if (!docsEqual) { if (![self shouldWaitForSyncedDocument:newDoc oldDocument:oldDoc]) { - [changeSet addChange:[FSTDocumentViewChange - changeWithDocument:newDoc - type:DocumentViewChangeType::kModified]]; + changeSet.AddChange(DocumentViewChange{newDoc, DocumentViewChange::Type::kModified}); changeApplied = YES; if (lastDocInLimit && self.query.comparator(newDoc, lastDocInLimit) > 0) { @@ -273,21 +318,15 @@ - (FSTViewDocumentChanges *)computeChangesWithDocuments:(const MaybeDocumentMap } } } else if (oldDocHadPendingMutations != newDocHasPendingMutations) { - [changeSet - addChange:[FSTDocumentViewChange changeWithDocument:newDoc - type:DocumentViewChangeType::kMetadata]]; + changeSet.AddChange(DocumentViewChange{newDoc, DocumentViewChange::Type::kMetadata}); changeApplied = YES; } } else if (!oldDoc && newDoc) { - [changeSet - addChange:[FSTDocumentViewChange changeWithDocument:newDoc - type:DocumentViewChangeType::kAdded]]; + changeSet.AddChange(DocumentViewChange{newDoc, DocumentViewChange::Type::kAdded}); changeApplied = YES; } else if (oldDoc && !newDoc) { - [changeSet - addChange:[FSTDocumentViewChange changeWithDocument:oldDoc - type:DocumentViewChangeType::kRemoved]]; + changeSet.AddChange(DocumentViewChange{oldDoc, DocumentViewChange::Type::kRemoved}); changeApplied = YES; if (lastDocInLimit) { @@ -299,35 +338,33 @@ - (FSTViewDocumentChanges *)computeChangesWithDocuments:(const MaybeDocumentMap if (changeApplied) { if (newDoc) { - newDocumentSet = [newDocumentSet documentSetByAddingDocument:newDoc]; + newDocumentSet = newDocumentSet.insert(newDoc); if (newDoc.hasLocalMutations) { newMutatedKeys = newMutatedKeys.insert(key); } else { newMutatedKeys = newMutatedKeys.erase(key); } } else { - newDocumentSet = [newDocumentSet documentSetByRemovingKey:key]; + newDocumentSet = newDocumentSet.erase(key); newMutatedKeys = newMutatedKeys.erase(key); } } } - if (self.query.limit) { - for (long i = newDocumentSet.count - self.query.limit; i > 0; --i) { - FSTDocument *oldDoc = [newDocumentSet lastDocument]; - newDocumentSet = [newDocumentSet documentSetByRemovingKey:oldDoc.key]; + if (self.query.limit != NSNotFound && newDocumentSet.size() > self.query.limit) { + for (size_t i = newDocumentSet.size() - self.query.limit; i > 0; --i) { + FSTDocument *oldDoc = newDocumentSet.GetLastDocument(); + newDocumentSet = newDocumentSet.erase(oldDoc.key); newMutatedKeys = newMutatedKeys.erase(oldDoc.key); - [changeSet - addChange:[FSTDocumentViewChange changeWithDocument:oldDoc - type:DocumentViewChangeType::kRemoved]]; + changeSet.AddChange(DocumentViewChange{oldDoc, DocumentViewChange::Type::kRemoved}); } } HARD_ASSERT(!needsRefill || !previousChanges, "View was refilled using docs that themselves needed refilling."); - return [[FSTViewDocumentChanges alloc] initWithDocumentSet:newDocumentSet - changeSet:changeSet + return [[FSTViewDocumentChanges alloc] initWithDocumentSet:std::move(newDocumentSet) + changeSet:std::move(changeSet) needsRefill:needsRefill mutatedKeys:newMutatedKeys]; } @@ -343,67 +380,67 @@ - (BOOL)shouldWaitForSyncedDocument:(FSTDocument *)newDoc oldDocument:(FSTDocume } - (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges { - return [self applyChangesToDocuments:docChanges targetChange:nil]; + return [self applyChangesToDocuments:docChanges targetChange:{}]; } - (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges - targetChange:(nullable FSTTargetChange *)targetChange { + targetChange:(const absl::optional &)targetChange { HARD_ASSERT(!docChanges.needsRefill, "Cannot apply changes that need a refill"); - FSTDocumentSet *oldDocuments = self.documentSet; - self.documentSet = docChanges.documentSet; + DocumentSet oldDocuments = *_documentSet; + *_documentSet = docChanges.documentSet; _mutatedKeys = docChanges.mutatedKeys; // Sort changes based on type and query comparator. - NSArray *changes = [docChanges.changeSet changes]; - changes = [changes sortedArrayUsingComparator:^NSComparisonResult(FSTDocumentViewChange *c1, - FSTDocumentViewChange *c2) { - NSComparisonResult typeComparison = FSTCompareDocumentViewChangeTypes(c1.type, c2.type); - if (typeComparison != NSOrderedSame) { - return typeComparison; - } - return self.query.comparator(c1.document, c2.document); - }]; + std::vector changes = docChanges.changeSet.GetChanges(); + std::sort(changes.begin(), changes.end(), + [self](const DocumentViewChange &lhs, const DocumentViewChange &rhs) { + int pos1 = GetDocumentViewChangeTypePosition(lhs.type()); + int pos2 = GetDocumentViewChangeTypePosition(rhs.type()); + if (pos1 != pos2) { + return pos1 < pos2; + } + return self.query.comparator(lhs.document(), rhs.document()) == NSOrderedAscending; + }); + [self applyTargetChange:targetChange]; NSArray *limboChanges = [self updateLimboDocuments]; BOOL synced = _limboDocuments.empty() && self.isCurrent; - FSTSyncState newSyncState = synced ? FSTSyncStateSynced : FSTSyncStateLocal; - BOOL syncStateChanged = newSyncState != self.syncState; + SyncState newSyncState = synced ? SyncState::Synced : SyncState::Local; + bool syncStateChanged = newSyncState != self.syncState; self.syncState = newSyncState; - if (changes.count == 0 && !syncStateChanged) { + if (changes.empty() && !syncStateChanged) { // No changes. - return [FSTViewChange changeWithSnapshot:nil limboChanges:limboChanges]; + return [FSTViewChange changeWithSnapshot:absl::nullopt limboChanges:limboChanges]; } else { - FSTViewSnapshot *snapshot = - [[FSTViewSnapshot alloc] initWithQuery:self.query - documents:docChanges.documentSet - oldDocuments:oldDocuments - documentChanges:changes - fromCache:newSyncState == FSTSyncStateLocal - mutatedKeys:docChanges.mutatedKeys - syncStateChanged:syncStateChanged - excludesMetadataChanges:NO]; - - return [FSTViewChange changeWithSnapshot:snapshot limboChanges:limboChanges]; + ViewSnapshot snapshot{self.query, + docChanges.documentSet, + oldDocuments, + std::move(changes), + docChanges.mutatedKeys, + /*from_cache=*/newSyncState == SyncState::Local, + syncStateChanged, + /*excludes_metadata_changes=*/false}; + + return [FSTViewChange changeWithSnapshot:std::move(snapshot) limboChanges:limboChanges]; } } - (FSTViewChange *)applyChangedOnlineState:(OnlineState)onlineState { if (self.isCurrent && onlineState == OnlineState::Offline) { // If we're offline, set `current` to NO and then call applyChanges to refresh our syncState - // and generate an FSTViewChange as appropriate. We are guaranteed to get a new FSTTargetChange + // and generate an FSTViewChange as appropriate. We are guaranteed to get a new `TargetChange` // that sets `current` back to YES once the client is back online. self.current = NO; - return - [self applyChangesToDocuments:[[FSTViewDocumentChanges alloc] - initWithDocumentSet:self.documentSet - changeSet:[FSTDocumentViewChangeSet changeSet] - needsRefill:NO - mutatedKeys:_mutatedKeys]]; + return [self applyChangesToDocuments:[[FSTViewDocumentChanges alloc] + initWithDocumentSet:*_documentSet + changeSet:DocumentViewChangeSet {} + needsRefill:NO + mutatedKeys:_mutatedKeys]]; } else { // No effect, just return a no-op FSTViewChange. - return [[FSTViewChange alloc] initWithSnapshot:nil limboChanges:@[]]; + return [[FSTViewChange alloc] initWithSnapshot:absl::nullopt limboChanges:@[]]; } } @@ -416,14 +453,14 @@ - (BOOL)shouldBeLimboDocumentKey:(const DocumentKey &)key { return NO; } // The local store doesn't think it's a result, so it shouldn't be in limbo. - if (![self.documentSet containsKey:key]) { + if (!_documentSet->ContainsKey(key)) { return NO; } // If there are local changes to the doc, they might explain why the server doesn't know that it's // part of the query. So don't put it in limbo. // TODO(klimt): Ideally, we would only consider changes that might actually affect this specific // query. - if ([self.documentSet documentForKey:key].hasLocalMutations) { + if (_documentSet->GetDocument(key).hasLocalMutations) { return NO; } // Everything else is in limbo. @@ -433,20 +470,22 @@ - (BOOL)shouldBeLimboDocumentKey:(const DocumentKey &)key { /** * Updates syncedDocuments and current based on the given change. */ -- (void)applyTargetChange:(nullable FSTTargetChange *)targetChange { - if (targetChange) { - for (const DocumentKey &key : targetChange.addedDocuments) { +- (void)applyTargetChange:(const absl::optional &)maybeTargetChange { + if (maybeTargetChange.has_value()) { + const TargetChange &target_change = maybeTargetChange.value(); + + for (const DocumentKey &key : target_change.added_documents()) { _syncedDocuments = _syncedDocuments.insert(key); } - for (const DocumentKey &key : targetChange.modifiedDocuments) { + for (const DocumentKey &key : target_change.modified_documents()) { HARD_ASSERT(_syncedDocuments.find(key) != _syncedDocuments.end(), "Modified document %s not found in view.", key.ToString()); } - for (const DocumentKey &key : targetChange.removedDocuments) { + for (const DocumentKey &key : target_change.removed_documents()) { _syncedDocuments = _syncedDocuments.erase(key); } - self.current = targetChange.current; + self.current = target_change.current(); } } @@ -460,7 +499,7 @@ - (void)applyTargetChange:(nullable FSTTargetChange *)targetChange { // TODO(klimt): Do this incrementally so that it's not quadratic when updating many documents. DocumentKeySet oldLimboDocuments = std::move(_limboDocuments); _limboDocuments = DocumentKeySet{}; - for (FSTDocument *doc in self.documentSet.documentEnumerator) { + for (FSTDocument *doc : *_documentSet) { if ([self shouldBeLimboDocumentKey:doc.key]) { _limboDocuments = _limboDocuments.insert(doc.key); } @@ -486,35 +525,4 @@ - (void)applyTargetChange:(nullable FSTTargetChange *)targetChange { @end -static inline int DocumentViewChangeTypePosition(DocumentViewChangeType changeType) { - switch (changeType) { - case DocumentViewChangeType::kRemoved: - return 0; - case DocumentViewChangeType::kAdded: - return 1; - case DocumentViewChangeType::kModified: - return 2; - case DocumentViewChangeType::kMetadata: - // A metadata change is converted to a modified change at the public API layer. Since we sort - // by document key and then change type, metadata and modified changes must be sorted - // equivalently. - return 2; - default: - HARD_FAIL("Unknown DocumentViewChangeType %s", changeType); - } -} - -static NSComparisonResult FSTCompareDocumentViewChangeTypes(DocumentViewChangeType c1, - DocumentViewChangeType c2) { - int pos1 = DocumentViewChangeTypePosition(c1); - int pos2 = DocumentViewChangeTypePosition(c2); - if (pos1 == pos2) { - return NSOrderedSame; - } else if (pos1 < pos2) { - return NSOrderedAscending; - } else { - return NSOrderedDescending; - } -} - NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTViewSnapshot.h b/Firestore/Source/Core/FSTViewSnapshot.h deleted file mode 100644 index 4af098bb539..00000000000 --- a/Firestore/Source/Core/FSTViewSnapshot.h +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" -#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" - -using firebase::firestore::model::DocumentKeySet; - -@class FSTDocument; -@class FSTQuery; -@class FSTDocumentSet; -@class FSTViewSnapshot; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTDocumentViewChange - -/** A change to a single document's state within a view. */ -@interface FSTDocumentViewChange : NSObject - -- (id)init __attribute__((unavailable("Use a static constructor method."))); - -+ (instancetype)changeWithDocument:(FSTDocument *)document - type:(firebase::firestore::core::DocumentViewChangeType)type; - -/** The type of change for the document. */ -@property(nonatomic, assign, readonly) firebase::firestore::core::DocumentViewChangeType type; -/** The document whose status changed. */ -@property(nonatomic, strong, readonly) FSTDocument *document; - -@end - -#pragma mark - FSTDocumentChangeSet - -/** The possibly states a document can be in w.r.t syncing from local storage to the backend. */ -typedef NS_ENUM(NSInteger, FSTSyncState) { - FSTSyncStateNone = 0, - FSTSyncStateLocal, - FSTSyncStateSynced, -}; - -/** A set of changes to documents with respect to a view. This set is mutable. */ -@interface FSTDocumentViewChangeSet : NSObject - -/** Returns a new empty change set. */ -+ (instancetype)changeSet; - -/** Takes a new change and applies it to the set. */ -- (void)addChange:(FSTDocumentViewChange *)change; - -/** Returns the set of all changes tracked in this set. */ -- (NSArray *)changes; - -@end - -#pragma mark - FSTViewSnapshot - -typedef void (^FSTViewSnapshotHandler)(FSTViewSnapshot *_Nullable snapshot, - NSError *_Nullable error); - -/** A view snapshot is an immutable capture of the results of a query and the changes to them. */ -@interface FSTViewSnapshot : NSObject - -- (instancetype)initWithQuery:(FSTQuery *)query - documents:(FSTDocumentSet *)documents - oldDocuments:(FSTDocumentSet *)oldDocuments - documentChanges:(NSArray *)documentChanges - fromCache:(BOOL)fromCache - mutatedKeys:(DocumentKeySet)mutatedKeys - syncStateChanged:(BOOL)syncStateChanged - excludesMetadataChanges:(BOOL)excludesMetadataChanges NS_DESIGNATED_INITIALIZER; - -- (instancetype)init NS_UNAVAILABLE; - -/** Returns a view snapshot as if all documents in the snapshot were added. */ -+ (instancetype)snapshotForInitialDocuments:(FSTDocumentSet *)documents - query:(FSTQuery *)query - mutatedKeys:(DocumentKeySet)mutatedKeys - fromCache:(BOOL)fromCache - excludesMetadataChanges:(BOOL)excludesMetadataChanges; - -/** The query this view is tracking the results for. */ -@property(nonatomic, strong, readonly) FSTQuery *query; - -/** The documents currently known to be results of the query. */ -@property(nonatomic, strong, readonly) FSTDocumentSet *documents; - -/** The documents of the last snapshot. */ -@property(nonatomic, strong, readonly) FSTDocumentSet *oldDocuments; - -/** The set of changes that have been applied to the documents. */ -@property(nonatomic, strong, readonly) NSArray *documentChanges; - -/** Whether any document in the snapshot was served from the local cache. */ -@property(nonatomic, assign, readonly, getter=isFromCache) BOOL fromCache; - -/** Whether any document in the snapshot has pending local writes. */ -@property(nonatomic, assign, readonly) BOOL hasPendingWrites; - -/** Whether the sync state changed as part of this snapshot. */ -@property(nonatomic, assign, readonly) BOOL syncStateChanged; - -/** Whether this snapshot has been filtered to not include metadata changes */ -@property(nonatomic, assign, readonly) BOOL excludesMetadataChanges; - -/** The document in this snapshot that have unconfirmed writes. */ -@property(nonatomic, assign, readonly) DocumentKeySet mutatedKeys; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTViewSnapshot.mm b/Firestore/Source/Core/FSTViewSnapshot.mm deleted file mode 100644 index 6e7e59979b9..00000000000 --- a/Firestore/Source/Core/FSTViewSnapshot.mm +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Core/FSTViewSnapshot.h" - -#include -#include - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentSet.h" - -#include "Firestore/core/src/firebase/firestore/immutable/sorted_map.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "Firestore/core/src/firebase/firestore/util/string_apple.h" -#include "Firestore/core/src/firebase/firestore/util/string_format.h" -#include "absl/strings/str_join.h" - -using firebase::firestore::core::DocumentViewChangeType; -using firebase::firestore::immutable::SortedMap; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::util::WrapNSString; -using firebase::firestore::util::StringFormat; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTDocumentViewChange - -@interface FSTDocumentViewChange () - -+ (instancetype)changeWithDocument:(FSTDocument *)document type:(DocumentViewChangeType)type; - -- (instancetype)initWithDocument:(FSTDocument *)document - type:(DocumentViewChangeType)type NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FSTDocumentViewChange - -+ (instancetype)changeWithDocument:(FSTDocument *)document type:(DocumentViewChangeType)type { - return [[FSTDocumentViewChange alloc] initWithDocument:document type:type]; -} - -- (instancetype)initWithDocument:(FSTDocument *)document type:(DocumentViewChangeType)type { - self = [super init]; - if (self) { - _document = document; - _type = type; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (self == other) { - return YES; - } - if (![other isKindOfClass:[FSTDocumentViewChange class]]) { - return NO; - } - FSTDocumentViewChange *otherChange = (FSTDocumentViewChange *)other; - return [self.document isEqual:otherChange.document] && self.type == otherChange.type; -} - -- (NSString *)description { - return [NSString - stringWithFormat:@"", (long)self.type, self.document]; -} - -@end - -#pragma mark - FSTDocumentViewChangeSet - -@implementation FSTDocumentViewChangeSet { - /** The set of all changes tracked so far, with redundant changes merged. */ - SortedMap _changeMap; -} - -+ (instancetype)changeSet { - return [[FSTDocumentViewChangeSet alloc] init]; -} - -- (NSString *)description { - std::string result = absl::StrJoin( - _changeMap, ",", - [](std::string *out, const std::pair &kv) { - out->append(StringFormat("%s: %s", kv.first, kv.second)); - }); - return WrapNSString(std::string{"{"} + result + "}"); -} - -- (void)addChange:(FSTDocumentViewChange *)change { - const DocumentKey &key = change.document.key; - auto oldChangeIter = _changeMap.find(key); - if (oldChangeIter == _changeMap.end()) { - _changeMap = _changeMap.insert(key, change); - return; - } - FSTDocumentViewChange *oldChange = oldChangeIter->second; - - // Merge the new change with the existing change. - if (change.type != DocumentViewChangeType::kAdded && - oldChange.type == DocumentViewChangeType::kMetadata) { - _changeMap = _changeMap.insert(key, change); - - } else if (change.type == DocumentViewChangeType::kMetadata && - oldChange.type != DocumentViewChangeType::kRemoved) { - FSTDocumentViewChange *newChange = [FSTDocumentViewChange changeWithDocument:change.document - type:oldChange.type]; - _changeMap = _changeMap.insert(key, newChange); - - } else if (change.type == DocumentViewChangeType::kModified && - oldChange.type == DocumentViewChangeType::kModified) { - FSTDocumentViewChange *newChange = - [FSTDocumentViewChange changeWithDocument:change.document - type:DocumentViewChangeType::kModified]; - _changeMap = _changeMap.insert(key, newChange); - } else if (change.type == DocumentViewChangeType::kModified && - oldChange.type == DocumentViewChangeType::kAdded) { - FSTDocumentViewChange *newChange = - [FSTDocumentViewChange changeWithDocument:change.document - type:DocumentViewChangeType::kAdded]; - _changeMap = _changeMap.insert(key, newChange); - } else if (change.type == DocumentViewChangeType::kRemoved && - oldChange.type == DocumentViewChangeType::kAdded) { - _changeMap = _changeMap.erase(key); - } else if (change.type == DocumentViewChangeType::kRemoved && - oldChange.type == DocumentViewChangeType::kModified) { - FSTDocumentViewChange *newChange = - [FSTDocumentViewChange changeWithDocument:oldChange.document - type:DocumentViewChangeType::kRemoved]; - _changeMap = _changeMap.insert(key, newChange); - } else if (change.type == DocumentViewChangeType::kAdded && - oldChange.type == DocumentViewChangeType::kRemoved) { - FSTDocumentViewChange *newChange = - [FSTDocumentViewChange changeWithDocument:change.document - type:DocumentViewChangeType::kModified]; - _changeMap = _changeMap.insert(key, newChange); - } else { - // This includes these cases, which don't make sense: - // Added -> Added - // Removed -> Removed - // Modified -> Added - // Removed -> Modified - // Metadata -> Added - // Removed -> Metadata - HARD_FAIL("Unsupported combination of changes: %s after %s", change.type, oldChange.type); - } -} - -- (NSArray *)changes { - NSMutableArray *changes = [NSMutableArray array]; - for (const auto &kv : _changeMap) { - FSTDocumentViewChange *change = kv.second; - [changes addObject:change]; - } - return changes; -} - -@end - -#pragma mark - FSTViewSnapshot - -@implementation FSTViewSnapshot - -- (instancetype)initWithQuery:(FSTQuery *)query - documents:(FSTDocumentSet *)documents - oldDocuments:(FSTDocumentSet *)oldDocuments - documentChanges:(NSArray *)documentChanges - fromCache:(BOOL)fromCache - mutatedKeys:(DocumentKeySet)mutatedKeys - syncStateChanged:(BOOL)syncStateChanged - excludesMetadataChanges:(BOOL)excludesMetadataChanges { - self = [super init]; - if (self) { - _query = query; - _documents = documents; - _oldDocuments = oldDocuments; - _documentChanges = documentChanges; - _fromCache = fromCache; - _mutatedKeys = mutatedKeys; - _syncStateChanged = syncStateChanged; - _excludesMetadataChanges = excludesMetadataChanges; - } - return self; -} - -+ (instancetype)snapshotForInitialDocuments:(FSTDocumentSet *)documents - query:(FSTQuery *)query - mutatedKeys:(DocumentKeySet)mutatedKeys - fromCache:(BOOL)fromCache - excludesMetadataChanges:(BOOL)excludesMetadataChanges { - NSMutableArray *viewChanges = [NSMutableArray array]; - for (FSTDocument *doc in documents.documentEnumerator) { - [viewChanges - addObject:[FSTDocumentViewChange changeWithDocument:doc - type:DocumentViewChangeType::kAdded]]; - } - return [[FSTViewSnapshot alloc] - initWithQuery:query - documents:documents - oldDocuments:[FSTDocumentSet documentSetWithComparator:query.comparator] - documentChanges:viewChanges - fromCache:fromCache - mutatedKeys:mutatedKeys - syncStateChanged:YES - excludesMetadataChanges:excludesMetadataChanges]; -} - -- (BOOL)hasPendingWrites { - return _mutatedKeys.size() != 0; -} - -- (NSString *)description { - return [NSString - stringWithFormat:@"", - self.query, self.documents, self.oldDocuments, self.documentChanges, - (self.fromCache ? @"YES" : @"NO"), - static_cast(self.mutatedKeys.size()), - (self.syncStateChanged ? @"YES" : @"NO"), - (self.excludesMetadataChanges ? @"YES" : @"NO")]; -} - -- (BOOL)isEqual:(id)object { - if (self == object) { - return YES; - } else if (![object isKindOfClass:[FSTViewSnapshot class]]) { - return NO; - } - - FSTViewSnapshot *other = object; - return [self.query isEqual:other.query] && [self.documents isEqual:other.documents] && - [self.oldDocuments isEqual:other.oldDocuments] && - [self.documentChanges isEqualToArray:other.documentChanges] && - self.fromCache == other.fromCache && self.mutatedKeys == other.mutatedKeys && - self.syncStateChanged == other.syncStateChanged && - self.excludesMetadataChanges == other.excludesMetadataChanges; -} - -- (NSUInteger)hash { - // Note: We are omitting `mutatedKeys` from the hash, since we don't have a straightforward - // way to compute its hash value. Since `FSTViewSnapshot` is currently not stored in an - // NSDictionary, this has no side effects. - - NSUInteger result = [self.query hash]; - result = 31 * result + [self.documents hash]; - result = 31 * result + [self.oldDocuments hash]; - result = 31 * result + [self.documentChanges hash]; - result = 31 * result + (self.fromCache ? 1231 : 1237); - result = 31 * result + (self.syncStateChanged ? 1231 : 1237); - result = 31 * result + (self.excludesMetadataChanges ? 1231 : 1237); - return result; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLRUGarbageCollector.h b/Firestore/Source/Local/FSTLRUGarbageCollector.h index 0b3cc7b3110..9d59813a94c 100644 --- a/Firestore/Source/Local/FSTLRUGarbageCollector.h +++ b/Firestore/Source/Local/FSTLRUGarbageCollector.h @@ -21,6 +21,8 @@ #import "FIRFirestoreSettings.h" #import "Firestore/Source/Local/FSTQueryData.h" + +#include "Firestore/core/src/firebase/firestore/local/query_cache.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/types.h" @@ -79,15 +81,13 @@ struct LruResults { * Enumerates all the targets that the delegate is aware of. This is typically all of the targets in * an FSTQueryCache. */ -- (void)enumerateTargetsUsingBlock:(void (^)(FSTQueryData *queryData, BOOL *stop))block; +- (void)enumerateTargetsUsingCallback:(const firebase::firestore::local::TargetCallback &)callback; /** * Enumerates all of the outstanding mutations. */ -- (void)enumerateMutationsUsingBlock: - (void (^)(const firebase::firestore::model::DocumentKey &key, - firebase::firestore::model::ListenSequenceNumber sequenceNumber, - BOOL *stop))block; +- (void)enumerateMutationsUsingCallback: + (const firebase::firestore::local::OrphanedDocumentCallback &)callback; /** * Removes all unreferenced documents from the cache that have a sequence number less than or equal diff --git a/Firestore/Source/Local/FSTLRUGarbageCollector.mm b/Firestore/Source/Local/FSTLRUGarbageCollector.mm index 2071a35b977..95a86fabb56 100644 --- a/Firestore/Source/Local/FSTLRUGarbageCollector.mm +++ b/Firestore/Source/Local/FSTLRUGarbageCollector.mm @@ -20,7 +20,6 @@ #include #include -#import "Firestore/Source/Local/FSTMutationQueue.h" #import "Firestore/Source/Local/FSTPersistence.h" #include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" @@ -160,14 +159,13 @@ - (ListenSequenceNumber)sequenceNumberForQueryCount:(NSUInteger)queryCount { return kFSTListenSequenceNumberInvalid; } RollingSequenceNumberBuffer buffer(queryCount); - // Pointer is necessary to access stack-allocated buffer from a block. - RollingSequenceNumberBuffer *ptr_to_buffer = &buffer; - [_delegate enumerateTargetsUsingBlock:^(FSTQueryData *queryData, BOOL *stop) { - ptr_to_buffer->AddElement(queryData.sequenceNumber); + + [_delegate enumerateTargetsUsingCallback:[&buffer](FSTQueryData *queryData) { + buffer.AddElement(queryData.sequenceNumber); }]; - [_delegate enumerateMutationsUsingBlock:^(const DocumentKey &docKey, - ListenSequenceNumber sequenceNumber, BOOL *stop) { - ptr_to_buffer->AddElement(sequenceNumber); + [_delegate enumerateMutationsUsingCallback:[&buffer](const DocumentKey &docKey, + ListenSequenceNumber sequenceNumber) { + buffer.AddElement(sequenceNumber); }]; return buffer.max_value(); } diff --git a/Firestore/Source/Local/FSTLevelDB.mm b/Firestore/Source/Local/FSTLevelDB.mm index 2e245c2d298..478e68ab4e5 100644 --- a/Firestore/Source/Local/FSTLevelDB.mm +++ b/Firestore/Source/Local/FSTLevelDB.mm @@ -21,20 +21,22 @@ #include #import "FIRFirestoreErrors.h" -#import "Firestore/Source/Core/FSTListenSequence.h" #import "Firestore/Source/Local/FSTLRUGarbageCollector.h" -#import "Firestore/Source/Local/FSTLevelDBMutationQueue.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" #include "Firestore/core/include/firebase/firestore/firestore_errors.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/core/database_info.h" +#include "Firestore/core/src/firebase/firestore/local/index_manager.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_index_manager.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_migrations.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_query_cache.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_transaction.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_util.h" +#include "Firestore/core/src/firebase/firestore/local/listen_sequence.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" @@ -59,16 +61,22 @@ using firebase::firestore::auth::User; using firebase::firestore::core::DatabaseInfo; using firebase::firestore::local::ConvertStatus; +using firebase::firestore::local::IndexManager; using firebase::firestore::local::LevelDbDocumentMutationKey; using firebase::firestore::local::LevelDbDocumentTargetKey; +using firebase::firestore::local::LevelDbIndexManager; using firebase::firestore::local::LevelDbMigrations; using firebase::firestore::local::LevelDbMutationKey; +using firebase::firestore::local::LevelDbMutationQueue; using firebase::firestore::local::LevelDbQueryCache; using firebase::firestore::local::LevelDbRemoteDocumentCache; using firebase::firestore::local::LevelDbTransaction; +using firebase::firestore::local::ListenSequence; using firebase::firestore::local::LruParams; +using firebase::firestore::local::OrphanedDocumentCallback; using firebase::firestore::local::ReferenceSet; using firebase::firestore::local::RemoteDocumentCache; +using firebase::firestore::local::TargetCallback; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::ListenSequenceNumber; @@ -92,7 +100,9 @@ - (size_t)byteSize; @property(nonatomic, assign, getter=isStarted) BOOL started; -- (firebase::firestore::local::LevelDbQueryCache *)queryCache; +- (LevelDbQueryCache *)queryCache; + +- (LevelDbMutationQueue *)mutationQueueForUser:(const User &)user; @end @@ -119,7 +129,8 @@ @implementation FSTLevelDBLRUDelegate { __weak FSTLevelDB *_db; ReferenceSet *_additionalReferences; ListenSequenceNumber _currentSequenceNumber; - FSTListenSequence *_listenSequence; + // PORTING NOTE: doesn't need to be a pointer once this class is ported to C++. + std::unique_ptr _listenSequence; } - (instancetype)initWithPersistence:(FSTLevelDB *)persistence lruParams:(LruParams)lruParams { @@ -133,13 +144,13 @@ - (instancetype)initWithPersistence:(FSTLevelDB *)persistence lruParams:(LruPara - (void)start { ListenSequenceNumber highestSequenceNumber = _db.queryCache->highest_listen_sequence_number(); - _listenSequence = [[FSTListenSequence alloc] initStartingAfter:highestSequenceNumber]; + _listenSequence = absl::make_unique(highestSequenceNumber); } - (void)transactionWillStart { HARD_ASSERT(_currentSequenceNumber == kFSTListenSequenceNumberInvalid, "Previous sequence number is still in effect"); - _currentSequenceNumber = [_listenSequence next]; + _currentSequenceNumber = _listenSequence->Next(); } - (void)transactionWillCommit { @@ -201,19 +212,18 @@ - (BOOL)isPinned:(const DocumentKey &)docKey { return NO; } -- (void)enumerateTargetsUsingBlock:(void (^)(FSTQueryData *queryData, BOOL *stop))block { - _db.queryCache->EnumerateTargets(block); +- (void)enumerateTargetsUsingCallback:(const TargetCallback &)callback { + _db.queryCache->EnumerateTargets(callback); } -- (void)enumerateMutationsUsingBlock: - (void (^)(const DocumentKey &key, ListenSequenceNumber sequenceNumber, BOOL *stop))block { - _db.queryCache->EnumerateOrphanedDocuments(block); +- (void)enumerateMutationsUsingCallback:(const OrphanedDocumentCallback &)callback { + _db.queryCache->EnumerateOrphanedDocuments(callback); } - (int)removeOrphanedDocumentsThroughSequenceNumber:(ListenSequenceNumber)upperBound { - __block int count = 0; + int count = 0; _db.queryCache->EnumerateOrphanedDocuments( - ^(const DocumentKey &docKey, ListenSequenceNumber sequenceNumber, BOOL *stop) { + [&count, self, upperBound](const DocumentKey &docKey, ListenSequenceNumber sequenceNumber) { if (sequenceNumber <= upperBound) { if (![self isPinned:docKey]) { count++; @@ -236,9 +246,9 @@ - (int)removeTargetsThroughSequenceNumber:(ListenSequenceNumber)sequenceNumber } - (size_t)sequenceNumberCount { - __block size_t totalCount = _db.queryCache->size(); - [self enumerateMutationsUsingBlock:^(const DocumentKey &key, ListenSequenceNumber sequenceNumber, - BOOL *stop) { + size_t totalCount = _db.queryCache->size(); + [self enumerateMutationsUsingCallback:[&totalCount](const DocumentKey &key, + ListenSequenceNumber sequenceNumber) { totalCount++; }]; return totalCount; @@ -274,10 +284,12 @@ @implementation FSTLevelDB { std::unique_ptr _transaction; std::unique_ptr _ptr; std::unique_ptr _documentCache; + std::unique_ptr _indexManager; FSTTransactionRunner _transactionRunner; FSTLevelDBLRUDelegate *_referenceDelegate; std::unique_ptr _queryCache; std::set _users; + std::unique_ptr _currentMutationQueue; } /** @@ -345,6 +357,7 @@ - (instancetype)initWithLevelDB:(std::unique_ptr)db _serializer = serializer; _queryCache = absl::make_unique(self, _serializer); _documentCache = absl::make_unique(self, _serializer); + _indexManager = absl::make_unique(self); _referenceDelegate = [[FSTLevelDBLRUDelegate alloc] initWithPersistence:self lruParams:lruParams]; _transactionRunner.SetBackingPersistence(self); @@ -365,8 +378,8 @@ - (size_t)byteSize { } HARD_ASSERT(iter->status().ok(), "Failed to iterate leveldb directory: %s", iter->status().error_message().c_str()); - HARD_ASSERT(count <= SIZE_MAX, "Overflowed counting bytes cached"); - return count; + HARD_ASSERT(count >= 0 && count <= SIZE_MAX, "Overflowed counting bytes cached"); + return static_cast(count); } - (const std::set &)users { @@ -461,9 +474,10 @@ - (LevelDbTransaction *)currentTransaction { #pragma mark - Persistence Factory methods -- (id)mutationQueueForUser:(const User &)user { +- (LevelDbMutationQueue *)mutationQueueForUser:(const User &)user { _users.insert(user.uid()); - return [FSTLevelDBMutationQueue mutationQueueWithUser:user db:self serializer:self.serializer]; + _currentMutationQueue.reset(new LevelDbMutationQueue(user, self, self.serializer)); + return _currentMutationQueue.get(); } - (LevelDbQueryCache *)queryCache { @@ -474,6 +488,10 @@ - (RemoteDocumentCache *)remoteDocumentCache { return _documentCache.get(); } +- (IndexManager *)indexManager { + return _indexManager.get(); +} + - (void)startTransaction:(absl::string_view)label { HARD_ASSERT(_transaction == nullptr, "Starting a transaction while one is already outstanding"); _transaction = absl::make_unique(_ptr.get(), label); diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.h b/Firestore/Source/Local/FSTLevelDBMutationQueue.h deleted file mode 100644 index 72fde7d1ba8..00000000000 --- a/Firestore/Source/Local/FSTLevelDBMutationQueue.h +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#include - -#import "Firestore/Source/Local/FSTMutationQueue.h" - -#include "Firestore/core/src/firebase/firestore/auth/user.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" -#include "leveldb/db.h" - -@class FSTLevelDB; -@class FSTLocalSerializer; - -NS_ASSUME_NONNULL_BEGIN - -/** A mutation queue for a specific user, backed by LevelDB. */ -@interface FSTLevelDBMutationQueue : NSObject - -- (instancetype)init __attribute__((unavailable("Use a static constructor"))); - -/** - * Creates a new mutation queue for the given user, in the given LevelDB. - * - * @param user The user for which to create a mutation queue. - * @param db The LevelDB in which to create the queue. - */ -+ (instancetype)mutationQueueWithUser:(const firebase::firestore::auth::User &)user - db:(FSTLevelDB *)db - serializer:(FSTLocalSerializer *)serializer; - -- (firebase::firestore::local::LevelDbMutationQueue *)mutationQueue; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.mm b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm deleted file mode 100644 index 7753db22f12..00000000000 --- a/Firestore/Source/Local/FSTLevelDBMutationQueue.mm +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTLevelDBMutationQueue.h" - -#include -#include -#include -#include -#include - -#import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Local/FSTLevelDB.h" -#import "Firestore/Source/Local/FSTLocalSerializer.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" - -#include "Firestore/core/src/firebase/firestore/auth/user.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_transaction.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_util.h" -#include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" -#include "Firestore/core/src/firebase/firestore/model/resource_path.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "Firestore/core/src/firebase/firestore/util/string_apple.h" -#include "Firestore/core/src/firebase/firestore/util/string_util.h" -#include "absl/memory/memory.h" -#include "absl/strings/match.h" -#include "leveldb/db.h" -#include "leveldb/write_batch.h" - -NS_ASSUME_NONNULL_BEGIN - -namespace util = firebase::firestore::util; -using firebase::firestore::auth::User; -using firebase::firestore::local::DescribeKey; -using firebase::firestore::local::LevelDbDocumentMutationKey; -using firebase::firestore::local::LevelDbMutationKey; -using firebase::firestore::local::LevelDbMutationQueue; -using firebase::firestore::local::LevelDbMutationQueueKey; -using firebase::firestore::local::LevelDbTransaction; -using firebase::firestore::local::LoadNextBatchIdFromDb; -using firebase::firestore::local::MakeStringView; -using firebase::firestore::model::BatchId; -using firebase::firestore::model::kBatchIdUnknown; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::ResourcePath; -using leveldb::DB; -using leveldb::Iterator; -using leveldb::ReadOptions; -using leveldb::Slice; -using leveldb::Status; -using leveldb::WriteBatch; -using leveldb::WriteOptions; - -static NSArray *toNSArray(const std::vector &vec) { - NSMutableArray *copy = [NSMutableArray array]; - for (auto &batch : vec) { - [copy addObject:batch]; - } - return copy; -} - -@interface FSTLevelDBMutationQueue () - -- (instancetype)initWithUserID:(std::string)userID - db:(FSTLevelDB *)db - serializer:(FSTLocalSerializer *)serializer - delegate:(std::unique_ptr)delegate - NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FSTLevelDBMutationQueue { - std::unique_ptr _delegate; -} - -+ (instancetype)mutationQueueWithUser:(const User &)user - db:(FSTLevelDB *)db - serializer:(FSTLocalSerializer *)serializer { - std::string userID = user.is_authenticated() ? user.uid() : ""; - - return [[FSTLevelDBMutationQueue alloc] - initWithUserID:std::move(userID) - db:db - serializer:serializer - delegate:absl::make_unique(user, db, serializer)]; -} - -- (instancetype)initWithUserID:(std::string)userID - db:(FSTLevelDB *)db - serializer:(FSTLocalSerializer *)serializer - delegate:(std::unique_ptr)delegate { - if (self = [super init]) { - _delegate = std::move(delegate); - } - return self; -} - -- (void)start { - _delegate->Start(); -} - -- (BOOL)isEmpty { - return _delegate->IsEmpty(); -} - -- (void)acknowledgeBatch:(FSTMutationBatch *)batch streamToken:(nullable NSData *)streamToken { - _delegate->AcknowledgeBatch(batch, streamToken); -} - -- (nullable NSData *)lastStreamToken { - return _delegate->GetLastStreamToken(); -} - -- (void)setLastStreamToken:(nullable NSData *)streamToken { - _delegate->SetLastStreamToken(streamToken); -} - -- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FIRTimestamp *)localWriteTime - mutations:(NSArray *)mutations { - return _delegate->AddMutationBatch(localWriteTime, mutations); -} - -- (nullable FSTMutationBatch *)lookupMutationBatch:(BatchId)batchID { - return _delegate->LookupMutationBatch(batchID); -} - -- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(BatchId)batchID { - return _delegate->NextMutationBatchAfterBatchId(batchID); -} - -- (NSArray *)allMutationBatchesAffectingDocumentKey: - (const DocumentKey &)documentKey { - return toNSArray(_delegate->AllMutationBatchesAffectingDocumentKey(documentKey)); -} - -- (NSArray *)allMutationBatchesAffectingDocumentKeys: - (const DocumentKeySet &)documentKeys { - return toNSArray(_delegate->AllMutationBatchesAffectingDocumentKeys(documentKeys)); -} - -- (NSArray *)allMutationBatchesAffectingQuery:(FSTQuery *)query { - return toNSArray(_delegate->AllMutationBatchesAffectingQuery(query)); -} - -- (NSArray *)allMutationBatches { - return toNSArray(_delegate->AllMutationBatches()); -} - -- (void)removeMutationBatch:(FSTMutationBatch *)batch { - _delegate->RemoveMutationBatch(batch); -} - -- (void)performConsistencyCheck { - _delegate->PerformConsistencyCheck(); -} - -- (LevelDbMutationQueue *)mutationQueue { - return _delegate.get(); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.h b/Firestore/Source/Local/FSTLocalDocumentsView.h deleted file mode 100644 index a559b7c2136..00000000000 --- a/Firestore/Source/Local/FSTLocalDocumentsView.h +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" -#include "Firestore/core/src/firebase/firestore/model/document_map.h" - -@class FSTMaybeDocument; -@class FSTQuery; -@protocol FSTMutationQueue; - -NS_ASSUME_NONNULL_BEGIN - -/** - * A readonly view of the local state of all documents we're tracking (i.e. we have a cached - * version in remoteDocumentCache or local mutations for the document). The view is computed by - * applying the mutations in the FSTMutationQueue to the FSTRemoteDocumentCache. - */ -@interface FSTLocalDocumentsView : NSObject - -+ (instancetype)viewWithRemoteDocumentCache: - (firebase::firestore::local::RemoteDocumentCache *)remoteDocumentCache - mutationQueue:(id)mutationQueue; - -- (instancetype)init __attribute__((unavailable("Use a static constructor"))); - -/** - * Get the local view of the document identified by `key`. - * - * @return Local view of the document or nil if we don't have any cached state for it. - */ -- (nullable FSTMaybeDocument *)documentForKey:(const firebase::firestore::model::DocumentKey &)key; - -/** - * Gets the local view of the documents identified by `keys`. - * - * If we don't have cached state for a document in `keys`, a FSTDeletedDocument will be stored - * for that key in the resulting set. - */ -- (firebase::firestore::model::MaybeDocumentMap)documentsForKeys: - (const firebase::firestore::model::DocumentKeySet &)keys; - -/** - * Similar to `documentsForKeys`, but creates the local view from the given - * `baseDocs` without retrieving documents from the local store. - */ -- (firebase::firestore::model::MaybeDocumentMap)localViewsForDocuments: - (const firebase::firestore::model::MaybeDocumentMap &)baseDocs; - -/** Performs a query against the local view of all documents. */ -- (firebase::firestore::model::DocumentMap)documentsMatchingQuery:(FSTQuery *)query; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.mm b/Firestore/Source/Local/FSTLocalDocumentsView.mm deleted file mode 100644 index bdc92a8dc08..00000000000 --- a/Firestore/Source/Local/FSTLocalDocumentsView.mm +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTLocalDocumentsView.h" - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Local/FSTMutationQueue.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" - -#include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/model/document_map.h" -#include "Firestore/core/src/firebase/firestore/model/resource_path.h" -#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" - -using firebase::firestore::local::RemoteDocumentCache; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::DocumentMap; -using firebase::firestore::model::MaybeDocumentMap; -using firebase::firestore::model::ResourcePath; -using firebase::firestore::model::SnapshotVersion; - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTLocalDocumentsView () -- (instancetype)initWithRemoteDocumentCache:(RemoteDocumentCache *)remoteDocumentCache - mutationQueue:(id)mutationQueue - NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, strong, readonly) id mutationQueue; -@end - -@implementation FSTLocalDocumentsView { - RemoteDocumentCache *_remoteDocumentCache; -} - -+ (instancetype)viewWithRemoteDocumentCache:(RemoteDocumentCache *)remoteDocumentCache - mutationQueue:(id)mutationQueue { - return [[FSTLocalDocumentsView alloc] initWithRemoteDocumentCache:remoteDocumentCache - mutationQueue:mutationQueue]; -} - -- (instancetype)initWithRemoteDocumentCache:(RemoteDocumentCache *)remoteDocumentCache - mutationQueue:(id)mutationQueue { - if (self = [super init]) { - _remoteDocumentCache = remoteDocumentCache; - _mutationQueue = mutationQueue; - } - return self; -} - -- (nullable FSTMaybeDocument *)documentForKey:(const DocumentKey &)key { - NSArray *batches = - [self.mutationQueue allMutationBatchesAffectingDocumentKey:key]; - return [self documentForKey:key inBatches:batches]; -} - -// Internal version of documentForKey: which allows reusing `batches`. -- (nullable FSTMaybeDocument *)documentForKey:(const DocumentKey &)key - inBatches:(NSArray *)batches { - FSTMaybeDocument *_Nullable document = _remoteDocumentCache->Get(key); - for (FSTMutationBatch *batch in batches) { - document = [batch applyToLocalDocument:document documentKey:key]; - } - - return document; -} - -// Returns the view of the given `docs` as they would appear after applying all -// mutations in the given `batches`. -- (MaybeDocumentMap)applyLocalMutationsToDocuments:(const MaybeDocumentMap &)docs - fromBatches:(NSArray *)batches { - MaybeDocumentMap results; - - for (const auto &kv : docs) { - const DocumentKey &key = kv.first; - FSTMaybeDocument *localView = kv.second; - for (FSTMutationBatch *batch in batches) { - localView = [batch applyToLocalDocument:localView documentKey:key]; - } - results = results.insert(key, localView); - } - return results; -} - -- (MaybeDocumentMap)documentsForKeys:(const DocumentKeySet &)keys { - MaybeDocumentMap docs = _remoteDocumentCache->GetAll(keys); - return [self localViewsForDocuments:docs]; -} - -/** - * Similar to `documentsForKeys`, but creates the local view from the given - * `baseDocs` without retrieving documents from the local store. - */ -- (MaybeDocumentMap)localViewsForDocuments:(const MaybeDocumentMap &)baseDocs { - MaybeDocumentMap results; - - DocumentKeySet allKeys; - for (const auto &kv : baseDocs) { - allKeys = allKeys.insert(kv.first); - } - NSArray *batches = - [self.mutationQueue allMutationBatchesAffectingDocumentKeys:allKeys]; - - MaybeDocumentMap docs = [self applyLocalMutationsToDocuments:baseDocs fromBatches:batches]; - - for (const auto &kv : docs) { - const DocumentKey &key = kv.first; - FSTMaybeDocument *maybeDoc = kv.second; - - // TODO(http://b/32275378): Don't conflate missing / deleted. - if (!maybeDoc) { - maybeDoc = [FSTDeletedDocument documentWithKey:key - version:SnapshotVersion::None() - hasCommittedMutations:NO]; - } - results = results.insert(key, maybeDoc); - } - - return results; -} - -- (DocumentMap)documentsMatchingQuery:(FSTQuery *)query { - if (DocumentKey::IsDocumentKey(query.path)) { - return [self documentsMatchingDocumentQuery:query.path]; - } else { - return [self documentsMatchingCollectionQuery:query]; - } -} - -- (DocumentMap)documentsMatchingDocumentQuery:(const ResourcePath &)docPath { - DocumentMap result; - // Just do a simple document lookup. - FSTMaybeDocument *doc = [self documentForKey:DocumentKey{docPath}]; - if ([doc isKindOfClass:[FSTDocument class]]) { - result = result.insert(doc.key, static_cast(doc)); - } - return result; -} - -- (DocumentMap)documentsMatchingCollectionQuery:(FSTQuery *)query { - DocumentMap results = _remoteDocumentCache->GetMatching(query); - // Get locally persisted mutation batches. - NSArray *matchingBatches = - [self.mutationQueue allMutationBatchesAffectingQuery:query]; - - for (FSTMutationBatch *batch in matchingBatches) { - for (FSTMutation *mutation in batch.mutations) { - // Only process documents belonging to the collection. - if (!query.path.IsImmediateParentOf(mutation.key.path())) { - continue; - } - - const DocumentKey &key = mutation.key; - // baseDoc may be nil for the documents that weren't yet written to the backend. - FSTMaybeDocument *baseDoc = nil; - auto found = results.underlying_map().find(key); - if (found != results.underlying_map().end()) { - baseDoc = found->second; - } - FSTMaybeDocument *mutatedDoc = [mutation applyToLocalDocument:baseDoc - baseDocument:baseDoc - localWriteTime:batch.localWriteTime]; - - if ([mutatedDoc isKindOfClass:[FSTDocument class]]) { - results = results.insert(key, static_cast(mutatedDoc)); - } else { - results = results.erase(key); - } - } - } - - // Finally, filter out any documents that don't actually match the query. Note that the extra - // reference here prevents ARC from deallocating the initial unfiltered results while we're - // enumerating them. - DocumentMap unfiltered = results; - for (const auto &kv : unfiltered.underlying_map()) { - const DocumentKey &key = kv.first; - FSTDocument *doc = static_cast(kv.second); - if (![query matchesDocument:doc]) { - results = results.erase(key); - } - } - - return results; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalSerializer.mm b/Firestore/Source/Local/FSTLocalSerializer.mm index 5ad70e43c47..97545aa4b63 100644 --- a/Firestore/Source/Local/FSTLocalSerializer.mm +++ b/Firestore/Source/Local/FSTLocalSerializer.mm @@ -17,6 +17,8 @@ #import "Firestore/Source/Local/FSTLocalSerializer.h" #include +#include +#include #import "FIRTimestamp.h" #import "Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h" @@ -181,8 +183,12 @@ - (FSTPBWriteBatch *)encodedMutationBatch:(FSTMutationBatch *)batch { proto.localWriteTime = [remoteSerializer encodedTimestamp:Timestamp{batch.localWriteTime.seconds, batch.localWriteTime.nanoseconds}]; + NSMutableArray *baseWrites = proto.baseWritesArray; + for (FSTMutation *baseMutation : [batch baseMutations]) { + [baseWrites addObject:[remoteSerializer encodedMutation:baseMutation]]; + } NSMutableArray *writes = proto.writesArray; - for (FSTMutation *mutation in batch.mutations) { + for (FSTMutation *mutation : [batch mutations]) { [writes addObject:[remoteSerializer encodedMutation:mutation]]; } return proto; @@ -192,9 +198,14 @@ - (FSTMutationBatch *)decodedMutationBatch:(FSTPBWriteBatch *)batch { FSTSerializerBeta *remoteSerializer = self.remoteSerializer; int batchID = batch.batchId; - NSMutableArray *mutations = [NSMutableArray array]; + + std::vector baseMutations; + for (GCFSWrite *write in batch.baseWritesArray) { + baseMutations.push_back([remoteSerializer decodedMutation:write]); + } + std::vector mutations; for (GCFSWrite *write in batch.writesArray) { - [mutations addObject:[remoteSerializer decodedMutation:write]]; + mutations.push_back([remoteSerializer decodedMutation:write]); } Timestamp localWriteTime = [remoteSerializer decodedTimestamp:batch.localWriteTime]; @@ -203,7 +214,8 @@ - (FSTMutationBatch *)decodedMutationBatch:(FSTPBWriteBatch *)batch { initWithBatchID:batchID localWriteTime:[FIRTimestamp timestampWithSeconds:localWriteTime.seconds() nanoseconds:localWriteTime.nanoseconds()] - mutations:mutations]; + baseMutations:std::move(baseMutations) + mutations:std::move(mutations)]; } - (FSTPBTarget *)encodedQueryData:(FSTQueryData *)queryData { diff --git a/Firestore/Source/Local/FSTLocalStore.h b/Firestore/Source/Local/FSTLocalStore.h index 60f8d2ee4a7..8d696fe9c95 100644 --- a/Firestore/Source/Local/FSTLocalStore.h +++ b/Firestore/Source/Local/FSTLocalStore.h @@ -16,6 +16,8 @@ #import +#include + #import "Firestore/Source/Local/FSTLRUGarbageCollector.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" @@ -25,6 +27,16 @@ #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" +namespace firebase { +namespace firestore { +namespace remote { + +class RemoteEvent; + +} // namespace remote +} // namespace firestore +} // namespace firebase + @class FSTLocalViewChanges; @class FSTLocalWriteResult; @class FSTMutation; @@ -32,7 +44,6 @@ @class FSTMutationBatchResult; @class FSTQuery; @class FSTQueryData; -@class FSTRemoteEvent; @protocol FSTPersistence; NS_ASSUME_NONNULL_BEGIN @@ -94,7 +105,7 @@ NS_ASSUME_NONNULL_BEGIN (const firebase::firestore::auth::User &)user; /** Accepts locally generated Mutations and commits them to storage. */ -- (FSTLocalWriteResult *)locallyWriteMutations:(NSArray *)mutations; +- (FSTLocalWriteResult *)locallyWriteMutations:(std::vector &&)mutations; /** Returns the current value of a document with a given key, or nil if not found. */ - (nullable FSTMaybeDocument *)readDocument:(const firebase::firestore::model::DocumentKey &)key; @@ -147,7 +158,8 @@ NS_ASSUME_NONNULL_BEGIN * * LocalDocuments are re-calculated if there are remaining mutations in the queue. */ -- (firebase::firestore::model::MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent; +- (firebase::firestore::model::MaybeDocumentMap)applyRemoteEvent: + (const firebase::firestore::remote::RemoteEvent &)remoteEvent; /** * Returns the keys of the documents that are associated with the given targetID in the remote diff --git a/Firestore/Source/Local/FSTLocalStore.mm b/Firestore/Source/Local/FSTLocalStore.mm index 11f0aec0898..97d542bd37d 100644 --- a/Firestore/Source/Local/FSTLocalStore.mm +++ b/Firestore/Source/Local/FSTLocalStore.mm @@ -16,38 +16,44 @@ #import "Firestore/Source/Local/FSTLocalStore.h" +#include #include #include #include +#include #import "FIRTimestamp.h" -#import "Firestore/Source/Core/FSTListenSequence.h" #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Local/FSTLRUGarbageCollector.h" -#import "Firestore/Source/Local/FSTLocalDocumentsView.h" #import "Firestore/Source/Local/FSTLocalViewChanges.h" #import "Firestore/Source/Local/FSTLocalWriteResult.h" -#import "Firestore/Source/Local/FSTMutationQueue.h" #import "Firestore/Source/Local/FSTPersistence.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/core/target_id_generator.h" #include "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" +#include "Firestore/core/src/firebase/firestore/local/local_documents_view.h" +#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" #include "Firestore/core/src/firebase/firestore/local/query_cache.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_map.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" +#include "absl/memory/memory.h" using firebase::firestore::auth::User; using firebase::firestore::core::TargetIdGenerator; +using firebase::firestore::local::LocalDocumentsView; using firebase::firestore::local::LruResults; +using firebase::firestore::local::MutationQueue; using firebase::firestore::local::QueryCache; using firebase::firestore::local::ReferenceSet; using firebase::firestore::local::RemoteDocumentCache; @@ -56,10 +62,15 @@ using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::DocumentMap; using firebase::firestore::model::DocumentVersionMap; +using firebase::firestore::model::FieldMask; +using firebase::firestore::model::FieldPath; using firebase::firestore::model::MaybeDocumentMap; using firebase::firestore::model::ListenSequenceNumber; +using firebase::firestore::model::Precondition; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; +using firebase::firestore::remote::RemoteEvent; +using firebase::firestore::remote::TargetChange; NS_ASSUME_NONNULL_BEGIN @@ -76,12 +87,6 @@ @interface FSTLocalStore () /** Manages our in-memory or durable persistence. */ @property(nonatomic, strong, readonly) id persistence; -/** The set of all mutations that have been sent but not yet been applied to the backend. */ -@property(nonatomic, strong) id mutationQueue; - -/** The "local" view of all documents (layering mutationQueue on top of remoteDocumentCache). */ -@property(nonatomic, strong) FSTLocalDocumentsView *localDocuments; - /** Maps a query to the data about that query. */ @property(nonatomic) QueryCache *queryCache; @@ -93,6 +98,11 @@ @implementation FSTLocalStore { /** The set of all cached remote documents. */ RemoteDocumentCache *_remoteDocumentCache; QueryCache *_queryCache; + /** The set of all mutations that have been sent but not yet been applied to the backend. */ + MutationQueue *_mutationQueue; + + /** The "local" view of all documents (layering mutationQueue on top of remoteDocumentCache). */ + std::unique_ptr _localDocuments; /** The set of document references maintained by any local views. */ ReferenceSet _localViewReferences; @@ -108,8 +118,8 @@ - (instancetype)initWithPersistence:(id)persistence _mutationQueue = [persistence mutationQueueForUser:initialUser]; _remoteDocumentCache = [persistence remoteDocumentCache]; _queryCache = [persistence queryCache]; - _localDocuments = [FSTLocalDocumentsView viewWithRemoteDocumentCache:_remoteDocumentCache - mutationQueue:_mutationQueue]; + _localDocuments = absl::make_unique(_remoteDocumentCache, _mutationQueue, + [_persistence indexManager]); [_persistence.referenceDelegate addInMemoryPins:&_localViewReferences]; _targetIDGenerator = TargetIdGenerator::QueryCacheTargetIdGenerator(0); @@ -124,99 +134,141 @@ - (void)start { } - (void)startMutationQueue { - self.persistence.run("Start MutationQueue", [&]() { [self.mutationQueue start]; }); + self.persistence.run("Start MutationQueue", [&]() { _mutationQueue->Start(); }); } - (MaybeDocumentMap)userDidChange:(const User &)user { // Swap out the mutation queue, grabbing the pending mutation batches before and after. - NSArray *oldBatches = self.persistence.run( + std::vector oldBatches = self.persistence.run( "OldBatches", - [&]() -> NSArray * { return [self.mutationQueue allMutationBatches]; }); + [&]() -> std::vector { return _mutationQueue->AllMutationBatches(); }); - self.mutationQueue = [self.persistence mutationQueueForUser:user]; + // The old one has a reference to the mutation queue, so nil it out first. + _localDocuments.reset(); + _mutationQueue = [self.persistence mutationQueueForUser:user]; [self startMutationQueue]; return self.persistence.run("NewBatches", [&]() -> MaybeDocumentMap { - NSArray *newBatches = [self.mutationQueue allMutationBatches]; + std::vector newBatches = _mutationQueue->AllMutationBatches(); // Recreate our LocalDocumentsView using the new MutationQueue. - self.localDocuments = [FSTLocalDocumentsView viewWithRemoteDocumentCache:_remoteDocumentCache - mutationQueue:self.mutationQueue]; + _localDocuments = absl::make_unique(_remoteDocumentCache, _mutationQueue, + [_persistence indexManager]); // Union the old/new changed keys. DocumentKeySet changedKeys; - for (NSArray *batches in @[ oldBatches, newBatches ]) { - for (FSTMutationBatch *batch in batches) { - for (FSTMutation *mutation in batch.mutations) { + for (const std::vector &batches : {oldBatches, newBatches}) { + for (FSTMutationBatch *batch : batches) { + for (FSTMutation *mutation : [batch mutations]) { changedKeys = changedKeys.insert(mutation.key); } } } // Return the set of all (potentially) changed documents as the result of the user change. - return [self.localDocuments documentsForKeys:changedKeys]; + return _localDocuments->GetDocuments(changedKeys); }); } -- (FSTLocalWriteResult *)locallyWriteMutations:(NSArray *)mutations { +- (FSTLocalWriteResult *)locallyWriteMutations:(std::vector &&)mutations { + FIRTimestamp *localWriteTime = [FIRTimestamp timestamp]; + DocumentKeySet keys; + for (FSTMutation *mutation : mutations) { + keys = keys.insert(mutation.key); + } + return self.persistence.run("Locally write mutations", [&]() -> FSTLocalWriteResult * { - FIRTimestamp *localWriteTime = [FIRTimestamp timestamp]; - FSTMutationBatch *batch = [self.mutationQueue addMutationBatchWithWriteTime:localWriteTime - mutations:mutations]; - DocumentKeySet keys = [batch keys]; - MaybeDocumentMap changedDocuments = [self.localDocuments documentsForKeys:keys]; + // Load and apply all existing mutations. This lets us compute the current base state for + // all non-idempotent transforms before applying any additional user-provided writes. + MaybeDocumentMap existingDocuments = _localDocuments->GetDocuments(keys); + + // For non-idempotent mutations (such as `FieldValue.increment()`), we record the base + // state in a separate patch mutation. This is later used to guarantee consistent values + // and prevents flicker even if the backend sends us an update that already includes our + // transform. + std::vector baseMutations; + for (FSTMutation *mutation : mutations) { + if (mutation.idempotent) { + continue; + } + + // Theoretically, we should only include non-idempotent fields in this field mask as this mask + // is used to prevent flicker for non-idempotent transforms by providing consistent base + // values. By including the fields for all DocumentTransforms, we incorrectly prevent rebasing + // of idempotent transforms (such as `arrayUnion()`) when any non-idempotent transforms are + // present. + // TODO(mrschmidt): Expose a method that only returns the a field mask for non-idempotent + // transforms + const FieldMask *fieldMask = [mutation fieldMask]; + if (fieldMask) { + // `documentsForKeys` is guaranteed to return a (nullable) entry for every document key. + FSTMaybeDocument *maybeDocument = existingDocuments.find(mutation.key)->second; + FSTObjectValue *baseValues = + [maybeDocument isKindOfClass:[FSTDocument class]] + ? [((FSTDocument *)maybeDocument).data objectByApplyingFieldMask:*fieldMask] + : [FSTObjectValue objectValue]; + // NOTE: The base state should only be applied if there's some existing document to + // override, so use a Precondition of exists=true + baseMutations.push_back([[FSTPatchMutation alloc] initWithKey:mutation.key + fieldMask:*fieldMask + value:baseValues + precondition:Precondition::Exists(true)]); + } + } + + FSTMutationBatch *batch = _mutationQueue->AddMutationBatch( + localWriteTime, std::move(baseMutations), std::move(mutations)); + MaybeDocumentMap changedDocuments = [batch applyToLocalDocumentSet:existingDocuments]; return [FSTLocalWriteResult resultForBatchID:batch.batchID changes:std::move(changedDocuments)]; }); } - (MaybeDocumentMap)acknowledgeBatchWithResult:(FSTMutationBatchResult *)batchResult { return self.persistence.run("Acknowledge batch", [&]() -> MaybeDocumentMap { - id mutationQueue = self.mutationQueue; - FSTMutationBatch *batch = batchResult.batch; - [mutationQueue acknowledgeBatch:batch streamToken:batchResult.streamToken]; + _mutationQueue->AcknowledgeBatch(batch, batchResult.streamToken); [self applyBatchResult:batchResult]; - [self.mutationQueue performConsistencyCheck]; + _mutationQueue->PerformConsistencyCheck(); - return [self.localDocuments documentsForKeys:batch.keys]; + return _localDocuments->GetDocuments(batch.keys); }); } - (MaybeDocumentMap)rejectBatchID:(BatchId)batchID { return self.persistence.run("Reject batch", [&]() -> MaybeDocumentMap { - FSTMutationBatch *toReject = [self.mutationQueue lookupMutationBatch:batchID]; + FSTMutationBatch *toReject = _mutationQueue->LookupMutationBatch(batchID); HARD_ASSERT(toReject, "Attempt to reject nonexistent batch!"); - [self.mutationQueue removeMutationBatch:toReject]; - [self.mutationQueue performConsistencyCheck]; + _mutationQueue->RemoveMutationBatch(toReject); + _mutationQueue->PerformConsistencyCheck(); - return [self.localDocuments documentsForKeys:toReject.keys]; + return _localDocuments->GetDocuments(toReject.keys); }); } - (nullable NSData *)lastStreamToken { - return [self.mutationQueue lastStreamToken]; + return _mutationQueue->GetLastStreamToken(); } - (void)setLastStreamToken:(nullable NSData *)streamToken { self.persistence.run("Set stream token", - [&]() { [self.mutationQueue setLastStreamToken:streamToken]; }); + [&]() { _mutationQueue->SetLastStreamToken(streamToken); }); } - (const SnapshotVersion &)lastRemoteSnapshotVersion { return self.queryCache->GetLastRemoteSnapshotVersion(); } -- (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { +- (MaybeDocumentMap)applyRemoteEvent:(const RemoteEvent &)remoteEvent { return self.persistence.run("Apply remote event", [&]() -> MaybeDocumentMap { // TODO(gsoltis): move the sequence number into the reference delegate. ListenSequenceNumber sequenceNumber = self.persistence.currentSequenceNumber; DocumentKeySet authoritativeUpdates; - for (const auto &entry : remoteEvent.targetChanges) { + for (const auto &entry : remoteEvent.target_changes()) { TargetId targetID = entry.first; - FSTTargetChange *change = entry.second; + const TargetChange &change = entry.second; // Do not ref/unref unassigned targetIDs - it may lead to leaks. auto found = _targetIDs.find(targetID); @@ -233,23 +285,23 @@ - (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { // If the document is only updated while removing it from a target then watch isn't obligated // to send the absolute latest version: it can send the first version that caused the document // not to match. - for (const DocumentKey &key : change.addedDocuments) { + for (const DocumentKey &key : change.added_documents()) { authoritativeUpdates = authoritativeUpdates.insert(key); } - for (const DocumentKey &key : change.modifiedDocuments) { + for (const DocumentKey &key : change.modified_documents()) { authoritativeUpdates = authoritativeUpdates.insert(key); } - _queryCache->RemoveMatchingKeys(change.removedDocuments, targetID); - _queryCache->AddMatchingKeys(change.addedDocuments, targetID); + _queryCache->RemoveMatchingKeys(change.removed_documents(), targetID); + _queryCache->AddMatchingKeys(change.added_documents(), targetID); // Update the resume token if the change includes one. Don't clear any preexisting value. // Bump the sequence number as well, so that documents being removed now are ordered later // than documents that were previously removed from this target. - NSData *resumeToken = change.resumeToken; + NSData *resumeToken = change.resume_token(); if (resumeToken.length > 0) { FSTQueryData *oldQueryData = queryData; - queryData = [queryData queryDataByReplacingSnapshotVersion:remoteEvent.snapshotVersion + queryData = [queryData queryDataByReplacingSnapshotVersion:remoteEvent.snapshot_version() resumeToken:resumeToken sequenceNumber:sequenceNumber]; _targetIDs[targetID] = queryData; @@ -261,16 +313,16 @@ - (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { } MaybeDocumentMap changedDocs; - const DocumentKeySet &limboDocuments = remoteEvent.limboDocumentChanges; + const DocumentKeySet &limboDocuments = remoteEvent.limbo_document_changes(); DocumentKeySet updatedKeys; - for (const auto &kv : remoteEvent.documentUpdates) { + for (const auto &kv : remoteEvent.document_updates()) { updatedKeys = updatedKeys.insert(kv.first); } // Each loop iteration only affects its "own" doc, so it's safe to get all the remote // documents in advance in a single call. MaybeDocumentMap existingDocs = _remoteDocumentCache->GetAll(updatedKeys); - for (const auto &kv : remoteEvent.documentUpdates) { + for (const auto &kv : remoteEvent.document_updates()) { const DocumentKey &key = kv.first; FSTMaybeDocument *doc = kv.second; FSTMaybeDocument *existingDoc = nil; @@ -304,7 +356,7 @@ - (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { // events when we get permission denied errors while trying to resolve the state of a locally // cached document that is in limbo. const SnapshotVersion &lastRemoteVersion = _queryCache->GetLastRemoteSnapshotVersion(); - const SnapshotVersion &remoteVersion = remoteEvent.snapshotVersion; + const SnapshotVersion &remoteVersion = remoteEvent.snapshot_version(); if (remoteVersion != SnapshotVersion::None()) { HARD_ASSERT(remoteVersion >= lastRemoteVersion, "Watch stream reverted to previous snapshot?? (%s < %s)", @@ -312,7 +364,7 @@ - (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { _queryCache->SetLastRemoteSnapshotVersion(remoteVersion); } - return [self.localDocuments localViewsForDocuments:changedDocs]; + return _localDocuments->GetLocalViewOfDocuments(changedDocs); }); } @@ -328,7 +380,7 @@ - (MaybeDocumentMap)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { */ - (BOOL)shouldPersistQueryData:(FSTQueryData *)newQueryData oldQueryData:(FSTQueryData *)oldQueryData - change:(FSTTargetChange *)change { + change:(const TargetChange &)change { // Avoid clearing any existing value if (newQueryData.resumeToken.length == 0) return NO; @@ -348,8 +400,8 @@ - (BOOL)shouldPersistQueryData:(FSTQueryData *)newQueryData // worth persisting. Note that the RemoteStore keeps an in-memory view of the currently active // targets which includes the current resume token, so stream failure or user changes will still // use an up-to-date resume token regardless of what we do here. - size_t changes = change.addedDocuments.size() + change.modifiedDocuments.size() + - change.removedDocuments.size(); + size_t changes = change.added_documents().size() + change.modified_documents().size() + + change.removed_documents().size(); return changes > 0; } @@ -368,14 +420,14 @@ - (void)notifyLocalViewChanges:(NSArray *)viewChanges { - (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(BatchId)batchID { FSTMutationBatch *result = self.persistence.run("NextMutationBatchAfterBatchID", [&]() -> FSTMutationBatch * { - return [self.mutationQueue nextMutationBatchAfterBatchID:batchID]; + return _mutationQueue->NextMutationBatchAfterBatchId(batchID); }); return result; } - (nullable FSTMaybeDocument *)readDocument:(const DocumentKey &)key { return self.persistence.run("ReadDocument", [&]() -> FSTMaybeDocument *_Nullable { - return [self.localDocuments documentForKey:key]; + return _localDocuments->GetDocument(key); }); } @@ -432,7 +484,7 @@ - (void)releaseQuery:(FSTQuery *)query { - (DocumentMap)executeQuery:(FSTQuery *)query { return self.persistence.run("ExecuteQuery", [&]() -> DocumentMap { - return [self.localDocuments documentsMatchingQuery:query]; + return _localDocuments->GetDocumentsMatchingQuery(query); }); } @@ -465,7 +517,7 @@ - (void)applyBatchResult:(FSTMutationBatchResult *)batchResult { } } - [self.mutationQueue removeMutationBatch:batch]; + _mutationQueue->RemoveMutationBatch(batch); } - (LruResults)collectGarbage:(FSTLRUGarbageCollector *)garbageCollector { diff --git a/Firestore/Source/Local/FSTLocalViewChanges.h b/Firestore/Source/Local/FSTLocalViewChanges.h index dcc20055adf..66ec8631194 100644 --- a/Firestore/Source/Local/FSTLocalViewChanges.h +++ b/Firestore/Source/Local/FSTLocalViewChanges.h @@ -16,14 +16,12 @@ #import +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/types.h" -@class FSTDocumentSet; @class FSTMutation; @class FSTQuery; -@class FSTRemoteEvent; -@class FSTViewSnapshot; NS_ASSUME_NONNULL_BEGIN @@ -38,7 +36,7 @@ NS_ASSUME_NONNULL_BEGIN addedKeys:(firebase::firestore::model::DocumentKeySet)addedKeys removedKeys:(firebase::firestore::model::DocumentKeySet)removedKeys; -+ (instancetype)changesForViewSnapshot:(FSTViewSnapshot *)viewSnapshot ++ (instancetype)changesForViewSnapshot:(const firebase::firestore::core::ViewSnapshot &)viewSnapshot withTargetID:(firebase::firestore::model::TargetId)targetID; - (id)init NS_UNAVAILABLE; diff --git a/Firestore/Source/Local/FSTLocalViewChanges.mm b/Firestore/Source/Local/FSTLocalViewChanges.mm index 19423f2fd15..5169f6b8811 100644 --- a/Firestore/Source/Local/FSTLocalViewChanges.mm +++ b/Firestore/Source/Local/FSTLocalViewChanges.mm @@ -18,10 +18,12 @@ #include -#import "Firestore/Source/Core/FSTViewSnapshot.h" #import "Firestore/Source/Model/FSTDocument.h" -using firebase::firestore::core::DocumentViewChangeType; +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" + +using firebase::firestore::core::DocumentViewChange; +using firebase::firestore::core::ViewSnapshot; using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::TargetId; @@ -38,19 +40,19 @@ @implementation FSTLocalViewChanges { DocumentKeySet _removedKeys; } -+ (instancetype)changesForViewSnapshot:(FSTViewSnapshot *)viewSnapshot ++ (instancetype)changesForViewSnapshot:(const ViewSnapshot &)viewSnapshot withTargetID:(TargetId)targetID { DocumentKeySet addedKeys; DocumentKeySet removedKeys; - for (FSTDocumentViewChange *docChange in viewSnapshot.documentChanges) { - switch (docChange.type) { - case DocumentViewChangeType::kAdded: - addedKeys = addedKeys.insert(docChange.document.key); + for (const DocumentViewChange &docChange : viewSnapshot.document_changes()) { + switch (docChange.type()) { + case DocumentViewChange::Type::kAdded: + addedKeys = addedKeys.insert(docChange.document().key); break; - case DocumentViewChangeType::kRemoved: - removedKeys = removedKeys.insert(docChange.document.key); + case DocumentViewChange::Type::kRemoved: + removedKeys = removedKeys.insert(docChange.document().key); break; default: diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.mm b/Firestore/Source/Local/FSTMemoryMutationQueue.mm deleted file mode 100644 index a8a50732fa3..00000000000 --- a/Firestore/Source/Local/FSTMemoryMutationQueue.mm +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Local/FSTMemoryMutationQueue.h" - -#import - -#include -#include - -#import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Local/FSTMemoryPersistence.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" - -#include "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" -#include "Firestore/core/src/firebase/firestore/local/document_reference.h" -#include "Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/model/resource_path.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "absl/memory/memory.h" - -using firebase::firestore::immutable::SortedSet; -using firebase::firestore::local::DocumentReference; -using firebase::firestore::local::MemoryMutationQueue; -using firebase::firestore::model::BatchId; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::ResourcePath; - -NS_ASSUME_NONNULL_BEGIN - -static NSArray *toNSArray(const std::vector &vec) { - NSMutableArray *copy = [NSMutableArray array]; - for (auto &batch : vec) { - [copy addObject:batch]; - } - return copy; -} - -@interface FSTMemoryMutationQueue () - -- (MemoryMutationQueue *)mutationQueue; - -@end - -@implementation FSTMemoryMutationQueue { - std::unique_ptr _delegate; -} - -- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence { - if (self = [super init]) { - _delegate = absl::make_unique(persistence); - } - return self; -} - -- (void)setLastStreamToken:(NSData *_Nullable)streamToken { - _delegate->SetLastStreamToken(streamToken); -} - -- (NSData *_Nullable)lastStreamToken { - return _delegate->GetLastStreamToken(); -} - -#pragma mark - FSTMutationQueue implementation - -- (void)start { - _delegate->Start(); -} - -- (BOOL)isEmpty { - return _delegate->IsEmpty(); -} - -- (void)acknowledgeBatch:(FSTMutationBatch *)batch streamToken:(nullable NSData *)streamToken { - _delegate->AcknowledgeBatch(batch, streamToken); -} - -- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FIRTimestamp *)localWriteTime - mutations:(NSArray *)mutations { - return _delegate->AddMutationBatch(localWriteTime, mutations); -} - -- (nullable FSTMutationBatch *)lookupMutationBatch:(BatchId)batchID { - return _delegate->LookupMutationBatch(batchID); -} - -- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(BatchId)batchID { - return _delegate->NextMutationBatchAfterBatchId(batchID); -} - -- (NSArray *)allMutationBatches { - return toNSArray(_delegate->AllMutationBatches()); -} - -- (NSArray *)allMutationBatchesAffectingDocumentKey: - (const DocumentKey &)documentKey { - return toNSArray(_delegate->AllMutationBatchesAffectingDocumentKey(documentKey)); -} - -- (NSArray *)allMutationBatchesAffectingDocumentKeys: - (const DocumentKeySet &)documentKeys { - return toNSArray(_delegate->AllMutationBatchesAffectingDocumentKeys(documentKeys)); -} - -- (NSArray *)allMutationBatchesAffectingQuery:(FSTQuery *)query { - return toNSArray(_delegate->AllMutationBatchesAffectingQuery(query)); -} - -- (void)removeMutationBatch:(FSTMutationBatch *)batch { - _delegate->RemoveMutationBatch(batch); -} - -- (void)performConsistencyCheck { - _delegate->PerformConsistencyCheck(); -} - -#pragma mark - FSTGarbageSource implementation - -- (BOOL)containsKey:(const DocumentKey &)key { - return _delegate->ContainsKey(key); -} - -#pragma mark - Helpers - -- (size_t)byteSizeWithSerializer:(FSTLocalSerializer *)serializer { - return _delegate->CalculateByteSize(serializer); -} - -- (MemoryMutationQueue *)mutationQueue { - return _delegate.get(); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryPersistence.mm b/Firestore/Source/Local/FSTMemoryPersistence.mm index e69f9d82502..d60f7e37f69 100644 --- a/Firestore/Source/Local/FSTMemoryPersistence.mm +++ b/Firestore/Source/Local/FSTMemoryPersistence.mm @@ -21,30 +21,35 @@ #include #include -#import "Firestore/Source/Core/FSTListenSequence.h" -#import "Firestore/Source/Local/FSTMemoryMutationQueue.h" -#include "absl/memory/memory.h" - #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/index_manager.h" +#include "Firestore/core/src/firebase/firestore/local/listen_sequence.h" +#include "Firestore/core/src/firebase/firestore/local/memory_index_manager.h" +#include "Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h" #include "Firestore/core/src/firebase/firestore/local/memory_query_cache.h" #include "Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "absl/memory/memory.h" using firebase::firestore::auth::HashUser; using firebase::firestore::auth::User; +using firebase::firestore::local::ListenSequence; using firebase::firestore::local::LruParams; +using firebase::firestore::local::MemoryIndexManager; +using firebase::firestore::local::MemoryMutationQueue; using firebase::firestore::local::MemoryQueryCache; using firebase::firestore::local::MemoryRemoteDocumentCache; using firebase::firestore::local::ReferenceSet; +using firebase::firestore::local::TargetCallback; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeyHash; using firebase::firestore::model::ListenSequenceNumber; using firebase::firestore::model::TargetId; using firebase::firestore::util::Status; -using MutationQueues = std::unordered_map; +using MutationQueues = std::unordered_map, HashUser>; NS_ASSUME_NONNULL_BEGIN @@ -54,6 +59,10 @@ - (MemoryQueryCache *)queryCache; - (MemoryRemoteDocumentCache *)remoteDocumentCache; +- (MemoryIndexManager *)indexManager; + +- (MemoryMutationQueue *)mutationQueueForUser:(const User &)user; + @property(nonatomic, readonly) MutationQueues &mutationQueues; @property(nonatomic, assign, getter=isStarted) BOOL started; @@ -75,7 +84,9 @@ @implementation FSTMemoryPersistence { std::unique_ptr _queryCache; /** The RemoteDocumentCache representing the persisted cache of remote documents. */ - MemoryRemoteDocumentCache _remoteDocumentCache; + std::unique_ptr _remoteDocumentCache; + + MemoryIndexManager _indexManager; FSTTransactionRunner _transactionRunner; @@ -102,6 +113,7 @@ + (instancetype)persistenceWithLruParams:(firebase::firestore::local::LruParams) - (instancetype)init { if (self = [super init]) { _queryCache = absl::make_unique(self); + _remoteDocumentCache = absl::make_unique(self); self.started = YES; } return self; @@ -133,13 +145,14 @@ - (ListenSequenceNumber)currentSequenceNumber { return _transactionRunner; } -- (id)mutationQueueForUser:(const User &)user { - id queue = _mutationQueues[user]; - if (!queue) { - queue = [[FSTMemoryMutationQueue alloc] initWithPersistence:self]; - _mutationQueues[user] = queue; +- (MemoryMutationQueue *)mutationQueueForUser:(const User &)user { + const std::unique_ptr &existing = _mutationQueues[user]; + if (!existing) { + _mutationQueues[user] = absl::make_unique(self); + return _mutationQueues[user].get(); + } else { + return existing.get(); } - return queue; } - (MemoryQueryCache *)queryCache { @@ -147,7 +160,11 @@ - (MemoryQueryCache *)queryCache { } - (MemoryRemoteDocumentCache *)remoteDocumentCache { - return &_remoteDocumentCache; + return _remoteDocumentCache.get(); +} + +- (MemoryIndexManager *)indexManager { + return &_indexManager; } @end @@ -161,7 +178,8 @@ @implementation FSTMemoryLRUReferenceDelegate { std::unordered_map _sequenceNumbers; ReferenceSet *_additionalReferences; FSTLRUGarbageCollector *_gc; - FSTListenSequence *_listenSequence; + // PORTING NOTE: when this class is ported to C++, this does not need to be a pointer + std::unique_ptr _listenSequence; ListenSequenceNumber _currentSequenceNumber; FSTLocalSerializer *_serializer; } @@ -176,7 +194,7 @@ - (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence // Theoretically this is always 0, since this is all in-memory... ListenSequenceNumber highestSequenceNumber = _persistence.queryCache->highest_listen_sequence_number(); - _listenSequence = [[FSTListenSequence alloc] initStartingAfter:highestSequenceNumber]; + _listenSequence = absl::make_unique(highestSequenceNumber); _serializer = serializer; } return self; @@ -210,27 +228,26 @@ - (void)limboDocumentUpdated:(const DocumentKey &)key { } - (void)startTransaction:(absl::string_view)label { - _currentSequenceNumber = [_listenSequence next]; + _currentSequenceNumber = _listenSequence->Next(); } - (void)commitTransaction { _currentSequenceNumber = kFSTListenSequenceNumberInvalid; } -- (void)enumerateTargetsUsingBlock:(void (^)(FSTQueryData *queryData, BOOL *stop))block { - return _persistence.queryCache->EnumerateTargets(block); +- (void)enumerateTargetsUsingCallback:(const TargetCallback &)callback { + return _persistence.queryCache->EnumerateTargets(callback); } -- (void)enumerateMutationsUsingBlock: - (void (^)(const DocumentKey &key, ListenSequenceNumber sequenceNumber, BOOL *stop))block { - BOOL stop = NO; +- (void)enumerateMutationsUsingCallback: + (const firebase::firestore::local::OrphanedDocumentCallback &)callback { for (const auto &entry : _sequenceNumbers) { ListenSequenceNumber sequenceNumber = entry.second; const DocumentKey &key = entry.first; // Pass in the exact sequence number as the upper bound so we know it won't be pinned by being // too recent. if (![self isPinnedAtSequenceNumber:sequenceNumber document:key]) { - block(key, sequenceNumber, &stop); + callback(key, sequenceNumber); } } } @@ -242,9 +259,9 @@ - (int)removeTargetsThroughSequenceNumber:(ListenSequenceNumber)sequenceNumber } - (size_t)sequenceNumberCount { - __block size_t totalCount = _persistence.queryCache->size(); - [self enumerateMutationsUsingBlock:^(const DocumentKey &key, ListenSequenceNumber sequenceNumber, - BOOL *stop) { + size_t totalCount = _persistence.queryCache->size(); + [self enumerateMutationsUsingCallback:[&totalCount](const DocumentKey &key, + ListenSequenceNumber sequenceNumber) { totalCount++; }]; return totalCount; @@ -270,7 +287,7 @@ - (void)removeReference:(const DocumentKey &)key { - (BOOL)mutationQueuesContainKey:(const DocumentKey &)key { const MutationQueues &queues = [_persistence mutationQueues]; for (const auto &entry : queues) { - if ([entry.second containsKey:key]) { + if (entry.second->ContainsKey(key)) { return YES; } } @@ -308,7 +325,7 @@ - (size_t)byteSize { count += _persistence.remoteDocumentCache->CalculateByteSize(_serializer); const MutationQueues &queues = [_persistence mutationQueues]; for (const auto &entry : queues) { - count += [entry.second byteSizeWithSerializer:_serializer]; + count += entry.second->CalculateByteSize(_serializer); } return count; } @@ -387,7 +404,7 @@ - (void)startTransaction:(__unused absl::string_view)label { - (BOOL)mutationQueuesContainKey:(const DocumentKey &)key { const MutationQueues &queues = [_persistence mutationQueues]; for (const auto &entry : queues) { - if ([entry.second containsKey:key]) { + if (entry.second->ContainsKey(key)) { return YES; } } diff --git a/Firestore/Source/Local/FSTMutationQueue.h b/Firestore/Source/Local/FSTMutationQueue.h deleted file mode 100644 index 6540e745b20..00000000000 --- a/Firestore/Source/Local/FSTMutationQueue.h +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" - -@class FSTMutation; -@class FSTMutationBatch; -@class FSTQuery; -@class FIRTimestamp; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTMutationQueue - -/** A queue of mutations to apply to the remote store. */ -@protocol FSTMutationQueue - -/** - * Starts the mutation queue, performing any initial reads that might be required to establish - * invariants, etc. - */ -- (void)start; - -/** Returns YES if this queue contains no mutation batches. */ -- (BOOL)isEmpty; - -/** Acknowledges the given batch. */ -- (void)acknowledgeBatch:(FSTMutationBatch *)batch streamToken:(nullable NSData *)streamToken; - -/** Returns the current stream token for this mutation queue. */ -- (nullable NSData *)lastStreamToken; - -/** Sets the stream token for this mutation queue. */ -- (void)setLastStreamToken:(nullable NSData *)streamToken; - -/** Creates a new mutation batch and adds it to this mutation queue. */ -- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FIRTimestamp *)localWriteTime - mutations:(NSArray *)mutations; - -/** Loads the mutation batch with the given batchID. */ -- (nullable FSTMutationBatch *)lookupMutationBatch:(firebase::firestore::model::BatchId)batchID; - -/** - * Gets the first unacknowledged mutation batch after the passed in batchId in the mutation queue - * or nil if empty. - * - * @param batchID The batch to search after, or kBatchIdUnknown for the first mutation in the - * queue. - * - * @return the next mutation or nil if there wasn't one. - */ -- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID: - (firebase::firestore::model::BatchId)batchID; - -/** Gets all mutation batches in the mutation queue. */ -// TODO(mikelehen): PERF: Current consumer only needs mutated keys; if we can provide that -// cheaply, we should replace this. -- (NSArray *)allMutationBatches; - -/** - * Finds all mutation batches that could @em possibly affect the given document key. Not all - * mutations in a batch will necessarily affect the document key, so when looping through the - * batch you'll need to check that the mutation itself matches the key. - * - * Note that because of this requirement implementations are free to return mutation batches that - * don't contain the document key at all if it's convenient. - */ -// TODO(mcg): This should really return an NSEnumerator -- (NSArray *)allMutationBatchesAffectingDocumentKey: - (const firebase::firestore::model::DocumentKey &)documentKey; - -/** - * Finds all mutation batches that could @em possibly affect the given document keys. Not all - * mutations in a batch will necessarily affect each key, so when looping through the batches you'll - * need to check that the mutation itself matches the key. - * - * Note that because of this requirement implementations are free to return mutation batches that - * don't contain any of the given document keys at all if it's convenient. - */ -// TODO(mcg): This should really return an NSEnumerator -- (NSArray *)allMutationBatchesAffectingDocumentKeys: - (const firebase::firestore::model::DocumentKeySet &)documentKeys; - -/** - * Finds all mutation batches that could affect the results for the given query. Not all - * mutations in a batch will necessarily affect the query, so when looping through the batch - * you'll need to check that the mutation itself matches the query. - * - * Note that because of this requirement implementations are free to return mutation batches that - * don't match the query at all if it's convenient. - * - * NOTE: A FSTPatchMutation does not need to include all fields in the query filter criteria in - * order to be a match (but any fields it does contain do need to match). - */ -// TODO(mikelehen): This should perhaps return an NSEnumerator, though I'm not sure we can avoid -// loading them all in memory. -- (NSArray *)allMutationBatchesAffectingQuery:(FSTQuery *)query; - -/** - * Removes the given mutation batch from the queue. This is useful in two circumstances: - * - * + Removing applied mutations from the head of the queue - * + Removing rejected mutations from anywhere in the queue - */ -- (void)removeMutationBatch:(FSTMutationBatch *)batch; - -/** Performs a consistency check, examining the mutation queue for any leaks, if possible. */ -- (void)performConsistencyCheck; - -// Visible for testing -- (firebase::firestore::local::MutationQueue *)mutationQueue; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTPersistence.h b/Firestore/Source/Local/FSTPersistence.h index e44ee6500d6..8595f107a1e 100644 --- a/Firestore/Source/Local/FSTPersistence.h +++ b/Firestore/Source/Local/FSTPersistence.h @@ -17,6 +17,8 @@ #import #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/index_manager.h" +#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" #include "Firestore/core/src/firebase/firestore/local/query_cache.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" @@ -26,7 +28,6 @@ #include "Firestore/core/src/firebase/firestore/util/status.h" @class FSTQueryData; -@protocol FSTMutationQueue; @protocol FSTReferenceDelegate; struct FSTTransactionRunner; @@ -69,21 +70,25 @@ NS_ASSUME_NONNULL_BEGIN - (void)shutdown; /** - * Returns an FSTMutationQueue representing the persisted mutations for the given user. + * Returns a MutationQueue representing the persisted mutations for the given user. * *

Note: The implementation is free to return the same instance every time this is called for a * given user. In particular, the memory-backed implementation does this to emulate the persisted * implementation to the extent possible (e.g. in the case of uid switching from * sally=>jack=>sally, sally's mutation queue will be preserved). */ -- (id)mutationQueueForUser:(const firebase::firestore::auth::User &)user; +- (firebase::firestore::local::MutationQueue *)mutationQueueForUser: + (const firebase::firestore::auth::User &)user; /** Creates an FSTQueryCache representing the persisted cache of queries. */ - (firebase::firestore::local::QueryCache *)queryCache; -/** Creates an FSTRemoteDocumentCache representing the persisted cache of remote documents. */ +/** Creates a RemoteDocumentCache representing the persisted cache of remote documents. */ - (firebase::firestore::local::RemoteDocumentCache *)remoteDocumentCache; +/** Creates an IndexManager that manages our persisted query indexes. */ +- (firebase::firestore::local::IndexManager *)indexManager; + @property(nonatomic, readonly, assign) const FSTTransactionRunner &run; /** diff --git a/Firestore/Source/Model/FSTDocument.h b/Firestore/Source/Model/FSTDocument.h index add79dc9309..acbec89d528 100644 --- a/Firestore/Source/Model/FSTDocument.h +++ b/Firestore/Source/Model/FSTDocument.h @@ -48,7 +48,7 @@ typedef NS_ENUM(NSInteger, FSTDocumentState) { /** * Whether this document has a local mutation applied that has not yet been acknowledged by Watch. */ -- (BOOL)hasPendingWrites; +- (bool)hasPendingWrites; @end @@ -65,8 +65,8 @@ typedef NS_ENUM(NSInteger, FSTDocumentState) { proto:(GCFSDocument *)proto; - (nullable FSTFieldValue *)fieldForPath:(const firebase::firestore::model::FieldPath &)path; -- (BOOL)hasLocalMutations; -- (BOOL)hasCommittedMutations; +- (bool)hasLocalMutations; +- (bool)hasCommittedMutations; @property(nonatomic, strong, readonly) FSTObjectValue *data; @@ -81,9 +81,9 @@ typedef NS_ENUM(NSInteger, FSTDocumentState) { @interface FSTDeletedDocument : FSTMaybeDocument + (instancetype)documentWithKey:(firebase::firestore::model::DocumentKey)key version:(firebase::firestore::model::SnapshotVersion)version - hasCommittedMutations:(BOOL)committedMutations; + hasCommittedMutations:(bool)committedMutations; -- (BOOL)hasCommittedMutations; +- (bool)hasCommittedMutations; @end diff --git a/Firestore/Source/Model/FSTDocument.mm b/Firestore/Source/Model/FSTDocument.mm index 40f5666f641..1e35d5bfd7f 100644 --- a/Firestore/Source/Model/FSTDocument.mm +++ b/Firestore/Source/Model/FSTDocument.mm @@ -55,7 +55,7 @@ - (instancetype)initWithKey:(DocumentKey)key version:(SnapshotVersion)version { return self; } -- (BOOL)hasPendingWrites { +- (bool)hasPendingWrites { @throw FSTAbstractMethodException(); // NOLINT } @@ -127,15 +127,15 @@ - (instancetype)initWithData:(FSTObjectValue *)data return self; } -- (BOOL)hasLocalMutations { +- (bool)hasLocalMutations { return _documentState == FSTDocumentStateLocalMutations; } -- (BOOL)hasCommittedMutations { +- (bool)hasCommittedMutations { return _documentState == FSTDocumentStateCommittedMutations; } -- (BOOL)hasPendingWrites { +- (bool)hasPendingWrites { return self.hasLocalMutations || self.hasCommittedMutations; } @@ -153,7 +153,7 @@ - (BOOL)isEqual:(id)other { } - (NSUInteger)hash { - NSUInteger result = [self.key hash]; + NSUInteger result = self.key.Hash(); result = result * 31 + self.version.Hash(); result = result * 31 + [self.data hash]; result = result * 31 + _documentState; @@ -174,12 +174,12 @@ - (nullable FSTFieldValue *)fieldForPath:(const FieldPath &)path { @end @implementation FSTDeletedDocument { - BOOL _hasCommittedMutations; + bool _hasCommittedMutations; } + (instancetype)documentWithKey:(DocumentKey)key version:(SnapshotVersion)version - hasCommittedMutations:(BOOL)committedMutations { + hasCommittedMutations:(bool)committedMutations { FSTDeletedDocument *deletedDocument = [[FSTDeletedDocument alloc] initWithKey:std::move(key) version:std::move(version)]; @@ -190,11 +190,11 @@ + (instancetype)documentWithKey:(DocumentKey)key return deletedDocument; } -- (BOOL)hasCommittedMutations { +- (bool)hasCommittedMutations { return _hasCommittedMutations; } -- (BOOL)hasPendingWrites { +- (bool)hasPendingWrites { return self.hasCommittedMutations; } @@ -212,7 +212,7 @@ - (BOOL)isEqual:(id)other { } - (NSUInteger)hash { - NSUInteger result = [self.key hash]; + NSUInteger result = self.key.Hash(); result = result * 31 + self.version.Hash(); result = result * 31 + (_hasCommittedMutations ? 1 : 0); return result; @@ -233,8 +233,8 @@ + (instancetype)documentWithKey:(DocumentKey)key version:(SnapshotVersion)versio return [[FSTUnknownDocument alloc] initWithKey:std::move(key) version:std::move(version)]; } -- (BOOL)hasPendingWrites { - return YES; +- (bool)hasPendingWrites { + return true; } - (BOOL)isEqual:(id)other { @@ -250,7 +250,7 @@ - (BOOL)isEqual:(id)other { } - (NSUInteger)hash { - NSUInteger result = [self.key hash]; + NSUInteger result = self.key.Hash(); result = result * 31 + self.version.Hash(); return result; } diff --git a/Firestore/Source/Model/FSTDocumentSet.h b/Firestore/Source/Model/FSTDocumentSet.h deleted file mode 100644 index 2087a3da59a..00000000000 --- a/Firestore/Source/Model/FSTDocumentSet.h +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/model/document_map.h" - -@class FSTDocument; - -NS_ASSUME_NONNULL_BEGIN - -/** - * DocumentSet is an immutable (copy-on-write) collection that holds documents in order specified - * by the provided comparator. We always add a document key comparator on top of what is provided - * to guarantee document equality based on the key. - */ -@interface FSTDocumentSet : NSObject - -/** Creates a new, empty FSTDocumentSet sorted by the given comparator, then by keys. */ -+ (instancetype)documentSetWithComparator:(NSComparator)comparator; - -- (instancetype)init __attribute__((unavailable("Use a static constructor instead"))); - -- (NSUInteger)count; - -/** Returns true if the dictionary contains no elements. */ -- (BOOL)isEmpty; - -/** Returns YES if this set contains a document with the given key. */ -- (BOOL)containsKey:(const firebase::firestore::model::DocumentKey &)key; - -/** Returns the document from this set with the given key if it exists or nil if it doesn't. */ -- (FSTDocument *_Nullable)documentForKey:(const firebase::firestore::model::DocumentKey &)key; - -/** - * Returns the first document in the set according to its built in ordering, or nil if the set - * is empty. - */ -- (FSTDocument *_Nullable)firstDocument; - -/** - * Returns the last document in the set according to its built in ordering, or nil if the set - * is empty. - */ -- (FSTDocument *_Nullable)lastDocument; - -/** - * Returns the index of the document with the provided key in the document set. Returns NSNotFound - * if the key is not present. - */ -- (NSUInteger)indexOfKey:(const firebase::firestore::model::DocumentKey &)key; - -- (NSEnumerator *)documentEnumerator; - -/** Returns a copy of the documents in this set as an array. This is O(n) on the size of the set. */ -- (NSArray *)arrayValue; - -/** - * Returns the documents as a `DocumentMap`. This is O(1) as this leverages - * our internal representation. - */ -- (const firebase::firestore::model::DocumentMap &)mapValue; - -/** Returns a new FSTDocumentSet that contains the given document. */ -- (instancetype)documentSetByAddingDocument:(FSTDocument *_Nullable)document; - -/** Returns a new FSTDocumentSet that excludes any document associated with the given key. */ -- (instancetype)documentSetByRemovingKey:(const firebase::firestore::model::DocumentKey &)key; -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentSet.mm b/Firestore/Source/Model/FSTDocumentSet.mm deleted file mode 100644 index c87e2039e54..00000000000 --- a/Firestore/Source/Model/FSTDocumentSet.mm +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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. - */ - -#include - -#import "Firestore/Source/Model/FSTDocumentSet.h" - -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/third_party/Immutable/FSTImmutableSortedSet.h" - -#include "Firestore/core/src/firebase/firestore/model/document_key.h" - -using firebase::firestore::model::DocumentMap; -using firebase::firestore::model::DocumentKey; - -NS_ASSUME_NONNULL_BEGIN - -/** - * The type of the main collection of documents in an FSTDocumentSet. - * @see FSTDocumentSet#sortedSet - */ -typedef FSTImmutableSortedSet SetType; - -@interface FSTDocumentSet () - -- (instancetype)initWithIndex:(DocumentMap &&)index - set:(SetType *)sortedSet NS_DESIGNATED_INITIALIZER; - -/** - * The main collection of documents in the FSTDocumentSet. The documents are ordered by a - * comparator supplied from a query. The SetType collection exists in addition to the index to - * allow ordered traversal of the FSTDocumentSet. - */ -@property(nonatomic, strong, readonly) SetType *sortedSet; -@end - -@implementation FSTDocumentSet { - /** - * An index of the documents in the FSTDocumentSet, indexed by document key. The index - * exists to guarantee the uniqueness of document keys in the set and to allow lookup and removal - * of documents by key. - */ - DocumentMap _index; -} - -+ (instancetype)documentSetWithComparator:(NSComparator)comparator { - SetType *set = [FSTImmutableSortedSet setWithComparator:comparator]; - return [[FSTDocumentSet alloc] initWithIndex:DocumentMap {} set:set]; -} - -- (instancetype)initWithIndex:(DocumentMap &&)index set:(SetType *)sortedSet { - self = [super init]; - if (self) { - _index = std::move(index); - _sortedSet = sortedSet; - } - return self; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isMemberOfClass:[FSTDocumentSet class]]) { - return NO; - } - - FSTDocumentSet *otherSet = (FSTDocumentSet *)other; - if ([self count] != [otherSet count]) { - return NO; - } - - NSEnumerator *selfIter = [self.sortedSet objectEnumerator]; - NSEnumerator *otherIter = [otherSet.sortedSet objectEnumerator]; - - FSTDocument *selfDoc = [selfIter nextObject]; - FSTDocument *otherDoc = [otherIter nextObject]; - while (selfDoc) { - if (![selfDoc isEqual:otherDoc]) { - return NO; - } - selfDoc = [selfIter nextObject]; - otherDoc = [otherIter nextObject]; - } - return YES; -} - -- (NSUInteger)hash { - NSUInteger hash = 0; - for (FSTDocument *doc in self.sortedSet.objectEnumerator) { - hash = 31 * hash + [doc hash]; - } - return hash; -} - -- (NSString *)description { - return [self.sortedSet description]; -} - -- (NSUInteger)count { - return _index.size(); -} - -- (BOOL)isEmpty { - return _index.empty(); -} - -- (BOOL)containsKey:(const DocumentKey &)key { - return _index.underlying_map().find(key) != _index.underlying_map().end(); -} - -- (FSTDocument *_Nullable)documentForKey:(const DocumentKey &)key { - auto found = _index.underlying_map().find(key); - return found != _index.underlying_map().end() ? static_cast(found->second) : nil; -} - -- (FSTDocument *_Nullable)firstDocument { - return [self.sortedSet firstObject]; -} - -- (FSTDocument *_Nullable)lastDocument { - return [self.sortedSet lastObject]; -} - -- (NSUInteger)indexOfKey:(const DocumentKey &)key { - FSTDocument *doc = [self documentForKey:key]; - return doc ? [self.sortedSet indexOfObject:doc] : NSNotFound; -} - -- (NSEnumerator *)documentEnumerator { - return [self.sortedSet objectEnumerator]; -} - -- (NSArray *)arrayValue { - NSMutableArray *result = [NSMutableArray arrayWithCapacity:self.count]; - for (FSTDocument *doc in self.documentEnumerator) { - [result addObject:doc]; - } - return result; -} - -- (const DocumentMap &)mapValue { - return _index; -} - -- (instancetype)documentSetByAddingDocument:(FSTDocument *_Nullable)document { - // TODO(mcg): look into making document nonnull. - if (!document) { - return self; - } - - // Remove any prior mapping of the document's key before adding, preventing sortedSet from - // accumulating values that aren't in the index. - FSTDocumentSet *removed = [self documentSetByRemovingKey:document.key]; - - DocumentMap index = removed->_index.insert(document.key, document); - SetType *set = [removed.sortedSet setByAddingObject:document]; - return [[FSTDocumentSet alloc] initWithIndex:std::move(index) set:set]; -} - -- (instancetype)documentSetByRemovingKey:(const DocumentKey &)key { - FSTDocument *doc = [self documentForKey:key]; - if (!doc) { - return self; - } - - DocumentMap index = _index.erase(key); - SetType *set = [self.sortedSet setByRemovingObject:doc]; - return [[FSTDocumentSet alloc] initWithIndex:std::move(index) set:set]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTFieldValue.h b/Firestore/Source/Model/FSTFieldValue.h index ae0d5b72552..5a9b696ce08 100644 --- a/Firestore/Source/Model/FSTFieldValue.h +++ b/Firestore/Source/Model/FSTFieldValue.h @@ -19,6 +19,7 @@ #import "Firestore/third_party/Immutable/FSTImmutableSortedDictionary.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" +#include "Firestore/core/src/firebase/firestore/model/field_mask.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" @class FSTDocumentKey; @@ -43,16 +44,12 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { }; /** Defines the return value for pending server timestamps. */ -typedef NS_ENUM(NSInteger, FSTServerTimestampBehavior) { - FSTServerTimestampBehaviorNone, - FSTServerTimestampBehaviorEstimate, - FSTServerTimestampBehaviorPrevious -}; +enum class ServerTimestampBehavior { None, Estimate, Previous }; /** Holds properties that define field value deserialization options. */ @interface FSTFieldValueOptions : NSObject -@property(nonatomic, readonly, assign) FSTServerTimestampBehavior serverTimestampBehavior; +@property(nonatomic, readonly, assign) ServerTimestampBehavior serverTimestampBehavior; @property(nonatomic) BOOL timestampsInSnapshotsEnabled; @@ -62,7 +59,7 @@ typedef NS_ENUM(NSInteger, FSTServerTimestampBehavior) { * Creates an FSTFieldValueOptions instance that specifies deserialization behavior for pending * server timestamps. */ -- (instancetype)initWithServerTimestampBehavior:(FSTServerTimestampBehavior)serverTimestampBehavior +- (instancetype)initWithServerTimestampBehavior:(ServerTimestampBehavior)serverTimestampBehavior timestampsInSnapshotsEnabled:(BOOL)timestampsInSnapshotsEnabled NS_DESIGNATED_INITIALIZER; @@ -251,6 +248,14 @@ typedef NS_ENUM(NSInteger, FSTServerTimestampBehavior) { * path does not exist within this object's structure, no change is performed. */ - (FSTObjectValue *)objectByDeletingPath:(const firebase::firestore::model::FieldPath &)fieldPath; + +/** + * Applies this field mask to the provided object value and returns an object that only contains + * fields that are specified in both the input object and this field mask. + */ +// TODO(mrschmidt): Once FieldValues are C++, move this to FieldMask to match other platforms. +- (FSTObjectValue *)objectByApplyingFieldMask: + (const firebase::firestore::model::FieldMask &)fieldMask; @end /** diff --git a/Firestore/Source/Model/FSTFieldValue.mm b/Firestore/Source/Model/FSTFieldValue.mm index 6a790102934..7730f3987c1 100644 --- a/Firestore/Source/Model/FSTFieldValue.mm +++ b/Firestore/Source/Model/FSTFieldValue.mm @@ -32,6 +32,7 @@ namespace util = firebase::firestore::util; using firebase::firestore::model::DatabaseId; +using firebase::firestore::model::FieldMask; using firebase::firestore::model::FieldPath; using firebase::firestore::util::Comparator; using firebase::firestore::util::CompareMixedNumber; @@ -47,7 +48,7 @@ @implementation FSTFieldValueOptions -- (instancetype)initWithServerTimestampBehavior:(FSTServerTimestampBehavior)serverTimestampBehavior +- (instancetype)initWithServerTimestampBehavior:(ServerTimestampBehavior)serverTimestampBehavior timestampsInSnapshotsEnabled:(BOOL)timestampsInSnapshotsEnabled { self = [super init]; @@ -494,11 +495,11 @@ - (id)value { - (id)valueWithOptions:(FSTFieldValueOptions *)options { switch (options.serverTimestampBehavior) { - case FSTServerTimestampBehaviorNone: + case ServerTimestampBehavior::None: return [NSNull null]; - case FSTServerTimestampBehaviorEstimate: + case ServerTimestampBehavior::Estimate: return [[FSTTimestampValue timestampValue:self.localWriteTime] valueWithOptions:options]; - case FSTServerTimestampBehaviorPrevious: + case ServerTimestampBehavior::Previous: return self.previousValue ? [self.previousValue valueWithOptions:options] : [NSNull null]; default: HARD_FAIL("Unexpected server timestamp option: %s", options.serverTimestampBehavior); @@ -873,6 +874,21 @@ - (FSTObjectValue *)objectBySettingValue:(FSTFieldValue *)value forField:(NSStri initWithImmutableDictionary:[_internalValue dictionaryBySettingObject:value forKey:field]]; } +- (FSTObjectValue *)objectByApplyingFieldMask:(const FieldMask &)fieldMask { + FSTObjectValue *filteredObject = self; + for (const FieldPath &path : fieldMask) { + if (path.empty()) { + return self; + } else { + FSTFieldValue *newValue = [self valueForPath:path]; + if (newValue) { + filteredObject = [filteredObject objectBySettingValue:newValue forPath:path]; + } + } + } + return filteredObject; +} + @end @interface FSTArrayValue () diff --git a/Firestore/Source/Model/FSTMutation.h b/Firestore/Source/Model/FSTMutation.h index 4eb22bc754a..6eb38e23de7 100644 --- a/Firestore/Source/Model/FSTMutation.h +++ b/Firestore/Source/Model/FSTMutation.h @@ -156,6 +156,16 @@ NS_ASSUME_NONNULL_BEGIN - (const firebase::firestore::model::Precondition &)precondition; +/** + * If applicable, returns the field mask for this mutation. Fields that are not included in this + * field mask are not modified when this mutation is applied. Mutations that replace all document + * values return 'nullptr'. + */ +- (const firebase::firestore::model::FieldMask *)fieldMask; + +/** Returns whether all operations in the mutation are idempotent. */ +@property(nonatomic, readonly) BOOL idempotent; + @end #pragma mark - FSTSetMutation @@ -224,7 +234,7 @@ NS_ASSUME_NONNULL_BEGIN * A mask to apply to |value|, where only fields that are in both the fieldMask and the value * will be updated. */ -- (const firebase::firestore::model::FieldMask &)fieldMask; +- (const firebase::firestore::model::FieldMask *)fieldMask; /** The fields and associated values to use when patching the document. */ @property(nonatomic, strong, readonly) FSTObjectValue *value; diff --git a/Firestore/Source/Model/FSTMutation.mm b/Firestore/Source/Model/FSTMutation.mm index fb74a828450..a805671636e 100644 --- a/Firestore/Source/Model/FSTMutation.mm +++ b/Firestore/Source/Model/FSTMutation.mm @@ -17,6 +17,7 @@ #import "Firestore/Source/Model/FSTMutation.h" #include +#include #include #include #include @@ -103,6 +104,14 @@ - (nullable FSTMaybeDocument *)applyToLocalDocument:(nullable FSTMaybeDocument * return _precondition; } +- (BOOL)idempotent { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (const FieldMask *)fieldMask { + @throw FSTAbstractMethodException(); // NOLINT +} + - (void)verifyKeyMatches:(nullable FSTMaybeDocument *)maybeDoc { if (maybeDoc) { HARD_ASSERT(maybeDoc.key == self.key, "Can only set a document with the same key"); @@ -185,6 +194,15 @@ - (FSTMaybeDocument *)applyToRemoteDocument:(nullable FSTMaybeDocument *)maybeDo version:mutationResult.version state:FSTDocumentStateCommittedMutations]; } + +- (const FieldMask *)fieldMask { + return nullptr; +} + +- (BOOL)idempotent { + return YES; +} + @end #pragma mark - FSTPatchMutation @@ -205,8 +223,8 @@ - (instancetype)initWithKey:(DocumentKey)key return self; } -- (const firebase::firestore::model::FieldMask &)fieldMask { - return _fieldMask; +- (const FieldMask *)fieldMask { + return &_fieldMask; } - (BOOL)isEqual:(id)other { @@ -218,18 +236,18 @@ - (BOOL)isEqual:(id)other { } FSTPatchMutation *otherMutation = (FSTPatchMutation *)other; - return self.key == otherMutation.key && self.fieldMask == otherMutation.fieldMask && + return self.key == otherMutation.key && _fieldMask == *(otherMutation.fieldMask) && [self.value isEqual:otherMutation.value] && self.precondition == otherMutation.precondition; } - (NSUInteger)hash { - return Hash(self.key, self.precondition, self.fieldMask, [self.value hash]); + return Hash(self.key, self.precondition, _fieldMask, [self.value hash]); } - (NSString *)description { return [NSString stringWithFormat:@"", - self.key.ToString().c_str(), self.fieldMask.ToString().c_str(), + self.key.ToString().c_str(), _fieldMask.ToString().c_str(), self.value, self.precondition.description()]; } @@ -288,7 +306,7 @@ - (FSTMaybeDocument *)applyToRemoteDocument:(nullable FSTMaybeDocument *)maybeDo - (FSTObjectValue *)patchObjectValue:(FSTObjectValue *)objectValue { FSTObjectValue *result = objectValue; - for (const FieldPath &fieldPath : self.fieldMask) { + for (const FieldPath &fieldPath : _fieldMask) { if (!fieldPath.empty()) { FSTFieldValue *newValue = [self.value valueForPath:fieldPath]; if (newValue) { @@ -301,11 +319,16 @@ - (FSTObjectValue *)patchObjectValue:(FSTObjectValue *)objectValue { return result; } +- (BOOL)idempotent { + return YES; +} + @end @implementation FSTTransformMutation { /** The field transforms to use when transforming the document. */ std::vector _fieldTransforms; + FieldMask _fieldMask; } - (instancetype)initWithKey:(DocumentKey)key @@ -315,6 +338,13 @@ - (instancetype)initWithKey:(DocumentKey)key // end up with an existing document. if (self = [super initWithKey:std::move(key) precondition:Precondition::Exists(true)]) { _fieldTransforms = std::move(fieldTransforms); + + std::set fields; + for (const auto &transform : _fieldTransforms) { + fields.insert(transform.path()); + } + + _fieldMask = FieldMask(std::move(fields)); } return self; } @@ -482,6 +512,19 @@ - (FSTObjectValue *)transformObject:(FSTObjectValue *)objectValue return objectValue; } +- (const FieldMask *)fieldMask { + return &_fieldMask; +} + +- (BOOL)idempotent { + for (const auto &transform : self.fieldTransforms) { + if (!transform.idempotent()) { + return NO; + } + } + return YES; +} + @end #pragma mark - FSTDeleteMutation @@ -542,6 +585,14 @@ - (FSTMaybeDocument *)applyToRemoteDocument:(nullable FSTMaybeDocument *)maybeDo hasCommittedMutations:YES]; } +- (const FieldMask *)fieldMask { + return nullptr; +} + +- (BOOL)idempotent { + return YES; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutationBatch.h b/Firestore/Source/Model/FSTMutationBatch.h index 0aace1d323f..af07fcbcba2 100644 --- a/Firestore/Source/Model/FSTMutationBatch.h +++ b/Firestore/Source/Model/FSTMutationBatch.h @@ -17,9 +17,11 @@ #import #include +#include #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_map.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" @@ -49,10 +51,14 @@ NS_ASSUME_NONNULL_BEGIN */ @interface FSTMutationBatch : NSObject -/** Initializes a mutation batch with the given batchID, localWriteTime, and mutations. */ +/** + * Initializes a mutation batch with the given batchID, localWriteTime, base mutations, and + * mutations. + */ - (instancetype)initWithBatchID:(firebase::firestore::model::BatchId)batchID localWriteTime:(FIRTimestamp *)localWriteTime - mutations:(NSArray *)mutations NS_DESIGNATED_INITIALIZER; + baseMutations:(std::vector &&)baseMutations + mutations:(std::vector &&)mutations NS_DESIGNATED_INITIALIZER; - (id)init NS_UNAVAILABLE; @@ -79,12 +85,31 @@ NS_ASSUME_NONNULL_BEGIN applyToLocalDocument:(FSTMaybeDocument *_Nullable)maybeDoc documentKey:(const firebase::firestore::model::DocumentKey &)documentKey; +/** Computes the local view for all provided documents given the mutations in this batch. */ +- (firebase::firestore::model::MaybeDocumentMap)applyToLocalDocumentSet: + (const firebase::firestore::model::MaybeDocumentMap &)documentSet; + /** Returns the set of unique keys referenced by all mutations in the batch. */ - (firebase::firestore::model::DocumentKeySet)keys; +/** The unique ID of this mutation batch. */ @property(nonatomic, assign, readonly) firebase::firestore::model::BatchId batchID; + +/** The original write time of this mutation. */ @property(nonatomic, strong, readonly) FIRTimestamp *localWriteTime; -@property(nonatomic, strong, readonly) NSArray *mutations; + +/** + * Mutations that are used to populate the base values when this mutation is applied locally. This + * can be used to locally overwrite values that are persisted in the remote document cache. Base + * mutations are never sent to the backend. + */ +- (const std::vector &)baseMutations; + +/** + * The user-provided mutations in this mutation batch. User-provided mutations are applied both + * locally and remotely on the backend. + */ +- (const std::vector &)mutations; @end @@ -102,13 +127,13 @@ NS_ASSUME_NONNULL_BEGIN */ + (instancetype)resultWithBatch:(FSTMutationBatch *)batch commitVersion:(firebase::firestore::model::SnapshotVersion)commitVersion - mutationResults:(NSArray *)mutationResults + mutationResults:(std::vector)mutationResults streamToken:(nullable NSData *)streamToken; - (const firebase::firestore::model::SnapshotVersion &)commitVersion; +- (const std::vector &)mutationResults; @property(nonatomic, strong, readonly) FSTMutationBatch *batch; -@property(nonatomic, strong, readonly) NSArray *mutationResults; @property(nonatomic, strong, readonly, nullable) NSData *streamToken; - (const firebase::firestore::model::DocumentVersionMap &)docVersions; diff --git a/Firestore/Source/Model/FSTMutationBatch.mm b/Firestore/Source/Model/FSTMutationBatch.mm index aad425f3f62..2d1e2c3ffa4 100644 --- a/Firestore/Source/Model/FSTMutationBatch.mm +++ b/Firestore/Source/Model/FSTMutationBatch.mm @@ -16,6 +16,7 @@ #import "Firestore/Source/Model/FSTMutationBatch.h" +#include #include #import "FIRTimestamp.h" @@ -23,32 +24,51 @@ #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTMutation.h" +#include "Firestore/core/src/firebase/firestore/model/document_map.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" +#include "Firestore/core/src/firebase/firestore/util/objc_compatibility.h" +namespace objc = firebase::firestore::util::objc; using firebase::firestore::model::BatchId; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeyHash; using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::DocumentVersionMap; +using firebase::firestore::model::MaybeDocumentMap; using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::util::Hash; NS_ASSUME_NONNULL_BEGIN -@implementation FSTMutationBatch +@implementation FSTMutationBatch { + std::vector _baseMutations; + std::vector _mutations; +} - (instancetype)initWithBatchID:(BatchId)batchID localWriteTime:(FIRTimestamp *)localWriteTime - mutations:(NSArray *)mutations { - HARD_ASSERT(mutations.count != 0, "Cannot create an empty mutation batch"); + baseMutations:(std::vector &&)baseMutations + mutations:(std::vector &&)mutations { + HARD_ASSERT(!mutations.empty(), "Cannot create an empty mutation batch"); self = [super init]; if (self) { _batchID = batchID; _localWriteTime = localWriteTime; - _mutations = mutations; + _baseMutations = std::move(baseMutations); + _mutations = std::move(mutations); } return self; } +- (const std::vector &)baseMutations { + return _baseMutations; +} + +- (const std::vector &)mutations { + return _mutations; +} + - (BOOL)isEqual:(id)other { if (self == other) { return YES; @@ -59,19 +79,26 @@ - (BOOL)isEqual:(id)other { FSTMutationBatch *otherBatch = (FSTMutationBatch *)other; return self.batchID == otherBatch.batchID && [self.localWriteTime isEqual:otherBatch.localWriteTime] && - [self.mutations isEqual:otherBatch.mutations]; + objc::Equals(_baseMutations, otherBatch.baseMutations) && + objc::Equals(_mutations, otherBatch.mutations); } - (NSUInteger)hash { NSUInteger result = (NSUInteger)self.batchID; result = result * 31 + self.localWriteTime.hash; - result = result * 31 + self.mutations.hash; + for (FSTMutation *mutation : _baseMutations) { + result = result * 31 + [mutation hash]; + } + for (FSTMutation *mutation : _mutations) { + result = result * 31 + [mutation hash]; + } return result; } - (NSString *)description { - return [NSString stringWithFormat:@"", - self.batchID, self.localWriteTime, self.mutations]; + return + [NSString stringWithFormat:@"", + self.batchID, self.localWriteTime, objc::Description(_mutations)]; } - (FSTMaybeDocument *_Nullable)applyToRemoteDocument:(FSTMaybeDocument *_Nullable)maybeDoc @@ -82,12 +109,12 @@ - (FSTMaybeDocument *_Nullable)applyToRemoteDocument:(FSTMaybeDocument *_Nullabl "applyTo: key %s doesn't match maybeDoc key %s", documentKey.ToString(), maybeDoc.key.ToString()); - HARD_ASSERT(mutationBatchResult.mutationResults.count == self.mutations.count, - "Mismatch between mutations length (%s) and results length (%s)", - self.mutations.count, mutationBatchResult.mutationResults.count); + HARD_ASSERT(mutationBatchResult.mutationResults.size() == _mutations.size(), + "Mismatch between mutations length (%s) and results length (%s)", _mutations.size(), + mutationBatchResult.mutationResults.size()); - for (NSUInteger i = 0; i < self.mutations.count; i++) { - FSTMutation *mutation = self.mutations[i]; + for (size_t i = 0; i < _mutations.size(); i++) { + FSTMutation *mutation = _mutations[i]; FSTMutationResult *mutationResult = mutationBatchResult.mutationResults[i]; if (mutation.key == documentKey) { maybeDoc = [mutation applyToRemoteDocument:maybeDoc mutationResult:mutationResult]; @@ -101,10 +128,21 @@ - (FSTMaybeDocument *_Nullable)applyToLocalDocument:(FSTMaybeDocument *_Nullable HARD_ASSERT(!maybeDoc || maybeDoc.key == documentKey, "applyTo: key %s doesn't match maybeDoc key %s", documentKey.ToString(), maybeDoc.key.ToString()); + + // First, apply the base state. This allows us to apply non-idempotent transform against a + // consistent set of values. + for (FSTMutation *mutation : _baseMutations) { + if (mutation.key == documentKey) { + maybeDoc = [mutation applyToLocalDocument:maybeDoc + baseDocument:maybeDoc + localWriteTime:self.localWriteTime]; + } + } + FSTMaybeDocument *baseDoc = maybeDoc; - for (NSUInteger i = 0; i < self.mutations.count; i++) { - FSTMutation *mutation = self.mutations[i]; + // Second, apply all user-provided mutations. + for (FSTMutation *mutation : _mutations) { if (mutation.key == documentKey) { maybeDoc = [mutation applyToLocalDocument:maybeDoc baseDocument:baseDoc @@ -114,10 +152,27 @@ - (FSTMaybeDocument *_Nullable)applyToLocalDocument:(FSTMaybeDocument *_Nullable return maybeDoc; } -// TODO(klimt): This could use NSMutableDictionary instead. +- (MaybeDocumentMap)applyToLocalDocumentSet:(const MaybeDocumentMap &)documentSet { + // TODO(mrschmidt): This implementation is O(n^2). If we iterate through the mutations first (as + // done in `applyToLocalDocument:documentKey:`), we can reduce the complexity to O(n). + + MaybeDocumentMap mutatedDocuments = documentSet; + for (FSTMutation *mutation : _mutations) { + const DocumentKey &key = mutation.key; + auto maybeDocument = mutatedDocuments.find(key); + FSTMaybeDocument *mutatedDocument = [self + applyToLocalDocument:(maybeDocument != mutatedDocuments.end() ? maybeDocument->second : nil) + documentKey:key]; + if (mutatedDocument) { + mutatedDocuments = mutatedDocuments.insert(key, mutatedDocument); + } + } + return mutatedDocuments; +} + - (DocumentKeySet)keys { DocumentKeySet set; - for (FSTMutation *mutation in self.mutations) { + for (FSTMutation *mutation : _mutations) { set = set.insert(mutation.key); } return set; @@ -130,25 +185,26 @@ - (DocumentKeySet)keys { @interface FSTMutationBatchResult () - (instancetype)initWithBatch:(FSTMutationBatch *)batch commitVersion:(SnapshotVersion)commitVersion - mutationResults:(NSArray *)mutationResults + mutationResults:(std::vector)mutationResults streamToken:(nullable NSData *)streamToken docVersions:(DocumentVersionMap)docVersions NS_DESIGNATED_INITIALIZER; @end @implementation FSTMutationBatchResult { SnapshotVersion _commitVersion; + std::vector _mutationResults; DocumentVersionMap _docVersions; } - (instancetype)initWithBatch:(FSTMutationBatch *)batch commitVersion:(SnapshotVersion)commitVersion - mutationResults:(NSArray *)mutationResults + mutationResults:(std::vector)mutationResults streamToken:(nullable NSData *)streamToken docVersions:(DocumentVersionMap)docVersions { if (self = [super init]) { _batch = batch; _commitVersion = std::move(commitVersion); - _mutationResults = mutationResults; + _mutationResults = std::move(mutationResults); _streamToken = streamToken; _docVersions = std::move(docVersions); } @@ -159,21 +215,25 @@ - (instancetype)initWithBatch:(FSTMutationBatch *)batch return _commitVersion; } +- (const std::vector &)mutationResults { + return _mutationResults; +} + - (const DocumentVersionMap &)docVersions { return _docVersions; } + (instancetype)resultWithBatch:(FSTMutationBatch *)batch commitVersion:(SnapshotVersion)commitVersion - mutationResults:(NSArray *)mutationResults + mutationResults:(std::vector)mutationResults streamToken:(nullable NSData *)streamToken { - HARD_ASSERT(batch.mutations.count == mutationResults.count, - "Mutations sent %s must equal results received %s", batch.mutations.count, - mutationResults.count); + HARD_ASSERT(batch.mutations.size() == mutationResults.size(), + "Mutations sent %s must equal results received %s", batch.mutations.size(), + mutationResults.size()); DocumentVersionMap docVersions; - NSArray *mutations = batch.mutations; - for (NSUInteger i = 0; i < mutations.count; i++) { + std::vector mutations = batch.mutations; + for (size_t i = 0; i < mutations.size(); i++) { absl::optional version = mutationResults[i].version; if (!version) { // deletes don't have a version, so we substitute the commitVersion @@ -186,7 +246,7 @@ + (instancetype)resultWithBatch:(FSTMutationBatch *)batch return [[FSTMutationBatchResult alloc] initWithBatch:batch commitVersion:std::move(commitVersion) - mutationResults:mutationResults + mutationResults:std::move(mutationResults) streamToken:streamToken docVersions:std::move(docVersions)]; } diff --git a/Firestore/Source/Public/FIRCollectionReference.h b/Firestore/Source/Public/FIRCollectionReference.h index bc9a56af932..39be000578e 100644 --- a/Firestore/Source/Public/FIRCollectionReference.h +++ b/Firestore/Source/Public/FIRCollectionReference.h @@ -29,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(CollectionReference) @interface FIRCollectionReference : FIRQuery -/** */ +/** :nodoc: */ - (id)init __attribute__((unavailable("FIRCollectionReference cannot be created directly."))); /** ID of the referenced collection. */ diff --git a/Firestore/Source/Public/FIRDocumentChange.h b/Firestore/Source/Public/FIRDocumentChange.h index 47170673068..3e4a0120d28 100644 --- a/Firestore/Source/Public/FIRDocumentChange.h +++ b/Firestore/Source/Public/FIRDocumentChange.h @@ -40,7 +40,7 @@ typedef NS_ENUM(NSInteger, FIRDocumentChangeType) { NS_SWIFT_NAME(DocumentChange) @interface FIRDocumentChange : NSObject -/** */ +/** :nodoc: */ - (id)init __attribute__((unavailable("FIRDocumentChange cannot be created directly."))); /** The type of change that occurred (added, modified, or removed). */ diff --git a/Firestore/Source/Public/FIRDocumentReference.h b/Firestore/Source/Public/FIRDocumentReference.h index 216a8dc93f4..c110bcd10e7 100644 --- a/Firestore/Source/Public/FIRDocumentReference.h +++ b/Firestore/Source/Public/FIRDocumentReference.h @@ -37,7 +37,7 @@ typedef void (^FIRDocumentSnapshotBlock)(FIRDocumentSnapshot *_Nullable snapshot NS_SWIFT_NAME(DocumentReference) @interface FIRDocumentReference : NSObject -/** */ +/** :nodoc: */ - (instancetype)init __attribute__((unavailable("FIRDocumentReference cannot be created directly."))); diff --git a/Firestore/Source/Public/FIRDocumentSnapshot.h b/Firestore/Source/Public/FIRDocumentSnapshot.h index 669fe07dca6..9a3f61b19a2 100644 --- a/Firestore/Source/Public/FIRDocumentSnapshot.h +++ b/Firestore/Source/Public/FIRDocumentSnapshot.h @@ -58,7 +58,7 @@ typedef NS_ENUM(NSInteger, FIRServerTimestampBehavior) { NS_SWIFT_NAME(DocumentSnapshot) @interface FIRDocumentSnapshot : NSObject -/** */ +/** :nodoc: */ - (instancetype)init __attribute__((unavailable("FIRDocumentSnapshot cannot be created directly."))); @@ -151,7 +151,7 @@ NS_SWIFT_NAME(DocumentSnapshot) NS_SWIFT_NAME(QueryDocumentSnapshot) @interface FIRQueryDocumentSnapshot : FIRDocumentSnapshot -/** */ +/** :nodoc: */ - (instancetype)init __attribute__((unavailable("FIRQueryDocumentSnapshot cannot be created directly."))); diff --git a/Firestore/Source/Public/FIRFieldValue.h b/Firestore/Source/Public/FIRFieldValue.h index d8965876cc2..24d1007a5e0 100644 --- a/Firestore/Source/Public/FIRFieldValue.h +++ b/Firestore/Source/Public/FIRFieldValue.h @@ -24,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(FieldValue) @interface FIRFieldValue : NSObject -/** */ +/** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** Used with updateData() to mark a field for deletion. */ @@ -61,6 +61,35 @@ NS_SWIFT_NAME(FieldValue) */ + (instancetype)fieldValueForArrayRemove:(NSArray *)elements NS_SWIFT_NAME(arrayRemove(_:)); +/** + * Returns a special value that can be used with setData() or updateData() that tells the server to + * increment the field's current value by the given value. + * + * If the current value is an integer or a double, both the current and the given value will be + * interpreted as doubles and all arithmetic will follow IEEE 754 semantics. Otherwise, the + * transformation will set the field to the given value. + * + * @param d The double value to increment by. + * @return The FieldValue sentinel for use in a call to setData() or update(). + */ ++ (instancetype)fieldValueForDoubleIncrement:(double)d NS_SWIFT_NAME(increment(_:)); + +/** + * Returns a special value that can be used with setData() or updateData() that tells the server to + * increment the field's current value by the given value. + * + * If the current field value is an integer, possible integer overflows are resolved to LONG_MAX or + * LONG_MIN. If the current field value is a double, both values will be interpreted as doubles and + * the arithmetic will follow IEEE 754 semantics. + * + * If field is not an integer or double, or if the field does not yet exist, the transformation + * will set the field to the given value. + * + * @param l The integer value to increment by. + * @return The FieldValue sentinel for use in a call to setData() or updateData(). + */ ++ (instancetype)fieldValueForIntegerIncrement:(int64_t)l NS_SWIFT_NAME(increment(_:)); + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFirestore.h b/Firestore/Source/Public/FIRFirestore.h index f2c914398ee..cd9302c4cd3 100644 --- a/Firestore/Source/Public/FIRFirestore.h +++ b/Firestore/Source/Public/FIRFirestore.h @@ -20,6 +20,7 @@ @class FIRCollectionReference; @class FIRDocumentReference; @class FIRFirestoreSettings; +@class FIRQuery; @class FIRTransaction; @class FIRWriteBatch; @@ -33,7 +34,7 @@ NS_SWIFT_NAME(Firestore) @interface FIRFirestore : NSObject #pragma mark - Initializing -/** */ +/** :nodoc: */ - (instancetype)init __attribute__((unavailable("Use a static constructor method."))); /** diff --git a/Firestore/Source/Public/FIRGeoPoint.h b/Firestore/Source/Public/FIRGeoPoint.h index ee7a7ea3d05..290b2b45ec2 100644 --- a/Firestore/Source/Public/FIRGeoPoint.h +++ b/Firestore/Source/Public/FIRGeoPoint.h @@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(GeoPoint) @interface FIRGeoPoint : NSObject -/** */ +/** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** diff --git a/Firestore/Source/Public/FIRQuery.h b/Firestore/Source/Public/FIRQuery.h index 2b02a3c4270..436c185b4a2 100644 --- a/Firestore/Source/Public/FIRQuery.h +++ b/Firestore/Source/Public/FIRQuery.h @@ -35,7 +35,7 @@ typedef void (^FIRQuerySnapshotBlock)(FIRQuerySnapshot *_Nullable snapshot, */ NS_SWIFT_NAME(Query) @interface FIRQuery : NSObject -/** */ +/** :nodoc: */ - (id)init __attribute__((unavailable("FIRQuery cannot be created directly."))); /** The `FIRFirestore` for the Firestore database (useful for performing transactions, etc.). */ diff --git a/Firestore/Source/Public/FIRQuerySnapshot.h b/Firestore/Source/Public/FIRQuerySnapshot.h index 6a7e60ddcef..268598fc0b1 100644 --- a/Firestore/Source/Public/FIRQuerySnapshot.h +++ b/Firestore/Source/Public/FIRQuerySnapshot.h @@ -31,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(QuerySnapshot) @interface FIRQuerySnapshot : NSObject -/** */ +/** :nodoc: */ - (id)init __attribute__((unavailable("FIRQuerySnapshot cannot be created directly."))); /** diff --git a/Firestore/Source/Public/FIRSnapshotMetadata.h b/Firestore/Source/Public/FIRSnapshotMetadata.h index f47f3839810..043d8198acb 100644 --- a/Firestore/Source/Public/FIRSnapshotMetadata.h +++ b/Firestore/Source/Public/FIRSnapshotMetadata.h @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(SnapshotMetadata) @interface FIRSnapshotMetadata : NSObject +/** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** diff --git a/Firestore/Source/Public/FIRTimestamp.h b/Firestore/Source/Public/FIRTimestamp.h index bf4aff47e05..cea316b9887 100644 --- a/Firestore/Source/Public/FIRTimestamp.h +++ b/Firestore/Source/Public/FIRTimestamp.h @@ -33,7 +33,7 @@ NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(Timestamp) @interface FIRTimestamp : NSObject -/** */ +/** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** diff --git a/Firestore/Source/Public/FIRTransaction.h b/Firestore/Source/Public/FIRTransaction.h index e53414d37af..ede0fb9d422 100644 --- a/Firestore/Source/Public/FIRTransaction.h +++ b/Firestore/Source/Public/FIRTransaction.h @@ -29,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(Transaction) @interface FIRTransaction : NSObject -/** */ +/** :nodoc: */ - (id)init __attribute__((unavailable("FIRTransaction cannot be created directly."))); /** diff --git a/Firestore/Source/Remote/FSTOnlineStateTracker.h b/Firestore/Source/Remote/FSTOnlineStateTracker.h deleted file mode 100644 index 56aad0539a8..00000000000 --- a/Firestore/Source/Remote/FSTOnlineStateTracker.h +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2018 Google - * - * 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 - -#include "Firestore/core/src/firebase/firestore/model/types.h" -#include "Firestore/core/src/firebase/firestore/util/async_queue.h" - -@protocol FSTOnlineStateDelegate; - -NS_ASSUME_NONNULL_BEGIN - -/** - * A component used by the FSTRemoteStore to track the OnlineState (that is, whether or not the - * client as a whole should be considered to be online or offline), implementing the appropriate - * heuristics. - * - * In particular, when the client is trying to connect to the backend, we allow up to - * kMaxWatchStreamFailures within kOnlineStateTimeout for a connection to succeed. If we have too - * many failures or the timeout elapses, then we set the OnlineState to Offline, and - * the client will behave as if it is offline (getDocument() calls will return cached data, etc.). - */ -@interface FSTOnlineStateTracker : NSObject - -- (instancetype)initWithWorkerQueue:(firebase::firestore::util::AsyncQueue *)queue; - -- (instancetype)init NS_UNAVAILABLE; - -/** A delegate to be notified on OnlineState changes. */ -@property(nonatomic, weak) id onlineStateDelegate; - -/** - * Called by FSTRemoteStore when a watch stream is started (including on each backoff attempt). - * - * If this is the first attempt, it sets the OnlineState to Unknown and starts the - * onlineStateTimer. - */ -- (void)handleWatchStreamStart; - -/** - * Called by FSTRemoteStore when a watch stream fails. - * - * Updates our OnlineState as appropriate. The first failure moves us to OnlineState::Unknown. - * We then may allow multiple failures (based on kMaxWatchStreamFailures) before we actually - * transition to OnlineState::Offline. - */ -- (void)handleWatchStreamFailure:(NSError *)error; - -/** - * Explicitly sets the OnlineState to the specified state. - * - * Note that this resets the timers / failure counters, etc. used by our Offline heuristics, so - * it must not be used in place of handleWatchStreamStart and handleWatchStreamFailure. - */ -- (void)updateState:(firebase::firestore::model::OnlineState)newState; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTOnlineStateTracker.mm b/Firestore/Source/Remote/FSTOnlineStateTracker.mm deleted file mode 100644 index be4feab4bfa..00000000000 --- a/Firestore/Source/Remote/FSTOnlineStateTracker.mm +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright 2018 Google - * - * 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 "Firestore/Source/Remote/FSTOnlineStateTracker.h" - -#include // NOLINT(build/c++11) - -#import "Firestore/Source/Remote/FSTRemoteStore.h" - -#include "Firestore/core/src/firebase/firestore/util/executor.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "Firestore/core/src/firebase/firestore/util/log.h" - -namespace chr = std::chrono; -using firebase::firestore::model::OnlineState; -using firebase::firestore::util::AsyncQueue; -using firebase::firestore::util::DelayedOperation; -using firebase::firestore::util::TimerId; - -NS_ASSUME_NONNULL_BEGIN - -namespace { - -// To deal with transient failures, we allow multiple stream attempts before giving up and -// transitioning from OnlineState Unknown to Offline. -// TODO(mikelehen): This used to be set to 2 as a mitigation for b/66228394. @jdimond thinks that -// bug is sufficiently fixed so that we can set this back to 1. If that works okay, we could -// potentially remove this logic entirely. -const int kMaxWatchStreamFailures = 1; - -// To deal with stream attempts that don't succeed or fail in a timely manner, we have a -// timeout for OnlineState to reach Online or Offline. If the timeout is reached, we transition -// to Offline rather than waiting indefinitely. -const AsyncQueue::Milliseconds kOnlineStateTimeout = chr::seconds(10); - -} // namespace - -@interface FSTOnlineStateTracker () - -/** The current OnlineState. */ -@property(nonatomic, assign) OnlineState state; - -/** - * A count of consecutive failures to open the stream. If it reaches the maximum defined by - * kMaxWatchStreamFailures, we'll revert to OnlineState::Offline. - */ -@property(nonatomic, assign) int watchStreamFailures; - -/** - * Whether the client should log a warning message if it fails to connect to the backend - * (initially YES, cleared after a successful stream, or if we've logged the message already). - */ -@property(nonatomic, assign) BOOL shouldWarnClientIsOffline; - -@end - -@implementation FSTOnlineStateTracker { - /** - * A timer that elapses after kOnlineStateTimeout, at which point we transition from OnlineState - * Unknown to Offline without waiting for the stream to actually fail (kMaxWatchStreamFailures - * times). - */ - DelayedOperation _onlineStateTimer; - - /** The worker queue to use for running timers (and to call onlineStateDelegate). */ - AsyncQueue *_workerQueue; -} - -- (instancetype)initWithWorkerQueue:(AsyncQueue *)workerQueue { - if (self = [super init]) { - _workerQueue = workerQueue; - _state = OnlineState::Unknown; - _shouldWarnClientIsOffline = YES; - } - return self; -} - -- (void)handleWatchStreamStart { - if (self.watchStreamFailures == 0) { - [self setAndBroadcastState:OnlineState::Unknown]; - - HARD_ASSERT(!_onlineStateTimer, "_onlineStateTimer shouldn't be started yet"); - _onlineStateTimer = - _workerQueue->EnqueueAfterDelay(kOnlineStateTimeout, TimerId::OnlineStateTimeout, [self] { - _onlineStateTimer = {}; - HARD_ASSERT(self.state == OnlineState::Unknown, - "Timer should be canceled if we transitioned to a different state."); - [self logClientOfflineWarningIfNecessaryWithReason: - [NSString stringWithFormat:@"Backend didn't respond within %lld seconds.", - chr::duration_cast(kOnlineStateTimeout) - .count()]]; - [self setAndBroadcastState:OnlineState::Offline]; - - // NOTE: handleWatchStreamFailure will continue to increment - // watchStreamFailures even though we are already marked Offline but this is - // non-harmful. - }); - } -} - -- (void)handleWatchStreamFailure:(NSError *)error { - if (self.state == OnlineState::Online) { - [self setAndBroadcastState:OnlineState::Unknown]; - - // To get to OnlineState::Online, updateState: must have been called which would have reset - // our heuristics. - HARD_ASSERT(self.watchStreamFailures == 0, "watchStreamFailures must be 0"); - HARD_ASSERT(!_onlineStateTimer, "_onlineStateTimer must not be set yet"); - } else { - self.watchStreamFailures++; - if (self.watchStreamFailures >= kMaxWatchStreamFailures) { - [self clearOnlineStateTimer]; - [self logClientOfflineWarningIfNecessaryWithReason: - [NSString stringWithFormat:@"Connection failed %d times. Most recent error: %@", - kMaxWatchStreamFailures, error]]; - [self setAndBroadcastState:OnlineState::Offline]; - } - } -} - -- (void)updateState:(OnlineState)newState { - [self clearOnlineStateTimer]; - self.watchStreamFailures = 0; - - if (newState == OnlineState::Online) { - // We've connected to watch at least once. Don't warn the developer about being offline going - // forward. - self.shouldWarnClientIsOffline = NO; - } - - [self setAndBroadcastState:newState]; -} - -- (void)setAndBroadcastState:(OnlineState)newState { - if (newState != self.state) { - self.state = newState; - [self.onlineStateDelegate applyChangedOnlineState:newState]; - } -} - -- (void)logClientOfflineWarningIfNecessaryWithReason:(NSString *)reason { - NSString *message = [NSString - stringWithFormat: - @"Could not reach Cloud Firestore backend. %@\n This typically indicates that your " - @"device does not have a healthy Internet connection at the moment. The client will " - @"operate in offline mode until it is able to successfully connect to the backend.", - reason]; - if (self.shouldWarnClientIsOffline) { - LOG_WARN("%s", message); - self.shouldWarnClientIsOffline = NO; - } else { - LOG_DEBUG("%s", message); - } -} - -- (void)clearOnlineStateTimer { - _onlineStateTimer.Cancel(); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteEvent.h b/Firestore/Source/Remote/FSTRemoteEvent.h deleted file mode 100644 index 3a97a4be138..00000000000 --- a/Firestore/Source/Remote/FSTRemoteEvent.h +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#include -#include -#include -#include - -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" -#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" -#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" -#include "Firestore/core/src/firebase/firestore/remote/watch_change.h" - -@class FSTMaybeDocument; -@class FSTQueryData; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTTargetChange - -/** - * An FSTTargetChange specifies the set of changes for a specific target as part of an - * FSTRemoteEvent. These changes track which documents are added, modified or emoved, as well as the - * target's resume token and whether the target is marked CURRENT. - * - * The actual changes *to* documents are not part of the FSTTargetChange since documents may be part - * of multiple targets. - */ -@interface FSTTargetChange : NSObject - -/** - * Creates a new target change with the given SnapshotVersion. - */ -- (instancetype)initWithResumeToken:(NSData *)resumeToken - current:(BOOL)current - addedDocuments:(firebase::firestore::model::DocumentKeySet)addedDocuments - modifiedDocuments:(firebase::firestore::model::DocumentKeySet)modifiedDocuments - removedDocuments:(firebase::firestore::model::DocumentKeySet)removedDocuments - NS_DESIGNATED_INITIALIZER; - -- (instancetype)init NS_UNAVAILABLE; - -/** - * An opaque, server-assigned token that allows watching a query to be resumed after - * disconnecting without retransmitting all the data that matches the query. The resume token - * essentially identifies a point in time from which the server should resume sending results. - */ -@property(nonatomic, strong, readonly) NSData *resumeToken; - -/** - * The "current" (synced) status of this target. Note that "current" has special meaning in the RPC - * protocol that implies that a target is both up-to-date and consistent with the rest of the watch - * stream. - */ -@property(nonatomic, assign, readonly) BOOL current; - -/** - * The set of documents that were newly assigned to this target as part of this remote event. - */ -- (const firebase::firestore::model::DocumentKeySet &)addedDocuments; - -/** - * The set of documents that were already assigned to this target but received an update during this - * remote event. - */ -- (const firebase::firestore::model::DocumentKeySet &)modifiedDocuments; - -/** - * The set of documents that were removed from this target as part of this remote event. - */ -- (const firebase::firestore::model::DocumentKeySet &)removedDocuments; - -@end - -#pragma mark - FSTRemoteEvent - -/** - * An event from the RemoteStore. It is split into targetChanges (changes to the state or the set - * of documents in our watched targets) and documentUpdates (changes to the actual documents). - */ -@interface FSTRemoteEvent : NSObject - -- (instancetype) - initWithSnapshotVersion:(firebase::firestore::model::SnapshotVersion)snapshotVersion - targetChanges: - (std::unordered_map) - targetChanges - targetMismatches: - (std::unordered_set)targetMismatches - documentUpdates: - (std::unordered_map)documentUpdates - limboDocuments:(firebase::firestore::model::DocumentKeySet)limboDocuments; - -/** The snapshot version this event brings us up to. */ -- (const firebase::firestore::model::SnapshotVersion &)snapshotVersion; - -/** - * A set of which document updates are due only to limbo resolution targets. - */ -- (const firebase::firestore::model::DocumentKeySet &)limboDocumentChanges; - -/** A map from target to changes to the target. See TargetChange. */ -- (const std::unordered_map &) - targetChanges; - -/** - * A set of targets that is known to be inconsistent. Listens for these targets should be - * re-established without resume tokens. - */ -- (const std::unordered_set &)targetMismatches; - -/** - * A set of which documents have changed or been deleted, along with the doc's new values (if not - * deleted). - */ -- (const std::unordered_map &)documentUpdates; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteEvent.mm b/Firestore/Source/Remote/FSTRemoteEvent.mm deleted file mode 100644 index 408c2501ce8..00000000000 --- a/Firestore/Source/Remote/FSTRemoteEvent.mm +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Remote/FSTRemoteEvent.h" - -#include -#include -#include -#include - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTViewSnapshot.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Util/FSTClasses.h" - -#include "Firestore/core/src/firebase/firestore/remote/watch_change.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "Firestore/core/src/firebase/firestore/util/hashing.h" -#include "Firestore/core/src/firebase/firestore/util/log.h" - -using firebase::firestore::core::DocumentViewChangeType; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::DocumentKeyHash; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::SnapshotVersion; -using firebase::firestore::model::TargetId; -using firebase::firestore::remote::DocumentWatchChange; -using firebase::firestore::remote::ExistenceFilterWatchChange; -using firebase::firestore::remote::WatchTargetChange; -using firebase::firestore::remote::WatchTargetChangeState; -using firebase::firestore::util::Hash; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTTargetChange - -@implementation FSTTargetChange { - DocumentKeySet _addedDocuments; - DocumentKeySet _modifiedDocuments; - DocumentKeySet _removedDocuments; -} - -- (instancetype)initWithResumeToken:(NSData *)resumeToken - current:(BOOL)current - addedDocuments:(DocumentKeySet)addedDocuments - modifiedDocuments:(DocumentKeySet)modifiedDocuments - removedDocuments:(DocumentKeySet)removedDocuments { - if (self = [super init]) { - _resumeToken = [resumeToken copy]; - _current = current; - _addedDocuments = std::move(addedDocuments); - _modifiedDocuments = std::move(modifiedDocuments); - _removedDocuments = std::move(removedDocuments); - } - return self; -} - -- (const DocumentKeySet &)addedDocuments { - return _addedDocuments; -} - -- (const DocumentKeySet &)modifiedDocuments { - return _modifiedDocuments; -} - -- (const DocumentKeySet &)removedDocuments { - return _removedDocuments; -} - -- (BOOL)isEqual:(id)other { - if (other == self) { - return YES; - } - if (![other isMemberOfClass:[FSTTargetChange class]]) { - return NO; - } - - return [self current] == [other current] && - [[self resumeToken] isEqualToData:[other resumeToken]] && - [self addedDocuments] == [other addedDocuments] && - [self modifiedDocuments] == [other modifiedDocuments] && - [self removedDocuments] == [other removedDocuments]; -} - -@end - -#pragma mark - FSTRemoteEvent - -@implementation FSTRemoteEvent { - SnapshotVersion _snapshotVersion; - std::unordered_map _targetChanges; - std::unordered_set _targetMismatches; - std::unordered_map _documentUpdates; - DocumentKeySet _limboDocumentChanges; -} - -- (instancetype) - initWithSnapshotVersion:(SnapshotVersion)snapshotVersion - targetChanges:(std::unordered_map)targetChanges - targetMismatches:(std::unordered_set)targetMismatches - documentUpdates:(std::unordered_map) - documentUpdates - limboDocuments:(DocumentKeySet)limboDocuments { - self = [super init]; - if (self) { - _snapshotVersion = std::move(snapshotVersion); - _targetChanges = std::move(targetChanges); - _targetMismatches = std::move(targetMismatches); - _documentUpdates = std::move(documentUpdates); - _limboDocumentChanges = std::move(limboDocuments); - } - return self; -} - -- (const SnapshotVersion &)snapshotVersion { - return _snapshotVersion; -} - -- (const DocumentKeySet &)limboDocumentChanges { - return _limboDocumentChanges; -} - -- (const std::unordered_map &)targetChanges { - return _targetChanges; -} - -- (const std::unordered_map &)documentUpdates { - return _documentUpdates; -} - -- (const std::unordered_set &)targetMismatches { - return _targetMismatches; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteStore.h b/Firestore/Source/Remote/FSTRemoteStore.h deleted file mode 100644 index 267c81e5ef9..00000000000 --- a/Firestore/Source/Remote/FSTRemoteStore.h +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#include - -#import "Firestore/Source/Remote/FSTRemoteEvent.h" - -#include "Firestore/core/src/firebase/firestore/auth/user.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" -#include "Firestore/core/src/firebase/firestore/remote/datastore.h" -#include "Firestore/core/src/firebase/firestore/util/async_queue.h" - -@class FSTLocalStore; -@class FSTMutationBatch; -@class FSTMutationBatchResult; -@class FSTQueryData; -@class FSTRemoteEvent; -@class FSTTransaction; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTRemoteSyncer - -/** - * A protocol that describes the actions the FSTRemoteStore needs to perform on a cooperating - * synchronization engine. - */ -@protocol FSTRemoteSyncer - -/** - * Applies one remote event to the sync engine, notifying any views of the changes, and releasing - * any pending mutation batches that would become visible because of the snapshot version the - * remote event contains. - */ -- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent; - -/** - * Rejects the listen for the given targetID. This can be triggered by the backend for any active - * target. - * - * @param targetID The targetID corresponding to a listen initiated via - * -listenToTargetWithQueryData: on FSTRemoteStore. - * @param error A description of the condition that has forced the rejection. Nearly always this - * will be an indication that the user is no longer authorized to see the data matching the - * target. - */ -- (void)rejectListenWithTargetID:(const firebase::firestore::model::TargetId)targetID - error:(NSError *)error; - -/** - * Applies the result of a successful write of a mutation batch to the sync engine, emitting - * snapshots in any views that the mutation applies to, and removing the batch from the mutation - * queue. - */ -- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult; - -/** - * Rejects the batch, removing the batch from the mutation queue, recomputing the local view of - * any documents affected by the batch and then, emitting snapshots with the reverted value. - */ -- (void)rejectFailedWriteWithBatchID:(firebase::firestore::model::BatchId)batchID - error:(NSError *)error; - -/** - * Returns the set of remote document keys for the given target ID. This list includes the - * documents that were assigned to the target when we received the last snapshot. - */ -- (firebase::firestore::model::DocumentKeySet)remoteKeysForTarget: - (firebase::firestore::model::TargetId)targetId; - -@end - -/** - * A protocol for the FSTRemoteStore online state delegate, called whenever the state of the - * online streams of the FSTRemoteStore changes. - * Note that this protocol only supports the watch stream for now. - */ -@protocol FSTOnlineStateDelegate - -/** Called whenever the online state of the watch stream changes */ -- (void)applyChangedOnlineState:(firebase::firestore::model::OnlineState)onlineState; - -@end - -#pragma mark - FSTRemoteStore - -/** - * FSTRemoteStore handles all interaction with the backend through a simple, clean interface. This - * class is not thread safe and should be only called from the worker dispatch queue. - */ -@interface FSTRemoteStore : NSObject - -- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore - datastore: - (std::shared_ptr)datastore - workerQueue:(firebase::firestore::util::AsyncQueue *)queue; - -- (instancetype)init NS_UNAVAILABLE; - -@property(nonatomic, weak) id syncEngine; - -@property(nonatomic, weak) id onlineStateDelegate; - -/** Starts up the remote store, creating streams, restoring state from LocalStore, etc. */ -- (void)start; - -/** Shuts down the remote store, tearing down connections and otherwise cleaning up. */ -- (void)shutdown; - -/** Temporarily disables the network. The network can be re-enabled using 'enableNetwork:'. */ -- (void)disableNetwork; - -/** Re-enables the network. Only to be called as the counterpart to 'disableNetwork:'. */ -- (void)enableNetwork; - -/** - * Tells the FSTRemoteStore that the currently authenticated user has changed. - * - * In response the remote store tears down streams and clears up any tracked operations that should - * not persist across users. Restarts the streams if appropriate. - */ -- (void)credentialDidChange; - -/** Listens to the target identified by the given FSTQueryData. */ -- (void)listenToTargetWithQueryData:(FSTQueryData *)queryData; - -/** Stops listening to the target with the given target ID. */ -- (void)stopListeningToTargetID:(firebase::firestore::model::TargetId)targetID; - -/** - * Tells the FSTRemoteStore that there are new mutations to process in the queue. This is typically - * called by FSTSyncEngine after it has sent mutations to FSTLocalStore. - * - * In response the remote store will pull mutations from the local store until the datastore - * instance reports that it cannot accept further in-progress writes. This mechanism serves to - * maintain a pipeline of in-flight requests between the `Datastore` and the server that - * applies them. - */ -- (void)fillWritePipeline; - -/** Returns a new transaction backed by this remote store. */ -- (FSTTransaction *)transaction; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteStore.mm b/Firestore/Source/Remote/FSTRemoteStore.mm deleted file mode 100644 index 507c4c6ec6f..00000000000 --- a/Firestore/Source/Remote/FSTRemoteStore.mm +++ /dev/null @@ -1,633 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 "Firestore/Source/Remote/FSTRemoteStore.h" - -#include -#include -#include -#include - -#import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTTransaction.h" -#import "Firestore/Source/Local/FSTLocalStore.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTMutation.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" -#import "Firestore/Source/Remote/FSTOnlineStateTracker.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" -#import "Firestore/Source/Remote/FSTStream.h" - -#include "Firestore/core/src/firebase/firestore/auth/user.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" -#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" -#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" -#include "Firestore/core/src/firebase/firestore/remote/stream.h" -#include "Firestore/core/src/firebase/firestore/util/error_apple.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "Firestore/core/src/firebase/firestore/util/log.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" -#include "Firestore/core/src/firebase/firestore/util/string_apple.h" -#include "absl/memory/memory.h" - -namespace util = firebase::firestore::util; -using firebase::firestore::auth::User; -using firebase::firestore::model::BatchId; -using firebase::firestore::model::kBatchIdUnknown; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::OnlineState; -using firebase::firestore::model::SnapshotVersion; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::TargetId; -using firebase::firestore::remote::Datastore; -using firebase::firestore::remote::WatchStream; -using firebase::firestore::remote::WriteStream; -using firebase::firestore::remote::DocumentWatchChange; -using firebase::firestore::remote::ExistenceFilterWatchChange; -using firebase::firestore::remote::WatchChange; -using firebase::firestore::remote::WatchChangeAggregator; -using firebase::firestore::remote::WatchTargetChange; -using firebase::firestore::remote::WatchTargetChangeState; -using util::AsyncQueue; -using util::Status; - -NS_ASSUME_NONNULL_BEGIN - -/** - * The maximum number of pending writes to allow. - * TODO(bjornick): Negotiate this value with the backend. - */ -static const int kMaxPendingWrites = 10; - -#pragma mark - FSTRemoteStore - -@interface FSTRemoteStore () - -/** - * The local store, used to fill the write pipeline with outbound mutations and resolve existence - * filter mismatches. Immutable after initialization. - */ -@property(nonatomic, strong, readonly) FSTLocalStore *localStore; - -#pragma mark Watch Stream - -@property(nonatomic, strong, readonly) FSTOnlineStateTracker *onlineStateTracker; - -/** - * A list of up to kMaxPendingWrites writes that we have fetched from the LocalStore via - * fillWritePipeline and have or will send to the write stream. - * - * Whenever writePipeline is not empty, the RemoteStore will attempt to start or restart the write - * stream. When the stream is established, the writes in the pipeline will be sent in order. - * - * Writes remain in writePipeline until they are acknowledged by the backend and thus will - * automatically be re-sent if the stream is interrupted / restarted before they're acknowledged. - * - * Write responses from the backend are linked to their originating request purely based on - * order, and so we can just remove writes from the front of the writePipeline as we receive - * responses. - */ -@property(nonatomic, strong, readonly) NSMutableArray *writePipeline; -@end - -@implementation FSTRemoteStore { - std::unique_ptr _watchChangeAggregator; - /** The client-side proxy for interacting with the backend. */ - - std::shared_ptr _datastore; - /** - * A mapping of watched targets that the client cares about tracking and the - * user has explicitly called a 'listen' for this target. - * - * These targets may or may not have been sent to or acknowledged by the - * server. On re-establishing the listen stream, these targets should be sent - * to the server. The targets removed with unlistens are removed eagerly - * without waiting for confirmation from the listen stream. */ - std::unordered_map _listenTargets; - - std::shared_ptr _watchStream; - std::shared_ptr _writeStream; - /** - * Set to YES by 'enableNetwork:' and NO by 'disableNetworkInternal:' and - * indicates the user-preferred network state. - */ - BOOL _isNetworkEnabled; -} - -- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore - datastore:(std::shared_ptr)datastore - workerQueue:(AsyncQueue *)queue { - if (self = [super init]) { - _localStore = localStore; - _datastore = std::move(datastore); - - _writePipeline = [NSMutableArray array]; - _onlineStateTracker = [[FSTOnlineStateTracker alloc] initWithWorkerQueue:queue]; - - _datastore->Start(); - // Create streams (but note they're not started yet) - _watchStream = _datastore->CreateWatchStream(self); - _writeStream = _datastore->CreateWriteStream(self); - - _isNetworkEnabled = NO; - } - return self; -} - -- (void)start { - // For now, all setup is handled by enableNetwork(). We might expand on this in the future. - [self enableNetwork]; -} - -@dynamic onlineStateDelegate; - -- (nullable id)onlineStateDelegate { - return self.onlineStateTracker.onlineStateDelegate; -} - -- (void)setOnlineStateDelegate:(nullable id)delegate { - self.onlineStateTracker.onlineStateDelegate = delegate; -} - -#pragma mark Online/Offline state - -- (BOOL)canUseNetwork { - // PORTING NOTE: This method exists mostly because web also has to take into - // account primary vs. secondary state. - return _isNetworkEnabled; -} - -- (void)enableNetwork { - _isNetworkEnabled = YES; - - if ([self canUseNetwork]) { - // Load any saved stream token from persistent storage - _writeStream->SetLastStreamToken([self.localStore lastStreamToken]); - - if ([self shouldStartWatchStream]) { - [self startWatchStream]; - } else { - [self.onlineStateTracker updateState:OnlineState::Unknown]; - } - - // This will start the write stream if necessary. - [self fillWritePipeline]; - } -} - -- (void)disableNetwork { - _isNetworkEnabled = NO; - [self disableNetworkInternal]; - - // Set the OnlineState to Offline so get()s return from cache, etc. - [self.onlineStateTracker updateState:OnlineState::Offline]; -} - -/** Disables the network, setting the OnlineState to the specified targetOnlineState. */ -- (void)disableNetworkInternal { - _watchStream->Stop(); - _writeStream->Stop(); - - if (self.writePipeline.count > 0) { - LOG_DEBUG("Stopping write stream with %s pending writes", - (unsigned long)self.writePipeline.count); - [self.writePipeline removeAllObjects]; - } - - [self cleanUpWatchStreamState]; -} - -#pragma mark Shutdown - -- (void)shutdown { - LOG_DEBUG("FSTRemoteStore %s shutting down", (__bridge void *)self); - _isNetworkEnabled = NO; - [self disableNetworkInternal]; - // Set the OnlineState to Unknown (rather than Offline) to avoid potentially triggering - // spurious listener events with cached data, etc. - [self.onlineStateTracker updateState:OnlineState::Unknown]; - _datastore->Shutdown(); -} - -- (void)credentialDidChange { - if ([self canUseNetwork]) { - // Tear down and re-create our network streams. This will ensure we get a fresh auth token - // for the new user and re-fill the write pipeline with new mutations from the LocalStore - // (since mutations are per-user). - LOG_DEBUG("FSTRemoteStore %s restarting streams for new credential", (__bridge void *)self); - _isNetworkEnabled = NO; - [self disableNetworkInternal]; - [self.onlineStateTracker updateState:OnlineState::Unknown]; - [self enableNetwork]; - } -} - -#pragma mark Watch Stream - -- (void)startWatchStream { - HARD_ASSERT([self shouldStartWatchStream], - "startWatchStream: called when shouldStartWatchStream: is false."); - _watchChangeAggregator = absl::make_unique(self); - _watchStream->Start(); - - [self.onlineStateTracker handleWatchStreamStart]; -} - -- (void)listenToTargetWithQueryData:(FSTQueryData *)queryData { - TargetId targetKey = queryData.targetID; - HARD_ASSERT(_listenTargets.find(targetKey) == _listenTargets.end(), - "listenToQuery called with duplicate target id: %s", targetKey); - - _listenTargets[targetKey] = queryData; - - if ([self shouldStartWatchStream]) { - [self startWatchStream]; - } else if (_watchStream->IsOpen()) { - [self sendWatchRequestWithQueryData:queryData]; - } -} - -- (void)sendWatchRequestWithQueryData:(FSTQueryData *)queryData { - _watchChangeAggregator->RecordPendingTargetRequest(queryData.targetID); - _watchStream->WatchQuery(queryData); -} - -- (void)stopListeningToTargetID:(TargetId)targetID { - size_t num_erased = _listenTargets.erase(targetID); - HARD_ASSERT(num_erased == 1, "stopListeningToTargetID: target not currently watched: %s", - targetID); - - if (_watchStream->IsOpen()) { - [self sendUnwatchRequestForTargetID:targetID]; - } - if (_listenTargets.empty()) { - if (_watchStream->IsOpen()) { - _watchStream->MarkIdle(); - } else if ([self canUseNetwork]) { - // Revert to OnlineState::Unknown if the watch stream is not open and we have no listeners, - // since without any listens to send we cannot confirm if the stream is healthy and upgrade - // to OnlineState::Online. - [self.onlineStateTracker updateState:OnlineState::Unknown]; - } - } -} - -- (void)sendUnwatchRequestForTargetID:(TargetId)targetID { - _watchChangeAggregator->RecordPendingTargetRequest(targetID); - _watchStream->UnwatchTargetId(targetID); -} - -/** - * Returns YES if the network is enabled, the watch stream has not yet been started and there are - * active watch targets. - */ -- (BOOL)shouldStartWatchStream { - return [self canUseNetwork] && !_watchStream->IsStarted() && !_listenTargets.empty(); -} - -- (void)cleanUpWatchStreamState { - _watchChangeAggregator.reset(); -} - -- (void)watchStreamDidOpen { - // Restore any existing watches. - for (const auto &kv : _listenTargets) { - [self sendWatchRequestWithQueryData:kv.second]; - } -} - -- (void)watchStreamDidChange:(const WatchChange &)change - snapshotVersion:(const SnapshotVersion &)snapshotVersion { - // Mark the connection as Online because we got a message from the server. - [self.onlineStateTracker updateState:OnlineState::Online]; - - if (change.type() == WatchChange::Type::TargetChange) { - const WatchTargetChange &watchTargetChange = static_cast(change); - if (watchTargetChange.state() == WatchTargetChangeState::Removed && - !watchTargetChange.cause().ok()) { - // There was an error on a target, don't wait for a consistent snapshot to raise events - return [self processTargetErrorForWatchChange:watchTargetChange]; - } else { - _watchChangeAggregator->HandleTargetChange(watchTargetChange); - } - } else if (change.type() == WatchChange::Type::Document) { - _watchChangeAggregator->HandleDocumentChange(static_cast(change)); - } else { - HARD_ASSERT(change.type() == WatchChange::Type::ExistenceFilter, - "Expected watchChange to be an instance of ExistenceFilterWatchChange"); - _watchChangeAggregator->HandleExistenceFilter( - static_cast(change)); - } - - if (snapshotVersion != SnapshotVersion::None() && - snapshotVersion >= [self.localStore lastRemoteSnapshotVersion]) { - // We have received a target change with a global snapshot if the snapshot version is not - // equal to SnapshotVersion.None(). - [self raiseWatchSnapshotWithSnapshotVersion:snapshotVersion]; - } -} - -- (void)watchStreamWasInterruptedWithError:(const Status &)error { - if (error.ok()) { - // Graceful stop (due to Stop() or idle timeout). Make sure that's desirable. - HARD_ASSERT(![self shouldStartWatchStream], - "Watch stream was stopped gracefully while still needed."); - } - - [self cleanUpWatchStreamState]; - - // If we still need the watch stream, retry the connection. - if ([self shouldStartWatchStream]) { - [self.onlineStateTracker handleWatchStreamFailure:util::MakeNSError(error)]; - - [self startWatchStream]; - } else { - // We don't need to restart the watch stream because there are no active targets. The online - // state is set to unknown because there is no active attempt at establishing a connection. - [self.onlineStateTracker updateState:OnlineState::Unknown]; - } -} - -/** - * Takes a batch of changes from the Datastore, repackages them as a RemoteEvent, and passes that - * on to the SyncEngine. - */ -- (void)raiseWatchSnapshotWithSnapshotVersion:(const SnapshotVersion &)snapshotVersion { - HARD_ASSERT(snapshotVersion != SnapshotVersion::None(), - "Can't raise event for unknown SnapshotVersion"); - - FSTRemoteEvent *remoteEvent = _watchChangeAggregator->CreateRemoteEvent(snapshotVersion); - - // Update in-memory resume tokens. FSTLocalStore will update the persistent view of these when - // applying the completed FSTRemoteEvent. - for (const auto &entry : remoteEvent.targetChanges) { - NSData *resumeToken = entry.second.resumeToken; - if (resumeToken.length > 0) { - TargetId targetID = entry.first; - auto found = _listenTargets.find(targetID); - FSTQueryData *queryData = found != _listenTargets.end() ? found->second : nil; - // A watched target might have been removed already. - if (queryData) { - _listenTargets[targetID] = - [queryData queryDataByReplacingSnapshotVersion:snapshotVersion - resumeToken:resumeToken - sequenceNumber:queryData.sequenceNumber]; - } - } - } - - // Re-establish listens for the targets that have been invalidated by existence filter - // mismatches. - for (TargetId targetID : remoteEvent.targetMismatches) { - auto found = _listenTargets.find(targetID); - if (found == _listenTargets.end()) { - // A watched target might have been removed already. - continue; - } - FSTQueryData *queryData = found->second; - - // Clear the resume token for the query, since we're in a known mismatch state. - queryData = [[FSTQueryData alloc] initWithQuery:queryData.query - targetID:targetID - listenSequenceNumber:queryData.sequenceNumber - purpose:queryData.purpose]; - _listenTargets[targetID] = queryData; - - // Cause a hard reset by unwatching and rewatching immediately, but deliberately don't send a - // resume token so that we get a full update. - [self sendUnwatchRequestForTargetID:targetID]; - - // Mark the query we send as being on behalf of an existence filter mismatch, but don't - // actually retain that in _listenTargets. This ensures that we flag the first re-listen this - // way without impacting future listens of this target (that might happen e.g. on reconnect). - FSTQueryData *requestQueryData = - [[FSTQueryData alloc] initWithQuery:queryData.query - targetID:targetID - listenSequenceNumber:queryData.sequenceNumber - purpose:FSTQueryPurposeExistenceFilterMismatch]; - [self sendWatchRequestWithQueryData:requestQueryData]; - } - - // Finally handle remote event - [self.syncEngine applyRemoteEvent:remoteEvent]; -} - -/** Process a target error and passes the error along to SyncEngine. */ -- (void)processTargetErrorForWatchChange:(const WatchTargetChange &)change { - HARD_ASSERT(!change.cause().ok(), "Handling target error without a cause"); - // Ignore targets that have been removed already. - for (TargetId targetID : change.target_ids()) { - auto found = _listenTargets.find(targetID); - if (found != _listenTargets.end()) { - _listenTargets.erase(found); - _watchChangeAggregator->RemoveTarget(targetID); - [self.syncEngine rejectListenWithTargetID:targetID error:util::MakeNSError(change.cause())]; - } - } -} - -- (DocumentKeySet)remoteKeysForTarget:(TargetId)targetID { - return [self.syncEngine remoteKeysForTarget:targetID]; -} - -- (nullable FSTQueryData *)queryDataForTarget:(TargetId)targetID { - auto found = _listenTargets.find(targetID); - return found != _listenTargets.end() ? found->second : nil; -} - -#pragma mark Write Stream - -/** - * Returns YES if the network is enabled, the write stream has not yet been started and there are - * pending writes. - */ -- (BOOL)shouldStartWriteStream { - return [self canUseNetwork] && !_writeStream->IsStarted() && self.writePipeline.count > 0; -} - -- (void)startWriteStream { - HARD_ASSERT([self shouldStartWriteStream], - "startWriteStream: called when shouldStartWriteStream: is false."); - _writeStream->Start(); -} - -/** - * Attempts to fill our write pipeline with writes from the LocalStore. - * - * Called internally to bootstrap or refill the write pipeline and by SyncEngine whenever there - * are new mutations to process. - * - * Starts the write stream if necessary. - */ -- (void)fillWritePipeline { - BatchId lastBatchIDRetrieved = - self.writePipeline.count == 0 ? kBatchIdUnknown : self.writePipeline.lastObject.batchID; - while ([self canAddToWritePipeline]) { - FSTMutationBatch *batch = [self.localStore nextMutationBatchAfterBatchID:lastBatchIDRetrieved]; - if (!batch) { - if (self.writePipeline.count == 0) { - _writeStream->MarkIdle(); - } - break; - } - [self addBatchToWritePipeline:batch]; - lastBatchIDRetrieved = batch.batchID; - } - - if ([self shouldStartWriteStream]) { - [self startWriteStream]; - } -} - -/** - * Returns YES if we can add to the write pipeline (i.e. it is not full and the network is enabled). - */ -- (BOOL)canAddToWritePipeline { - return [self canUseNetwork] && self.writePipeline.count < kMaxPendingWrites; -} - -/** - * Queues additional writes to be sent to the write stream, sending them immediately if the write - * stream is established. - */ -- (void)addBatchToWritePipeline:(FSTMutationBatch *)batch { - HARD_ASSERT([self canAddToWritePipeline], "addBatchToWritePipeline called when pipeline is full"); - - [self.writePipeline addObject:batch]; - - if (_writeStream->IsOpen() && _writeStream->handshake_complete()) { - _writeStream->WriteMutations(batch.mutations); - } -} - -- (void)writeStreamDidOpen { - _writeStream->WriteHandshake(); -} - -/** - * Handles a successful handshake response from the server, which is our cue to send any pending - * writes. - */ -- (void)writeStreamDidCompleteHandshake { - // Record the stream token. - [self.localStore setLastStreamToken:_writeStream->GetLastStreamToken()]; - - // Send the write pipeline now that the stream is established. - for (FSTMutationBatch *write in self.writePipeline) { - _writeStream->WriteMutations(write.mutations); - } -} - -/** Handles a successful StreamingWriteResponse from the server that contains a mutation result. */ -- (void)writeStreamDidReceiveResponseWithVersion:(const SnapshotVersion &)commitVersion - mutationResults:(NSArray *)results { - // This is a response to a write containing mutations and should be correlated to the first - // write in our write pipeline. - NSMutableArray *writePipeline = self.writePipeline; - FSTMutationBatch *batch = writePipeline[0]; - [writePipeline removeObjectAtIndex:0]; - - FSTMutationBatchResult *batchResult = - [FSTMutationBatchResult resultWithBatch:batch - commitVersion:commitVersion - mutationResults:results - streamToken:_writeStream->GetLastStreamToken()]; - [self.syncEngine applySuccessfulWriteWithResult:batchResult]; - - // It's possible that with the completion of this mutation another slot has freed up. - [self fillWritePipeline]; -} - -/** - * Handles the closing of the StreamingWrite RPC, either because of an error or because the RPC - * has been terminated by the client or the server. - */ -- (void)writeStreamWasInterruptedWithError:(const Status &)error { - if (error.ok()) { - // Graceful stop (due to Stop() or idle timeout). Make sure that's desirable. - HARD_ASSERT(![self shouldStartWriteStream], - "Write stream was stopped gracefully while still needed."); - } - - // If the write stream closed due to an error, invoke the error callbacks if there are pending - // writes. - if (!error.ok() && self.writePipeline.count > 0) { - if (_writeStream->handshake_complete()) { - // This error affects the actual writes. - [self handleWriteError:error]; - } else { - // If there was an error before the handshake finished, it's possible that the server is - // unable to process the stream token we're sending. (Perhaps it's too old?) - [self handleHandshakeError:error]; - } - } - - // The write stream might have been started by refilling the write pipeline for failed writes - if ([self shouldStartWriteStream]) { - [self startWriteStream]; - } -} - -- (void)handleHandshakeError:(const Status &)error { - HARD_ASSERT(!error.ok(), "Handling write error with status OK."); - // Reset the token if it's a permanent error, signaling the write stream is - // no longer valid. Note that the handshake does not count as a write: see - // comments on `Datastore::IsPermanentWriteError` for details. - if (Datastore::IsPermanentError(error)) { - NSString *token = [_writeStream->GetLastStreamToken() base64EncodedStringWithOptions:0]; - LOG_DEBUG("FSTRemoteStore %s error before completed handshake; resetting stream token %s: " - "error code: '%s', details: '%s'", - (__bridge void *)self, token, error.code(), error.error_message()); - _writeStream->SetLastStreamToken(nil); - [self.localStore setLastStreamToken:nil]; - } else { - // Some other error, don't reset stream token. Our stream logic will just retry with exponential - // backoff. - } -} - -- (void)handleWriteError:(const Status &)error { - HARD_ASSERT(!error.ok(), "Handling write error with status OK."); - // Only handle permanent errors here. If it's transient, just let the retry logic kick in. - if (!Datastore::IsPermanentWriteError(error)) { - return; - } - - // If this was a permanent error, the request itself was the problem so it's not going to - // succeed if we resend it. - FSTMutationBatch *batch = self.writePipeline[0]; - [self.writePipeline removeObjectAtIndex:0]; - - // In this case it's also unlikely that the server itself is melting down--this was just a - // bad request so inhibit backoff on the next restart. - _writeStream->InhibitBackoff(); - - [self.syncEngine rejectFailedWriteWithBatchID:batch.batchID error:util::MakeNSError(error)]; - - // It's possible that with the completion of this mutation another slot has freed up. - [self fillWritePipeline]; -} - -- (FSTTransaction *)transaction { - return [FSTTransaction transactionWithDatastore:_datastore.get()]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTSerializerBeta.mm b/Firestore/Source/Remote/FSTSerializerBeta.mm index 93e6f115035..e1115729fd8 100644 --- a/Firestore/Source/Remote/FSTSerializerBeta.mm +++ b/Firestore/Source/Remote/FSTSerializerBeta.mm @@ -66,6 +66,7 @@ using firebase::firestore::model::FieldMask; using firebase::firestore::model::FieldPath; using firebase::firestore::model::FieldTransform; +using firebase::firestore::model::NumericIncrementTransform; using firebase::firestore::model::Precondition; using firebase::firestore::model::ResourcePath; using firebase::firestore::model::ServerTimestampTransform; @@ -355,7 +356,8 @@ - (FSTReferenceValue *)decodedReferenceValue:(NSString *)resourceName { HARD_ASSERT(database_id == *self.databaseID, "Database %s:%s cannot encode reference from %s:%s", self.databaseID->project_id(), self.databaseID->database_id(), database_id.project_id(), database_id.database_id()); - return [FSTReferenceValue referenceValue:key databaseID:self.databaseID]; + return [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithDocumentKey:key] + databaseID:self.databaseID]; } - (GCFSArrayValue *)encodedArrayValue:(FSTArrayValue *)arrayValue { @@ -476,7 +478,7 @@ - (GCFSWrite *)encodedMutation:(FSTMutation *)mutation { } else if (mutationClass == [FSTPatchMutation class]) { FSTPatchMutation *patch = (FSTPatchMutation *)mutation; proto.update = [self encodedDocumentWithFields:patch.value key:patch.key]; - proto.updateMask = [self encodedFieldMask:patch.fieldMask]; + proto.updateMask = [self encodedFieldMask:*(patch.fieldMask)]; } else if (mutationClass == [FSTTransformMutation class]) { FSTTransformMutation *transform = (FSTTransformMutation *)mutation; @@ -610,7 +612,10 @@ - (GCFSDocumentTransform_FieldTransform *)encodedFieldTransform: } else if (fieldTransform.transformation().type() == TransformOperation::Type::ArrayRemove) { proto.removeAllFromArray_p = [self encodedArrayTransformElements:ArrayTransform::Elements(fieldTransform.transformation())]; - + } else if (fieldTransform.transformation().type() == TransformOperation::Type::Increment) { + const NumericIncrementTransform &incrementTransform = + static_cast(fieldTransform.transformation()); + proto.increment = [self encodedFieldValue:incrementTransform.operand()]; } else { HARD_FAIL("Unknown transform: %s type", fieldTransform.transformation().type()); } @@ -665,6 +670,14 @@ - (GCFSArrayValue *)encodedArrayTransformElements:(const std::vector([self decodedFieldValue:proto.increment]); + fieldTransforms.emplace_back(FieldPath::FromServerFormat(util::MakeString(proto.fieldPath)), + absl::make_unique(operand)); + break; + } + default: HARD_FAIL("Unknown transform: %s", proto); } @@ -764,10 +777,16 @@ - (FSTQuery *)decodedQueryFromDocumentsTarget:(GCFSTarget_DocumentsTarget *)targ - (GCFSTarget_QueryTarget *)encodedQueryTarget:(FSTQuery *)query { // Dissect the path into parent, collectionId, and optional key filter. GCFSTarget_QueryTarget *queryTarget = [GCFSTarget_QueryTarget message]; - if (query.path.size() == 0) { - queryTarget.parent = [self encodedQueryPath:query.path]; + const ResourcePath &path = query.path; + if (query.collectionGroup) { + HARD_ASSERT(path.size() % 2 == 0, + "Collection group queries should be within a document path or root."); + queryTarget.parent = [self encodedQueryPath:path]; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = query.collectionGroup; + from.allDescendants = YES; + [queryTarget.structuredQuery.fromArray addObject:from]; } else { - const ResourcePath &path = query.path; HARD_ASSERT(path.size() % 2 != 0, "Document queries with filters are not supported."); queryTarget.parent = [self encodedQueryPath:path.PopLast()]; GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; @@ -805,13 +824,18 @@ - (FSTQuery *)decodedQueryFromQueryTarget:(GCFSTarget_QueryTarget *)target { ResourcePath path = [self decodedQueryPath:target.parent]; GCFSStructuredQuery *query = target.structuredQuery; + NSString *collectionGroup; NSUInteger fromCount = query.fromArray_Count; if (fromCount > 0) { HARD_ASSERT(fromCount == 1, "StructuredQuery.from with more than one collection is not supported."); GCFSStructuredQuery_CollectionSelector *from = query.fromArray[0]; - path = path.Append(util::MakeString(from.collectionId)); + if (from.allDescendants) { + collectionGroup = from.collectionId; + } else { + path = path.Append(util::MakeString(from.collectionId)); + } } NSArray *filterBy; @@ -844,6 +868,7 @@ - (FSTQuery *)decodedQueryFromQueryTarget:(GCFSTarget_QueryTarget *)target { } return [[FSTQuery alloc] initWithPath:path + collectionGroup:collectionGroup filterBy:filterBy orderBy:orderBy limit:limit diff --git a/Firestore/Source/Remote/FSTStream.h b/Firestore/Source/Remote/FSTStream.h deleted file mode 100644 index d4183379329..00000000000 --- a/Firestore/Source/Remote/FSTStream.h +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2017 Google - * - * 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 - -#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" -#include "Firestore/core/src/firebase/firestore/remote/watch_change.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" - -@class FSTMutationResult; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTWatchStreamDelegate - -/** A protocol defining the events that can be emitted by the FSTWatchStream. */ -@protocol FSTWatchStreamDelegate - -/** Called by the FSTWatchStream when it is ready to accept outbound request messages. */ -- (void)watchStreamDidOpen; - -/** - * Called by the FSTWatchStream with changes and the snapshot versions included in in the - * WatchChange responses sent back by the server. - */ -- (void)watchStreamDidChange:(const firebase::firestore::remote::WatchChange &)change - snapshotVersion:(const firebase::firestore::model::SnapshotVersion &)snapshotVersion; - -/** - * Called by the FSTWatchStream when the underlying streaming RPC is interrupted for whatever - * reason, usually because of an error, but possibly due to an idle timeout. The error passed to - * this method may be nil, in which case the stream was closed without attributable fault. - * - * NOTE: This will not be called after `stop` is called on the stream. See "Starting and Stopping" - * on FSTStream for details. - */ -- (void)watchStreamWasInterruptedWithError:(const firebase::firestore::util::Status &)error; - -@end - -#pragma mark - FSTWriteStreamDelegate - -@protocol FSTWriteStreamDelegate - -/** Called by the FSTWriteStream when it is ready to accept outbound request messages. */ -- (void)writeStreamDidOpen; - -/** - * Called by the FSTWriteStream upon a successful handshake response from the server, which is the - * receiver's cue to send any pending writes. - */ -- (void)writeStreamDidCompleteHandshake; - -/** - * Called by the FSTWriteStream upon receiving a StreamingWriteResponse from the server that - * contains mutation results. - */ -- (void)writeStreamDidReceiveResponseWithVersion: - (const firebase::firestore::model::SnapshotVersion &)commitVersion - mutationResults:(NSArray *)results; - -/** - * Called when the FSTWriteStream's underlying RPC is interrupted for whatever reason, usually - * because of an error, but possibly due to an idle timeout. The error passed to this method may be - * nil, in which case the stream was closed without attributable fault. - * - * NOTE: This will not be called after `stop` is called on the stream. See "Starting and Stopping" - * on FSTStream for details. - */ -- (void)writeStreamWasInterruptedWithError:(const firebase::firestore::util::Status &)error; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTAsyncQueryListener.h b/Firestore/Source/Util/FSTAsyncQueryListener.h index 30a1a733699..82a3712df8a 100644 --- a/Firestore/Source/Util/FSTAsyncQueryListener.h +++ b/Firestore/Source/Util/FSTAsyncQueryListener.h @@ -16,8 +16,7 @@ #import -#import "Firestore/Source/Core/FSTViewSnapshot.h" - +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/util/executor.h" NS_ASSUME_NONNULL_BEGIN @@ -30,7 +29,8 @@ NS_ASSUME_NONNULL_BEGIN @interface FSTAsyncQueryListener : NSObject - (instancetype)initWithExecutor:(firebase::firestore::util::Executor*)executor - snapshotHandler:(FSTViewSnapshotHandler)snapshotHandler NS_DESIGNATED_INITIALIZER; + snapshotHandler:(firebase::firestore::core::ViewSnapshotHandler&&)snapshotHandler + NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; @@ -41,7 +41,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)mute; /** Creates an asynchronous version of the provided snapshot handler. */ -- (FSTViewSnapshotHandler)asyncSnapshotHandler; +- (firebase::firestore::core::ViewSnapshotHandler)asyncSnapshotHandler; @end diff --git a/Firestore/Source/Util/FSTAsyncQueryListener.mm b/Firestore/Source/Util/FSTAsyncQueryListener.mm index 2a02b3dc4a8..8133feeac46 100644 --- a/Firestore/Source/Util/FSTAsyncQueryListener.mm +++ b/Firestore/Source/Util/FSTAsyncQueryListener.mm @@ -16,16 +16,22 @@ #import "Firestore/Source/Util/FSTAsyncQueryListener.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" + +using firebase::firestore::core::ViewSnapshot; +using firebase::firestore::core::ViewSnapshotHandler; using firebase::firestore::util::Executor; +using firebase::firestore::util::StatusOr; @implementation FSTAsyncQueryListener { volatile BOOL _muted; - FSTViewSnapshotHandler _snapshotHandler; + ViewSnapshotHandler _snapshotHandler; Executor *_executor; } - (instancetype)initWithExecutor:(Executor *)executor - snapshotHandler:(FSTViewSnapshotHandler)snapshotHandler { + snapshotHandler:(ViewSnapshotHandler &&)snapshotHandler { if (self = [super init]) { _executor = executor; _snapshotHandler = snapshotHandler; @@ -33,16 +39,17 @@ - (instancetype)initWithExecutor:(Executor *)executor return self; } -- (FSTViewSnapshotHandler)asyncSnapshotHandler { +- (ViewSnapshotHandler)asyncSnapshotHandler { // Retain `self` strongly in resulting snapshot handler so that even if the // user releases the `FSTAsyncQueryListener` we'll continue to deliver // events. This is done specifically to facilitate the common case where // users just want to turn on notifications "forever" and don't want to have // to keep track of our handle to keep them going. - return ^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error) { - self->_executor->Execute([self, snapshot, error] { + return [self](const StatusOr &maybe_snapshot) { + // TODO(c++14): move into lambda. + self->_executor->Execute([self, maybe_snapshot] { if (!self->_muted) { - self->_snapshotHandler(snapshot, error); + self->_snapshotHandler(maybe_snapshot); } }); }; diff --git a/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift b/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift index d946f8999cf..e36ec498272 100644 --- a/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift +++ b/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift @@ -20,5 +20,6 @@ import FirebaseFirestore internal func isFirestorePassthroughType(_ value: T) -> Bool { return T.self == GeoPoint.self || - T.self == Timestamp.self + T.self == Timestamp.self || + T.self == DocumentReference.self } diff --git a/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift b/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift index 1c422d54a70..73de5e6a2f2 100644 --- a/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift +++ b/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift @@ -24,7 +24,7 @@ import FirebaseFirestore * to be marked required but that can't be done in an extension. Declaring the extension on the * protocol sidesteps this issue. */ -fileprivate protocol CodableGeoPoint: Codable { +private protocol CodableGeoPoint: Codable { var latitude: Double { get } var longitude: Double { get } @@ -32,7 +32,7 @@ fileprivate protocol CodableGeoPoint: Codable { } /** The keys in a GeoPoint. Must match the properties of CodableGeoPoint. */ -fileprivate enum GeoPointKeys: String, CodingKey { +private enum GeoPointKeys: String, CodingKey { case latitude case longitude } diff --git a/Firestore/Swift/Source/Codable/Timestamp+Codable.swift b/Firestore/Swift/Source/Codable/Timestamp+Codable.swift index 1581caaf14d..0db5d274d75 100644 --- a/Firestore/Swift/Source/Codable/Timestamp+Codable.swift +++ b/Firestore/Swift/Source/Codable/Timestamp+Codable.swift @@ -24,7 +24,7 @@ import FirebaseFirestore * to be marked required but that can't be done in an extension. Declaring the extension on the * protocol sidesteps this issue. */ -fileprivate protocol CodableTimestamp: Codable { +private protocol CodableTimestamp: Codable { var seconds: Int64 { get } var nanoseconds: Int32 { get } @@ -32,7 +32,7 @@ fileprivate protocol CodableTimestamp: Codable { } /** The keys in a GeoPoint. Must match the properties of CodableGeoPoint. */ -fileprivate enum TimestampKeys: String, CodingKey { +private enum TimestampKeys: String, CodingKey { case seconds case nanoseconds } diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift index bbc1f7beee5..27504065b53 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift @@ -29,6 +29,19 @@ extension Firestore { } } +extension DocumentSnapshot { + public func toObject(_ type: T.Type, with serverTimestampBehavior: ServerTimestampBehavior = .none) throws -> T { + guard let data: [String: Any] = self.data(with: serverTimestampBehavior) else { + throw DecodingError.valueNotFound(T.self, DecodingError.Context(codingPath: [], debugDescription: "The DocumentSnapshot has not data")) + } + let decoder = _FirestoreDecoder(referencing: data) + guard let value = try decoder.unbox(data, as: T.self) else { + throw DecodingError.valueNotFound(T.self, DecodingError.Context(codingPath: [], debugDescription: "The given dictionary was invalid")) + } + return value + } +} + class _FirestoreDecoder: Decoder { /// Options set on the top-level encoder to pass down the decoding hierarchy. diff --git a/Firestore/Swift/Tests/API/BasicCompileTests.swift b/Firestore/Swift/Tests/API/BasicCompileTests.swift index 628196e4cb7..d3634fe6bf2 100644 --- a/Firestore/Swift/Tests/API/BasicCompileTests.swift +++ b/Firestore/Swift/Tests/API/BasicCompileTests.swift @@ -19,7 +19,7 @@ import Foundation import XCTest -import FirebaseFirestore +import Firebase class BasicCompileTests: XCTestCase { func testCompiled() { @@ -99,6 +99,9 @@ func makeQuery(collection collectionRef: CollectionReference) -> Query { .order(by: "name", descending: true) .limit(to: 10) + // TODO(b/116617988): collectionGroup query. + // query = collectionRef.firestore.collectionGroup("collection") + return query } @@ -410,7 +413,11 @@ func types() { let _: Firestore let _: FirestoreSettings let _: GeoPoint + let _: Firebase.GeoPoint + let _: FirebaseFirestore.GeoPoint let _: Timestamp + let _: Firebase.Timestamp + let _: FirebaseFirestore.Timestamp let _: ListenerRegistration let _: Query let _: QuerySnapshot diff --git a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift index ec6b7c331db..03410834a43 100644 --- a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift +++ b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift @@ -19,12 +19,12 @@ import FirebaseFirestore @testable import FirebaseFirestoreSwift import XCTest -fileprivate func assertRoundTrip(model: X, encoded: [String: Any]) -> Void { +private func assertRoundTrip(model: X, encoded: [String: Any]) -> Void { let enc = assertEncodes(model, encoded: encoded) assertDecodes(enc, encoded: model) } -fileprivate func assertEncodes(_ model: X, encoded: [String: Any]) -> [String: Any] { +private func assertEncodes(_ model: X, encoded: [String: Any]) -> [String: Any] { do { let enc = try Firestore.Encoder().encode(model) XCTAssertEqual(enc as NSDictionary, encoded as NSDictionary) @@ -35,7 +35,7 @@ fileprivate func assertEncodes(_ model: X, encoded: [Str return ["": -1] } -fileprivate func assertDecodes(_ model: [String: Any], encoded: X) -> Void { +private func assertDecodes(_ model: [String: Any], encoded: X) -> Void { do { let decoded = try Firestore.Decoder().decode(X.self, from: model) XCTAssertEqual(decoded, encoded) @@ -44,7 +44,7 @@ fileprivate func assertDecodes(_ model: [String: Any], e } } -fileprivate func assertDecodingThrows(_ model: [String: Any], encoded: X) -> Void { +private func assertDecodingThrows(_ model: [String: Any], encoded: X) -> Void { do { _ = try Firestore.Decoder().decode(X.self, from: model) } catch { @@ -189,28 +189,32 @@ class CodableDocumentTests: XCTestCase { } // Uncomment if we decide to reenable embedded DocumentReference's -// -// func testDocumentReference() { -// struct Model: Codable, Equatable { -// let doc: DocumentReference -// } -// let d = FSTTestDocRef("abc/xyz") -// let model = Model(doc: d) -// assertRoundTrip(model: model, encoded: ["doc": d]) -// } -// -// // DocumentReference is not Codable unless embedded in a Firestore object. -// func testDocumentReferenceEncodes() { -// let doc = FSTTestDocRef("abc/xyz") -// do { -// _ = try JSONEncoder().encode(doc) -// XCTFail("Failed to throw") -// } catch FirebaseFirestoreSwift.FirestoreEncodingError.encodingIsNotSupported { -// return -// } catch { -// XCTFail("Unrecognized error: \(error)") -// } -// } + + func testDocumentReference() { + struct Model: Codable, Equatable { + let doc: DocumentReference + } + let d = FSTTestDocRef("abc/xyz") + let model = Model(doc: d) + assertRoundTrip(model: model, encoded: ["doc": d]) + } + + // DocumentReference is not Codable unless embedded in a Firestore object. + func testDocumentReferenceEncodes() { + struct Model: Codable, Equatable { + let doc: DocumentReference + } + let d = FSTTestDocRef("abc/xyz") + let model = Model(doc: d) + do { + let obj = try Firestore.Encoder().encode(model) + assertRoundTrip(model: model, encoded: obj) + } catch FirebaseFirestoreSwift.FirestoreEncodingError.encodingIsNotSupported { + return + } catch { + XCTFail("Unrecognized error: \(error)") + } + } func testTimestamp() { struct Model: Codable, Equatable { @@ -373,4 +377,57 @@ class CodableDocumentTests: XCTestCase { XCTAssertNil(encodedDict["mi"]) XCTAssertNil(encodedDict["mb"]) } + + func testToObject() { + let base: DocumentSnapshot = FSTTestDocSnapshot("rooms/foo", 1, [ + "s": "abc", + "d": 123, + "f": -4, + "l": 1_234_567_890_123, + "i": -4444, + "b": false, + "sh": 123, + "byte": 45, + "uchar": 44, + "ai": [1, 2, 3, 4], + "si": ["abc", "def"], + "caseSensitive": "aaa", + "casESensitive": "bbb", + "casESensitivE": "ccc", + ], false, false) + struct Model: Codable, Equatable { + let s: String + let d: Double + let f: Float + let l: CLongLong + let i: Int + let b: Bool + let sh: CShort + let byte: CChar + let uchar: CUnsignedChar + let ai: [Int] + let si: [String] + let caseSensitive: String + let casESensitive: String + let casESensitivE: String + } + let dict = [ + "s": "abc", + "d": 123, + "f": -4, + "l": 1_234_567_890_123, + "i": -4444, + "b": false, + "sh": 123, + "byte": 45, + "uchar": 44, + "ai": [1, 2, 3, 4], + "si": ["abc", "def"], + "caseSensitive": "aaa", + "casESensitive": "bbb", + "casESensitivE": "ccc", + ] as [String: Any] + let model: Model = try! base.toObject(Model.self) + assertRoundTrip(model: model, encoded: dict) + } } diff --git a/Firestore/core/CMakeLists.txt b/Firestore/core/CMakeLists.txt index a7fd72a8508..b9812921311 100644 --- a/Firestore/core/CMakeLists.txt +++ b/Firestore/core/CMakeLists.txt @@ -15,6 +15,7 @@ add_subdirectory(include/firebase/firestore) add_subdirectory(src/firebase/firestore) +add_subdirectory(src/firebase/firestore/api) add_subdirectory(src/firebase/firestore/auth) add_subdirectory(src/firebase/firestore/core) add_subdirectory(src/firebase/firestore/immutable) diff --git a/Firestore/core/include/firebase/firestore/timestamp.h b/Firestore/core/include/firebase/firestore/timestamp.h index 1736981ed34..fe4ac8f318f 100644 --- a/Firestore/core/include/firebase/firestore/timestamp.h +++ b/Firestore/core/include/firebase/firestore/timestamp.h @@ -19,6 +19,7 @@ #include #include +#include #include #if !defined(_STLPORT_VERSION) @@ -148,6 +149,8 @@ class Timestamp { * don't rely on the format of the string. */ std::string ToString() const; + friend std::ostream& operator<<(std::ostream& out, + const Timestamp& timestamp); private: // Checks that the number of seconds is within the supported date range, and diff --git a/Firestore/core/src/firebase/firestore/api/CMakeLists.txt b/Firestore/core/src/firebase/firestore/api/CMakeLists.txt new file mode 100644 index 00000000000..cc241aa0c7e --- /dev/null +++ b/Firestore/core/src/firebase/firestore/api/CMakeLists.txt @@ -0,0 +1,23 @@ +# Copyright 2019 Google +# +# 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. + +cc_library( + firebase_firestore_api + SOURCES + snapshot_metadata.cc + snapshot_metadata.h + DEPENDS + absl_meta + firebase_firestore_util +) diff --git a/Firestore/core/src/firebase/firestore/api/document_reference.h b/Firestore/core/src/firebase/firestore/api/document_reference.h new file mode 100644 index 00000000000..49d74cd5262 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/api/document_reference.h @@ -0,0 +1,109 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_DOCUMENT_REFERENCE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_DOCUMENT_REFERENCE_H_ + +#if !defined(__OBJC__) +#error "This header only supports Objective-C++" +#endif // !defined(__OBJC__) + +#import + +#include +#include +#include + +#import "FIRDocumentReference.h" +#import "FIRFirestoreSource.h" +#import "FIRListenerRegistration.h" + +#include "Firestore/core/src/firebase/firestore/api/document_snapshot.h" +#include "Firestore/core/src/firebase/firestore/core/listen_options.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/util/statusor_callback.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRFirestore; +@class FSTMutation; + +namespace firebase { +namespace firestore { +namespace api { + +class Firestore; + +class DocumentReference { + public: + using Completion = void (^)(NSError* _Nullable error) _Nullable; + using DocumentCompletion = void (^)(FIRDocumentSnapshot* _Nullable document, + NSError* _Nullable error) _Nullable; + + DocumentReference() = default; + DocumentReference(model::ResourcePath path, Firestore* firestore); + DocumentReference(model::DocumentKey document_key, Firestore* firestore) + : firestore_{firestore}, key_{std::move(document_key)} { + } + + size_t Hash() const; + + Firestore* firestore() const { + return firestore_; + } + const model::DocumentKey& key() const { + return key_; + } + + const std::string& document_id() const; + + // TODO(varconst) uncomment when core API CollectionReference is implemented. + // CollectionReference Parent() const; + + std::string Path() const; + + // TODO(varconst) uncomment when core API CollectionReference is implemented. + // CollectionReference GetCollectionReference( + // const std::string& collection_path) const; + + void SetData(std::vector&& mutations, Completion completion); + + void UpdateData(std::vector&& mutations, Completion completion); + + void DeleteDocument(Completion completion); + + void GetDocument(FIRFirestoreSource source, + util::StatusOrCallback&& completion); + + id AddSnapshotListener( + util::StatusOrCallback&& listener, + core::ListenOptions options); + + private: + Firestore* firestore_ = nullptr; + model::DocumentKey key_; +}; + +bool operator==(const DocumentReference& lhs, const DocumentReference& rhs); + +} // namespace api +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_DOCUMENT_REFERENCE_H_ diff --git a/Firestore/core/src/firebase/firestore/api/document_reference.mm b/Firestore/core/src/firebase/firestore/api/document_reference.mm new file mode 100644 index 00000000000..4ab3196f3fd --- /dev/null +++ b/Firestore/core/src/firebase/firestore/api/document_reference.mm @@ -0,0 +1,231 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/api/document_reference.h" + +#include + +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRListenerRegistration+Internal.h" +#import "Firestore/Source/Core/FSTEventManager.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Util/FSTAsyncQueryListener.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" +#include "Firestore/core/src/firebase/firestore/model/precondition.h" +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/util/error_apple.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" +#include "Firestore/core/src/firebase/firestore/util/objc_compatibility.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" + +NS_ASSUME_NONNULL_BEGIN + +namespace firebase { +namespace firestore { +namespace api { + +namespace objc = util::objc; +using core::ViewSnapshot; +using core::ViewSnapshotHandler; +using model::DocumentKey; +using model::Precondition; +using model::ResourcePath; +using util::MakeNSError; +using util::Status; +using util::StatusOr; +using util::StatusOrCallback; + +DocumentReference::DocumentReference(model::ResourcePath path, + Firestore* firestore) + : firestore_{firestore} { + if (path.size() % 2 != 0) { + HARD_FAIL( + "Invalid document reference. Document references must have an even " + "number of segments, but %s has %s", + path.CanonicalString(), path.size()); + } + key_ = DocumentKey{std::move(path)}; +} + +size_t DocumentReference::Hash() const { + return util::Hash(firestore_, key_); +} + +const std::string& DocumentReference::document_id() const { + return key_.path().last_segment(); +} + +// TODO(varconst) uncomment when core API CollectionReference is implemented. +// CollectionReference DocumentReference::Parent() const { +// return CollectionReference{firestore_, key_.path().PopLast()}; +// } + +std::string DocumentReference::Path() const { + return key_.path().CanonicalString(); +} + +// TODO(varconst) uncomment when core API CollectionReference is implemented. +// CollectionReference DocumentReference::GetCollectionReference( +// const std::string& collection_path) const { +// ResourcePath sub_path = ResourcePath::FromString(collection_path); +// ResourcePath path = key_.path().Append(sub_path); +// return CollectionReference{firestore_, path}; +// } + +void DocumentReference::SetData(std::vector&& mutations, + Completion completion) { + [firestore_->client() writeMutations:std::move(mutations) + completion:completion]; +} + +void DocumentReference::UpdateData(std::vector&& mutations, + Completion completion) { + return [firestore_->client() writeMutations:std::move(mutations) + completion:completion]; +} + +void DocumentReference::DeleteDocument(Completion completion) { + FSTDeleteMutation* mutation = + [[FSTDeleteMutation alloc] initWithKey:key_ + precondition:Precondition::None()]; + [firestore_->client() writeMutations:{mutation} completion:completion]; +} + +void DocumentReference::GetDocument( + FIRFirestoreSource source, + StatusOrCallback&& completion) { + if (source == FIRFirestoreSourceCache) { + [firestore_->client() getDocumentFromLocalCache:*this + completion:std::move(completion)]; + return; + } + + ListenOptions options( + /*include_query_metadata_changes=*/true, + /*include_document_metadata_changes=*/true, + /*wait_for_sync_when_online=*/true); + + // TODO(varconst): replace with a synchronization primitive that doesn't + // require libdispatch. See + // https://github.com/firebase/firebase-ios-sdk/blob/3ccbdcdc65c93c4621c045c3c6d15de9dcefa23f/Firestore/Source/Core/FSTFirestoreClient.mm#L161 + // for an example. + dispatch_semaphore_t registered = dispatch_semaphore_create(0); + auto listener_registration = std::make_shared>(); + StatusOrCallback listener = + [listener_registration, registered, completion, + source](StatusOr maybe_snapshot) { + if (!maybe_snapshot.ok()) { + completion(std::move(maybe_snapshot)); + return; + } + + DocumentSnapshot snapshot = std::move(maybe_snapshot).ValueOrDie(); + + // Remove query first before passing event to user to avoid user actions + // affecting the now stale query. + dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); + [*listener_registration remove]; + + if (!snapshot.exists() && snapshot.metadata().from_cache()) { + // TODO(dimond): Reconsider how to raise missing documents when + // offline. If we're online and the document doesn't exist then we + // call the completion with a document with document.exists set to + // false. If we're offline however, we call the completion handler + // with an error. Two options: 1) Cache the negative response from the + // server so we can deliver that even when you're offline. + // 2) Actually call the completion handler with an error if the + // document doesn't exist when you are offline. + completion( + Status{FirestoreErrorCode::Unavailable, + "Failed to get document because the client is offline."}); + } else if (snapshot.exists() && snapshot.metadata().from_cache() && + source == FIRFirestoreSourceServer) { + completion(Status{FirestoreErrorCode::Unavailable, + "Failed to get document from server. (However, " + "this document does exist in the local cache. Run " + "again without setting source to " + "FIRFirestoreSourceServer to retrieve the cached " + "document.)"}); + } else { + completion(std::move(snapshot)); + } + }; + + *listener_registration = + AddSnapshotListener(std::move(listener), std::move(options)); + dispatch_semaphore_signal(registered); +} + +id DocumentReference::AddSnapshotListener( + StatusOrCallback&& listener, ListenOptions options) { + Firestore* firestore = firestore_; + FSTQuery* query = [FSTQuery queryWithPath:key_.path()]; + DocumentKey key = key_; + + ViewSnapshotHandler handler = + [key, listener, firestore](const StatusOr& maybe_snapshot) { + if (!maybe_snapshot.ok()) { + listener(maybe_snapshot.status()); + return; + } + + const ViewSnapshot& snapshot = maybe_snapshot.ValueOrDie(); + HARD_ASSERT(snapshot.documents().size() <= 1, + "Too many documents returned on a document query"); + FSTDocument* document = snapshot.documents().GetDocument(key); + + bool has_pending_writes = + document + ? snapshot.mutated_keys().contains(key) + // We don't raise `has_pending_writes` for deleted documents. + : false; + + DocumentSnapshot result{firestore, std::move(key), document, + snapshot.from_cache(), has_pending_writes}; + listener(std::move(result)); + }; + + FSTAsyncQueryListener* async_listener = [[FSTAsyncQueryListener alloc] + initWithExecutor:firestore_->client().userExecutor + snapshotHandler:std::move(handler)]; + + FSTQueryListener* internal_listener = [firestore_->client() + listenToQuery:query + options:options + viewSnapshotHandler:[async_listener asyncSnapshotHandler]]; + return [[FSTListenerRegistration alloc] initWithClient:firestore_->client() + asyncListener:async_listener + internalListener:internal_listener]; +} + +bool operator==(const DocumentReference& lhs, const DocumentReference& rhs) { + return lhs.firestore() == rhs.firestore() && lhs.key() == rhs.key(); +} + +} // namespace api +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/api/document_snapshot.h b/Firestore/core/src/firebase/firestore/api/document_snapshot.h new file mode 100644 index 00000000000..4fcafa2b7d5 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/api/document_snapshot.h @@ -0,0 +1,110 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_DOCUMENT_SNAPSHOT_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_DOCUMENT_SNAPSHOT_H_ + +#if !defined(__OBJC__) +#error "This header only supports Objective-C++" +#endif // !defined(__OBJC__) + +#import + +#include +#include + +#import "Firestore/Source/Model/FSTFieldValue.h" + +#include "Firestore/core/src/firebase/firestore/api/snapshot_metadata.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/field_path.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FSTDocument; + +namespace firebase { +namespace firestore { +namespace api { + +class DocumentReference; +class Firestore; + +class DocumentSnapshot { + public: + DocumentSnapshot() = default; + + DocumentSnapshot(Firestore* firestore, + model::DocumentKey document_key, + FSTDocument* _Nullable document, + SnapshotMetadata metadata) + : firestore_{firestore}, + internal_key_{std::move(document_key)}, + internal_document_{document}, + metadata_{std::move(metadata)} { + } + + DocumentSnapshot(Firestore* firestore, + model::DocumentKey document_key, + FSTDocument* _Nullable document, + bool from_cache, + bool has_pending_writes) + : firestore_{firestore}, + internal_key_{std::move(document_key)}, + internal_document_{document}, + metadata_{has_pending_writes, from_cache} { + } + + size_t Hash() const; + + bool exists() const { + return internal_document_ != nil; + } + FSTDocument* internal_document() const { + return internal_document_; + } + std::string document_id() const; + + const SnapshotMetadata& metadata() const { + return metadata_; + } + + DocumentReference CreateReference() const; + + FSTObjectValue* _Nullable GetData() const; + id _Nullable GetValue(const model::FieldPath& field_path) const; + + Firestore* firestore() const { + return firestore_; + } + + friend bool operator==(const DocumentSnapshot& lhs, + const DocumentSnapshot& rhs); + + private: + Firestore* firestore_ = nullptr; + model::DocumentKey internal_key_; + FSTDocument* internal_document_ = nil; + SnapshotMetadata metadata_; +}; + +} // namespace api +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_DOCUMENT_SNAPSHOT_H_ diff --git a/Firestore/core/src/firebase/firestore/api/document_snapshot.mm b/Firestore/core/src/firebase/firestore/api/document_snapshot.mm new file mode 100644 index 00000000000..f07885b3a2a --- /dev/null +++ b/Firestore/core/src/firebase/firestore/api/document_snapshot.mm @@ -0,0 +1,66 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/api/document_snapshot.h" + +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/Model/FSTDocument.h" + +#include "Firestore/core/src/firebase/firestore/util/hashing.h" +#include "Firestore/core/src/firebase/firestore/util/objc_compatibility.h" + +NS_ASSUME_NONNULL_BEGIN + +namespace firebase { +namespace firestore { +namespace api { + +namespace objc = util::objc; +using model::DocumentKey; +using model::FieldPath; + +size_t DocumentSnapshot::Hash() const { + return util::Hash(firestore_, internal_key_, internal_document_, metadata_); +} + +DocumentReference DocumentSnapshot::CreateReference() const { + return DocumentReference{internal_key_, firestore_}; +} + +std::string DocumentSnapshot::document_id() const { + return internal_key_.path().last_segment(); +} + +FSTObjectValue* _Nullable DocumentSnapshot::GetData() const { + return internal_document_ == nil ? nil : [internal_document_ data]; +} + +id _Nullable DocumentSnapshot::GetValue(const FieldPath& field_path) const { + return [[internal_document_ data] valueForPath:field_path]; +} + +bool operator==(const DocumentSnapshot& lhs, const DocumentSnapshot& rhs) { + return lhs.firestore_ == rhs.firestore_ && + lhs.internal_key_ == rhs.internal_key_ && + objc::Equals(lhs.internal_document_, rhs.internal_document_) && + lhs.metadata_ == rhs.metadata_; +} + +} // namespace api +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/api/firestore.h b/Firestore/core/src/firebase/firestore/api/firestore.h new file mode 100644 index 00000000000..4be3b0bb4ae --- /dev/null +++ b/Firestore/core/src/firebase/firestore/api/firestore.h @@ -0,0 +1,129 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_FIRESTORE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_FIRESTORE_H_ + +#if !defined(__OBJC__) +#error "This header only supports Objective-C++" +#endif // !defined(__OBJC__) + +#import + +#include +#include // NOLINT(build/c++11) +#include +#include +#include "dispatch/dispatch.h" + +#include "Firestore/core/src/firebase/firestore/auth/credentials_provider.h" +#include "Firestore/core/src/firebase/firestore/model/database_id.h" +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRApp; +@class FIRCollectionReference; +@class FIRFirestore; +@class FIRFirestoreSettings; +@class FIRQuery; +@class FIRTransaction; +@class FIRWriteBatch; +@class FSTFirestoreClient; + +namespace firebase { +namespace firestore { +namespace api { + +class DocumentReference; + +class Firestore { + public: + using TransactionBlock = id _Nullable (^)(FIRTransaction*, NSError** error); + using ErrorCompletion = void (^)(NSError* _Nullable error); + using ResultOrErrorCompletion = void (^)(id _Nullable result, + NSError* _Nullable error); + + Firestore() = default; + + Firestore(std::string project_id, + std::string database, + std::string persistence_key, + std::unique_ptr credentials_provider, + std::unique_ptr worker_queue, + void* extension); + + const model::DatabaseId& database_id() const { + return database_id_; + } + + const std::string& persistence_key() const { + return persistence_key_; + } + + FSTFirestoreClient* client() { + return client_; + } + + util::AsyncQueue* worker_queue(); + + void* extension() { + return extension_; + } + + FIRFirestoreSettings* settings() const; + void set_settings(FIRFirestoreSettings* settings); + + FIRCollectionReference* GetCollection(absl::string_view collection_path); + DocumentReference GetDocument(absl::string_view document_path); + FIRWriteBatch* GetBatch(); + FIRQuery* GetCollectionGroup(NSString* collection_id); + + void RunTransaction(TransactionBlock update_block, + dispatch_queue_t queue, + ResultOrErrorCompletion completion); + + void Shutdown(ErrorCompletion completion); + + void EnableNetwork(ErrorCompletion completion); + void DisableNetwork(ErrorCompletion completion); + + private: + void EnsureClientConfigured(); + + model::DatabaseId database_id_; + std::unique_ptr credentials_provider_; + std::string persistence_key_; + FSTFirestoreClient* client_ = nil; + + // Ownership will be transferred to `FSTFirestoreClient` as soon as the + // client is created. + std::unique_ptr worker_queue_; + + void* extension_ = nullptr; + + FIRFirestoreSettings* settings_ = nil; + + mutable std::mutex mutex_; +}; + +} // namespace api +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_FIRESTORE_H_ diff --git a/Firestore/core/src/firebase/firestore/api/firestore.mm b/Firestore/core/src/firebase/firestore/api/firestore.mm new file mode 100644 index 00000000000..e248dbc4ba9 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/api/firestore.mm @@ -0,0 +1,202 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/api/firestore.h" + +#import "FIRFirestoreSettings.h" +#import "Firestore/Source/API/FIRCollectionReference+Internal.h" +#import "Firestore/Source/API/FIRDocumentReference+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRQuery+Internal.h" +#import "Firestore/Source/API/FIRTransaction+Internal.h" +#import "Firestore/Source/API/FIRWriteBatch+Internal.h" +#import "Firestore/Source/Core/FSTFirestoreClient.h" +#import "Firestore/Source/Core/FSTQuery.h" + +#include "Firestore/core/src/firebase/firestore/api/document_reference.h" +#include "Firestore/core/src/firebase/firestore/auth/firebase_credentials_provider_apple.h" +#include "Firestore/core/src/firebase/firestore/core/transaction.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "absl/memory/memory.h" + +namespace firebase { +namespace firestore { +namespace api { + +using firebase::firestore::api::Firestore; +using firebase::firestore::auth::CredentialsProvider; +using firebase::firestore::core::DatabaseInfo; +using firebase::firestore::core::Transaction; +using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::ResourcePath; +using util::AsyncQueue; +using util::Executor; +using util::ExecutorLibdispatch; + +Firestore::Firestore(std::string project_id, + std::string database, + std::string persistence_key, + std::unique_ptr credentials_provider, + std::unique_ptr worker_queue, + void* extension) + : database_id_{std::move(project_id), std::move(database)}, + credentials_provider_{std::move(credentials_provider)}, + persistence_key_{std::move(persistence_key)}, + worker_queue_{std::move(worker_queue)}, + extension_{extension} { + settings_ = [[FIRFirestoreSettings alloc] init]; +} + +AsyncQueue* Firestore::worker_queue() { + return [client_ workerQueue]; +} + +FIRFirestoreSettings* Firestore::settings() const { + std::lock_guard lock{mutex_}; + // Disallow mutation of our internal settings + return [settings_ copy]; +} + +void Firestore::set_settings(FIRFirestoreSettings* settings) { + std::lock_guard lock{mutex_}; + // As a special exception, don't throw if the same settings are passed + // repeatedly. This should make it more friendly to create a Firestore + // instance. + if (client_ && ![settings_ isEqual:settings]) { + HARD_FAIL( + "Firestore instance has already been started and its settings can " + "no longer be changed. You can only set settings before calling any " + "other methods on a Firestore instance."); + } + settings_ = [settings copy]; +} + +FIRCollectionReference* Firestore::GetCollection( + absl::string_view collection_path) { + EnsureClientConfigured(); + ResourcePath path = ResourcePath::FromString(collection_path); + return [FIRCollectionReference + referenceWithPath:path + firestore:[FIRFirestore recoverFromFirestore:this]]; +} + +DocumentReference Firestore::GetDocument(absl::string_view document_path) { + EnsureClientConfigured(); + return DocumentReference{ResourcePath::FromString(document_path), this}; +} + +FIRWriteBatch* Firestore::GetBatch() { + EnsureClientConfigured(); + FIRFirestore* wrapper = [FIRFirestore recoverFromFirestore:this]; + + return [FIRWriteBatch writeBatchWithFirestore:wrapper]; +} + +FIRQuery* Firestore::GetCollectionGroup(NSString* collection_id) { + EnsureClientConfigured(); + FIRFirestore* wrapper = [FIRFirestore recoverFromFirestore:this]; + + return + [FIRQuery referenceWithQuery:[FSTQuery queryWithPath:ResourcePath::Empty() + collectionGroup:collection_id] + firestore:wrapper]; +} + +void Firestore::RunTransaction(TransactionBlock update_block, + dispatch_queue_t queue, + ResultOrErrorCompletion completion) { + EnsureClientConfigured(); + FIRFirestore* wrapper = [FIRFirestore recoverFromFirestore:this]; + + FSTTransactionBlock wrapped_update = + ^(std::shared_ptr internal_transaction, + void (^internal_completion)(id _Nullable, NSError* _Nullable)) { + FIRTransaction* transaction = [FIRTransaction + transactionWithInternalTransaction:std::move(internal_transaction) + firestore:wrapper]; + + dispatch_async(queue, ^{ + NSError* _Nullable error = nil; + id _Nullable result = update_block(transaction, &error); + if (error) { + // Force the result to be nil in the case of an error, in case the + // user set both. + result = nil; + } + internal_completion(result, error); + }); + }; + + [client_ transactionWithRetries:5 + updateBlock:wrapped_update + completion:completion]; +} + +void Firestore::Shutdown(ErrorCompletion completion) { + if (!client_) { + if (completion) { + // We should be dispatching the callback on the user dispatch queue + // but if the client is nil here that queue was never created. + completion(nil); + } + } else { + [client_ shutdownWithCompletion:completion]; + } +} + +void Firestore::EnableNetwork(ErrorCompletion completion) { + EnsureClientConfigured(); + [client_ enableNetworkWithCompletion:completion]; +} + +void Firestore::DisableNetwork(ErrorCompletion completion) { + EnsureClientConfigured(); + [client_ disableNetworkWithCompletion:completion]; +} + +void Firestore::EnsureClientConfigured() { + std::lock_guard lock{mutex_}; + + if (!client_) { + // These values are validated elsewhere; this is just double-checking: + HARD_ASSERT(settings_.host, "FirestoreSettings.host cannot be nil."); + HARD_ASSERT(settings_.dispatchQueue, + "FirestoreSettings.dispatchQueue cannot be nil."); + + DatabaseInfo database_info(database_id_, persistence_key_, + util::MakeString(settings_.host), + settings_.sslEnabled); + + std::unique_ptr user_executor = + absl::make_unique(settings_.dispatchQueue); + + HARD_ASSERT(worker_queue_, "Expected non-null worker queue"); + client_ = + [FSTFirestoreClient clientWithDatabaseInfo:database_info + settings:settings_ + credentialsProvider:credentials_provider_.get() + userExecutor:std::move(user_executor) + workerQueue:std::move(worker_queue_)]; + } +} + +} // namespace api +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/api/query_snapshot.h b/Firestore/core/src/firebase/firestore/api/query_snapshot.h new file mode 100644 index 00000000000..7c5f49dde79 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/api/query_snapshot.h @@ -0,0 +1,110 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_QUERY_SNAPSHOT_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_QUERY_SNAPSHOT_H_ + +#if !defined(__OBJC__) +#error "This header only supports Objective-C++" +#endif // !defined(__OBJC__) + +#import + +#include +#include + +#include "Firestore/core/src/firebase/firestore/api/document_snapshot.h" +#include "Firestore/core/src/firebase/firestore/api/snapshot_metadata.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FSTQuery; + +namespace firebase { +namespace firestore { +namespace api { + +/** + * A `QuerySnapshot` contains zero or more `DocumentSnapshot` objects. + */ +class QuerySnapshot { + public: + QuerySnapshot(Firestore* firestore, + FSTQuery* query, + core::ViewSnapshot&& snapshot, + SnapshotMetadata metadata) + : firestore_(firestore), + internal_query_(query), + snapshot_(std::move(snapshot)), + metadata_(std::move(metadata)) { + } + + size_t Hash() const; + + /** + * Indicates whether this `QuerySnapshot` is empty (contains no documents). + */ + bool empty() const { + return snapshot_.documents().empty(); + } + + /** The count of documents in this `QuerySnapshot`. */ + size_t size() const { + return snapshot_.documents().size(); + } + + Firestore* firestore() const { + return firestore_; + } + + FSTQuery* internal_query() const { + return internal_query_; + } + + const core::ViewSnapshot& view_snapshot() const { + return snapshot_; + } + + /** + * Metadata about this snapshot, concerning its source and if it has local + * modifications. + */ + const SnapshotMetadata& metadata() const { + return metadata_; + } + + /** Iterates over the `DocumentSnapshots` that make up this query snapshot. */ + void ForEachDocument( + const std::function& callback) const; + + friend bool operator==(const QuerySnapshot& lhs, const QuerySnapshot& rhs); + + private: + Firestore* firestore_ = nullptr; + FSTQuery* internal_query_ = nil; + core::ViewSnapshot snapshot_; + SnapshotMetadata metadata_; +}; + +} // namespace api +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_QUERY_SNAPSHOT_H_ diff --git a/Firestore/core/src/firebase/firestore/api/query_snapshot.mm b/Firestore/core/src/firebase/firestore/api/query_snapshot.mm new file mode 100644 index 00000000000..7992cc15fba --- /dev/null +++ b/Firestore/core/src/firebase/firestore/api/query_snapshot.mm @@ -0,0 +1,71 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/api/query_snapshot.h" + +#include + +#import "Firestore/Source/API/FIRDocumentChange+Internal.h" +#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRQuery+Internal.h" +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Util/FSTUsageValidation.h" + +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" +#include "Firestore/core/src/firebase/firestore/util/objc_compatibility.h" + +NS_ASSUME_NONNULL_BEGIN + +namespace firebase { +namespace firestore { +namespace api { + +namespace objc = util::objc; +using api::Firestore; +using core::ViewSnapshot; +using model::DocumentSet; + +bool operator==(const QuerySnapshot& lhs, const QuerySnapshot& rhs) { + return lhs.firestore_ == rhs.firestore_ && + objc::Equals(lhs.internal_query_, rhs.internal_query_) && + lhs.snapshot_ == rhs.snapshot_ && lhs.metadata_ == rhs.metadata_; +} + +size_t QuerySnapshot::Hash() const { + return util::Hash(firestore_, internal_query_, snapshot_, metadata_); +} + +void QuerySnapshot::ForEachDocument( + const std::function& callback) const { + DocumentSet documentSet = snapshot_.documents(); + bool from_cache = metadata_.from_cache(); + + for (FSTDocument* document : documentSet) { + bool has_pending_writes = snapshot_.mutated_keys().contains(document.key); + DocumentSnapshot snap(firestore_, document.key, document, from_cache, + has_pending_writes); + callback(std::move(snap)); + } +} + +} // namespace api +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/api/snapshot_metadata.cc b/Firestore/core/src/firebase/firestore/api/snapshot_metadata.cc new file mode 100644 index 00000000000..1790bfc6bf6 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/api/snapshot_metadata.cc @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/api/snapshot_metadata.h" + +#include "Firestore/core/src/firebase/firestore/util/hashing.h" + +namespace firebase { +namespace firestore { +namespace api { + +bool operator==(const SnapshotMetadata& lhs, const SnapshotMetadata& rhs) { + return lhs.pending_writes_ == rhs.pending_writes_ && + lhs.from_cache_ == rhs.from_cache_; +} + +size_t SnapshotMetadata::Hash() const { + return util::Hash(pending_writes_, from_cache_); +} + +} // namespace api +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/api/snapshot_metadata.h b/Firestore/core/src/firebase/firestore/api/snapshot_metadata.h new file mode 100644 index 00000000000..9fe2b808b4d --- /dev/null +++ b/Firestore/core/src/firebase/firestore/api/snapshot_metadata.h @@ -0,0 +1,64 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_SNAPSHOT_METADATA_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_SNAPSHOT_METADATA_H_ + +#include + +namespace firebase { +namespace firestore { +namespace api { + +/** Metadata about a snapshot, describing the state of the snapshot. */ +class SnapshotMetadata { + public: + SnapshotMetadata() = default; + SnapshotMetadata(bool pending_writes, bool from_cache) + : pending_writes_(pending_writes), from_cache_(from_cache) { + } + + /** + * Returns true if the snapshot contains the result of local writes (e.g. + * set() or update() calls) that have not yet been committed to the backend. + */ + bool pending_writes() const { + return pending_writes_; + } + + /** + * Returns true if the snapshot was created from cached data rather than + * guaranteed up-to-date server data. + */ + bool from_cache() const { + return from_cache_; + } + + friend bool operator==(const SnapshotMetadata& lhs, + const SnapshotMetadata& rhs); + + size_t Hash() const; + + private: + bool pending_writes_ = false; + bool from_cache_ = false; +}; + +} // namespace api +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_SNAPSHOT_METADATA_H_ diff --git a/Firestore/core/src/firebase/firestore/core/CMakeLists.txt b/Firestore/core/src/firebase/firestore/core/CMakeLists.txt index 6e734049fdc..2481596abca 100644 --- a/Firestore/core/src/firebase/firestore/core/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/core/CMakeLists.txt @@ -19,6 +19,7 @@ cc_library( database_info.h filter.cc filter.h + listen_options.h target_id_generator.cc target_id_generator.h query.cc diff --git a/Firestore/core/src/firebase/firestore/core/listen_options.h b/Firestore/core/src/firebase/firestore/core/listen_options.h new file mode 100644 index 00000000000..913fe8693d2 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/listen_options.h @@ -0,0 +1,91 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_LISTEN_OPTIONS_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_LISTEN_OPTIONS_H_ + +namespace firebase { +namespace firestore { +namespace core { + +class ListenOptions { + public: + ListenOptions() = default; + + /** + * Creates a new ListenOptions. + * + * @param include_query_metadata_changes Raise events when only metadata of + * the query changes. + * @param include_document_metadata_changes Raise events when only metadata of + * documents changes. + * @param wait_for_sync_when_online Wait for a sync with the server when + * online, but still raise events while offline + */ + ListenOptions(bool include_query_metadata_changes, + bool include_document_metadata_changes, + bool wait_for_sync_when_online) + : include_query_metadata_changes_(include_query_metadata_changes), + include_document_metadata_changes_(include_document_metadata_changes), + wait_for_sync_when_online_(wait_for_sync_when_online) { + } + + /** + * Creates a default ListenOptions, with metadata changes and + * wait_for_sync_when_online disabled. + */ + static ListenOptions DefaultOptions() { + return ListenOptions( + /*include_query_metadata_changes=*/false, + /*include_document_metadata_changes=*/false, + /*wait_for_sync_when_online=*/false); + } + + /** + * Creates a ListenOptions which optionally includes both query and document + * metadata changes. + */ + static ListenOptions FromIncludeMetadataChanges( + bool include_metadata_changes) { + return ListenOptions( + /*include_query_metadata_changes=*/include_metadata_changes, + /*include_document_metadata_changes=*/include_metadata_changes, + /*wait_for_sync_when_online=*/false); + } + + bool include_query_metadata_changes() const { + return include_query_metadata_changes_; + } + + bool include_document_metadata_changes() const { + return include_document_metadata_changes_; + } + + bool wait_for_sync_when_online() const { + return wait_for_sync_when_online_; + } + + private: + bool include_query_metadata_changes_ = false; + bool include_document_metadata_changes_ = false; + bool wait_for_sync_when_online_ = false; +}; + +} // namespace core +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_LISTEN_OPTIONS_H_ diff --git a/Firestore/core/src/firebase/firestore/core/query.h b/Firestore/core/src/firebase/firestore/core/query.h index 41d0df136bf..1d9bb37a6b9 100644 --- a/Firestore/core/src/firebase/firestore/core/query.h +++ b/Firestore/core/src/firebase/firestore/core/query.h @@ -92,6 +92,8 @@ class Query { // existing filters, plus the new one. (Both Query and Filter objects are // immutable.) Filters are not shared across unrelated Query instances. std::vector> filters_; + + // TODO(rsgowman): Port collection group queries logic. }; inline bool operator==(const Query& lhs, const Query& rhs) { diff --git a/Firestore/core/src/firebase/firestore/core/transaction.h b/Firestore/core/src/firebase/firestore/core/transaction.h new file mode 100644 index 00000000000..37d9d664326 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/transaction.h @@ -0,0 +1,142 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_TRANSACTION_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_TRANSACTION_H_ + +#if !defined(__OBJC__) +#error "This header only supports Objective-C++" +#endif // !defined(__OBJC__) + +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/core/user_data.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/precondition.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/remote/datastore.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "absl/types/optional.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FSTMaybeDocument; +@class FSTMutation; + +namespace firebase { +namespace firestore { +namespace core { + +class Transaction { + public: + // TODO(varconst): once `FSTMaybeDocument` is replaced with a C++ equivalent, + // this function could take a single `StatusOr` parameter. + using LookupCallback = std::function&, const util::Status&)>; + using CommitCallback = std::function; + + Transaction() = default; + explicit Transaction(remote::Datastore* transaction); + + /** + * Takes a set of keys and asynchronously attempts to fetch all the documents + * from the backend, ignoring any local changes. + */ + void Lookup(const std::vector& keys, + LookupCallback&& callback); + + /** + * Stores mutation for the given key and set data, to be committed when + * `Commit` is called. + */ + void Set(const model::DocumentKey& key, ParsedSetData&& data); + + /** + * Stores mutations for the given key and update data, to be committed when + * `Commit` is called. + */ + void Update(const model::DocumentKey& key, ParsedUpdateData&& data); + + /** + * Stores a delete mutation for the given key, to be committed when `Commit` + * is called. + */ + void Delete(const model::DocumentKey& key); + + /** + * Attempts to commit the mutations set on this transaction. Invokes the given + * callback when finished. Once this is called, no other mutations or + * commits are allowed on the transaction. + */ + void Commit(CommitCallback&& callback); + + private: + /** + * Every time a document is read, this should be called to record its version. + * If we read two different versions of the same document, this will return an + * error. When the transaction is committed, the versions recorded will be set + * as preconditions on the writes sent to the backend. + */ + util::Status RecordVersion(FSTMaybeDocument* doc); + + /** Stores mutations to be written when `Commit` is called. */ + void WriteMutations(std::vector&& mutations); + + /** + * Returns version of this doc when it was read in this transaction as a + * precondition, or no precondition if it was not read. + */ + model::Precondition CreatePrecondition(const model::DocumentKey& key); + + /** + * Returns the precondition for a document if the operation is an update. Will + * return a failed status if an error occurred. + */ + util::StatusOr CreateUpdatePrecondition( + const model::DocumentKey& key); + + void EnsureCommitNotCalled(); + + absl::optional GetVersion( + const model::DocumentKey& key) const; + + remote::Datastore* datastore_ = nullptr; + + std::vector mutations_; + bool committed_ = false; + + /** + * An error that may have occurred as a consequence of a write. If set, needs + * to be raised in the completion handler instead of trying to commit. + */ + util::Status last_write_error_; + + std::unordered_map + read_versions_; +}; + +} // namespace core +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_TRANSACTION_H_ diff --git a/Firestore/core/src/firebase/firestore/core/transaction.mm b/Firestore/core/src/firebase/firestore/core/transaction.mm new file mode 100644 index 00000000000..d4fa6ca8946 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/transaction.mm @@ -0,0 +1,210 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/core/transaction.h" + +#include +#include +#include + +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTMutation.h" + +#include "Firestore/core/include/firebase/firestore/firestore_errors.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" + +using firebase::firestore::FirestoreErrorCode; +using firebase::firestore::core::ParsedSetData; +using firebase::firestore::core::ParsedUpdateData; +using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::DocumentKeyHash; +using firebase::firestore::model::Precondition; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::remote::Datastore; +using firebase::firestore::util::Status; +using firebase::firestore::util::StatusOr; + +namespace firebase { +namespace firestore { +namespace core { + +Transaction::Transaction(Datastore* datastore) + : datastore_{NOT_NULL(datastore)} { +} + +Status Transaction::RecordVersion(FSTMaybeDocument* doc) { + SnapshotVersion doc_version; + + if ([doc isKindOfClass:[FSTDocument class]]) { + doc_version = doc.version; + } else if ([doc isKindOfClass:[FSTDeletedDocument class]]) { + // For deleted docs, we must record an explicit no version to build the + // right precondition when writing. + doc_version = SnapshotVersion::None(); + } else { + HARD_FAIL("Unexpected document type in transaction: %s", + NSStringFromClass([doc class])); + } + + absl::optional existing_version = GetVersion(doc.key); + if (existing_version.has_value()) { + if (doc_version != existing_version.value()) { + // This transaction will fail no matter what. + return Status{FirestoreErrorCode::Aborted, + "Document version changed between two reads."}; + } + return Status::OK(); + } else { + read_versions_[doc.key] = doc_version; + return Status::OK(); + } +} + +void Transaction::Lookup(const std::vector& keys, + LookupCallback&& callback) { + EnsureCommitNotCalled(); + + HARD_ASSERT(mutations_.empty(), + "Transactions lookups are invalid after writes."); + + datastore_->LookupDocuments( + keys, [this, callback](const std::vector& documents, + const Status& status) { + if (!status.ok()) { + callback({}, status); + return; + } + + for (FSTMaybeDocument* doc : documents) { + Status record_error = RecordVersion(doc); + if (!record_error.ok()) { + callback({}, record_error); + return; + } + } + + callback(documents, Status::OK()); + }); +} + +void Transaction::WriteMutations(std::vector&& mutations) { + EnsureCommitNotCalled(); + // `move` will become appropriate once `FSTMutation` is replaced by the C++ + // equivalent. + std::move(mutations.begin(), mutations.end(), std::back_inserter(mutations_)); +} + +Precondition Transaction::CreatePrecondition(const DocumentKey& key) { + absl::optional version = GetVersion(key); + if (version.has_value()) { + return Precondition::UpdateTime(version.value()); + } else { + return Precondition::None(); + } +} + +StatusOr Transaction::CreateUpdatePrecondition( + const DocumentKey& key) { + absl::optional version = GetVersion(key); + + if (version.has_value() && version.value() == SnapshotVersion::None()) { + // The document to update doesn't exist, so fail the transaction. + return Status{FirestoreErrorCode::Aborted, + "Can't update a document that doesn't exist."}; + } else if (version.has_value()) { + // Document exists, just base precondition on document update time. + return Precondition::UpdateTime(version.value()); + } else { + // Document was not read, so we just use the preconditions for a blind + // update. + return Precondition::Exists(true); + } +} + +void Transaction::Set(const DocumentKey& key, ParsedSetData&& data) { + WriteMutations(std::move(data).ToMutations(key, CreatePrecondition(key))); +} + +void Transaction::Update(const DocumentKey& key, ParsedUpdateData&& data) { + StatusOr maybe_precondition = CreateUpdatePrecondition(key); + if (!maybe_precondition.ok()) { + last_write_error_ = maybe_precondition.status(); + } else { + WriteMutations( + std::move(data).ToMutations(key, maybe_precondition.ValueOrDie())); + } +} + +void Transaction::Delete(const DocumentKey& key) { + FSTMutation* mutation = + [[FSTDeleteMutation alloc] initWithKey:key + precondition:CreatePrecondition(key)]; + WriteMutations({mutation}); + + // Since the delete will be applied before all following writes, we need to + // ensure that the precondition for the next write will be exists: false. + read_versions_[key] = SnapshotVersion::None(); +} + +void Transaction::Commit(CommitCallback&& callback) { + EnsureCommitNotCalled(); + + // If there was an error writing, raise that error now + if (!last_write_error_.ok()) { + callback(last_write_error_); + return; + } + + // Make a list of read documents that haven't been written. + std::unordered_set unwritten; + for (const auto& kv : read_versions_) { + unwritten.insert(kv.first); + }; + // For each mutation, note that the doc was written. + for (FSTMutation* mutation : mutations_) { + unwritten.erase(mutation.key); + } + + if (!unwritten.empty()) { + // TODO(klimt): This is a temporary restriction, until "verify" is supported + // on the backend. + callback( + Status{FirestoreErrorCode::FailedPrecondition, + "Every document read in a transaction must also be written in " + "that transaction."}); + } else { + committed_ = true; + datastore_->CommitMutations(mutations_, std::move(callback)); + } +} + +void Transaction::EnsureCommitNotCalled() { + HARD_ASSERT(!committed_, "A transaction object cannot be used after its " + "update callback has been invoked."); +} + +absl::optional Transaction::GetVersion( + const DocumentKey& key) const { + auto found = read_versions_.find(key); + if (found != read_versions_.end()) { + return found->second; + } + return absl::nullopt; +} + +} // namespace core +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/core/user_data.h b/Firestore/core/src/firebase/firestore/core/user_data.h index cdb059af5c0..d3c3bf94b1e 100644 --- a/Firestore/core/src/firebase/firestore/core/user_data.h +++ b/Firestore/core/src/firebase/firestore/core/user_data.h @@ -266,7 +266,7 @@ class ParsedSetData { * * This method consumes the values stored in the ParsedSetData */ - NSArray* ToMutations( + std::vector ToMutations( const model::DocumentKey& key, const model::Precondition& precondition) &&; @@ -299,7 +299,7 @@ class ParsedUpdateData { * * This method consumes the values stored in the ParsedUpdateData */ - NSArray* ToMutations( + std::vector ToMutations( const model::DocumentKey& key, const model::Precondition& precondition) &&; diff --git a/Firestore/core/src/firebase/firestore/core/user_data.mm b/Firestore/core/src/firebase/firestore/core/user_data.mm index 525c0ee212d..237e49f234c 100644 --- a/Firestore/core/src/firebase/firestore/core/user_data.mm +++ b/Firestore/core/src/firebase/firestore/core/user_data.mm @@ -210,25 +210,30 @@ patch_{true} { } -NSArray* ParsedSetData::ToMutations( +std::vector ParsedSetData::ToMutations( const DocumentKey& key, const Precondition& precondition) && { - NSMutableArray* mutations = [NSMutableArray array]; + std::vector mutations; if (patch_) { - [mutations - addObject:[[FSTPatchMutation alloc] initWithKey:key - fieldMask:std::move(field_mask_) - value:data_ - precondition:precondition]]; + FSTMutation* mutation = + [[FSTPatchMutation alloc] initWithKey:key + fieldMask:std::move(field_mask_) + value:data_ + precondition:precondition]; + mutations.push_back(mutation); } else { - [mutations addObject:[[FSTSetMutation alloc] initWithKey:key - value:data_ - precondition:precondition]]; + FSTMutation* mutation = [[FSTSetMutation alloc] initWithKey:key + value:data_ + precondition:precondition]; + mutations.push_back(mutation); } + if (!field_transforms_.empty()) { - [mutations - addObject:[[FSTTransformMutation alloc] initWithKey:key - fieldTransforms:field_transforms_]]; + FSTMutation* mutation = + [[FSTTransformMutation alloc] initWithKey:key + fieldTransforms:field_transforms_]; + mutations.push_back(mutation); } + return mutations; } @@ -243,19 +248,24 @@ field_transforms_{std::move(field_transforms)} { } -NSArray* ParsedUpdateData::ToMutations( +std::vector ParsedUpdateData::ToMutations( const DocumentKey& key, const Precondition& precondition) && { - NSMutableArray* mutations = [NSMutableArray array]; - [mutations - addObject:[[FSTPatchMutation alloc] initWithKey:key - fieldMask:std::move(field_mask_) - value:data_ - precondition:precondition]]; + std::vector mutations; + + FSTMutation* mutation = + [[FSTPatchMutation alloc] initWithKey:key + fieldMask:std::move(field_mask_) + value:data_ + precondition:precondition]; + mutations.push_back(mutation); + if (!field_transforms_.empty()) { - [mutations addObject:[[FSTTransformMutation alloc] - initWithKey:key - fieldTransforms:std::move(field_transforms_)]]; + FSTMutation* mutation = + [[FSTTransformMutation alloc] initWithKey:key + fieldTransforms:std::move(field_transforms_)]; + mutations.push_back(mutation); } + return mutations; } diff --git a/Firestore/core/src/firebase/firestore/core/view_snapshot.h b/Firestore/core/src/firebase/firestore/core/view_snapshot.h index a9b9ba63837..279ea706952 100644 --- a/Firestore/core/src/firebase/firestore/core/view_snapshot.h +++ b/Firestore/core/src/firebase/firestore/core/view_snapshot.h @@ -17,24 +17,187 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_VIEW_SNAPSHOT_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_VIEW_SNAPSHOT_H_ +#if !defined(__OBJC__) +#error "This header only supports Objective-C++" +#endif // !defined(__OBJC__) + +#include +#include +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/immutable/sorted_map.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FSTDocument; +@class FSTQuery; + namespace firebase { namespace firestore { namespace core { +/** A change to a single document's state within a view. */ +class DocumentViewChange { + public: + /** + * The types of changes that can happen to a document with respect to a view. + * NOTE: We sort document changes by their type, so the ordering of this enum + * is significant. + */ + enum class Type { kRemoved = 0, kAdded, kModified, kMetadata }; + + DocumentViewChange() = default; + + DocumentViewChange(FSTDocument* document, Type type) + : document_{document}, type_{type} { + } + + FSTDocument* document() const { + return document_; + } + DocumentViewChange::Type type() const { + return type_; + } + + std::string ToString() const; + size_t Hash() const; + + private: + FSTDocument* document_ = nullptr; + Type type_{}; +}; + +bool operator==(const DocumentViewChange& lhs, const DocumentViewChange& rhs); + +/** + * The possible states a document can be in w.r.t syncing from local storage to + * the backend. + */ +enum class SyncState { None = 0, Local, Synced }; + /** - * The types of changes that can happen to a document with respect to a view. - * NOTE: We sort document changes by their type, so the ordering of this enum is - * significant. + * A set of changes to docs in a query, merging duplicate events for the same + * doc. */ -enum class DocumentViewChangeType { - kRemoved = 0, - kAdded, - kModified, - kMetadata +class DocumentViewChangeSet { + public: + /** Takes a new change and applies it to the set. */ + void AddChange(DocumentViewChange&& change); + + /** Returns the set of all changes tracked in this set. */ + std::vector GetChanges() const; + + std::string ToString() const; + + private: + /** The set of all changes tracked so far, with redundant changes merged. */ + immutable::SortedMap change_map_; }; +class ViewSnapshot; + +using ViewSnapshotHandler = + std::function&)>; + +/** + * A view snapshot is an immutable capture of the results of a query and the + * changes to them. + */ +class ViewSnapshot { + public: + ViewSnapshot(FSTQuery* query, + model::DocumentSet documents, + model::DocumentSet old_documents, + std::vector document_changes, + model::DocumentKeySet mutated_keys, + bool from_cache, + bool sync_state_changed, + bool excludes_metadata_changes); + + /** + * Returns a view snapshot as if all documents in the snapshot were + * added. + */ + static ViewSnapshot FromInitialDocuments(FSTQuery* query, + model::DocumentSet documents, + model::DocumentKeySet mutated_keys, + bool from_cache, + bool excludes_metadata_changes); + + /** The query this view is tracking the results for. */ + FSTQuery* query() const { + return query_; + } + + /** The documents currently known to be results of the query. */ + const model::DocumentSet& documents() const { + return documents_; + } + + /** The documents of the last snapshot. */ + const model::DocumentSet& old_documents() const { + return old_documents_; + } + + /** The set of changes that have been applied to the documents. */ + const std::vector& document_changes() const { + return document_changes_; + } + + /** Whether any document in the snapshot was served from the local cache. */ + bool from_cache() const { + return from_cache_; + } + + /** Whether any document in the snapshot has pending local writes. */ + bool has_pending_writes() const { + return !mutated_keys_.empty(); + } + + /** Whether the sync state changed as part of this snapshot. */ + bool sync_state_changed() const { + return sync_state_changed_; + } + + /** Whether this snapshot has been filtered to not include metadata changes */ + bool excludes_metadata_changes() const { + return excludes_metadata_changes_; + } + + /** The document in this snapshot that have unconfirmed writes. */ + model::DocumentKeySet mutated_keys() const { + return mutated_keys_; + } + + std::string ToString() const; + friend std::ostream& operator<<(std::ostream& out, const ViewSnapshot& value); + size_t Hash() const; + + private: + FSTQuery* query_ = nil; + + model::DocumentSet documents_; + model::DocumentSet old_documents_; + std::vector document_changes_; + model::DocumentKeySet mutated_keys_; + + bool from_cache_ = false; + bool sync_state_changed_ = false; + bool excludes_metadata_changes_ = false; +}; + +bool operator==(const ViewSnapshot& lhs, const ViewSnapshot& rhs); + } // namespace core } // namespace firestore } // namespace firebase +NS_ASSUME_NONNULL_END + #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_VIEW_SNAPSHOT_H_ diff --git a/Firestore/core/src/firebase/firestore/core/view_snapshot.mm b/Firestore/core/src/firebase/firestore/core/view_snapshot.mm new file mode 100644 index 00000000000..6663063298c --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/view_snapshot.mm @@ -0,0 +1,210 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" + +#include + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Model/FSTDocument.h" + +#include "Firestore/core/src/firebase/firestore/model/document_set.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" +#include "Firestore/core/src/firebase/firestore/util/objc_compatibility.h" +#include "Firestore/core/src/firebase/firestore/util/string_format.h" +#include "Firestore/core/src/firebase/firestore/util/to_string.h" + +namespace firebase { +namespace firestore { +namespace core { + +namespace objc = util::objc; +using model::DocumentKey; +using model::DocumentKeySet; +using model::DocumentSet; +using util::StringFormat; + +// DocumentViewChange + +std::string DocumentViewChange::ToString() const { + return StringFormat("", + util::ToString(document()), type()); +} + +size_t DocumentViewChange::Hash() const { + size_t document_hash = static_cast([document() hash]); + return util::Hash(document_hash, static_cast(type())); +} + +bool operator==(const DocumentViewChange& lhs, const DocumentViewChange& rhs) { + return objc::Equals(lhs.document(), rhs.document()) && + lhs.type() == rhs.type(); +} + +// DocumentViewChangeSet + +void DocumentViewChangeSet::AddChange(DocumentViewChange&& change) { + const DocumentKey& key = change.document().key; + auto old_change_iter = change_map_.find(key); + if (old_change_iter == change_map_.end()) { + change_map_ = change_map_.insert(key, change); + return; + } + + const DocumentViewChange& old = old_change_iter->second; + DocumentViewChange::Type old_type = old.type(); + DocumentViewChange::Type new_type = change.type(); + + // Merge the new change with the existing change. + if (new_type != DocumentViewChange::Type::kAdded && + old_type == DocumentViewChange::Type::kMetadata) { + change_map_ = change_map_.insert(key, change); + + } else if (new_type == DocumentViewChange::Type::kMetadata && + old_type != DocumentViewChange::Type::kRemoved) { + DocumentViewChange new_change{change.document(), old_type}; + change_map_ = change_map_.insert(key, new_change); + + } else if (new_type == DocumentViewChange::Type::kModified && + old_type == DocumentViewChange::Type::kModified) { + DocumentViewChange new_change{change.document(), + DocumentViewChange::Type::kModified}; + change_map_ = change_map_.insert(key, new_change); + + } else if (new_type == DocumentViewChange::Type::kModified && + old_type == DocumentViewChange::Type::kAdded) { + DocumentViewChange new_change{change.document(), + DocumentViewChange::Type::kAdded}; + change_map_ = change_map_.insert(key, new_change); + + } else if (new_type == DocumentViewChange::Type::kRemoved && + old_type == DocumentViewChange::Type::kAdded) { + change_map_ = change_map_.erase(key); + + } else if (new_type == DocumentViewChange::Type::kRemoved && + old_type == DocumentViewChange::Type::kModified) { + DocumentViewChange new_change{old.document(), + DocumentViewChange::Type::kRemoved}; + change_map_ = change_map_.insert(key, new_change); + + } else if (new_type == DocumentViewChange::Type::kAdded && + old_type == DocumentViewChange::Type::kRemoved) { + DocumentViewChange new_change{change.document(), + DocumentViewChange::Type::kModified}; + change_map_ = change_map_.insert(key, new_change); + + } else { + // This includes these cases, which don't make sense: + // Added -> Added + // Removed -> Removed + // Modified -> Added + // Removed -> Modified + // Metadata -> Added + // Removed -> Metadata + HARD_FAIL("Unsupported combination of changes: %s after %s", new_type, + old_type); + } +} + +std::vector DocumentViewChangeSet::GetChanges() const { + std::vector changes; + for (const auto& kv : change_map_) { + const DocumentViewChange& change = kv.second; + changes.push_back(change); + } + return changes; +} + +std::string DocumentViewChangeSet::ToString() const { + return util::ToString(change_map_); +} + +// ViewSnapshot + +ViewSnapshot::ViewSnapshot(FSTQuery* query, + DocumentSet documents, + DocumentSet old_documents, + std::vector document_changes, + model::DocumentKeySet mutated_keys, + bool from_cache, + bool sync_state_changed, + bool excludes_metadata_changes) + : query_{query}, + documents_{std::move(documents)}, + old_documents_{std::move(old_documents)}, + document_changes_{std::move(document_changes)}, + mutated_keys_{std::move(mutated_keys)}, + from_cache_{from_cache}, + sync_state_changed_{sync_state_changed}, + excludes_metadata_changes_{excludes_metadata_changes} { +} + +ViewSnapshot ViewSnapshot::FromInitialDocuments( + FSTQuery* query, + DocumentSet documents, + DocumentKeySet mutated_keys, + bool from_cache, + bool excludes_metadata_changes) { + std::vector view_changes; + for (FSTDocument* doc : documents) { + view_changes.emplace_back(doc, DocumentViewChange::Type::kAdded); + } + + return ViewSnapshot{query, documents, + /*old_documents=*/ + DocumentSet{query.comparator}, std::move(view_changes), + std::move(mutated_keys), from_cache, + /*sync_state_changed=*/true, excludes_metadata_changes}; +} + +std::string ViewSnapshot::ToString() const { + return StringFormat( + "", + query(), documents_.ToString(), old_documents_.ToString(), + objc::Description(document_changes()), from_cache(), + mutated_keys().size(), sync_state_changed(), excludes_metadata_changes()); +} + +std::ostream& operator<<(std::ostream& out, const ViewSnapshot& value) { + return out << value.ToString(); +} + +size_t ViewSnapshot::Hash() const { + // Note: We are omitting `mutated_keys_` from the hash, since we don't have a + // straightforward way to compute its hash value. Since `ViewSnapshot` is + // currently not stored in any dictionaries, this has no side effects. + + return util::Hash([query() hash], documents(), old_documents(), + document_changes(), from_cache(), sync_state_changed(), + excludes_metadata_changes()); +} + +bool operator==(const ViewSnapshot& lhs, const ViewSnapshot& rhs) { + return objc::Equals(lhs.query(), rhs.query()) && + lhs.documents() == rhs.documents() && + lhs.old_documents() == rhs.old_documents() && + lhs.document_changes() == rhs.document_changes() && + lhs.from_cache() == rhs.from_cache() && + lhs.mutated_keys() == rhs.mutated_keys() && + lhs.sync_state_changed() == rhs.sync_state_changed() && + lhs.excludes_metadata_changes() == rhs.excludes_metadata_changes(); +} + +} // namespace core +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/immutable/array_sorted_map.h b/Firestore/core/src/firebase/firestore/immutable/array_sorted_map.h index b1c93985c78..1a7076d0e8d 100644 --- a/Firestore/core/src/firebase/firestore/immutable/array_sorted_map.h +++ b/Firestore/core/src/firebase/firestore/immutable/array_sorted_map.h @@ -23,10 +23,11 @@ #include #include #include +#include #include "Firestore/core/src/firebase/firestore/immutable/keys_view.h" #include "Firestore/core/src/firebase/firestore/immutable/map_entry.h" -#include "Firestore/core/src/firebase/firestore/immutable/sorted_map_base.h" +#include "Firestore/core/src/firebase/firestore/immutable/sorted_container.h" #include "Firestore/core/src/firebase/firestore/util/comparison.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/range.h" @@ -151,8 +152,7 @@ class ArraySortedMap : public SortedMapBase { */ ArraySortedMap(std::initializer_list entries, const C& comparator = C()) - : array_{std::make_shared(entries.begin(), entries.end())}, - key_comparator_{comparator} { + : array_{SortedArray(entries, comparator)}, key_comparator_{comparator} { } /** Returns true if the map contains no elements. */ @@ -334,6 +334,16 @@ class ArraySortedMap : public SortedMapBase { return kEmptyArray; } + static array_pointer SortedArray(std::initializer_list entries, + const C& comparator) { + std::vector sorted{entries.begin(), entries.end()}; + std::sort(sorted.begin(), sorted.end(), + [&comparator](const value_type& lhs, const value_type& rhs) { + return comparator(lhs.first, rhs.first); + }); + return std::make_shared(sorted.begin(), sorted.end()); + } + ArraySortedMap(const array_pointer& array, const key_comparator_type& key_comparator) noexcept : array_{array}, key_comparator_{key_comparator} { diff --git a/Firestore/core/src/firebase/firestore/immutable/llrb_node.h b/Firestore/core/src/firebase/firestore/immutable/llrb_node.h index 80c2d865455..0046580364e 100644 --- a/Firestore/core/src/firebase/firestore/immutable/llrb_node.h +++ b/Firestore/core/src/firebase/firestore/immutable/llrb_node.h @@ -21,7 +21,7 @@ #include #include "Firestore/core/src/firebase/firestore/immutable/llrb_node_iterator.h" -#include "Firestore/core/src/firebase/firestore/immutable/sorted_map_base.h" +#include "Firestore/core/src/firebase/firestore/immutable/sorted_container.h" namespace firebase { namespace firestore { diff --git a/Firestore/core/src/firebase/firestore/immutable/sorted_map_base.cc b/Firestore/core/src/firebase/firestore/immutable/sorted_container.cc similarity index 89% rename from Firestore/core/src/firebase/firestore/immutable/sorted_map_base.cc rename to Firestore/core/src/firebase/firestore/immutable/sorted_container.cc index 954bdb9cd73..637270d3f71 100644 --- a/Firestore/core/src/firebase/firestore/immutable/sorted_map_base.cc +++ b/Firestore/core/src/firebase/firestore/immutable/sorted_container.cc @@ -14,18 +14,16 @@ * limitations under the License. */ -#include "Firestore/core/src/firebase/firestore/immutable/sorted_map_base.h" +#include "Firestore/core/src/firebase/firestore/immutable/sorted_container.h" namespace firebase { namespace firestore { namespace immutable { -namespace impl { // Define external storage for constants: +constexpr SortedContainer::size_type SortedContainer::npos; constexpr SortedMapBase::size_type SortedMapBase::kFixedSize; -constexpr SortedMapBase::size_type SortedMapBase::npos; -} // namespace impl } // namespace immutable } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/immutable/sorted_map_base.h b/Firestore/core/src/firebase/firestore/immutable/sorted_container.h similarity index 77% rename from Firestore/core/src/firebase/firestore/immutable/sorted_map_base.h rename to Firestore/core/src/firebase/firestore/immutable/sorted_container.h index a19bd778830..1cf5eb79c5c 100644 --- a/Firestore/core/src/firebase/firestore/immutable/sorted_map_base.h +++ b/Firestore/core/src/firebase/firestore/immutable/sorted_container.h @@ -14,25 +14,25 @@ * limitations under the License. */ -#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_MAP_BASE_H_ -#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_MAP_BASE_H_ +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_CONTAINER_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_CONTAINER_H_ #include namespace firebase { namespace firestore { namespace immutable { -namespace impl { /** - * A base class for implementing sorted maps, containing types and constants - * that don't depend upon the template parameters to the main class. + * A base class for implementing immutable sorted containers, containing types + * and constants that don't depend upon the template parameters to the main + * class. * * Note that this exists as a base class rather than as just a namespace in * order to make it possible for users of the SortedMap classes to avoid needing * to declare storage for each instantiation of the template. */ -class SortedMapBase { +class SortedContainer { public: /** * The type of size() methods on immutable collections. Note: @@ -42,6 +42,23 @@ class SortedMapBase { */ using size_type = uint32_t; + /** + * A sentinel return value that indicates not found. Functionally similar to + * std::string::npos. + */ + static constexpr size_type npos = static_cast(-1); +}; + +/** + * A base class for implementing sorted maps, containing types and constants + * that don't depend upon the template parameters to the main class. + * + * Note that this exists as a base class rather than as just a namespace in + * order to make it possible for users of the SortedMap classes to avoid needing + * to declare storage for each instantiation of the template. + */ +class SortedMapBase : public SortedContainer { + public: /** * The maximum size of an ArraySortedMap. * @@ -52,19 +69,11 @@ class SortedMapBase { * inserting and lookups. Feel free to empirically determine this constant, * but don't expect much gain in real world performance. */ - // TODO(wilhuff): actually use this for switching implementations. static constexpr size_type kFixedSize = 25; - - /** - * A sentinel return value that indicates not found. Functionally similar to - * std::string::npos. - */ - static constexpr size_type npos = static_cast(-1); }; -} // namespace impl } // namespace immutable } // namespace firestore } // namespace firebase -#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_MAP_BASE_H_ +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_CONTAINER_H_ diff --git a/Firestore/core/src/firebase/firestore/immutable/sorted_map.h b/Firestore/core/src/firebase/firestore/immutable/sorted_map.h index 21dcfd40ef3..f3c998bd80e 100644 --- a/Firestore/core/src/firebase/firestore/immutable/sorted_map.h +++ b/Firestore/core/src/firebase/firestore/immutable/sorted_map.h @@ -21,7 +21,7 @@ #include "Firestore/core/src/firebase/firestore/immutable/array_sorted_map.h" #include "Firestore/core/src/firebase/firestore/immutable/keys_view.h" -#include "Firestore/core/src/firebase/firestore/immutable/sorted_map_base.h" +#include "Firestore/core/src/firebase/firestore/immutable/sorted_container.h" #include "Firestore/core/src/firebase/firestore/immutable/sorted_map_iterator.h" #include "Firestore/core/src/firebase/firestore/immutable/tree_sorted_map.h" #include "Firestore/core/src/firebase/firestore/util/comparison.h" @@ -36,8 +36,10 @@ namespace immutable { * has methods to efficiently create new maps that are mutations of it. */ template > -class SortedMap : public impl::SortedMapBase { +class SortedMap : public SortedMapBase { public: + using key_type = K; + using mapped_type = V; /** The type of the entries stored in the map. */ using value_type = std::pair; using array_type = impl::ArraySortedMap; @@ -202,7 +204,7 @@ class SortedMap : public impl::SortedMapBase { tree_type result = tree_.erase(key); if (result.empty()) { // Flip back to the array representation for empty arrays. - return SortedMap{}; + return SortedMap{comparator()}; } return SortedMap{std::move(result)}; } diff --git a/Firestore/core/src/firebase/firestore/immutable/sorted_set.h b/Firestore/core/src/firebase/firestore/immutable/sorted_set.h index 0b64a167836..36b6e16ef03 100644 --- a/Firestore/core/src/firebase/firestore/immutable/sorted_set.h +++ b/Firestore/core/src/firebase/firestore/immutable/sorted_set.h @@ -20,8 +20,8 @@ #include #include +#include "Firestore/core/src/firebase/firestore/immutable/sorted_container.h" #include "Firestore/core/src/firebase/firestore/immutable/sorted_map.h" -#include "Firestore/core/src/firebase/firestore/immutable/sorted_map_base.h" #include "Firestore/core/src/firebase/firestore/util/comparison.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/hashing.h" @@ -46,7 +46,7 @@ template , typename V = impl::Empty, typename M = SortedMap> -class SortedSet { +class SortedSet : public SortedContainer { public: using size_type = typename M::size_type; using value_type = K; @@ -101,7 +101,7 @@ class SortedSet { return const_iterator{map_.min()}; } - const K& max() const { + const_iterator max() const { return const_iterator{map_.max()}; } diff --git a/Firestore/core/src/firebase/firestore/immutable/tree_sorted_map.h b/Firestore/core/src/firebase/firestore/immutable/tree_sorted_map.h index 9fd51c33643..9a1635c2902 100644 --- a/Firestore/core/src/firebase/firestore/immutable/tree_sorted_map.h +++ b/Firestore/core/src/firebase/firestore/immutable/tree_sorted_map.h @@ -26,7 +26,7 @@ #include "Firestore/core/src/firebase/firestore/immutable/keys_view.h" #include "Firestore/core/src/firebase/firestore/immutable/llrb_node.h" #include "Firestore/core/src/firebase/firestore/immutable/map_entry.h" -#include "Firestore/core/src/firebase/firestore/immutable/sorted_map_base.h" +#include "Firestore/core/src/firebase/firestore/immutable/sorted_container.h" #include "Firestore/core/src/firebase/firestore/util/comparator_holder.h" #include "Firestore/core/src/firebase/firestore/util/comparison.h" diff --git a/Firestore/core/src/firebase/firestore/local/CMakeLists.txt b/Firestore/core/src/firebase/firestore/local/CMakeLists.txt index f27fcb2f82b..dbffa9e46c7 100644 --- a/Firestore/core/src/firebase/firestore/local/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/local/CMakeLists.txt @@ -16,10 +16,18 @@ if(HAVE_LEVELDB) cc_library( firebase_firestore_local_persistence_leveldb SOURCES + leveldb_index_manager.h + #leveldb_index_manager.mm leveldb_key.cc leveldb_key.h leveldb_migrations.cc leveldb_migrations.h + leveldb_mutation_queue.h + #leveldb_mutation_queue.mm + leveldb_query_cache.h + #leveldb_query_cache.mm + leveldb_remote_document_cache.h + #leveldb_remote_document_cache.mm leveldb_transaction.cc leveldb_transaction.h leveldb_util.cc @@ -46,14 +54,29 @@ endif() cc_library( firebase_firestore_local SOURCES - document_reference.h - document_reference.cc + document_key_reference.h + document_key_reference.cc + index_manager.h + listen_sequence.h + local_documents_view.h + local_documents_view.mm local_serializer.h local_serializer.cc + memory_index_manager.cc + memory_index_manager.h + memory_mutation_queue.h + #memory_mutation_queue.mm + memory_query_cache.h + #memory_query_cache.mm + memory_remote_document_cache.h + #memory_remote_document_cache.mm + mutation_queue.h + query_cache.h query_data.cc query_data.h reference_set.cc reference_set.h + remote_document_cache.h DEPENDS # TODO(b/111328563) Force nanopb first to work around ODR violations protobuf-nanopb diff --git a/Firestore/core/src/firebase/firestore/local/document_reference.cc b/Firestore/core/src/firebase/firestore/local/document_key_reference.cc similarity index 74% rename from Firestore/core/src/firebase/firestore/local/document_reference.cc rename to Firestore/core/src/firebase/firestore/local/document_key_reference.cc index e3f1794ed18..61c1b718456 100644 --- a/Firestore/core/src/firebase/firestore/local/document_reference.cc +++ b/Firestore/core/src/firebase/firestore/local/document_key_reference.cc @@ -14,10 +14,11 @@ * limitations under the License. */ +#include "Firestore/core/src/firebase/firestore/local/document_key_reference.h" + #include #include -#include "Firestore/core/src/firebase/firestore/local/document_reference.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/util/comparison.h" #include "Firestore/core/src/firebase/firestore/util/hashing.h" @@ -29,22 +30,23 @@ namespace local { using model::DocumentKey; using util::ComparisonResult; -bool operator==(const DocumentReference& lhs, const DocumentReference& rhs) { +bool operator==(const DocumentKeyReference& lhs, + const DocumentKeyReference& rhs) { return lhs.key_ == rhs.key_ && lhs.ref_id_ == rhs.ref_id_; } -size_t DocumentReference::Hash() const { +size_t DocumentKeyReference::Hash() const { return util::Hash(key_.ToString(), ref_id_); } -std::string DocumentReference::ToString() const { - return util::StringFormat("", +std::string DocumentKeyReference::ToString() const { + return util::StringFormat("", key_.ToString(), ref_id_); } /** Sorts document references by key then ID. */ -bool DocumentReference::ByKey::operator()(const DocumentReference& lhs, - const DocumentReference& rhs) const { +bool DocumentKeyReference::ByKey::operator()( + const DocumentKeyReference& lhs, const DocumentKeyReference& rhs) const { util::Comparator key_less; if (key_less(lhs.key_, rhs.key_)) return true; if (key_less(rhs.key_, lhs.key_)) return false; @@ -54,8 +56,8 @@ bool DocumentReference::ByKey::operator()(const DocumentReference& lhs, } /** Sorts document references by ID then key. */ -bool DocumentReference::ById::operator()(const DocumentReference& lhs, - const DocumentReference& rhs) const { +bool DocumentKeyReference::ById::operator()( + const DocumentKeyReference& lhs, const DocumentKeyReference& rhs) const { util::Comparator id_less; if (id_less(lhs.ref_id_, rhs.ref_id_)) return true; if (id_less(rhs.ref_id_, lhs.ref_id_)) return false; diff --git a/Firestore/core/src/firebase/firestore/local/document_reference.h b/Firestore/core/src/firebase/firestore/local/document_key_reference.h similarity index 74% rename from Firestore/core/src/firebase/firestore/local/document_reference.h rename to Firestore/core/src/firebase/firestore/local/document_key_reference.h index 3160ca42788..257971b5b2d 100644 --- a/Firestore/core/src/firebase/firestore/local/document_reference.h +++ b/Firestore/core/src/firebase/firestore/local/document_key_reference.h @@ -14,8 +14,8 @@ * limitations under the License. */ -#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_DOCUMENT_REFERENCE_H_ -#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_DOCUMENT_REFERENCE_H_ +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_DOCUMENT_KEY_REFERENCE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_DOCUMENT_KEY_REFERENCE_H_ #include #include @@ -39,13 +39,13 @@ namespace local { * * Not to be confused with FIRDocumentReference. */ -class DocumentReference { +class DocumentKeyReference { public: - DocumentReference() { + DocumentKeyReference() { } /** Initializes the document reference with the given key and Id. */ - DocumentReference(model::DocumentKey key, int32_t ref_id) + DocumentKeyReference(model::DocumentKey key, int32_t ref_id) : key_{std::move(key)}, ref_id_{ref_id} { } @@ -63,23 +63,23 @@ class DocumentReference { return ref_id_; } - friend bool operator==(const DocumentReference& lhs, - const DocumentReference& rhs); + friend bool operator==(const DocumentKeyReference& lhs, + const DocumentKeyReference& rhs); size_t Hash() const; std::string ToString() const; /** Sorts document references by key then Id. */ - struct ByKey : public util::Comparator { - bool operator()(const DocumentReference& lhs, - const DocumentReference& rhs) const; + struct ByKey : public util::Comparator { + bool operator()(const DocumentKeyReference& lhs, + const DocumentKeyReference& rhs) const; }; /** Sorts document references by Id then key. */ - struct ById : public util::Comparator { - bool operator()(const DocumentReference& lhs, - const DocumentReference& rhs) const; + struct ById : public util::Comparator { + bool operator()(const DocumentKeyReference& lhs, + const DocumentKeyReference& rhs) const; }; private: @@ -94,4 +94,4 @@ class DocumentReference { } // namespace firestore } // namespace firebase -#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_DOCUMENT_REFERENCE_H_ +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_DOCUMENT_KEY_REFERENCE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/index_manager.h b/Firestore/core/src/firebase/firestore/local/index_manager.h new file mode 100644 index 00000000000..c4bad32eb86 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/index_manager.h @@ -0,0 +1,65 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_INDEX_MANAGER_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_INDEX_MANAGER_H_ + +#include +#include + +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" + +namespace firebase { +namespace firestore { +namespace local { + +/** + * Represents a set of indexes that are used to execute queries efficiently. + * + * Currently the only index is a [collection id] => [parent path] index, used + * to execute Collection Group queries. + */ +class IndexManager { + public: + virtual ~IndexManager() { + } + + /** + * Creates an index entry mapping the collectionId (last segment of the path) + * to the parent path (either the containing document location or the empty + * path for root-level collections). Index entries can be retrieved via + * GetCollectionParents(). + * + * NOTE: Currently we don't remove index entries. If this ends up being an + * issue we can devise some sort of GC strategy. + */ + virtual void AddToCollectionParentIndex( + const model::ResourcePath& collection_path) = 0; + + /** + * Retrieves all parent locations containing the given collectionId, as a set + * of paths (each path being either a document location or the empty path for + * a root-level collection). + */ + virtual std::vector GetCollectionParents( + const std::string& collection_id) = 0; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_INDEX_MANAGER_H_ diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_index_manager.h b/Firestore/core/src/firebase/firestore/local/leveldb_index_manager.h new file mode 100644 index 00000000000..3b44cbd0d2e --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/leveldb_index_manager.h @@ -0,0 +1,70 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_INDEX_MANAGER_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_INDEX_MANAGER_H_ + +#if !defined(__OBJC__) +#error "For now, this file must only be included by ObjC source files." +#endif // !defined(__OBJC__) + +#include +#include + +#include "Firestore/core/src/firebase/firestore/local/index_manager.h" +#include "Firestore/core/src/firebase/firestore/local/memory_index_manager.h" +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" + +@class FSTLevelDB; + +NS_ASSUME_NONNULL_BEGIN + +namespace firebase { +namespace firestore { +namespace local { + +/** A persisted implementation of IndexManager. */ +class LevelDbIndexManager : public IndexManager { + public: + explicit LevelDbIndexManager(FSTLevelDB* db); + + void AddToCollectionParentIndex( + const model::ResourcePath& collection_path) override; + + std::vector GetCollectionParents( + const std::string& collection_id) override; + + private: + // This instance is owned by FSTLevelDB; avoid a retain cycle. + __weak FSTLevelDB* db_; + + /** + * An in-memory copy of the index entries we've already written since the SDK + * launched. Used to avoid re-writing the same entry repeatedly. + * + * This is *NOT* a complete cache of what's in persistence and so can never + * be used to satisfy reads. + */ + MemoryCollectionParentIndex collection_parents_cache_; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_INDEX_MANAGER_H_ diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_index_manager.mm b/Firestore/core/src/firebase/firestore/local/leveldb_index_manager.mm new file mode 100644 index 00000000000..ef7773b541e --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/leveldb_index_manager.mm @@ -0,0 +1,81 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/local/leveldb_index_manager.h" + +#include +#include + +#include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" +#include "Firestore/core/src/firebase/firestore/local/memory_index_manager.h" +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "absl/strings/match.h" + +#import "Firestore/Source/Local/FSTLevelDB.h" + +NS_ASSUME_NONNULL_BEGIN + +namespace firebase { +namespace firestore { +namespace local { + +using model::ResourcePath; + +LevelDbIndexManager::LevelDbIndexManager(FSTLevelDB* db) : db_(db) { +} + +void LevelDbIndexManager::AddToCollectionParentIndex( + const ResourcePath& collection_path) { + HARD_ASSERT(collection_path.size() % 2 == 1, "Expected a collection path."); + + if (collection_parents_cache_.Add(collection_path)) { + std::string collection_id = collection_path.last_segment(); + ResourcePath parent_path = collection_path.PopLast(); + + std::string key = + LevelDbCollectionParentKey::Key(collection_id, parent_path); + std::string empty_buffer; + db_.currentTransaction->Put(key, empty_buffer); + } +} + +std::vector LevelDbIndexManager::GetCollectionParents( + const std::string& collection_id) { + std::vector results; + + auto index_iterator = db_.currentTransaction->NewIterator(); + std::string index_prefix = + LevelDbCollectionParentKey::KeyPrefix(collection_id); + LevelDbCollectionParentKey row_key; + for (index_iterator->Seek(index_prefix); index_iterator->Valid(); + index_iterator->Next()) { + if (!absl::StartsWith(index_iterator->key(), index_prefix) || + !row_key.Decode(index_iterator->key()) || + row_key.collection_id() != collection_id) { + break; + } + + results.push_back(row_key.parent()); + } + return results; +} + +} // namespace local +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_key.cc b/Firestore/core/src/firebase/firestore/local/leveldb_key.cc index d0875dc65d5..e0f632896a2 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_key.cc +++ b/Firestore/core/src/firebase/firestore/local/leveldb_key.cc @@ -45,6 +45,7 @@ const char* kQueryTargetsTable = "query_target"; const char* kTargetDocumentsTable = "target_document"; const char* kDocumentTargetsTable = "document_target"; const char* kRemoteDocumentsTable = "remote_document"; +const char* kCollectionParentsTable = "collection_parent"; /** * Labels for the components of keys. These serve to make keys self-describing. @@ -89,6 +90,12 @@ enum ComponentLabel { /** A component containing a user Id. */ UserId = 13, + /** + * A component containing a standalone collection ID (e.g. as used by the + * collection_parent table, but not for collection IDs within paths). + */ + CollectionId = 14, + /** * A path segment describes just a single segment in a resource path. Path * segments that occur sequentially in a key represent successive segments in @@ -159,6 +166,17 @@ class Reader { return ReadLabeledString(ComponentLabel::UserId); } + std::string ReadCollectionId() { + return ReadLabeledString(ComponentLabel::CollectionId); + } + + /** + * Reads component labels and strings from the key until it finds a component + * label other than ComponentLabel::PathSegment (or the key is exhausted). + * All matched path segments are assembled into a ResourcePath. + */ + ResourcePath ReadResourcePath(); + /** * Reads component labels and strings from the key until it finds a component * label other than ComponentLabel::PathSegment (or the key is exhausted). @@ -358,7 +376,7 @@ class Reader { bool ok_; }; -DocumentKey Reader::ReadDocumentKey() { +ResourcePath Reader::ReadResourcePath() { std::vector path_segments; while (!empty()) { // Advance a temporary slice to avoid advancing contents into the next key @@ -375,7 +393,13 @@ DocumentKey Reader::ReadDocumentKey() { path_segments.push_back(std::move(segment)); } - ResourcePath path{std::move(path_segments)}; + return ResourcePath{std::move(path_segments)}; +} + +DocumentKey Reader::ReadDocumentKey() { + ResourcePath path = ReadResourcePath(); + + // Avoid assertion failures in DocumentKey if path is invalid. if (ok_ && !path.empty() && DocumentKey::IsDocumentKey(path)) { return DocumentKey{std::move(path)}; } @@ -419,10 +443,10 @@ std::string Reader::Describe() { src_ = saved_source; if (label == ComponentLabel::PathSegment) { - DocumentKey document_key = ReadDocumentKey(); + ResourcePath resource_path = ReadResourcePath(); if (ok_) { absl::StrAppend(&description, - " key=", document_key.path().CanonicalString()); + " path=", resource_path.CanonicalString()); } } else if (label == ComponentLabel::TableName) { @@ -455,6 +479,12 @@ std::string Reader::Describe() { absl::StrAppend(&description, " user_id=", user_id); } + } else if (label == ComponentLabel::CollectionId) { + std::string collection_id = ReadCollectionId(); + if (ok_) { + absl::StrAppend(&description, " collection_id=", collection_id); + } + } else { absl::StrAppend(&description, " unknown label=", static_cast(label)); Fail(); @@ -502,6 +532,10 @@ class Writer { WriteLabeledString(ComponentLabel::UserId, user_id); } + void WriteCollectionId(absl::string_view collection_id) { + WriteLabeledString(ComponentLabel::CollectionId, collection_id); + } + /** * For each segment in the given resource path writes a * ComponentLabel::PathSegment component label and a string containing the @@ -848,6 +882,39 @@ bool LevelDbRemoteDocumentKey::Decode(absl::string_view key) { return reader.ok(); } +std::string LevelDbCollectionParentKey::KeyPrefix() { + Writer writer; + writer.WriteTableName(kCollectionParentsTable); + return writer.result(); +} + +std::string LevelDbCollectionParentKey::KeyPrefix( + absl::string_view collection_id) { + Writer writer; + writer.WriteTableName(kCollectionParentsTable); + writer.WriteCollectionId(collection_id); + return writer.result(); +} + +std::string LevelDbCollectionParentKey::Key(absl::string_view collection_id, + const ResourcePath& parent) { + Writer writer; + writer.WriteTableName(kCollectionParentsTable); + writer.WriteCollectionId(collection_id); + writer.WriteResourcePath(parent); + writer.WriteTerminator(); + return writer.result(); +} + +bool LevelDbCollectionParentKey::Decode(absl::string_view key) { + Reader reader{key}; + reader.ReadTableNameMatching(kCollectionParentsTable); + collection_id_ = reader.ReadCollectionId(); + parent_ = reader.ReadResourcePath(); + reader.ReadTerminator(); + return reader.ok(); +} + } // namespace local } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_key.h b/Firestore/core/src/firebase/firestore/local/leveldb_key.h index 4bbf738e5ed..505cb871214 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_key.h +++ b/Firestore/core/src/firebase/firestore/local/leveldb_key.h @@ -77,6 +77,11 @@ namespace local { // remote_documents: // - table_name: string = "remote_document" // - path: ResourcePath +// +// collection_parents: +// - table_name: string = "collection_parent" +// - collectionId: string +// - parent: ResourcePath /** * Parses the given key and returns a human readable description of its @@ -520,6 +525,59 @@ class LevelDbRemoteDocumentKey { model::DocumentKey document_key_; }; +/** + * A key in the collection parents index, which stores an association between a + * Collection ID (e.g. 'messages') to a parent path (e.g. '/chats/123') that + * contains it as a (sub)collection. This is used to efficiently find all + * collections to query when performing a Collection Group query. Note that the + * parent path will be an empty path in the case of root-level collections. + */ +class LevelDbCollectionParentKey { + public: + /** + * Creates a key prefix that points just before the first key in the table. + */ + static std::string KeyPrefix(); + + /** + * Creates a key prefix that points just before the first key for the given + * collection_id. + */ + static std::string KeyPrefix(absl::string_view collection_id); + + /** + * Creates a complete key that points to a specific collection_id and parent. + */ + static std::string Key(absl::string_view collection_id, + const model::ResourcePath& parent); + + /** + * Decodes the given complete key, storing the decoded values in this + * instance. + * + * @return true if the key successfully decoded, false otherwise. If false is + * returned, this instance is in an undefined state until the next call to + * `Decode()`. + */ + ABSL_MUST_USE_RESULT + bool Decode(absl::string_view key); + + /** The collection_id, as encoded in the key. */ + const std::string& collection_id() const { + return collection_id_; + } + + /** The parent path, as encoded in the key. */ + const model::ResourcePath& parent() const { + return parent_; + } + + private: + // Deliberately uninitialized: will be assigned in Decode + std::string collection_id_; + model::ResourcePath parent_; +}; + } // namespace local } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_migrations.cc b/Firestore/core/src/firebase/firestore/local/leveldb_migrations.cc index 8b780a104d3..dda29e78354 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_migrations.cc +++ b/Firestore/core/src/firebase/firestore/local/leveldb_migrations.cc @@ -22,6 +22,7 @@ #include "Firestore/Protos/nanopb/firestore/local/mutation.nanopb.h" #include "Firestore/Protos/nanopb/firestore/local/target.nanopb.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" +#include "Firestore/core/src/firebase/firestore/local/memory_index_manager.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/types.h" #include "Firestore/core/src/firebase/firestore/nanopb/reader.h" @@ -36,6 +37,8 @@ using leveldb::Iterator; using leveldb::Slice; using leveldb::Status; using leveldb::WriteOptions; +using model::DocumentKey; +using model::ResourcePath; using nanopb::Reader; using nanopb::Writer; @@ -60,8 +63,9 @@ namespace { * * Migration 4 ensures that every document in the remote document cache * has a sentinel row with a sequence number. * * Migration 5 drops held write acks. + * * Migration 6 populates the collection_parents index. */ -const LevelDbMigrations::SchemaVersion kSchemaVersion = 5; +const LevelDbMigrations::SchemaVersion kSchemaVersion = 6; /** * Save the given version number as the current version of the schema of the @@ -214,6 +218,8 @@ void EnsureSentinelRow(LevelDbTransaction* transaction, } /** + * Migration 4. + * * Ensure each document in the remote document table has a corresponding * sentinel row in the document target index. */ @@ -241,6 +247,65 @@ void EnsureSentinelRows(leveldb::DB* db) { transaction.Commit(); } +// Helper to add an index entry iff we haven't already written it (as determined +// by the provided cache). +void EnsureCollectionParentRow(LevelDbTransaction* transaction, + MemoryCollectionParentIndex* cache, + const DocumentKey& key) { + const ResourcePath& collection_path = key.path().PopLast(); + if (cache->Add(collection_path)) { + std::string collection_id = collection_path.last_segment(); + ResourcePath parent_path = collection_path.PopLast(); + + std::string key = + LevelDbCollectionParentKey::Key(collection_id, parent_path); + std::string empty_buffer; + transaction->Put(key, empty_buffer); + } +} + +/** + * Migration 6. + * + * Creates appropriate LevelDbCollectionParentKey rows for all collections + * of documents in the remote document cache and mutation queue. + */ +void EnsureCollectionParentsIndex(leveldb::DB* db) { + LevelDbTransaction transaction(db, "Ensure Collection Parents Index"); + + MemoryCollectionParentIndex cache; + + // Index existing remote documents. + std::string documents_prefix = LevelDbRemoteDocumentKey::KeyPrefix(); + auto it = transaction.NewIterator(); + it->Seek(documents_prefix); + LevelDbRemoteDocumentKey document_key; + for (; it->Valid() && absl::StartsWith(it->key(), documents_prefix); + it->Next()) { + HARD_ASSERT(document_key.Decode(it->key()), + "Failed to decode document key"); + + EnsureCollectionParentRow(&transaction, &cache, + document_key.document_key()); + } + + // Index existing mutations. + std::string mutations_prefix = LevelDbDocumentMutationKey::KeyPrefix(); + it = transaction.NewIterator(); + it->Seek(mutations_prefix); + LevelDbDocumentMutationKey key; + for (; it->Valid() && absl::StartsWith(it->key(), mutations_prefix); + it->Next()) { + HARD_ASSERT(key.Decode(it->key()), + "Failed to decode document-mutation key"); + + EnsureCollectionParentRow(&transaction, &cache, key.document_key()); + } + + SaveVersion(6, &transaction); + transaction.Commit(); +} + } // namespace LevelDbMigrations::SchemaVersion LevelDbMigrations::ReadSchemaVersion( @@ -287,6 +352,10 @@ void LevelDbMigrations::RunMigrations(leveldb::DB* db, if (from_version < 5 && to_version >= 5) { RemoveAcknowledgedMutations(db); } + + if (from_version < 6 && to_version >= 6) { + EnsureCollectionParentsIndex(db); + } } } // namespace local diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h index ef205ef276b..676cc3ff1c3 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h +++ b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h @@ -70,8 +70,10 @@ class LevelDbMutationQueue : public MutationQueue { void AcknowledgeBatch(FSTMutationBatch* batch, NSData* _Nullable stream_token) override; - FSTMutationBatch* AddMutationBatch(FIRTimestamp* local_write_time, - NSArray* mutations) override; + FSTMutationBatch* AddMutationBatch( + FIRTimestamp* local_write_time, + std::vector&& base_mutations, + std::vector&& mutations) override; void RemoveMutationBatch(FSTMutationBatch* batch) override; diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm index b020c563dab..0abb5f14bd1 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm +++ b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm @@ -17,6 +17,7 @@ #include "Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h" #include +#include #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" #import "Firestore/Source/Core/FSTQuery.h" @@ -29,6 +30,7 @@ #include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/util/string_util.h" +#include "Firestore/core/src/firebase/firestore/util/to_string.h" #include "absl/strings/match.h" NS_ASSUME_NONNULL_BEGIN @@ -154,14 +156,17 @@ BatchId LoadNextBatchIdFromDb(DB* db) { } FSTMutationBatch* LevelDbMutationQueue::AddMutationBatch( - FIRTimestamp* local_write_time, NSArray* mutations) { + FIRTimestamp* local_write_time, + std::vector&& base_mutations, + std::vector&& mutations) { BatchId batch_id = next_batch_id_; next_batch_id_++; FSTMutationBatch* batch = [[FSTMutationBatch alloc] initWithBatchID:batch_id localWriteTime:local_write_time - mutations:mutations]; + baseMutations:std::move(base_mutations) + mutations:std::move(mutations)]; std::string key = mutation_batch_key(batch_id); db_.currentTransaction->Put(key, [serializer_ encodedMutationBatch:batch]); @@ -171,9 +176,11 @@ BatchId LoadNextBatchIdFromDb(DB* db) { // buffer (and the parser will see all default values). std::string empty_buffer; - for (FSTMutation* mutation in mutations) { + for (FSTMutation* mutation : [batch mutations]) { key = LevelDbDocumentMutationKey::Key(user_id_, mutation.key, batch_id); db_.currentTransaction->Put(key, empty_buffer); + + db_.indexManager->AddToCollectionParentIndex(mutation.key.path().PopLast()); } return batch; @@ -197,7 +204,7 @@ BatchId LoadNextBatchIdFromDb(DB* db) { db_.currentTransaction->Delete(key); - for (FSTMutation* mutation in batch.mutations) { + for (FSTMutation* mutation : [batch mutations]) { key = LevelDbDocumentMutationKey::Key(user_id_, mutation.key, batch_id); db_.currentTransaction->Delete(key); [db_.referenceDelegate removeMutationReference:mutation.key]; @@ -265,6 +272,9 @@ BatchId LoadNextBatchIdFromDb(DB* db) { LevelDbMutationQueue::AllMutationBatchesAffectingQuery(FSTQuery* query) { HARD_ASSERT(![query isDocumentQuery], "Document queries shouldn't go down this path"); + HARD_ASSERT( + ![query isCollectionGroupQuery], + "CollectionGroup queries should be handled in LocalDocumentsView"); const ResourcePath& query_path = query.path; size_t immediate_children_path_length = query_path.size() + 1; @@ -468,4 +478,4 @@ BatchId LoadNextBatchIdFromDb(DB* db) { } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.h b/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.h index 6dae8bb7974..caf28c403cf 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.h +++ b/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.h @@ -47,11 +47,6 @@ namespace local { /** Cached Queries backed by LevelDB. */ class LevelDbQueryCache : public QueryCache { public: - /** Enumerator callback type for orphaned documents */ - typedef void (^OrphanedDocumentEnumerator)(const model::DocumentKey&, - model::ListenSequenceNumber, - BOOL*); - /** * Retrieves the global singleton metadata row from the given database, if it * exists. @@ -75,7 +70,7 @@ class LevelDbQueryCache : public QueryCache { FSTQueryData* _Nullable GetTarget(FSTQuery* query) override; - void EnumerateTargets(TargetEnumerator block) override; + void EnumerateTargets(const TargetCallback& callback) override; int RemoveTargets(model::ListenSequenceNumber upper_bound, const std::unordered_map& @@ -123,7 +118,7 @@ class LevelDbQueryCache : public QueryCache { // Non-interface methods void Start(); - void EnumerateOrphanedDocuments(OrphanedDocumentEnumerator block); + void EnumerateOrphanedDocuments(const OrphanedDocumentCallback& callback); private: void Save(FSTQueryData* query_data); diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.mm b/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.mm index a9e14635bac..4412c466bfb 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.mm +++ b/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.mm @@ -170,16 +170,15 @@ return nil; } -void LevelDbQueryCache::EnumerateTargets(TargetEnumerator block) { +void LevelDbQueryCache::EnumerateTargets(const TargetCallback& callback) { // Enumerate all targets, give their sequence numbers. std::string target_prefix = LevelDbTargetKey::KeyPrefix(); auto it = db_.currentTransaction->NewIterator(); it->Seek(target_prefix); - BOOL stop = NO; - for (; !stop && it->Valid() && absl::StartsWith(it->key(), target_prefix); + for (; it->Valid() && absl::StartsWith(it->key(), target_prefix); it->Next()) { FSTQueryData* target = DecodeTarget(it->value()); - block(target, &stop); + callback(target); } } @@ -306,23 +305,22 @@ } void LevelDbQueryCache::EnumerateOrphanedDocuments( - OrphanedDocumentEnumerator block) { + const OrphanedDocumentCallback& callback) { std::string document_target_prefix = LevelDbDocumentTargetKey::KeyPrefix(); auto it = db_.currentTransaction->NewIterator(); it->Seek(document_target_prefix); ListenSequenceNumber next_to_report = 0; DocumentKey key_to_report; LevelDbDocumentTargetKey key; - BOOL stop = NO; - for (; !stop && it->Valid() && - absl::StartsWith(it->key(), document_target_prefix); + + for (; it->Valid() && absl::StartsWith(it->key(), document_target_prefix); it->Next()) { HARD_ASSERT(key.Decode(it->key()), "Failed to decode DocumentTarget key"); if (key.IsSentinel()) { // if next_to_report is non-zero, report it, this is a new key so the last // one must be not be a member of any targets. if (next_to_report != 0) { - block(key_to_report, next_to_report, &stop); + callback(key_to_report, next_to_report); } // set next_to_report to be this sequence number. It's the next one we // might report, if we don't find any targets for this document. @@ -335,10 +333,10 @@ next_to_report = 0; } } - // if not stop and next_to_report is non-zero, report it. We didn't find any - // targets for that document, and we weren't asked to stop. - if (!stop && next_to_report != 0) { - block(key_to_report, next_to_report, &stop); + // if next_to_report is non-zero, report it. We didn't find any targets for + // that document, and we weren't asked to stop. + if (next_to_report != 0) { + callback(key_to_report, next_to_report); } } diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.h b/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.h index 536d74c1d33..99bf754b87a 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.h +++ b/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.h @@ -57,7 +57,8 @@ class LevelDbRemoteDocumentCache : public RemoteDocumentCache { FSTMaybeDocument* DecodeMaybeDocument(absl::string_view encoded, const model::DocumentKey& key); - FSTLevelDB* db_; + // This instance is owned by FSTLevelDB; avoid a retain cycle. + __weak FSTLevelDB* db_; FSTLocalSerializer* serializer_; }; diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.mm b/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.mm index b30aa705c90..69002e03795 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.mm +++ b/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.mm @@ -47,6 +47,8 @@ std::string ldb_key = LevelDbRemoteDocumentKey::Key(document.key); db_.currentTransaction->Put(ldb_key, [serializer_ encodedMaybeDocument:document]); + + db_.indexManager->AddToCollectionParentIndex(document.key.path().PopLast()); } void LevelDbRemoteDocumentCache::Remove(const DocumentKey& key) { @@ -90,23 +92,41 @@ } DocumentMap LevelDbRemoteDocumentCache::GetMatching(FSTQuery* query) { + HARD_ASSERT( + ![query isCollectionGroupQuery], + "CollectionGroup queries should be handled in LocalDocumentsView"); + DocumentMap results; + // Use the query path as a prefix for testing if a document matches the query. + const model::ResourcePath& query_path = query.path; + size_t immediate_children_path_length = query_path.size() + 1; + // Documents are ordered by key, so we can use a prefix scan to narrow down // the documents we need to match the query against. - std::string startKey = LevelDbRemoteDocumentKey::KeyPrefix(query.path); + std::string start_key = LevelDbRemoteDocumentKey::KeyPrefix(query_path); auto it = db_.currentTransaction->NewIterator(); - it->Seek(startKey); + it->Seek(start_key); + + LevelDbRemoteDocumentKey current_key; + for (; it->Valid() && current_key.Decode(it->key()); it->Next()) { + // The query is actually returning any path that starts with the query path + // prefix which may include documents in subcollections. For example, a + // query on 'rooms' will return rooms/abc/messages/xyx but we shouldn't + // match it. Fix this by discarding rows with document keys more than one + // segment longer than the query path. + const DocumentKey& document_key = current_key.document_key(); + if (document_key.path().size() != immediate_children_path_length) { + continue; + } - LevelDbRemoteDocumentKey currentKey; - for (; it->Valid() && currentKey.Decode(it->key()); it->Next()) { - FSTMaybeDocument* maybeDoc = - DecodeMaybeDocument(it->value(), currentKey.document_key()); - if (!query.path.IsPrefixOf(maybeDoc.key.path())) { + FSTMaybeDocument* maybe_doc = + DecodeMaybeDocument(it->value(), document_key); + if (!query_path.IsPrefixOf(maybe_doc.key.path())) { break; - } else if ([maybeDoc isKindOfClass:[FSTDocument class]]) { + } else if ([maybe_doc isKindOfClass:[FSTDocument class]]) { results = - results.insert(maybeDoc.key, static_cast(maybeDoc)); + results.insert(maybe_doc.key, static_cast(maybe_doc)); } } diff --git a/Firestore/core/src/firebase/firestore/local/listen_sequence.h b/Firestore/core/src/firebase/firestore/local/listen_sequence.h new file mode 100644 index 00000000000..6172e977f6a --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/listen_sequence.h @@ -0,0 +1,49 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LISTEN_SEQUENCE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LISTEN_SEQUENCE_H_ + +#include "Firestore/core/src/firebase/firestore/model/types.h" + +namespace firebase { +namespace firestore { +namespace local { + +/** + * ListenSequence is a monotonic sequence. It is initialized with a minimum + * value to exceed. All subsequent calls to next will return increasing values. + */ +class ListenSequence { + public: + explicit ListenSequence(model::ListenSequenceNumber starting_after) + : previous_sequence_number_(starting_after) { + } + + model::ListenSequenceNumber Next() { + previous_sequence_number_++; + return previous_sequence_number_; + } + + private: + model::ListenSequenceNumber previous_sequence_number_; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LISTEN_SEQUENCE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/local_documents_view.h b/Firestore/core/src/firebase/firestore/local/local_documents_view.h new file mode 100644 index 00000000000..b4dd6501385 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/local_documents_view.h @@ -0,0 +1,117 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LOCAL_DOCUMENTS_VIEW_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LOCAL_DOCUMENTS_VIEW_H_ + +#import + +#include + +#include "Firestore/core/src/firebase/firestore/local/index_manager.h" +#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" +#include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_map.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FSTMaybeDocument; +@class FSTMutationBatch; +@class FSTQuery; + +namespace firebase { +namespace firestore { +namespace local { + +/** + * A readonly view of the local state of all documents we're tracking (i.e. we + * have a cached version in remoteDocumentCache or local mutations for the + * document). The view is computed by applying the mutations in the + * FSTMutationQueue to the FSTRemoteDocumentCache. + */ +class LocalDocumentsView { + public: + LocalDocumentsView(RemoteDocumentCache* remote_document_cache, + MutationQueue* mutation_queue, + IndexManager* index_manager) + : remote_document_cache_{remote_document_cache}, + mutation_queue_{mutation_queue}, + index_manager_{index_manager} { + } + + /** + * Gets the local view of the document identified by `key`. + * + * @return Local view of the document or nil if we don't have any cached state + * for it. + */ + FSTMaybeDocument* _Nullable GetDocument(const model::DocumentKey& key); + + /** + * Gets the local view of the documents identified by `keys`. + * + * If we don't have cached state for a document in `keys`, a + * FSTDeletedDocument will be stored for that key in the resulting set. + */ + model::MaybeDocumentMap GetDocuments(const model::DocumentKeySet& keys); + + /** + * Similar to `documentsForKeys`, but creates the local view from the given + * `baseDocs` without retrieving documents from the local store. + */ + model::MaybeDocumentMap GetLocalViewOfDocuments( + const model::MaybeDocumentMap& base_docs); + + /** Performs a query against the local view of all documents. */ + model::DocumentMap GetDocumentsMatchingQuery(FSTQuery* query); + + private: + /** Internal version of GetDocument that allows re-using batches. */ + FSTMaybeDocument* _Nullable GetDocument( + const model::DocumentKey& key, + const std::vector& batches); + + /** + * Returns the view of the given `docs` as they would appear after applying + * all mutations in the given `batches`. + */ + model::MaybeDocumentMap ApplyLocalMutationsToDocuments( + const model::MaybeDocumentMap& docs, + const std::vector& batches); + + /** Performs a simple document lookup for the given path. */ + model::DocumentMap GetDocumentsMatchingDocumentQuery( + const model::ResourcePath& doc_path); + + model::DocumentMap GetDocumentsMatchingCollectionGroupQuery(FSTQuery* query); + + /** Queries the remote documents and overlays mutations. */ + model::DocumentMap GetDocumentsMatchingCollectionQuery(FSTQuery* query); + + RemoteDocumentCache* remote_document_cache_; + MutationQueue* mutation_queue_; + IndexManager* index_manager_; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LOCAL_DOCUMENTS_VIEW_H_ diff --git a/Firestore/core/src/firebase/firestore/local/local_documents_view.mm b/Firestore/core/src/firebase/firestore/local/local_documents_view.mm new file mode 100644 index 00000000000..c27692b231b --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/local_documents_view.mm @@ -0,0 +1,222 @@ +/* + * Copyright 2017 Google + * + * 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 "Firestore/core/src/firebase/firestore/local/local_documents_view.h" + +#include + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Model/FSTDocument.h" +#import "Firestore/Source/Model/FSTMutation.h" +#import "Firestore/Source/Model/FSTMutationBatch.h" + +#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" +#include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_map.h" +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" + +NS_ASSUME_NONNULL_BEGIN + +namespace firebase { +namespace firestore { +namespace local { + +using model::DocumentKey; +using model::DocumentKeySet; +using model::DocumentMap; +using model::MaybeDocumentMap; +using model::ResourcePath; +using model::SnapshotVersion; +using util::MakeString; + +FSTMaybeDocument* _Nullable LocalDocumentsView::GetDocument( + const DocumentKey& key) { + std::vector batches = + mutation_queue_->AllMutationBatchesAffectingDocumentKey(key); + return GetDocument(key, batches); +} + +FSTMaybeDocument* _Nullable LocalDocumentsView::GetDocument( + const DocumentKey& key, const std::vector& batches) { + FSTMaybeDocument* _Nullable document = remote_document_cache_->Get(key); + for (FSTMutationBatch* batch : batches) { + document = [batch applyToLocalDocument:document documentKey:key]; + } + + return document; +} + +MaybeDocumentMap LocalDocumentsView::ApplyLocalMutationsToDocuments( + const MaybeDocumentMap& docs, + const std::vector& batches) { + MaybeDocumentMap results; + + for (const auto& kv : docs) { + const DocumentKey& key = kv.first; + FSTMaybeDocument* local_view = kv.second; + for (FSTMutationBatch* batch : batches) { + local_view = [batch applyToLocalDocument:local_view documentKey:key]; + } + results = results.insert(key, local_view); + } + return results; +} + +MaybeDocumentMap LocalDocumentsView::GetDocuments(const DocumentKeySet& keys) { + MaybeDocumentMap docs = remote_document_cache_->GetAll(keys); + return GetLocalViewOfDocuments(docs); +} + +/** + * Similar to `documentsForKeys`, but creates the local view from the given + * `baseDocs` without retrieving documents from the local store. + */ +MaybeDocumentMap LocalDocumentsView::GetLocalViewOfDocuments( + const MaybeDocumentMap& base_docs) { + MaybeDocumentMap results; + + DocumentKeySet all_keys; + for (const auto& kv : base_docs) { + all_keys = all_keys.insert(kv.first); + } + std::vector batches = + mutation_queue_->AllMutationBatchesAffectingDocumentKeys(all_keys); + + MaybeDocumentMap docs = ApplyLocalMutationsToDocuments(base_docs, batches); + + for (const auto& kv : docs) { + const DocumentKey& key = kv.first; + FSTMaybeDocument* maybe_doc = kv.second; + + // TODO(http://b/32275378): Don't conflate missing / deleted. + if (!maybe_doc) { + maybe_doc = [FSTDeletedDocument documentWithKey:key + version:SnapshotVersion::None() + hasCommittedMutations:NO]; + } + results = results.insert(key, maybe_doc); + } + + return results; +} + +DocumentMap LocalDocumentsView::GetDocumentsMatchingQuery(FSTQuery* query) { + if ([query isDocumentQuery]) { + return GetDocumentsMatchingDocumentQuery(query.path); + } else if ([query isCollectionGroupQuery]) { + return GetDocumentsMatchingCollectionGroupQuery(query); + } else { + return GetDocumentsMatchingCollectionQuery(query); + } +} + +DocumentMap LocalDocumentsView::GetDocumentsMatchingDocumentQuery( + const ResourcePath& doc_path) { + DocumentMap result; + // Just do a simple document lookup. + FSTMaybeDocument* doc = GetDocument(DocumentKey{doc_path}); + if ([doc isKindOfClass:[FSTDocument class]]) { + result = result.insert(doc.key, static_cast(doc)); + } + return result; +} + +model::DocumentMap LocalDocumentsView::GetDocumentsMatchingCollectionGroupQuery( + FSTQuery* query) { + HARD_ASSERT( + query.path.empty(), + "Currently we only support collection group queries at the root."); + + std::string collection_id = MakeString(query.collectionGroup); + std::vector parents = + index_manager_->GetCollectionParents(collection_id); + DocumentMap results; + + // Perform a collection query against each parent that contains the + // collection_id and aggregate the results. + for (const ResourcePath& parent : parents) { + FSTQuery* collection_query = + [query collectionQueryAtPath:parent.Append(collection_id)]; + DocumentMap collection_results = + GetDocumentsMatchingCollectionQuery(collection_query); + for (const auto& kv : collection_results.underlying_map()) { + const DocumentKey& key = kv.first; + FSTDocument* doc = static_cast(kv.second); + results = results.insert(key, doc); + } + } + return results; +} + +DocumentMap LocalDocumentsView::GetDocumentsMatchingCollectionQuery( + FSTQuery* query) { + DocumentMap results = remote_document_cache_->GetMatching(query); + // Get locally persisted mutation batches. + std::vector matchingBatches = + mutation_queue_->AllMutationBatchesAffectingQuery(query); + + for (FSTMutationBatch* batch : matchingBatches) { + for (FSTMutation* mutation : [batch mutations]) { + // Only process documents belonging to the collection. + if (!query.path.IsImmediateParentOf(mutation.key.path())) { + continue; + } + + const DocumentKey& key = mutation.key; + // base_doc may be nil for the documents that weren't yet written to the + // backend. + FSTMaybeDocument* base_doc = nil; + auto found = results.underlying_map().find(key); + if (found != results.underlying_map().end()) { + base_doc = found->second; + } + FSTMaybeDocument* mutated_doc = + [mutation applyToLocalDocument:base_doc + baseDocument:base_doc + localWriteTime:batch.localWriteTime]; + + if ([mutated_doc isKindOfClass:[FSTDocument class]]) { + results = results.insert(key, static_cast(mutated_doc)); + } else { + results = results.erase(key); + } + } + } + + // Finally, filter out any documents that don't actually match the query. Note + // that the extra reference here prevents DocumentMap's destructor from + // deallocating the initial unfiltered results while we're iterating over + // them. + DocumentMap unfiltered = results; + for (const auto& kv : unfiltered.underlying_map()) { + const DocumentKey& key = kv.first; + auto* doc = static_cast(kv.second); + if (![query matchesDocument:doc]) { + results = results.erase(key); + } + } + + return results; +} + +} // namespace local +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/local/local_serializer.cc b/Firestore/core/src/firebase/firestore/local/local_serializer.cc index 49d25968f4f..2b7944f09b3 100644 --- a/Firestore/core/src/firebase/firestore/local/local_serializer.cc +++ b/Firestore/core/src/firebase/firestore/local/local_serializer.cc @@ -29,6 +29,7 @@ #include "Firestore/core/src/firebase/firestore/model/no_document.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/unknown_document.h" +#include "Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/string_format.h" @@ -42,9 +43,9 @@ using model::MaybeDocument; using model::Mutation; using model::MutationBatch; using model::NoDocument; -using model::ObjectValue; using model::SnapshotVersion; using model::UnknownDocument; +using nanopb::CheckedSize; using nanopb::Reader; using nanopb::Writer; using remote::MakeArray; @@ -124,13 +125,11 @@ google_firestore_v1_Document LocalSerializer::EncodeDocument( rpc_serializer_.EncodeString(rpc_serializer_.EncodeKey(doc.key())); // Encode Document.fields (unless it's empty) - size_t count = doc.data().object_value().internal_value.size(); - HARD_ASSERT(count <= std::numeric_limits::max(), - "Unable to encode specified document. Too many fields."); - result.fields_count = static_cast(count); + pb_size_t count = CheckedSize(doc.data().GetInternalValue().size()); + result.fields_count = count; result.fields = MakeArray(count); int i = 0; - for (const auto& kv : doc.data().object_value().internal_value) { + for (const auto& kv : doc.data().GetInternalValue()) { result.fields[i].key = rpc_serializer_.EncodeString(kv.first); result.fields[i].value = rpc_serializer_.EncodeFieldValue(kv.second); i++; @@ -258,10 +257,8 @@ firestore_client_WriteBatch LocalSerializer::EncodeMutationBatch( firestore_client_WriteBatch result{}; result.batch_id = mutation_batch.batch_id(); - size_t count = mutation_batch.mutations().size(); - HARD_ASSERT(count <= std::numeric_limits::max(), - "Unable to encode specified mutation batch. Too many mutations."); - result.writes_count = static_cast(count); + pb_size_t count = CheckedSize(mutation_batch.mutations().size()); + result.writes_count = count; result.writes = MakeArray(count); int i = 0; for (const std::unique_ptr& mutation : mutation_batch.mutations()) { diff --git a/Firestore/core/src/firebase/firestore/local/memory_index_manager.cc b/Firestore/core/src/firebase/firestore/local/memory_index_manager.cc new file mode 100644 index 00000000000..08274167c3c --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/memory_index_manager.cc @@ -0,0 +1,67 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/local/memory_index_manager.h" + +#include +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" + +namespace firebase { +namespace firestore { +namespace local { + +using model::ResourcePath; + +bool MemoryCollectionParentIndex::Add(const ResourcePath& collection_path) { + HARD_ASSERT(collection_path.size() % 2 == 1, "Expected a collection path."); + + std::string collection_id = collection_path.last_segment(); + ResourcePath parent_path = collection_path.PopLast(); + std::set& existingParents = index_[collection_id]; + bool inserted = existingParents.insert(parent_path).second; + return inserted; +} + +std::vector MemoryCollectionParentIndex::GetEntries( + const std::string& collection_id) const { + std::vector result; + auto found = index_.find(collection_id); + if (found != index_.end()) { + const std::set& parent_paths = found->second; + std::copy(parent_paths.begin(), parent_paths.end(), + std::back_inserter(result)); + } + return result; +} + +void MemoryIndexManager::AddToCollectionParentIndex( + const ResourcePath& collection_path) { + collection_parents_index_.Add(collection_path); +} + +std::vector MemoryIndexManager::GetCollectionParents( + const std::string& collection_id) { + return collection_parents_index_.GetEntries(collection_id); +} + +} // namespace local +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/local/memory_index_manager.h b/Firestore/core/src/firebase/firestore/local/memory_index_manager.h new file mode 100644 index 00000000000..06a0eb83ba1 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/memory_index_manager.h @@ -0,0 +1,66 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_INDEX_MANAGER_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_INDEX_MANAGER_H_ + +#include +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/local/index_manager.h" +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" + +namespace firebase { +namespace firestore { +namespace local { + +/** + * Internal implementation of the collection-parent index. Also used for + * in-memory caching by LevelDbIndexManager and initial index population during + * schema migration. + */ +class MemoryCollectionParentIndex { + public: + // Returns false if the entry already existed. + bool Add(const model::ResourcePath& collection_path); + + std::vector GetEntries( + const std::string& collection_id) const; + + private: + std::unordered_map> index_; +}; + +/** An in-memory implementation of IndexManager. */ +class MemoryIndexManager : public IndexManager { + public: + void AddToCollectionParentIndex( + const model::ResourcePath& collection_path) override; + + std::vector GetCollectionParents( + const std::string& collection_id) override; + + private: + MemoryCollectionParentIndex collection_parents_index_; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_INDEX_MANAGER_H_ diff --git a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h index a47a2899576..927317cc3df 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h +++ b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h @@ -29,7 +29,7 @@ #import "Firestore/Source/Public/FIRTimestamp.h" #include "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" -#include "Firestore/core/src/firebase/firestore/local/document_reference.h" +#include "Firestore/core/src/firebase/firestore/local/document_key_reference.h" #include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" @@ -58,8 +58,10 @@ class MemoryMutationQueue : public MutationQueue { void AcknowledgeBatch(FSTMutationBatch* batch, NSData* _Nullable stream_token) override; - FSTMutationBatch* AddMutationBatch(FIRTimestamp* local_write_time, - NSArray* mutations) override; + FSTMutationBatch* AddMutationBatch( + FIRTimestamp* local_write_time, + std::vector&& base_mutations, + std::vector&& mutations) override; void RemoveMutationBatch(FSTMutationBatch* batch) override; @@ -92,8 +94,8 @@ class MemoryMutationQueue : public MutationQueue { void SetLastStreamToken(NSData* _Nullable token) override; private: - using DocumentReferenceSet = - immutable::SortedSet; + using DocumentKeyReferenceSet = + immutable::SortedSet; std::vector AllMutationBatchesWithIds( const std::set& batch_ids); @@ -109,7 +111,8 @@ class MemoryMutationQueue : public MutationQueue { */ int IndexOfBatchId(model::BatchId batch_id); - FSTMemoryPersistence* persistence_; + // This instance is owned by FSTMemoryPersistence; avoid a retain cycle. + __weak FSTMemoryPersistence* persistence_; /** * A FIFO queue of all mutations to apply to the backend. Mutations are added * to the end of the queue as they're written, and removed from the front of @@ -144,7 +147,7 @@ class MemoryMutationQueue : public MutationQueue { NSData* _Nullable last_stream_token_; /** An ordered mapping between documents and the mutation batch IDs. */ - DocumentReferenceSet batches_by_document_key_; + DocumentKeyReferenceSet batches_by_document_key_; }; } // namespace local diff --git a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm index 5979976d4fb..aace5e5ce3c 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm +++ b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm @@ -16,6 +16,8 @@ #include "Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h" +#include + #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Local/FSTLocalSerializer.h" @@ -23,7 +25,7 @@ #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" -#include "Firestore/core/src/firebase/firestore/local/document_reference.h" +#include "Firestore/core/src/firebase/firestore/local/document_key_reference.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" @@ -72,8 +74,10 @@ } FSTMutationBatch* MemoryMutationQueue::AddMutationBatch( - FIRTimestamp* local_write_time, NSArray* mutations) { - HARD_ASSERT(mutations.count > 0, "Mutation batches should not be empty"); + FIRTimestamp* local_write_time, + std::vector&& base_mutations, + std::vector&& mutations) { + HARD_ASSERT(!mutations.empty(), "Mutation batches should not be empty"); BatchId batch_id = next_batch_id_; next_batch_id_++; @@ -87,13 +91,17 @@ FSTMutationBatch* batch = [[FSTMutationBatch alloc] initWithBatchID:batch_id localWriteTime:local_write_time - mutations:mutations]; + baseMutations:std::move(base_mutations) + mutations:std::move(mutations)]; queue_.push_back(batch); - // Track references by document key. - for (FSTMutation* mutation in batch.mutations) { + // Track references by document key and index collection parents. + for (FSTMutation* mutation : [batch mutations]) { batches_by_document_key_ = batches_by_document_key_.insert( - DocumentReference{mutation.key, batch_id}); + DocumentKeyReference{mutation.key, batch_id}); + + persistence_.indexManager->AddToCollectionParentIndex( + mutation.key.path().PopLast()); } return batch; @@ -109,11 +117,11 @@ queue_.erase(queue_.begin()); // Remove entries from the index too. - for (FSTMutation* mutation in batch.mutations) { + for (FSTMutation* mutation : [batch mutations]) { const DocumentKey& key = mutation.key; [persistence_.referenceDelegate removeMutationReference:key]; - DocumentReference reference{key, batch.batchID}; + DocumentKeyReference reference{key, batch.batchID}; batches_by_document_key_ = batches_by_document_key_.erase(reference); } } @@ -124,7 +132,7 @@ // First find the set of affected batch IDs. std::set batch_ids; for (const DocumentKey& key : document_keys) { - DocumentReference start{key, 0}; + DocumentKeyReference start{key, 0}; for (const auto& reference : batches_by_document_key_.values_from(start)) { if (key != reference.key()) break; @@ -141,7 +149,7 @@ const DocumentKey& key) { std::vector result; - DocumentReference start{key, 0}; + DocumentKeyReference start{key, 0}; for (const auto& reference : batches_by_document_key_.values_from(start)) { if (key != reference.key()) break; @@ -154,6 +162,10 @@ std::vector MemoryMutationQueue::AllMutationBatchesAffectingQuery(FSTQuery* query) { + HARD_ASSERT( + ![query isCollectionGroupQuery], + "CollectionGroup queries should be handled in LocalDocumentsView"); + // Use the query path as a prefix for testing if a document matches the query. const ResourcePath& prefix = query.path; size_t immediate_children_path_length = prefix.size() + 1; @@ -166,7 +178,7 @@ if (!DocumentKey::IsDocumentKey(start_path)) { start_path = start_path.Append(""); } - DocumentReference start{DocumentKey{start_path}, 0}; + DocumentKeyReference start{DocumentKey{start_path}, 0}; // Find unique batchIDs referenced by all documents potentially matching the // query. @@ -230,7 +242,7 @@ bool MemoryMutationQueue::ContainsKey(const model::DocumentKey& key) { // Create a reference with a zero ID as the start position to find any // document reference with this key. - DocumentReference reference{key, 0}; + DocumentKeyReference reference{key, 0}; auto range = batches_by_document_key_.values_from(reference); auto begin = range.begin(); return begin != range.end() && begin->key() == key; diff --git a/Firestore/core/src/firebase/firestore/local/memory_query_cache.h b/Firestore/core/src/firebase/firestore/local/memory_query_cache.h index ad11b693e85..ac34fca078e 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_query_cache.h +++ b/Firestore/core/src/firebase/firestore/local/memory_query_cache.h @@ -32,6 +32,7 @@ #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/util/objc_compatibility.h" @class FSTLocalSerializer; @class FSTMemoryPersistence; @@ -57,7 +58,7 @@ class MemoryQueryCache : public QueryCache { FSTQueryData* _Nullable GetTarget(FSTQuery* query) override; - void EnumerateTargets(TargetEnumerator block) override; + void EnumerateTargets(const TargetCallback& callback) override; int RemoveTargets(model::ListenSequenceNumber upper_bound, const std::unordered_map& @@ -78,7 +79,7 @@ class MemoryQueryCache : public QueryCache { size_t CalculateByteSize(FSTLocalSerializer* serializer); size_t size() const override { - return [queries_ count]; + return queries_.size(); } model::ListenSequenceNumber highest_listen_sequence_number() const override { @@ -94,7 +95,9 @@ class MemoryQueryCache : public QueryCache { void SetLastRemoteSnapshotVersion(model::SnapshotVersion version) override; private: - FSTMemoryPersistence* persistence_; + // This instance is owned by FSTMemoryPersistence; avoid a retain cycle. + __weak FSTMemoryPersistence* persistence_; + /** The highest sequence number encountered */ model::ListenSequenceNumber highest_listen_sequence_number_; /** The highest numbered target ID encountered. */ @@ -103,9 +106,16 @@ class MemoryQueryCache : public QueryCache { model::SnapshotVersion last_remote_snapshot_version_; /** Maps a query to the data about that query. */ - NSMutableDictionary* queries_; - /** A ordered bidirectional mapping between documents and the remote target - * IDs. */ + std::unordered_map, + util::objc::EqualTo> + queries_; + + /** + * A ordered bidirectional mapping between documents and the remote target + * IDs. + */ ReferenceSet references_; }; diff --git a/Firestore/core/src/firebase/firestore/local/memory_query_cache.mm b/Firestore/core/src/firebase/firestore/local/memory_query_cache.mm index 9c76b36e664..05a69858d41 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_query_cache.mm +++ b/Firestore/core/src/firebase/firestore/local/memory_query_cache.mm @@ -15,12 +15,16 @@ */ #include "Firestore/core/src/firebase/firestore/local/memory_query_cache.h" + #import +#include + #import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Local/FSTMemoryPersistence.h" #import "Firestore/Source/Local/FSTQueryData.h" + #include "Firestore/core/src/firebase/firestore/model/document_key.h" using firebase::firestore::model::DocumentKey; @@ -40,7 +44,7 @@ highest_listen_sequence_number_(ListenSequenceNumber(0)), highest_target_id_(TargetId(0)), last_remote_snapshot_version_(SnapshotVersion::None()), - queries_([NSMutableDictionary dictionary]) { + queries_() { } void MemoryQueryCache::AddTarget(FSTQueryData* query_data) { @@ -59,36 +63,41 @@ } void MemoryQueryCache::RemoveTarget(FSTQueryData* query_data) { - [queries_ removeObjectForKey:query_data.query]; + queries_.erase(query_data.query); references_.RemoveReferences(query_data.targetID); } FSTQueryData* _Nullable MemoryQueryCache::GetTarget(FSTQuery* query) { - return queries_[query]; + auto iter = queries_.find(query); + return iter == queries_.end() ? nil : iter->second; } -void MemoryQueryCache::EnumerateTargets(TargetEnumerator block) { - [queries_ enumerateKeysAndObjectsUsingBlock:^( - FSTQuery* query, FSTQueryData* query_data, BOOL* stop) { - block(query_data, stop); - }]; +void MemoryQueryCache::EnumerateTargets(const TargetCallback& callback) { + for (const auto& kv : queries_) { + callback(kv.second); + } } int MemoryQueryCache::RemoveTargets( model::ListenSequenceNumber upper_bound, const std::unordered_map& live_targets) { - NSMutableArray* toRemove = [NSMutableArray array]; - [queries_ enumerateKeysAndObjectsUsingBlock:^( - FSTQuery* query, FSTQueryData* queryData, BOOL* stop) { - if (queryData.sequenceNumber <= upper_bound) { - if (live_targets.find(queryData.targetID) == live_targets.end()) { - [toRemove addObject:query]; - references_.RemoveReferences(queryData.targetID); + std::vector to_remove; + for (const auto& kv : queries_) { + FSTQuery* query = kv.first; + FSTQueryData* query_data = kv.second; + + if (query_data.sequenceNumber <= upper_bound) { + if (live_targets.find(query_data.targetID) == live_targets.end()) { + to_remove.push_back(query); + references_.RemoveReferences(query_data.targetID); } } - }]; - [queries_ removeObjectsForKeys:toRemove]; - return static_cast([toRemove count]); + } + + for (FSTQuery* element : to_remove) { + queries_.erase(element); + } + return static_cast(to_remove.size()); } void MemoryQueryCache::AddMatchingKeys(const DocumentKeySet& keys, @@ -116,11 +125,11 @@ } size_t MemoryQueryCache::CalculateByteSize(FSTLocalSerializer* serializer) { - __block size_t count = 0; - [queries_ enumerateKeysAndObjectsUsingBlock:^( - FSTQuery* query, FSTQueryData* query_data, BOOL* stop) { + size_t count = 0; + for (const auto& kv : queries_) { + FSTQueryData* query_data = kv.second; count += [[serializer encodedQueryData:query_data] serializedSize]; - }]; + } return count; } diff --git a/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h b/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h index d1eebd08e99..e0f3cda628a 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h +++ b/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h @@ -32,6 +32,7 @@ @class FSTLocalSerializer; @class FSTMaybeDocument; @class FSTMemoryLRUReferenceDelegate; +@class FSTMemoryPersistence; @class FSTQuery; NS_ASSUME_NONNULL_BEGIN @@ -42,6 +43,8 @@ namespace local { class MemoryRemoteDocumentCache : public RemoteDocumentCache { public: + explicit MemoryRemoteDocumentCache(FSTMemoryPersistence *persistence); + void Add(FSTMaybeDocument *document) override; void Remove(const model::DocumentKey &key) override; @@ -58,6 +61,9 @@ class MemoryRemoteDocumentCache : public RemoteDocumentCache { private: /** Underlying cache of documents. */ model::MaybeDocumentMap docs_; + + // This instance is owned by FSTMemoryPersistence; avoid a retain cycle. + __weak FSTMemoryPersistence *persistence_; }; } // namespace local diff --git a/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.mm b/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.mm index 2514eaaa16d..82503a0f604 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.mm +++ b/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.mm @@ -20,6 +20,8 @@ #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Local/FSTMemoryPersistence.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" + using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::DocumentMap; @@ -45,8 +47,16 @@ size_t DocumentKeyByteSize(const DocumentKey& key) { } } // namespace +MemoryRemoteDocumentCache::MemoryRemoteDocumentCache( + FSTMemoryPersistence* persistence) { + persistence_ = persistence; +} + void MemoryRemoteDocumentCache::Add(FSTMaybeDocument* document) { docs_ = docs_.insert(document.key, document); + + persistence_.indexManager->AddToCollectionParentIndex( + document.key.path().PopLast()); } void MemoryRemoteDocumentCache::Remove(const DocumentKey& key) { @@ -71,6 +81,10 @@ size_t DocumentKeyByteSize(const DocumentKey& key) { } DocumentMap MemoryRemoteDocumentCache::GetMatching(FSTQuery* query) { + HARD_ASSERT( + ![query isCollectionGroupQuery], + "CollectionGroup queries should be handled in LocalDocumentsView"); + DocumentMap results; // Documents are ordered by key, so we can use a prefix scan to narrow down @@ -122,4 +136,4 @@ size_t DocumentKeyByteSize(const DocumentKey& key) { } // namespace local } // namespace firestore -} // namespace firebase \ No newline at end of file +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/local/mutation_queue.h b/Firestore/core/src/firebase/firestore/local/mutation_queue.h index 784a7e55f56..6977b73c1e1 100644 --- a/Firestore/core/src/firebase/firestore/local/mutation_queue.h +++ b/Firestore/core/src/firebase/firestore/local/mutation_queue.h @@ -59,9 +59,19 @@ class MutationQueue { virtual void AcknowledgeBatch(FSTMutationBatch* batch, NSData* _Nullable stream_token) = 0; - /** Creates a new mutation batch and adds it to this mutation queue. */ + /** + * Creates a new mutation batch and adds it to this mutation queue. + * + * @param local_write_time The original write time of this mutation. + * @param base_mutations Mutations that are used to populate the base values + * when this mutation is applied locally. These mutations are used to locally + * overwrite values that are persisted in the remote document cache. + * @param mutations The user-provided mutations in this mutation batch. + */ virtual FSTMutationBatch* AddMutationBatch( - FIRTimestamp* local_write_time, NSArray* mutations) = 0; + FIRTimestamp* local_write_time, + std::vector&& base_mutations, + std::vector&& mutations) = 0; /** * Removes the given mutation batch from the queue. This is useful in two diff --git a/Firestore/core/src/firebase/firestore/local/query_cache.h b/Firestore/core/src/firebase/firestore/local/query_cache.h index 9fa61c1b519..75240ceb9c1 100644 --- a/Firestore/core/src/firebase/firestore/local/query_cache.h +++ b/Firestore/core/src/firebase/firestore/local/query_cache.h @@ -23,6 +23,7 @@ #import +#include #include #include "Firestore/core/src/firebase/firestore/model/document_key.h" @@ -39,6 +40,11 @@ namespace firebase { namespace firestore { namespace local { +using OrphanedDocumentCallback = + std::function; + +using TargetCallback = std::function; + /** * Represents cached targets received from the remote backend. This contains * both a mapping between targets and the documents that matched them according @@ -49,8 +55,6 @@ namespace local { */ class QueryCache { public: - typedef void (^TargetEnumerator)(FSTQueryData*, BOOL*); - virtual ~QueryCache() { } @@ -89,7 +93,7 @@ class QueryCache { */ virtual FSTQueryData* _Nullable GetTarget(FSTQuery* query) = 0; - virtual void EnumerateTargets(TargetEnumerator block) = 0; + virtual void EnumerateTargets(const TargetCallback& callback) = 0; virtual int RemoveTargets( model::ListenSequenceNumber upper_bound, diff --git a/Firestore/core/src/firebase/firestore/local/reference_set.cc b/Firestore/core/src/firebase/firestore/local/reference_set.cc index 9529b1ffed7..57431d75c9f 100644 --- a/Firestore/core/src/firebase/firestore/local/reference_set.cc +++ b/Firestore/core/src/firebase/firestore/local/reference_set.cc @@ -15,8 +15,9 @@ */ #include "Firestore/core/src/firebase/firestore/local/reference_set.h" + #include "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" -#include "Firestore/core/src/firebase/firestore/local/document_reference.h" +#include "Firestore/core/src/firebase/firestore/local/document_key_reference.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" namespace firebase { @@ -27,7 +28,7 @@ using model::DocumentKey; using model::DocumentKeySet; void ReferenceSet::AddReference(const DocumentKey& key, int id) { - DocumentReference reference{key, id}; + DocumentKeyReference reference{key, id}; by_key_ = by_key_.insert(reference); by_id_ = by_id_.insert(reference); } @@ -39,7 +40,7 @@ void ReferenceSet::AddReferences(const DocumentKeySet& keys, int id) { } void ReferenceSet::RemoveReference(const DocumentKey& key, int id) { - RemoveReference(DocumentReference{key, id}); + RemoveReference(DocumentKeyReference{key, id}); } void ReferenceSet::RemoveReferences( @@ -50,8 +51,8 @@ void ReferenceSet::RemoveReferences( } DocumentKeySet ReferenceSet::RemoveReferences(int id) { - DocumentReference start{DocumentKey::Empty(), id}; - DocumentReference end{DocumentKey::Empty(), id + 1}; + DocumentKeyReference start{DocumentKey::Empty(), id}; + DocumentKeyReference end{DocumentKey::Empty(), id + 1}; DocumentKeySet removed{}; @@ -65,19 +66,19 @@ DocumentKeySet ReferenceSet::RemoveReferences(int id) { void ReferenceSet::RemoveAllReferences() { auto initial = by_key_; - for (const DocumentReference& reference : initial) { + for (const DocumentKeyReference& reference : initial) { RemoveReference(reference); } } -void ReferenceSet::RemoveReference(const DocumentReference& reference) { +void ReferenceSet::RemoveReference(const DocumentKeyReference& reference) { by_key_ = by_key_.erase(reference); by_id_ = by_id_.erase(reference); } DocumentKeySet ReferenceSet::ReferencedKeys(int id) { - DocumentReference start{DocumentKey::Empty(), id}; - DocumentReference end{DocumentKey::Empty(), id + 1}; + DocumentKeyReference start{DocumentKey::Empty(), id}; + DocumentKeyReference end{DocumentKey::Empty(), id + 1}; DocumentKeySet keys; for (const auto& reference : by_id_.values_in(start, end)) { @@ -89,7 +90,7 @@ DocumentKeySet ReferenceSet::ReferencedKeys(int id) { bool ReferenceSet::ContainsKey(const DocumentKey& key) { // Create a reference with a zero ID as the start position to find any // document reference with this key. - DocumentReference start{key, 0}; + DocumentKeyReference start{key, 0}; auto range = by_key_.values_from(start); auto begin = range.begin(); diff --git a/Firestore/core/src/firebase/firestore/local/reference_set.h b/Firestore/core/src/firebase/firestore/local/reference_set.h index 58677e200ff..0e14c998bbd 100644 --- a/Firestore/core/src/firebase/firestore/local/reference_set.h +++ b/Firestore/core/src/firebase/firestore/local/reference_set.h @@ -18,7 +18,7 @@ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_REFERENCE_SET_H_ #include "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" -#include "Firestore/core/src/firebase/firestore/local/document_reference.h" +#include "Firestore/core/src/firebase/firestore/local/document_key_reference.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" @@ -31,7 +31,7 @@ namespace local { * (either a TargetId or BatchId). As references are added to or removed from * the set corresponding events are emitted to a registered garbage collector. * - * Each reference is represented by a DocumentReference object. Each of them + * Each reference is represented by a DocumentKeyReference object. Each of them * contains enough information to uniquely identify the reference. They are all * stored primarily in a set sorted by key. A document is considered garbage if * there's no references in that set (this can be efficiently checked thanks to @@ -81,10 +81,11 @@ class ReferenceSet { bool ContainsKey(const model::DocumentKey& key); private: - void RemoveReference(const DocumentReference& reference); + void RemoveReference(const DocumentKeyReference& reference); - immutable::SortedSet by_key_; - immutable::SortedSet by_id_; + immutable::SortedSet + by_key_; + immutable::SortedSet by_id_; }; } // namespace local diff --git a/Firestore/core/src/firebase/firestore/model/document.cc b/Firestore/core/src/firebase/firestore/model/document.cc index 06deb50576b..da540c88324 100644 --- a/Firestore/core/src/firebase/firestore/model/document.cc +++ b/Firestore/core/src/firebase/firestore/model/document.cc @@ -24,7 +24,7 @@ namespace firebase { namespace firestore { namespace model { -Document::Document(FieldValue&& data, +Document::Document(ObjectValue&& data, DocumentKey key, SnapshotVersion version, DocumentState document_state) @@ -32,7 +32,6 @@ Document::Document(FieldValue&& data, data_(std::move(data)), document_state_(document_state) { set_type(Type::Document); - HARD_ASSERT(FieldValue::Type::Object == data.type()); } bool Document::Equals(const MaybeDocument& other) const { diff --git a/Firestore/core/src/firebase/firestore/model/document.h b/Firestore/core/src/firebase/firestore/model/document.h index 9afa327f64b..acd04212572 100644 --- a/Firestore/core/src/firebase/firestore/model/document.h +++ b/Firestore/core/src/firebase/firestore/model/document.h @@ -50,14 +50,14 @@ enum class DocumentState { class Document : public MaybeDocument { public: /** - * Construct a document. FieldValue must be passed by rvalue. + * Construct a document. ObjectValue must be passed by rvalue. */ - Document(FieldValue&& data, + Document(ObjectValue&& data, DocumentKey key, SnapshotVersion version, DocumentState document_state); - const FieldValue& data() const { + const ObjectValue& data() const { return data_; } @@ -81,7 +81,7 @@ class Document : public MaybeDocument { bool Equals(const MaybeDocument& other) const override; private: - FieldValue data_; // This is of type Object. + ObjectValue data_; DocumentState document_state_; }; diff --git a/Firestore/core/src/firebase/firestore/model/document_key.h b/Firestore/core/src/firebase/firestore/model/document_key.h index a795e3ea90a..2443e26a9fb 100644 --- a/Firestore/core/src/firebase/firestore/model/document_key.h +++ b/Firestore/core/src/firebase/firestore/model/document_key.h @@ -51,10 +51,6 @@ class DocumentKey { explicit DocumentKey(ResourcePath&& path); #if defined(__OBJC__) - operator FSTDocumentKey*() const { - return [FSTDocumentKey keyWithDocumentKey:*this]; - } - NSUInteger Hash() const { return util::Hash(ToString()); } @@ -68,7 +64,7 @@ class DocumentKey { * Creates and returns a new document key using '/' to split the string into * segments. */ - static DocumentKey FromPathString(const absl::string_view path) { + static DocumentKey FromPathString(absl::string_view path) { return DocumentKey{ResourcePath::FromString(path)}; } @@ -90,6 +86,12 @@ class DocumentKey { return path_ ? *path_ : Empty().path(); } + /** Returns true if the document is in the specified collectionId. */ + bool HasCollectionId(absl::string_view collection_id) const { + size_t size = path().size(); + return size >= 2 && path()[size - 2] == collection_id; + } + private: // This is an optimization to make passing DocumentKey around cheaper (it's // copied often). diff --git a/Firestore/core/src/firebase/firestore/model/document_set.h b/Firestore/core/src/firebase/firestore/model/document_set.h new file mode 100644 index 00000000000..003f1050a3c --- /dev/null +++ b/Firestore/core/src/firebase/firestore/model/document_set.h @@ -0,0 +1,174 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_DOCUMENT_SET_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_DOCUMENT_SET_H_ + +#if !defined(__OBJC__) +#error "This header only supports Objective-C++" +#endif // !defined(__OBJC__) + +#import + +#include +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/immutable/sorted_container.h" +#include "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_map.h" +#include "Firestore/core/src/firebase/firestore/util/comparison.h" + +@class FSTDocument; + +NS_ASSUME_NONNULL_BEGIN + +namespace firebase { +namespace firestore { +namespace model { + +/** + * A C++ comparator that returns less-than, implemented by delegating to + * an NSComparator. + */ +class DocumentSetComparator { + public: + explicit DocumentSetComparator(NSComparator delegate = nil) + : delegate_(delegate) { + } + + bool operator()(FSTDocument* lhs, FSTDocument* rhs) const { + return delegate_(lhs, rhs) == NSOrderedAscending; + } + + private: + NSComparator delegate_; +}; + +/** + * DocumentSet is an immutable (copy-on-write) collection that holds documents + * in order specified by the provided comparator. We always add a document key + * comparator on top of what is provided to guarantee document equality based on + * the key. + */ +class DocumentSet : public immutable::SortedContainer, + public util::Equatable { + public: + /** + * The type of the main collection of documents in an DocumentSet. + * @see sorted_set_. + */ + using SetType = immutable::SortedSet; + + // STL container types + using value_type = FSTDocument*; + using const_iterator = SetType::const_iterator; + + /** + * Creates a new, empty DocumentSet sorted by the given comparator, then by + * keys. + */ + explicit DocumentSet(NSComparator comparator); + + size_t size() const { + return index_.size(); + } + + /** Returns true if the dictionary contains no elements. */ + bool empty() const { + return index_.empty(); + } + + /** Returns true if this set contains a document with the given key. */ + bool ContainsKey(const DocumentKey& key) const; + + SetType::const_iterator begin() const { + return sorted_set_.begin(); + } + SetType::const_iterator end() const { + return sorted_set_.end(); + } + + /** + * Returns the document from this set with the given key if it exists or nil + * if it doesn't. + */ + FSTDocument* _Nullable GetDocument(const DocumentKey& key) const; + + /** + * Returns the first document in the set according to its built in ordering, + * or nil if the set is empty. + */ + FSTDocument* _Nullable GetFirstDocument() const; + + /** + * Returns the last document in the set according to its built in ordering, or + * nil if the set is empty. + */ + FSTDocument* _Nullable GetLastDocument() const; + + /** + * Returns the index of the document with the provided key in the document + * set. Returns `npos` if the key is not present. + */ + size_t IndexOf(const DocumentKey& key) const; + + /** Returns a new DocumentSet that contains the given document. */ + DocumentSet insert(FSTDocument* _Nullable document) const; + + /** + * Returns a new DocumentSet that excludes any document associated with + * the given key. + */ + DocumentSet erase(const DocumentKey& key) const; + + friend bool operator==(const DocumentSet& lhs, const DocumentSet& rhs); + + std::string ToString() const; + friend std::ostream& operator<<(std::ostream& os, const DocumentSet& set); + + size_t Hash() const; + + private: + DocumentSet(DocumentMap&& index, SetType&& sorted_set) + : index_(std::move(index)), sorted_set_(std::move(sorted_set)) { + } + + /** + * An index of the documents in the DocumentSet, indexed by document key. + * The index exists to guarantee the uniqueness of document keys in the set + * and to allow lookup and removal of documents by key. + */ + DocumentMap index_; + + /** + * The main collection of documents in the DocumentSet. The documents are + * ordered by a comparator supplied from a query. The SetType collection + * exists in addition to the index to allow ordered traversal of the + * DocumentSet. + */ + SetType sorted_set_; +}; + +} // namespace model +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_DOCUMENT_SET_H_ diff --git a/Firestore/core/src/firebase/firestore/model/document_set.mm b/Firestore/core/src/firebase/firestore/model/document_set.mm new file mode 100644 index 00000000000..47cb3d263dd --- /dev/null +++ b/Firestore/core/src/firebase/firestore/model/document_set.mm @@ -0,0 +1,121 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/model/document_set.h" + +#include +#include + +#import "Firestore/Source/Model/FSTDocument.h" + +#include "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/util/objc_compatibility.h" +#include "absl/algorithm/container.h" + +NS_ASSUME_NONNULL_BEGIN + +namespace firebase { +namespace firestore { +namespace model { + +namespace objc = util::objc; +using immutable::SortedSet; + +DocumentSet::DocumentSet(NSComparator comparator) + : index_{}, sorted_set_{DocumentSetComparator{comparator}} { +} + +bool operator==(const DocumentSet& lhs, const DocumentSet& rhs) { + return absl::c_equal(lhs.sorted_set_, rhs.sorted_set_, + [](FSTDocument* left_doc, FSTDocument* right_doc) { + return [left_doc isEqual:right_doc]; + }); +} + +std::string DocumentSet::ToString() const { + return util::ToString(sorted_set_); +} + +std::ostream& operator<<(std::ostream& os, const DocumentSet& set) { + return os << set.ToString(); +} + +size_t DocumentSet::Hash() const { + size_t hash = 0; + for (FSTDocument* doc : sorted_set_) { + hash = 31 * hash + [doc hash]; + } + return hash; +} + +bool DocumentSet::ContainsKey(const DocumentKey& key) const { + return index_.underlying_map().find(key) != index_.underlying_map().end(); +} + +FSTDocument* _Nullable DocumentSet::GetDocument(const DocumentKey& key) const { + auto found = index_.underlying_map().find(key); + return found != index_.underlying_map().end() + ? static_cast(found->second) + : nil; +} + +FSTDocument* _Nullable DocumentSet::GetFirstDocument() const { + auto result = sorted_set_.min(); + return result != sorted_set_.end() ? *result : nil; +} + +FSTDocument* _Nullable DocumentSet::GetLastDocument() const { + auto result = sorted_set_.max(); + return result != sorted_set_.end() ? *result : nil; +} + +size_t DocumentSet::IndexOf(const DocumentKey& key) const { + FSTDocument* doc = GetDocument(key); + return doc ? sorted_set_.find_index(doc) : npos; +} + +DocumentSet DocumentSet::insert(FSTDocument* _Nullable document) const { + // TODO(mcg): look into making document nonnull. + if (!document) { + return *this; + } + + // Remove any prior mapping of the document's key before adding, preventing + // sortedSet from accumulating values that aren't in the index. + DocumentSet removed = erase(document.key); + + DocumentMap index = removed.index_.insert(document.key, document); + SetType set = removed.sorted_set_.insert(document); + return {std::move(index), std::move(set)}; +} + +DocumentSet DocumentSet::erase(const DocumentKey& key) const { + FSTDocument* doc = GetDocument(key); + if (!doc) { + return *this; + } + + DocumentMap index = index_.erase(key); + SetType set = sorted_set_.erase(doc); + return {std::move(index), std::move(set)}; +} + +} // namespace model +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/model/field_mask.h b/Firestore/core/src/firebase/firestore/model/field_mask.h index eabad446c5d..d8cdfba6e61 100644 --- a/Firestore/core/src/firebase/firestore/model/field_mask.h +++ b/Firestore/core/src/firebase/firestore/model/field_mask.h @@ -51,6 +51,9 @@ class FieldMask { explicit FieldMask(std::set fields) : fields_{std::move(fields)} { } + FieldMask(const FieldMask& f) : fields_{f.begin(), f.end()} { + } + const_iterator begin() const { return fields_.begin(); } diff --git a/Firestore/core/src/firebase/firestore/model/field_transform.h b/Firestore/core/src/firebase/firestore/model/field_transform.h index 1a7127a8fa6..4591802dd7a 100644 --- a/Firestore/core/src/firebase/firestore/model/field_transform.h +++ b/Firestore/core/src/firebase/firestore/model/field_transform.h @@ -44,6 +44,10 @@ class FieldTransform { return *transformation_.get(); } + bool idempotent() const { + return transformation_->idempotent(); + } + bool operator==(const FieldTransform& other) const { return path_ == other.path_ && *transformation_ == *other.transformation_; } diff --git a/Firestore/core/src/firebase/firestore/model/field_value.cc b/Firestore/core/src/firebase/firestore/model/field_value.cc index 37acaf485ad..fee10451e9a 100644 --- a/Firestore/core/src/firebase/firestore/model/field_value.cc +++ b/Firestore/core/src/firebase/firestore/model/field_value.cc @@ -23,6 +23,7 @@ #include #include +#include "Firestore/core/src/firebase/firestore/immutable/sorted_map.h" #include "Firestore/core/src/firebase/firestore/util/comparison.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "absl/memory/memory.h" @@ -36,23 +37,6 @@ namespace model { using Type = FieldValue::Type; using firebase::firestore::util::ComparisonResult; -namespace { - -// Makes a copy excluding the specified child, which is expected to be assigned -// different value afterwards. -ObjectValue::Map CopyExcept(const ObjectValue::Map& object_map, - const std::string exclude) { - ObjectValue::Map copy; - for (const auto& kv : object_map) { - if (kv.first != exclude) { - copy[kv.first] = kv.second; - } - } - return copy; -} - -} // namespace - FieldValue::FieldValue(const FieldValue& value) { *this = value; } @@ -108,8 +92,8 @@ FieldValue& FieldValue::operator=(const FieldValue& value) { } case Type::Object: { // copy-and-swap - ObjectValue::Map tmp = value.object_value_->internal_value; - std::swap(object_value_->internal_value, tmp); + Map tmp = *value.object_value_; + std::swap(*object_value_, tmp); break; } default: @@ -160,73 +144,58 @@ bool FieldValue::Comparable(Type lhs, Type rhs) { } } -FieldValue FieldValue::Set(const FieldPath& field_path, - FieldValue value) const { - HARD_ASSERT(type() == Type::Object, - "Cannot set field for non-object FieldValue"); +// TODO(rsgowman): Reorder this file to match its header. +ObjectValue ObjectValue::Set(const FieldPath& field_path, + const FieldValue& value) const { HARD_ASSERT(!field_path.empty(), "Cannot set field for empty path on FieldValue"); // Set the value by recursively calling on child object. const std::string& child_name = field_path.first_segment(); - const ObjectValue::Map& object_map = object_value_->internal_value; if (field_path.size() == 1) { - // TODO(zxu): Once immutable type is available, rewrite these. - ObjectValue::Map copy = CopyExcept(object_map, child_name); - copy[child_name] = std::move(value); - return FieldValue::FromMap(std::move(copy)); + return SetChild(child_name, value); } else { - ObjectValue::Map copy = CopyExcept(object_map, child_name); - const auto iter = object_map.find(child_name); - if (iter == object_map.end() || iter->second.type() != Type::Object) { - copy[child_name] = - FieldValue::FromMap({}).Set(field_path.PopFirst(), std::move(value)); - } else { - copy[child_name] = - iter->second.Set(field_path.PopFirst(), std::move(value)); + ObjectValue child = ObjectValue::Empty(); + const auto iter = fv_.object_value_->find(child_name); + if (iter != fv_.object_value_->end() && + iter->second.type() == Type::Object) { + child = ObjectValue(iter->second); } - return FieldValue::FromMap(std::move(copy)); + ObjectValue new_child = child.Set(field_path.PopFirst(), value); + return SetChild(child_name, new_child.fv_); } } -FieldValue FieldValue::Delete(const FieldPath& field_path) const { - HARD_ASSERT(type() == Type::Object, - "Cannot delete field for non-object FieldValue"); +ObjectValue ObjectValue::Delete(const FieldPath& field_path) const { HARD_ASSERT(!field_path.empty(), "Cannot delete field for empty path on FieldValue"); // Delete the value by recursively calling on child object. const std::string& child_name = field_path.first_segment(); - const ObjectValue::Map& object_map = object_value_->internal_value; if (field_path.size() == 1) { - // TODO(zxu): Once immutable type is available, rewrite these. - ObjectValue::Map copy = CopyExcept(object_map, child_name); - return FieldValue::FromMap(std::move(copy)); + return ObjectValue::FromMap(fv_.object_value_->erase(child_name)); } else { - const auto iter = object_map.find(child_name); - if (iter == object_map.end() || iter->second.type() != Type::Object) { + const auto iter = fv_.object_value_->find(child_name); + if (iter != fv_.object_value_->end() && + iter->second.type() == Type::Object) { + ObjectValue new_child = + ObjectValue(iter->second).Delete(field_path.PopFirst()); + return SetChild(child_name, new_child.fv_); + } else { // If the found value isn't an object, it cannot contain the remaining // segments of the path. We don't actually change a primitive value to // an object for a delete. return *this; - } else { - ObjectValue::Map copy = CopyExcept(object_map, child_name); - copy[child_name] = - object_map.at(child_name).Delete(field_path.PopFirst()); - return FieldValue::FromMap(std::move(copy)); } } } -absl::optional FieldValue::Get(const FieldPath& field_path) const { - HARD_ASSERT(type() == Type::Object, - "Cannot get field for non-object FieldValue"); - const FieldValue* current = this; +absl::optional ObjectValue::Get(const FieldPath& field_path) const { + const FieldValue* current = &this->fv_; for (const auto& path : field_path) { if (current->type() != Type::Object) { return absl::nullopt; } - const ObjectValue::Map& object_map = current->object_value_->internal_value; - const auto iter = object_map.find(path); - if (iter == object_map.end()) { + const auto iter = current->object_value_->find(path); + if (iter == current->object_value_->end()) { return absl::nullopt; } else { current = &iter->second; @@ -235,28 +204,33 @@ absl::optional FieldValue::Get(const FieldPath& field_path) const { return *current; } -const FieldValue& FieldValue::Null() { - static const FieldValue kNullInstance; - return kNullInstance; +ObjectValue ObjectValue::SetChild(const std::string& child_name, + const FieldValue& value) const { + return ObjectValue::FromMap(fv_.object_value_->insert(child_name, value)); } -const FieldValue& FieldValue::True() { - static const FieldValue kTrueInstance(true); - return kTrueInstance; +FieldValue FieldValue::Null() { + return FieldValue(); } -const FieldValue& FieldValue::False() { - static const FieldValue kFalseInstance(false); - return kFalseInstance; +FieldValue FieldValue::True() { + return FieldValue(true); } -const FieldValue& FieldValue::FromBoolean(bool value) { +FieldValue FieldValue::False() { + return FieldValue(false); +} + +FieldValue FieldValue::FromBoolean(bool value) { return value ? True() : False(); } -const FieldValue& FieldValue::Nan() { - static const FieldValue kNanInstance = FieldValue::FromDouble(NAN); - return kNanInstance; +FieldValue FieldValue::Nan() { + return FieldValue::FromDouble(NAN); +} + +FieldValue FieldValue::EmptyObject() { + return FieldValue::FromMap(FieldValue::Map()); } FieldValue FieldValue::FromInteger(int64_t value) { @@ -361,18 +335,23 @@ FieldValue FieldValue::FromArray(std::vector&& value) { return result; } -FieldValue FieldValue::FromMap(const ObjectValue::Map& value) { - ObjectValue::Map copy(value); +FieldValue FieldValue::FromMap(const FieldValue::Map& value) { + FieldValue::Map copy(value); return FromMap(std::move(copy)); } -FieldValue FieldValue::FromMap(ObjectValue::Map&& value) { +FieldValue FieldValue::FromMap(FieldValue::Map&& value) { FieldValue result; result.SwitchTo(Type::Object); - std::swap(result.object_value_->internal_value, value); + std::swap(*result.object_value_, value); return result; } +bool operator<(const FieldValue::Map& lhs, const FieldValue::Map& rhs) { + return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), + rhs.end()); +} + bool operator<(const FieldValue& lhs, const FieldValue& rhs) { if (!FieldValue::Comparable(lhs.type(), rhs.type())) { return lhs.type() < rhs.type(); @@ -466,7 +445,7 @@ void FieldValue::SwitchTo(const Type type) { array_value_.~unique_ptr>(); break; case Type::Object: - object_value_.~unique_ptr(); + object_value_.~unique_ptr(); break; default: {} // The other types where there is nothing to worry about. } @@ -503,13 +482,20 @@ void FieldValue::SwitchTo(const Type type) { absl::make_unique>()); break; case Type::Object: - new (&object_value_) - std::unique_ptr(absl::make_unique()); + new (&object_value_) std::unique_ptr(absl::make_unique()); break; default: {} // The other types where there is nothing to worry about. } } +ObjectValue ObjectValue::FromMap(const FieldValue::Map& value) { + return ObjectValue(FieldValue::FromMap(value)); +} + +ObjectValue ObjectValue::FromMap(FieldValue::Map&& value) { + return ObjectValue(FieldValue::FromMap(std::move(value))); +} + } // namespace model } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/model/field_value.h b/Firestore/core/src/firebase/firestore/model/field_value.h index debba4d985e..c7137c12185 100644 --- a/Firestore/core/src/firebase/firestore/model/field_value.h +++ b/Firestore/core/src/firebase/firestore/model/field_value.h @@ -18,13 +18,14 @@ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_FIELD_VALUE_H_ #include -#include #include #include +#include #include #include "Firestore/core/include/firebase/firestore/geo_point.h" #include "Firestore/core/include/firebase/firestore/timestamp.h" +#include "Firestore/core/src/firebase/firestore/immutable/sorted_map.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" @@ -46,19 +47,6 @@ struct ReferenceValue { const DatabaseId* database_id; }; -// TODO(rsgowman): Expand this to roughly match the java class -// c.g.f.f.model.value.ObjectValue. Probably move it to a similar namespace as -// well. (FieldValue itself is also in the value package in java.) Also do the -// same with the other FooValue values that FieldValue can return. -class FieldValue; -struct ObjectValue { - // TODO(rsgowman): These will eventually be private. We do want the serializer - // to be able to directly access these (possibly implying 'friend' usage, or a - // getInternalValue() like java has.) - using Map = std::map; - Map internal_value; -}; - /** * tagged-union class representing an immutable data value as stored in * Firestore. FieldValue represents all the different kinds of values @@ -66,6 +54,8 @@ struct ObjectValue { */ class FieldValue { public: + using Map = immutable::SortedMap; + /** * All the different kinds of values that can be stored in fields in * a document. The types of the same comparison order should be defined @@ -127,6 +117,11 @@ class FieldValue { return integer_value_; } + double double_value() const { + HARD_ASSERT(tag_ == Type::Double); + return double_value_; + } + Timestamp timestamp_value() const { HARD_ASSERT(tag_ == Type::Timestamp); return *timestamp_value_; @@ -137,45 +132,28 @@ class FieldValue { return *string_value_; } - const ObjectValue& object_value() const { - HARD_ASSERT(tag_ == Type::Object); - return *object_value_; + const std::vector& blob_value() const { + HARD_ASSERT(tag_ == Type::Blob); + return *blob_value_; } - /** - * Returns a FieldValue with the field at the named path set to value. - * Any absent parent of the field will also be created accordingly. - * - * @param field_path The field path to set. Cannot be empty. - * @param value The value to set. - * @return A new FieldValue with the field set. - */ - FieldValue Set(const FieldPath& field_path, FieldValue value) const; - - /** - * Returns a FieldValue with the field path deleted. If there is no field at - * the specified path, the returned value is an identical copy. - * - * @param field_path The field path to remove. Cannot be empty. - * @return A new FieldValue with the field path removed. - */ - FieldValue Delete(const FieldPath& field_path) const; + const GeoPoint& geo_point_value() const { + HARD_ASSERT(tag_ == Type::GeoPoint); + return *geo_point_value_; + } - /** - * Returns the value at the given path or absl::nullopt. If the path is empty, - * an identical copy of the FieldValue is returned. - * - * @param field_path the path to search. - * @return The value at the path or absl::nullopt if it doesn't exist. - */ - absl::optional Get(const FieldPath& field_path) const; + const std::vector& array_value() const { + HARD_ASSERT(tag_ == Type::Array); + return *array_value_; + } /** factory methods. */ - static const FieldValue& Null(); - static const FieldValue& True(); - static const FieldValue& False(); - static const FieldValue& Nan(); - static const FieldValue& FromBoolean(bool value); + static FieldValue Null(); + static FieldValue True(); + static FieldValue False(); + static FieldValue Nan(); + static FieldValue EmptyObject(); + static FieldValue FromBoolean(bool value); static FieldValue FromInteger(int64_t value); static FieldValue FromDouble(double value); static FieldValue FromTimestamp(const Timestamp& value); @@ -193,12 +171,14 @@ class FieldValue { static FieldValue FromGeoPoint(const GeoPoint& value); static FieldValue FromArray(const std::vector& value); static FieldValue FromArray(std::vector&& value); - static FieldValue FromMap(const ObjectValue::Map& value); - static FieldValue FromMap(ObjectValue::Map&& value); + static FieldValue FromMap(const Map& value); + static FieldValue FromMap(Map&& value); friend bool operator<(const FieldValue& lhs, const FieldValue& rhs); private: + friend class ObjectValue; + explicit FieldValue(bool value) : tag_(Type::Boolean), boolean_value_(value) { } @@ -221,10 +201,73 @@ class FieldValue { std::unique_ptr reference_value_; std::unique_ptr geo_point_value_; std::unique_ptr> array_value_; - std::unique_ptr object_value_; + std::unique_ptr object_value_; }; }; +/** A structured object value stored in Firestore. */ +class ObjectValue { + public: + explicit ObjectValue(FieldValue fv) : fv_(std::move(fv)) { + HARD_ASSERT(fv_.type() == FieldValue::Type::Object); + } + + static ObjectValue Empty() { + return ObjectValue(FieldValue::EmptyObject()); + } + + static ObjectValue FromMap(const FieldValue::Map& value); + static ObjectValue FromMap(FieldValue::Map&& value); + + /** + * Returns the value at the given path or absl::nullopt. If the path is empty, + * an identical copy of the FieldValue is returned. + * + * @param field_path the path to search. + * @return The value at the path or absl::nullopt if it doesn't exist. + */ + absl::optional Get(const FieldPath& field_path) const; + + /** + * Returns a FieldValue with the field at the named path set to value. + * Any absent parent of the field will also be created accordingly. + * + * @param field_path The field path to set. Cannot be empty. + * @param value The value to set. + * @return A new FieldValue with the field set. + */ + ObjectValue Set(const FieldPath& field_path, const FieldValue& value) const; + + /** + * Returns a FieldValue with the field path deleted. If there is no field at + * the specified path, the returned value is an identical copy. + * + * @param field_path The field path to remove. Cannot be empty. + * @return A new FieldValue with the field path removed. + */ + ObjectValue Delete(const FieldPath& field_path) const; + + // TODO(rsgowman): Add Value() method? + // + // Java has a value() method which returns a (non-immutable) java.util.Map, + // which is a copy of the immutable map, but with some fields (such as server + // timestamps) optionally resolved. Do we need the same here? + + const FieldValue::Map& GetInternalValue() const { + return *fv_.object_value_; + } + + private: + friend bool operator<(const ObjectValue& lhs, const ObjectValue& rhs); + + ObjectValue SetChild(const std::string& child_name, + const FieldValue& value) const; + + FieldValue fv_; +}; + +bool operator<(const FieldValue::Map& lhs, const FieldValue::Map& rhs); + /** Compares against another FieldValue. */ bool operator<(const FieldValue& lhs, const FieldValue& rhs); @@ -250,7 +293,7 @@ inline bool operator==(const FieldValue& lhs, const FieldValue& rhs) { /** Compares against another ObjectValue. */ inline bool operator<(const ObjectValue& lhs, const ObjectValue& rhs) { - return lhs.internal_value < rhs.internal_value; + return lhs.fv_ < rhs.fv_; } inline bool operator>(const ObjectValue& lhs, const ObjectValue& rhs) { diff --git a/Firestore/core/src/firebase/firestore/model/mutation.cc b/Firestore/core/src/firebase/firestore/model/mutation.cc index 8b18f35d14c..e863b34520a 100644 --- a/Firestore/core/src/firebase/firestore/model/mutation.cc +++ b/Firestore/core/src/firebase/firestore/model/mutation.cc @@ -21,6 +21,7 @@ #include "Firestore/core/src/firebase/firestore/model/document.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" +#include "Firestore/core/src/firebase/firestore/model/field_value.h" #include "Firestore/core/src/firebase/firestore/model/no_document.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" @@ -54,12 +55,10 @@ bool Mutation::equal_to(const Mutation& other) const { } SetMutation::SetMutation(DocumentKey&& key, - FieldValue&& value, + ObjectValue&& value, Precondition&& precondition) : Mutation(std::move(key), std::move(precondition)), value_(std::move(value)) { - // TODO(rsgowman): convert param to ObjectValue instead of FieldValue? - HARD_ASSERT(value_.type() == FieldValue::Type::Object); } MaybeDocumentPtr SetMutation::ApplyToRemoteDocument( @@ -74,7 +73,7 @@ MaybeDocumentPtr SetMutation::ApplyToRemoteDocument( // the server has accepted the mutation so the precondition must have held. const SnapshotVersion& version = mutation_result.version(); - return absl::make_unique(FieldValue(value_), key(), version, + return absl::make_unique(ObjectValue(value_), key(), version, DocumentState::kCommittedMutations); } @@ -89,7 +88,7 @@ MaybeDocumentPtr SetMutation::ApplyToLocalView( } SnapshotVersion version = GetPostMutationVersion(maybe_doc.get()); - return absl::make_unique(FieldValue(value_), key(), version, + return absl::make_unique(ObjectValue(value_), key(), version, DocumentState::kLocalMutations); } @@ -99,14 +98,12 @@ bool SetMutation::equal_to(const Mutation& other) const { } PatchMutation::PatchMutation(DocumentKey&& key, - FieldValue&& value, + ObjectValue&& value, FieldMask&& mask, Precondition&& precondition) : Mutation(std::move(key), std::move(precondition)), value_(std::move(value)), mask_(std::move(mask)) { - // TODO(rsgowman): convert param to ObjectValue instead of FieldValue? - HARD_ASSERT(value_.type() == FieldValue::Type::Object); } MaybeDocumentPtr PatchMutation::ApplyToRemoteDocument( @@ -131,7 +128,7 @@ MaybeDocumentPtr PatchMutation::ApplyToRemoteDocument( } const SnapshotVersion& version = mutation_result.version(); - FieldValue new_data = PatchDocument(maybe_doc.get()); + ObjectValue new_data = PatchDocument(maybe_doc.get()); return absl::make_unique(std::move(new_data), key(), version, DocumentState::kCommittedMutations); } @@ -147,21 +144,20 @@ MaybeDocumentPtr PatchMutation::ApplyToLocalView( } SnapshotVersion version = GetPostMutationVersion(maybe_doc.get()); - FieldValue new_data = PatchDocument(maybe_doc.get()); + ObjectValue new_data = PatchDocument(maybe_doc.get()); return absl::make_unique(std::move(new_data), key(), version, DocumentState::kLocalMutations); } -FieldValue PatchMutation::PatchDocument(const MaybeDocument* maybe_doc) const { +ObjectValue PatchMutation::PatchDocument(const MaybeDocument* maybe_doc) const { if (maybe_doc && maybe_doc->type() == MaybeDocument::Type::Document) { return PatchObject(static_cast(maybe_doc)->data()); } else { - return PatchObject(FieldValue::FromMap({})); + return PatchObject(ObjectValue::Empty()); } } -FieldValue PatchMutation::PatchObject(FieldValue obj) const { - HARD_ASSERT(obj.type() == FieldValue::Type::Object); +ObjectValue PatchMutation::PatchObject(ObjectValue obj) const { for (const FieldPath& path : mask_) { if (!path.empty()) { absl::optional new_value = value_.Get(path); diff --git a/Firestore/core/src/firebase/firestore/model/mutation.h b/Firestore/core/src/firebase/firestore/model/mutation.h index 08b4c35aeef..8e4ad562405 100644 --- a/Firestore/core/src/firebase/firestore/model/mutation.h +++ b/Firestore/core/src/firebase/firestore/model/mutation.h @@ -46,7 +46,7 @@ class MutationResult { public: MutationResult( SnapshotVersion&& version, - const std::shared_ptr>& transform_results) + const std::shared_ptr>& transform_results) : version_(std::move(version)), transform_results_(std::move(transform_results)) { } @@ -67,19 +67,19 @@ class MutationResult { /** * The resulting fields returned from the backend after a TransformMutation - * has been committed. Contains one FieldValue for each FieldTransform + * has been committed. Contains one ObjectValue for each FieldTransform * that was in the mutation. * * Will be null if the mutation was not a TransformMutation. */ - const std::shared_ptr>& transform_results() + const std::shared_ptr>& transform_results() const { return transform_results_; } private: const SnapshotVersion version_; - const std::shared_ptr> transform_results_; + const std::shared_ptr> transform_results_; }; /** @@ -217,7 +217,7 @@ inline bool operator!=(const Mutation& lhs, const Mutation& rhs) { class SetMutation : public Mutation { public: SetMutation(DocumentKey&& key, - FieldValue&& value, + ObjectValue&& value, Precondition&& precondition); Type type() const override { @@ -234,7 +234,7 @@ class SetMutation : public Mutation { const Timestamp& local_write_time) const override; /** Returns the object value to use when setting the document. */ - const FieldValue& value() const { + const ObjectValue& value() const { return value_; } @@ -242,7 +242,7 @@ class SetMutation : public Mutation { bool equal_to(const Mutation& other) const override; private: - const FieldValue value_; + const ObjectValue value_; }; /** @@ -261,7 +261,7 @@ class SetMutation : public Mutation { class PatchMutation : public Mutation { public: PatchMutation(DocumentKey&& key, - FieldValue&& value, + ObjectValue&& value, FieldMask&& mask, Precondition&& precondition); @@ -281,7 +281,7 @@ class PatchMutation : public Mutation { /** * Returns the fields and associated values to use when patching the document. */ - const FieldValue& value() const { + const ObjectValue& value() const { return value_; } @@ -297,10 +297,10 @@ class PatchMutation : public Mutation { bool equal_to(const Mutation& other) const override; private: - FieldValue PatchDocument(const MaybeDocument* maybe_doc) const; - FieldValue PatchObject(FieldValue obj) const; + ObjectValue PatchDocument(const MaybeDocument* maybe_doc) const; + ObjectValue PatchObject(ObjectValue obj) const; - const FieldValue value_; + const ObjectValue value_; const FieldMask mask_; }; diff --git a/Firestore/core/src/firebase/firestore/model/transform_operations.h b/Firestore/core/src/firebase/firestore/model/transform_operations.h index a5c5f821336..8002907037f 100644 --- a/Firestore/core/src/firebase/firestore/model/transform_operations.h +++ b/Firestore/core/src/firebase/firestore/model/transform_operations.h @@ -44,6 +44,7 @@ class TransformOperation { ServerTimestamp, ArrayUnion, ArrayRemove, + Increment, Test, // Purely for test purpose. }; @@ -67,6 +68,9 @@ class TransformOperation { virtual FSTFieldValue* ApplyToRemoteDocument( FSTFieldValue* previousValue, FSTFieldValue* transformResult) const = 0; + /** Returns whether this field transform is idempotent. */ + virtual bool idempotent() const = 0; + /** Returns whether the two are equal. */ virtual bool operator==(const TransformOperation& other) const = 0; @@ -83,9 +87,6 @@ class TransformOperation { /** Transforms a value into a server-generated timestamp. */ class ServerTimestampTransform : public TransformOperation { public: - ~ServerTimestampTransform() override { - } - Type type() const override { return Type::ServerTimestamp; } @@ -103,6 +104,10 @@ class ServerTimestampTransform : public TransformOperation { return transformResult; } + bool idempotent() const override { + return true; + } + bool operator==(const TransformOperation& other) const override { // All ServerTimestampTransform objects are equal. return other.type() == Type::ServerTimestamp; @@ -136,9 +141,6 @@ class ArrayTransform : public TransformOperation { : type_(type), elements_(std::move(elements)) { } - ~ArrayTransform() override { - } - Type type() const override { return type_; } @@ -162,6 +164,10 @@ class ArrayTransform : public TransformOperation { return elements_; } + bool idempotent() const override { + return true; + } + bool operator==(const TransformOperation& other) const override { if (other.type() != type()) { return false; @@ -233,6 +239,107 @@ class ArrayTransform : public TransformOperation { } }; +/** + * Implements the backend semantics for locally computed NUMERIC_ADD (increment) + * transforms. Converts all field values to longs or doubles and resolves + * overflows to LONG_MAX/LONG_MIN. + */ +class NumericIncrementTransform : public TransformOperation { + public: + explicit NumericIncrementTransform(FSTNumberValue* operand) + : operand_(operand) { + } + + Type type() const override { + return Type::Increment; + } + + FSTFieldValue* ApplyToLocalView( + FSTFieldValue* previousValue, + FIRTimestamp* /* localWriteTime */) const override { + // Return an integer value only if the previous value and the operand is an + // integer. + if ([previousValue isKindOfClass:[FSTIntegerValue class]] && + [operand_ isKindOfClass:[FSTIntegerValue class]]) { + int64_t sum = SafeIncrement( + (static_cast(previousValue)).internalValue, + (static_cast(operand_)).internalValue); + return [FSTIntegerValue integerValue:sum]; + } else if ([previousValue isKindOfClass:[FSTIntegerValue class]]) { + double sum = + (static_cast(previousValue)).internalValue + + OperandAsDouble(); + return [FSTDoubleValue doubleValue:sum]; + } else if ([previousValue isKindOfClass:[FSTDoubleValue class]]) { + double sum = (static_cast(previousValue)).internalValue + + OperandAsDouble(); + return [FSTDoubleValue doubleValue:sum]; + } else { + // If the existing value is not a number, use the value of the transform + // as the new base value. + return operand_; + } + } + + FSTFieldValue* ApplyToRemoteDocument( + FSTFieldValue*, FSTFieldValue* transformResult) const override { + return transformResult; + } + + FSTNumberValue* operand() const { + return operand_; + } + + bool idempotent() const override { + return false; + } + + bool operator==(const TransformOperation& other) const override { + if (other.type() != type()) { + return false; + } + auto numeric_add = static_cast(other); + return [operand_ isEqual:numeric_add.operand_]; + } + + // For Objective-C++ hash; to be removed after migration. + // Do NOT use in C++ code. + NSUInteger Hash() const override { + NSUInteger result = 37; + result = 31 * result + [operand_ hash]; + return result; + } + + private: + FSTNumberValue* operand_; + + /** + * Implements integer addition. Overflows are resolved to LONG_MAX/LONG_MIN. + */ + int64_t SafeIncrement(int64_t x, int64_t y) const { + if (x > 0 && y > LONG_MAX - x) { + return LONG_MAX; + } + + if (x < 0 && y < LONG_MIN - x) { + return LONG_MIN; + } + + return x + y; + } + + double OperandAsDouble() const { + if ([operand_ isKindOfClass:[FSTDoubleValue class]]) { + return (static_cast(operand_)).internalValue; + } else if ([operand_ isKindOfClass:[FSTIntegerValue class]]) { + return (static_cast(operand_)).internalValue; + } else { + HARD_FAIL("Expected 'operand' to be of FSTNumerValue type, but was %s", + NSStringFromClass([operand_ class])); + } + } +}; + } // namespace model } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/nanopb/nanopb_string.cc b/Firestore/core/src/firebase/firestore/nanopb/nanopb_string.cc index 89a9be66db4..90850021e76 100644 --- a/Firestore/core/src/firebase/firestore/nanopb/nanopb_string.cc +++ b/Firestore/core/src/firebase/firestore/nanopb/nanopb_string.cc @@ -19,12 +19,18 @@ #include #include +#include "Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h" + namespace firebase { namespace firestore { namespace nanopb { +String::~String() { + std::free(bytes_); +} + /* static */ pb_bytes_array_t* String::MakeBytesArray(absl::string_view value) { - auto size = static_cast(value.size()); + pb_size_t size = CheckedSize(value.size()); // Allocate one extra byte for the null terminator that's not necessarily // there in a string_view. As long as we're making a copy, might as well diff --git a/Firestore/core/src/firebase/firestore/nanopb/nanopb_string.h b/Firestore/core/src/firebase/firestore/nanopb/nanopb_string.h index 444d53d9541..53a96682306 100644 --- a/Firestore/core/src/firebase/firestore/nanopb/nanopb_string.h +++ b/Firestore/core/src/firebase/firestore/nanopb/nanopb_string.h @@ -72,9 +72,7 @@ class String : public util::Comparable { swap(*this, other); } - ~String() { - delete bytes_; - } + ~String(); String& operator=(String other) { swap(*this, other); diff --git a/Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h b/Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h new file mode 100644 index 00000000000..667c28a6b6d --- /dev/null +++ b/Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h @@ -0,0 +1,42 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_NANOPB_NANOPB_UTIL_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_NANOPB_NANOPB_UTIL_H_ + +#include + +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" + +namespace firebase { +namespace firestore { +namespace nanopb { + +/** + * Static casts the given size_t value down to a nanopb compatible size, after + * asserting that the value isn't out of range. + */ +inline pb_size_t CheckedSize(size_t size) { + HARD_ASSERT(size <= PB_SIZE_MAX, + "Size exceeds nanopb limits. Too many entries."); + return static_cast(size); +} + +} // namespace nanopb +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_NANOPB_NANOPB_UTIL_H_ diff --git a/Firestore/core/src/firebase/firestore/remote/datastore.h b/Firestore/core/src/firebase/firestore/remote/datastore.h index d6d8a358619..30ab827b9a9 100644 --- a/Firestore/core/src/firebase/firestore/remote/datastore.h +++ b/Firestore/core/src/firebase/firestore/remote/datastore.h @@ -22,6 +22,7 @@ #endif // !defined(__OBJC__) #import + #include #include #include @@ -45,7 +46,6 @@ #include "grpcpp/support/status.h" #import "Firestore/Source/Core/FSTTypes.h" -#import "Firestore/Source/Remote/FSTStream.h" namespace firebase { namespace firestore { @@ -68,6 +68,12 @@ namespace remote { */ class Datastore : public std::enable_shared_from_this { public: + // TODO(varconst): once `FSTMaybeDocument` is replaced with a C++ equivalent, + // this function could take a single `StatusOr` parameter. + using LookupCallback = std::function&, const util::Status&)>; + using CommitCallback = std::function; + Datastore(const core::DatabaseInfo& database_info, util::AsyncQueue* worker_queue, auth::CredentialsProvider* credentials); @@ -85,18 +91,18 @@ class Datastore : public std::enable_shared_from_this { * shared channel. */ virtual std::shared_ptr CreateWatchStream( - id delegate); + WatchStreamCallback* callback); /** * Creates a new `WriteStream` that is still unstarted but uses a common * shared channel. */ virtual std::shared_ptr CreateWriteStream( - id delegate); + WriteStreamCallback* callback); - void CommitMutations(NSArray* mutations, - FSTVoidErrorBlock completion); + void CommitMutations(const std::vector& mutations, + CommitCallback&& callback); void LookupDocuments(const std::vector& keys, - FSTVoidMaybeDocumentArrayErrorBlock completion); + LookupCallback&& callback); /** Returns true if the given error is a gRPC ABORTED error. */ static bool IsAbortedError(const util::Status& status); @@ -156,19 +162,18 @@ class Datastore : public std::enable_shared_from_this { private: void PollGrpcQueue(); - void CommitMutationsWithCredentials(const auth::Token& token, - NSArray* mutations, - FSTVoidErrorBlock completion); - void OnCommitMutationsResponse(const util::StatusOr& result, - FSTVoidErrorBlock completion); + void CommitMutationsWithCredentials( + const auth::Token& token, + const std::vector& mutations, + CommitCallback&& callback); void LookupDocumentsWithCredentials( const auth::Token& token, const std::vector& keys, - FSTVoidMaybeDocumentArrayErrorBlock completion); + LookupCallback&& callback); void OnLookupDocumentsResponse( const util::StatusOr>& result, - FSTVoidMaybeDocumentArrayErrorBlock completion); + const LookupCallback& callback); using OnCredentials = std::function&)>; void ResumeRpcWithCredentials(const OnCredentials& on_token); diff --git a/Firestore/core/src/firebase/firestore/remote/datastore.mm b/Firestore/core/src/firebase/firestore/remote/datastore.mm index 9cf387fdbec..05806c2d4bc 100644 --- a/Firestore/core/src/firebase/firestore/remote/datastore.mm +++ b/Firestore/core/src/firebase/firestore/remote/datastore.mm @@ -151,35 +151,38 @@ void LogGrpcCallFinished(absl::string_view rpc_name, } std::shared_ptr Datastore::CreateWatchStream( - id delegate) { + WatchStreamCallback* callback) { return std::make_shared(worker_queue_, credentials_, serializer_bridge_.GetSerializer(), - &grpc_connection_, delegate); + &grpc_connection_, callback); } std::shared_ptr Datastore::CreateWriteStream( - id delegate) { + WriteStreamCallback* callback) { return std::make_shared(worker_queue_, credentials_, serializer_bridge_.GetSerializer(), - &grpc_connection_, delegate); + &grpc_connection_, callback); } -void Datastore::CommitMutations(NSArray* mutations, - FSTVoidErrorBlock completion) { +void Datastore::CommitMutations(const std::vector& mutations, + CommitCallback&& callback) { ResumeRpcWithCredentials( - [this, mutations, completion](const StatusOr& maybe_credentials) { + // TODO(c++14): move into lambda. + [this, mutations, + callback](const StatusOr& maybe_credentials) mutable { if (!maybe_credentials.ok()) { - completion(util::MakeNSError(maybe_credentials.status())); + callback(maybe_credentials.status()); return; } CommitMutationsWithCredentials(maybe_credentials.ValueOrDie(), - mutations, completion); + mutations, std::move(callback)); }); } -void Datastore::CommitMutationsWithCredentials(const Token& token, - NSArray* mutations, - FSTVoidErrorBlock completion) { +void Datastore::CommitMutationsWithCredentials( + const Token& token, + const std::vector& mutations, + CommitCallback&& callback) { grpc::ByteBuffer message = serializer_bridge_.ToByteBuffer( serializer_bridge_.CreateCommitRequest(mutations)); @@ -189,43 +192,36 @@ void LogGrpcCallFinished(absl::string_view rpc_name, active_calls_.push_back(std::move(call_owning)); call->Start( - [this, call, completion](const StatusOr& result) { + // TODO(c++14): move into lambda. + [this, call, callback](const StatusOr& result) { LogGrpcCallFinished("CommitRequest", call, result.status()); HandleCallStatus(result.status()); - OnCommitMutationsResponse(result, completion); + // Response is deliberately ignored + callback(result.status()); RemoveGrpcCall(call); }); } -void Datastore::OnCommitMutationsResponse( - const StatusOr& result, FSTVoidErrorBlock completion) { - if (result.ok()) { - completion(/*Response is deliberately ignored*/ nil); - } else { - completion(util::MakeNSError(result.status())); - } -} - -void Datastore::LookupDocuments( - const std::vector& keys, - FSTVoidMaybeDocumentArrayErrorBlock completion) { +void Datastore::LookupDocuments(const std::vector& keys, + LookupCallback&& callback) { ResumeRpcWithCredentials( - [this, keys, completion](const StatusOr& maybe_credentials) { + // TODO(c++14): move into lambda. + [this, keys, callback](const StatusOr& maybe_credentials) mutable { if (!maybe_credentials.ok()) { - completion(nil, util::MakeNSError(maybe_credentials.status())); + callback({}, maybe_credentials.status()); return; } LookupDocumentsWithCredentials(maybe_credentials.ValueOrDie(), keys, - completion); + std::move(callback)); }); } void Datastore::LookupDocumentsWithCredentials( const Token& token, const std::vector& keys, - FSTVoidMaybeDocumentArrayErrorBlock completion) { + LookupCallback&& callback) { grpc::ByteBuffer message = serializer_bridge_.ToByteBuffer( serializer_bridge_.CreateLookupRequest(keys)); @@ -235,12 +231,13 @@ void LogGrpcCallFinished(absl::string_view rpc_name, GrpcStreamingReader* call = call_owning.get(); active_calls_.push_back(std::move(call_owning)); - call->Start([this, call, completion]( + // TODO(c++14): move into lambda. + call->Start([this, call, callback]( const StatusOr>& result) { LogGrpcCallFinished("BatchGetDocuments", call, result.status()); HandleCallStatus(result.status()); - OnLookupDocumentsResponse(result, completion); + OnLookupDocumentsResponse(result, callback); RemoveGrpcCall(call); }); @@ -248,21 +245,17 @@ void LogGrpcCallFinished(absl::string_view rpc_name, void Datastore::OnLookupDocumentsResponse( const StatusOr>& result, - FSTVoidMaybeDocumentArrayErrorBlock completion) { + const LookupCallback& callback) { if (!result.ok()) { - completion(nil, util::MakeNSError(result.status())); + callback({}, result.status()); return; } Status parse_status; std::vector responses = std::move(result).ValueOrDie(); - NSArray* docs = + std::vector docs = serializer_bridge_.MergeLookupResponses(responses, &parse_status); - if (parse_status.ok()) { - completion(docs, nil); - } else { - completion(nil, util::MakeNSError(parse_status)); - } + callback(docs, parse_status); } void Datastore::ResumeRpcWithCredentials(const OnCredentials& on_credentials) { diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_connection.h b/Firestore/core/src/firebase/firestore/remote/grpc_connection.h index 7eb17654b37..8d201f42818 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_connection.h +++ b/Firestore/core/src/firebase/firestore/remote/grpc_connection.h @@ -22,6 +22,8 @@ #include #include +#include "Firestore/core/src/firebase/firestore/util/warnings.h" + #include "Firestore/core/src/firebase/firestore/auth/token.h" #include "Firestore/core/src/firebase/firestore/core/database_info.h" #include "Firestore/core/src/firebase/firestore/remote/connectivity_monitor.h" @@ -35,10 +37,9 @@ #include "grpcpp/channel.h" #include "grpcpp/client_context.h" #include "grpcpp/completion_queue.h" -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdocumentation" +SUPPRESS_DOCUMENTATION_WARNINGS_BEGIN() #include "grpcpp/generic/generic_stub.h" -#pragma clang diagnostic pop +SUPPRESS_END() namespace firebase { namespace firestore { diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_root_certificate_finder_apple.mm b/Firestore/core/src/firebase/firestore/remote/grpc_root_certificate_finder_apple.mm index 4cde6ea75dd..b7e0db4b0f5 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_root_certificate_finder_apple.mm +++ b/Firestore/core/src/firebase/firestore/remote/grpc_root_certificate_finder_apple.mm @@ -16,14 +16,15 @@ #include "Firestore/core/src/firebase/firestore/remote/grpc_root_certificate_finder.h" +#import + #include #include "Firestore/core/src/firebase/firestore/util/filesystem.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" #include "Firestore/core/src/firebase/firestore/util/statusor.h" - -#import "Firestore/Source/Core/FSTFirestoreClient.h" +#include "absl/strings/str_cat.h" namespace firebase { namespace firestore { @@ -34,45 +35,106 @@ using util::StatusOr; using util::StringFormat; +namespace { + +/** + * Finds the roots.pem certificate file in the given resource bundle and logs + * the outcome. + * + * @param bundle The bundle to check. Can be a nested bundle in Resources or + * an app or framework bundle to look in directly. + * @param parent The parent bundle of the bundle to search. Used for logging. + */ +NSString* _Nullable FindCertFileInResourceBundle(NSBundle* _Nullable bundle, + NSBundle* _Nullable parent) { + if (!bundle) return nil; + + NSString* path = [bundle pathForResource:@"roots" ofType:@"pem"]; + if (util::LogIsDebugEnabled()) { + std::string message = + absl::StrCat("roots.pem ", path ? "found " : "not found ", "in bundle ", + util::MakeString([bundle bundleIdentifier])); + if (parent) { + absl::StrAppend(&message, " (in parent ", + util::MakeString([parent bundleIdentifier]), ")"); + } + LOG_DEBUG("%s", message); + } + + return path; +} + +/** + * Finds gRPCCertificates.bundle inside the given parent, if it exists. + * + * This function exists mostly to handle differences in platforms. + * On iOS, resources are nested directly within the top-level of the parent + * bundle, but on macOS this will actually be in Contents/Resources. + * + * @param parent A framework or app bundle to check. + * @return The nested gRPCCertificates.bundle if found, otherwise nil. + */ +NSBundle* _Nullable FindCertBundleInParent(NSBundle* _Nullable parent) { + if (!parent) return nil; + + NSString* path = [parent pathForResource:@"gRPCCertificates" + ofType:@"bundle"]; + if (!path) return nil; + + return [[NSBundle alloc] initWithPath:path]; +} + +NSBundle* _Nullable FindFirestoreFrameworkBundle() { + // Load FIRFirestore reflectively to avoid a circular reference at build time. + Class firestore_class = objc_getClass("FIRFirestore"); + if (!firestore_class) return nil; + + return [NSBundle bundleForClass:firestore_class]; +} + +/** + * Finds the path to the roots.pem certificates file, wherever it may be. + * + * Carthage users will find roots.pem inside gRPCCertificates.bundle in + * the main bundle. + * + * There have been enough variations and workarounds posted on this that + * this also accepts the roots.pem file outside gRPCCertificates.bundle. + */ NSString* FindPathToCertificatesFile() { - // Certificates file might be present in either the gRPC-C++ bundle or (for + // Certificates file might be present in either the gRPC-C++ framework or (for // some projects) in the main bundle. NSBundle* bundles[] = { - // Try to load certificates bundled by gRPC-C++. + // CocoaPods: try to load from the gRPC-C++ Framework. [NSBundle bundleWithIdentifier:@"org.cocoapods.grpcpp"], - // Users manually adding resources to the project may add the - // certificate to the main application bundle. Note that `mainBundle` is - // nil for unit tests of library projects, so it cannot fully substitute - // for checking the framework bundle. + + // Carthage: try to load from the FirebaseFirestore.framework + FindFirestoreFrameworkBundle(), + + // Carthage and manual projects: users manually adding resources to the + // project may add the certificate to the main application bundle. Note + // that `mainBundle` is nil for unit tests of library projects. [NSBundle mainBundle], }; - // search for the roots.pem file in each of these resource locations - NSString* possibleResources[] = { - @"gRPCCertificates.bundle/roots", - @"roots", - }; + NSString* path = nil; - for (NSBundle* bundle : bundles) { - if (!bundle) { - continue; - } + for (NSBundle* parent : bundles) { + if (!parent) continue; - for (NSString* resource : possibleResources) { - NSString* path = [bundle pathForResource:resource ofType:@"pem"]; - if (path) { - LOG_DEBUG("%s.pem found in bundle %s", resource, - [bundle bundleIdentifier]); - return path; - } else { - LOG_DEBUG("%s.pem not found in bundle %s", resource, - [bundle bundleIdentifier]); - } - } + NSBundle* certs_bundle = FindCertBundleInParent(parent); + path = FindCertFileInResourceBundle(certs_bundle, parent); + if (path) break; + + path = FindCertFileInResourceBundle(parent, nil); + if (path) break; } - return nil; + + return path; } +} // namespace + std::string LoadGrpcRootCertificate() { NSString* path = FindPathToCertificatesFile(); HARD_ASSERT( diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_stream.h b/Firestore/core/src/firebase/firestore/remote/grpc_stream.h index 4864bf29aba..9b0a528cfe2 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_stream.h +++ b/Firestore/core/src/firebase/firestore/remote/grpc_stream.h @@ -25,6 +25,8 @@ #include #include +#include "Firestore/core/src/firebase/firestore/util/warnings.h" + #include "Firestore/core/src/firebase/firestore/remote/grpc_call.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_completion.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_stream_observer.h" @@ -32,10 +34,9 @@ #include "Firestore/core/src/firebase/firestore/util/status.h" #include "absl/types/optional.h" #include "grpcpp/client_context.h" -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdocumentation" +SUPPRESS_DOCUMENTATION_WARNINGS_BEGIN() #include "grpcpp/generic/generic_stub.h" -#pragma clang diagnostic pop +SUPPRESS_END() #include "grpcpp/support/byte_buffer.h" namespace firebase { diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_streaming_reader.h b/Firestore/core/src/firebase/firestore/remote/grpc_streaming_reader.h index 9b2042097c8..c31b0962ea7 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_streaming_reader.h +++ b/Firestore/core/src/firebase/firestore/remote/grpc_streaming_reader.h @@ -22,15 +22,16 @@ #include #include +#include "Firestore/core/src/firebase/firestore/util/warnings.h" + #include "Firestore/core/src/firebase/firestore/remote/grpc_stream.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_stream_observer.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/src/firebase/firestore/util/statusor.h" #include "grpcpp/client_context.h" -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdocumentation" +SUPPRESS_DOCUMENTATION_WARNINGS_BEGIN() #include "grpcpp/generic/generic_stub.h" -#pragma clang diagnostic pop +SUPPRESS_END() #include "grpcpp/support/byte_buffer.h" namespace firebase { diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_unary_call.h b/Firestore/core/src/firebase/firestore/remote/grpc_unary_call.h index ff3e518a588..3edb2bb01a2 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_unary_call.h +++ b/Firestore/core/src/firebase/firestore/remote/grpc_unary_call.h @@ -21,16 +21,17 @@ #include #include +#include "Firestore/core/src/firebase/firestore/util/warnings.h" + #include "Firestore/core/src/firebase/firestore/remote/grpc_call.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_completion.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/src/firebase/firestore/util/statusor.h" #include "grpcpp/client_context.h" -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdocumentation" +SUPPRESS_DOCUMENTATION_WARNINGS_BEGIN() #include "grpcpp/generic/generic_stub.h" -#pragma clang diagnostic pop +SUPPRESS_END() #include "grpcpp/support/byte_buffer.h" namespace firebase { diff --git a/Firestore/core/src/firebase/firestore/remote/online_state_tracker.cc b/Firestore/core/src/firebase/firestore/remote/online_state_tracker.cc new file mode 100644 index 00000000000..afdc69d93d4 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/remote/online_state_tracker.cc @@ -0,0 +1,150 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/remote/online_state_tracker.h" + +#include // NOLINT(build/c++11) + +#include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/log.h" +#include "Firestore/core/src/firebase/firestore/util/string_format.h" + +namespace chr = std::chrono; +using firebase::firestore::model::OnlineState; +using firebase::firestore::util::AsyncQueue; +using firebase::firestore::util::DelayedOperation; +using firebase::firestore::util::Status; +using firebase::firestore::util::StringFormat; +using firebase::firestore::util::TimerId; + +namespace { + +// To deal with transient failures, we allow multiple stream attempts before +// giving up and transitioning from OnlineState Unknown to Offline. +// TODO(mikelehen): This used to be set to 2 as a mitigation for b/66228394. +// @jdimond thinks that bug is sufficiently fixed so that we can set this back +// to 1. If that works okay, we could potentially remove this logic entirely. +const int kMaxWatchStreamFailures = 1; + +// To deal with stream attempts that don't succeed or fail in a timely manner, +// we have a timeout for OnlineState to reach Online or Offline. If the timeout +// is reached, we transition to Offline rather than waiting indefinitely. +const AsyncQueue::Milliseconds kOnlineStateTimeout = chr::seconds(10); + +} // namespace + +namespace firebase { +namespace firestore { +namespace remote { + +void OnlineStateTracker::HandleWatchStreamStart() { + if (watch_stream_failures_ != 0) { + return; + } + + SetAndBroadcast(OnlineState::Unknown); + + HARD_ASSERT(!online_state_timer_, + "online_state_timer_ shouldn't be started yet"); + online_state_timer_ = worker_queue_->EnqueueAfterDelay( + kOnlineStateTimeout, TimerId::OnlineStateTimeout, [this] { + online_state_timer_ = {}; + + HARD_ASSERT(state_ == OnlineState::Unknown, + "Timer should be canceled if we transitioned to a " + "different state."); + LogClientOfflineWarningIfNecessary(StringFormat( + "Backend didn't respond within %s seconds.", + chr::duration_cast(kOnlineStateTimeout).count())); + SetAndBroadcast(OnlineState::Offline); + + // NOTE: `HandleWatchStreamFailure` will continue to increment + // `watch_stream_failures_` even though we are already marked `Offline` + // but this is non-harmful. + }); +} + +void OnlineStateTracker::HandleWatchStreamFailure(const Status& error) { + if (state_ == OnlineState::Online) { + SetAndBroadcast(OnlineState::Unknown); + + // To get to `OnlineState`::Online, `UpdateState` must have been called + // which would have reset our heuristics. + HARD_ASSERT(watch_stream_failures_ == 0, + "watch_stream_failures_ must be 0"); + HARD_ASSERT(!online_state_timer_, + "online_state_timer_ must not be set yet"); + } else { + ++watch_stream_failures_; + + if (watch_stream_failures_ >= kMaxWatchStreamFailures) { + ClearOnlineStateTimer(); + + LogClientOfflineWarningIfNecessary( + StringFormat("Connection failed %s times. Most recent error: %s", + kMaxWatchStreamFailures, error.error_message())); + + SetAndBroadcast(OnlineState::Offline); + } + } +} + +void OnlineStateTracker::UpdateState(OnlineState new_state) { + ClearOnlineStateTimer(); + watch_stream_failures_ = 0; + + if (new_state == OnlineState::Online) { + // We've connected to watch at least once. Don't warn the developer about + // being offline going forward. + should_warn_client_is_offline_ = false; + } + + SetAndBroadcast(new_state); +} + +void OnlineStateTracker::SetAndBroadcast(OnlineState new_state) { + if (new_state != state_) { + state_ = new_state; + online_state_handler_(new_state); + } +} + +void OnlineStateTracker::LogClientOfflineWarningIfNecessary( + const std::string& reason) { + std::string message = StringFormat( + "Could not reach Cloud Firestore backend. %s\n This " + "typically indicates that your device does not have a " + "healthy Internet connection at the moment. The client will " + "operate in offline mode until it is able to successfully " + "connect to the backend.", + reason); + + if (should_warn_client_is_offline_) { + LOG_WARN("%s", message); + should_warn_client_is_offline_ = false; + } else { + LOG_DEBUG("%s", message); + } +} + +void OnlineStateTracker::ClearOnlineStateTimer() { + online_state_timer_.Cancel(); +} + +} // namespace remote +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/remote/online_state_tracker.h b/Firestore/core/src/firebase/firestore/remote/online_state_tracker.h new file mode 100644 index 00000000000..a061745a207 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/remote/online_state_tracker.h @@ -0,0 +1,124 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_ONLINE_STATE_TRACKER_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_ONLINE_STATE_TRACKER_H_ + +#include +#include + +#include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" + +namespace firebase { +namespace firestore { +namespace remote { + +/** + * A component used by the `RemoteStore` to track the `OnlineState` (that is, + * whether or not the client as a whole should be considered to be online or + * offline), implementing the appropriate heuristics. + * + * In particular, when the client is trying to connect to the backend, we allow + * up to `kMaxWatchStreamFailures` within `kOnlineStateTimeout` for a connection + * to succeed. If we have too many failures or the timeout elapses, then we set + * the `OnlineState` to `Offline`, and the client will behave as if it is + * offline (`getDocument()` calls will return cached data, etc.). + */ +class OnlineStateTracker { + public: + OnlineStateTracker() = default; + + OnlineStateTracker( + util::AsyncQueue* worker_queue, + std::function online_state_handler) + : worker_queue_{worker_queue}, + online_state_handler_{online_state_handler} { + } + + /** + * Called by `RemoteStore` when a watch stream is started (including on + * each backoff attempt). + * + * If this is the first attempt, it sets the `OnlineState` to `Unknown` and + * starts the `onlineStateTimer`. + */ + void HandleWatchStreamStart(); + + /** + * Called by `RemoteStore` when a watch stream fails. + * + * Updates our `OnlineState` as appropriate. The first failure moves us to + * `OnlineState::Unknown`. We then may allow multiple failures (based on + * `kMaxWatchStreamFailures`) before we actually transition to + * `OnlineState::Offline`. + */ + void HandleWatchStreamFailure(const util::Status& error); + + /** + * Explicitly sets the `OnlineState` to the specified state. + * + * Note that this resets the timers / failure counters, etc. used by our + * `Offline` heuristics, so it must not be used in place of + * `HandleWatchStreamStart` and `HandleWatchStreamFailure`. + */ + void UpdateState(model::OnlineState new_state); + + private: + void SetAndBroadcast(model::OnlineState new_state); + void LogClientOfflineWarningIfNecessary(const std::string& reason); + void ClearOnlineStateTimer(); + + /** The current `OnlineState`. */ + model::OnlineState state_ = model::OnlineState::Unknown; + + /** + * A count of consecutive failures to open the stream. If it reaches the + * maximum defined by `kMaxWatchStreamFailures`, we'll revert to + * `OnlineState::Offline`. + */ + int watch_stream_failures_ = 0; + + /** + * A timer that elapses after `kOnlineStateTimeout`, at which point we + * transition from `OnlineState` `Unknown` to `Offline` without waiting for + * the stream to actually fail (`kMaxWatchStreamFailures` times). + */ + util::DelayedOperation online_state_timer_; + + /** + * Whether the client should log a warning message if it fails to connect to + * the backend (initially true, cleared after a successful stream, or if we've + * logged the message already). + */ + bool should_warn_client_is_offline_ = true; + + /** + * The worker queue to use for running timers (and to call + * `online_state_handler_`). + */ + util::AsyncQueue* worker_queue_ = nullptr; + + /** A callback to be notified on `OnlineState` changes. */ + std::function online_state_handler_; +}; + +} // namespace remote +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_ONLINE_STATE_TRACKER_H_ diff --git a/Firestore/core/src/firebase/firestore/remote/remote_event.h b/Firestore/core/src/firebase/firestore/remote/remote_event.h index e19a9badc06..8ce1e38c280 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_event.h +++ b/Firestore/core/src/firebase/firestore/remote/remote_event.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" @@ -39,36 +40,114 @@ @class FSTMaybeDocument; @class FSTQueryData; -@class FSTRemoteEvent; -@class FSTTargetChange; NS_ASSUME_NONNULL_BEGIN +namespace firebase { +namespace firestore { +namespace remote { + /** - * Interface implemented by RemoteStore to expose target metadata to the + * Interface implemented by `RemoteStore` to expose target metadata to the * `WatchChangeAggregator`. */ -@protocol FSTTargetMetadataProvider +class TargetMetadataProvider { + public: + virtual ~TargetMetadataProvider() { + } -/** - * Returns the set of remote document keys for the given target ID as of the - * last raised snapshot. - */ -- (firebase::firestore::model::DocumentKeySet)remoteKeysForTarget: - (firebase::firestore::model::TargetId)targetID; + /** + * Returns the set of remote document keys for the given target ID as of the + * last raised snapshot. + */ + virtual model::DocumentKeySet GetRemoteKeysForTarget( + model::TargetId target_id) const = 0; + + /** + * Returns the FSTQueryData for an active target ID or 'null' if this query + * has become inactive + */ + virtual FSTQueryData* GetQueryDataForTarget( + model::TargetId target_id) const = 0; +}; /** - * Returns the FSTQueryData for an active target ID or 'null' if this query has - * become inactive + * A `TargetChange` specifies the set of changes for a specific target as part + * of an `RemoteEvent`. These changes track which documents are added, + * modified or removed, as well as the target's resume token and whether the + * target is marked CURRENT. + * + * The actual changes *to* documents are not part of the `TargetChange` since + * documents may be part of multiple targets. */ -- (nullable FSTQueryData*)queryDataForTarget: - (firebase::firestore::model::TargetId)targetID; +class TargetChange { + public: + TargetChange() = default; + + TargetChange(NSData* resume_token, + bool current, + model::DocumentKeySet added_documents, + model::DocumentKeySet modified_documents, + model::DocumentKeySet removed_documents) + : resume_token_{resume_token}, + current_{current}, + added_documents_{std::move(added_documents)}, + modified_documents_{std::move(modified_documents)}, + removed_documents_{std::move(removed_documents)} { + } -@end + /** + * An opaque, server-assigned token that allows watching a query to be resumed + * after disconnecting without retransmitting all the data that matches the + * query. The resume token essentially identifies a point in time from which + * the server should resume sending results. + */ + NSData* resume_token() const { + return resume_token_; + } -namespace firebase { -namespace firestore { -namespace remote { + /** + * The "current" (synced) status of this target. Note that "current" has + * special meaning in the RPC protocol that implies that a target is both + * up-to-date and consistent with the rest of the watch stream. + */ + bool current() const { + return current_; + } + + /** + * The set of documents that were newly assigned to this target as part of + * this remote event. + */ + const model::DocumentKeySet& added_documents() const { + return added_documents_; + } + + /** + * The set of documents that were already assigned to this target but received + * an update during this remote event. + */ + const model::DocumentKeySet& modified_documents() const { + return modified_documents_; + } + + /** + * The set of documents that were removed from this target as part of this + * remote event. + */ + const model::DocumentKeySet& removed_documents() const { + return removed_documents_; + } + + private: + NSData* resume_token_ = nil; + bool current_ = false; + model::DocumentKeySet added_documents_; + model::DocumentKeySet modified_documents_; + model::DocumentKeySet removed_documents_; +}; + +bool operator==(const TargetChange& lhs, const TargetChange& rhs); /** Tracks the internal state of a Watch target. */ class TargetState { @@ -78,12 +157,12 @@ class TargetState { /** * Whether this target has been marked 'current'. * - * 'Current' has special meaning in the RPC protocol: It implies that the + * 'current' has special meaning in the RPC protocol: It implies that the * Watch backend has sent us all changes up to the point at which the target * was added and that the target is consistent with the rest of the watch * stream. */ - bool Current() const { + bool current() const { return current_; } @@ -114,13 +193,13 @@ class TargetState { * To reset the document changes after raising this snapshot, call * `ClearPendingChanges()`. */ - FSTTargetChange* ToTargetChange() const; + TargetChange ToTargetChange() const; /** Resets the document changes and sets `HasPendingChanges` to false. */ void ClearPendingChanges(); void AddDocumentChange(const model::DocumentKey& document_key, - core::DocumentViewChangeType type); + core::DocumentViewChange::Type type); void RemoveDocumentChange(const model::DocumentKey& document_key); void RecordPendingTargetRequest(); void RecordTargetResponse(); @@ -140,7 +219,7 @@ class TargetState { * always reflect the current set of changes against the last issued snapshot. */ std::unordered_map document_changes_; @@ -156,6 +235,75 @@ class TargetState { bool has_pending_changes_ = true; }; +/** + * An event from the RemoteStore. It is split into `TargetChanges` (changes to + * the state or the set of documents in our watched targets) and + * `DocumentUpdates` (changes to the actual documents). + */ +class RemoteEvent { + public: + RemoteEvent(model::SnapshotVersion snapshot_version, + std::unordered_map target_changes, + std::unordered_set target_mismatches, + std::unordered_map document_updates, + model::DocumentKeySet limbo_document_changes) + : snapshot_version_{snapshot_version}, + target_changes_{std::move(target_changes)}, + target_mismatches_{std::move(target_mismatches)}, + document_updates_{std::move(document_updates)}, + limbo_document_changes_{std::move(limbo_document_changes)} { + } + + /** The snapshot version this event brings us up to. */ + const model::SnapshotVersion& snapshot_version() const { + return snapshot_version_; + } + + /** A map from target to changes to the target. See `TargetChange`. */ + const std::unordered_map& target_changes() + const { + return target_changes_; + } + + /** + * A set of targets that is known to be inconsistent. Listens for these + * targets should be re-established without resume tokens. + */ + const std::unordered_set& target_mismatches() const { + return target_mismatches_; + } + + /** + * A set of which documents have changed or been deleted, along with the doc's + * new values (if not deleted). + */ + const std::unordered_map& + document_updates() const { + return document_updates_; + } + + /** + * A set of which document updates are due only to limbo resolution targets. + */ + const model::DocumentKeySet& limbo_document_changes() const { + return limbo_document_changes_; + } + + private: + model::SnapshotVersion snapshot_version_; + std::unordered_map target_changes_; + std::unordered_set target_mismatches_; + std::unordered_map + document_updates_; + model::DocumentKeySet limbo_document_changes_; +}; + /** * A helper class to accumulate watch changes into a `RemoteEvent` and other * target information. @@ -163,9 +311,7 @@ class TargetState { class WatchChangeAggregator { public: explicit WatchChangeAggregator( - id target_metadata_provider) - : target_metadata_provider_{target_metadata_provider} { - } + TargetMetadataProvider* target_metadata_provider); /** * Processes and adds the `DocumentWatchChange` to the current set of changes. @@ -190,8 +336,7 @@ class WatchChangeAggregator { * taken from the initializer. Resets the accumulated changes before * returning. */ - FSTRemoteEvent* CreateRemoteEvent( - const model::SnapshotVersion& snapshot_version); + RemoteEvent CreateRemoteEvent(const model::SnapshotVersion& snapshot_version); /** Removes the in-memory state for the provided target. */ void RemoveTarget(model::TargetId target_id); @@ -292,7 +437,7 @@ class WatchChangeAggregator { */ std::unordered_set pending_target_resets_; - id target_metadata_provider_; + TargetMetadataProvider* target_metadata_provider_ = nullptr; }; } // namespace remote diff --git a/Firestore/core/src/firebase/firestore/remote/remote_event.mm b/Firestore/core/src/firebase/firestore/remote/remote_event.mm index d9203f532c8..aa2fa33aca3 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_event.mm +++ b/Firestore/core/src/firebase/firestore/remote/remote_event.mm @@ -21,9 +21,8 @@ #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Remote/FSTRemoteEvent.h" -using firebase::firestore::core::DocumentViewChangeType; +using firebase::firestore::core::DocumentViewChange; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::SnapshotVersion; @@ -33,6 +32,16 @@ namespace firestore { namespace remote { +// TargetChange + +bool operator==(const TargetChange& lhs, const TargetChange& rhs) { + return [lhs.resume_token() isEqualToData:rhs.resume_token()] && + lhs.current() == rhs.current() && + lhs.added_documents() == rhs.added_documents() && + lhs.modified_documents() == rhs.modified_documents() && + lhs.removed_documents() == rhs.removed_documents(); +} + // TargetState TargetState::TargetState() : resume_token_{[NSData data]} { @@ -45,23 +54,23 @@ } } -FSTTargetChange* TargetState::ToTargetChange() const { +TargetChange TargetState::ToTargetChange() const { DocumentKeySet added_documents; DocumentKeySet modified_documents; DocumentKeySet removed_documents; for (const auto& entry : document_changes_) { const DocumentKey& document_key = entry.first; - DocumentViewChangeType change_type = entry.second; + DocumentViewChange::Type change_type = entry.second; switch (change_type) { - case DocumentViewChangeType::kAdded: + case DocumentViewChange::Type::kAdded: added_documents = added_documents.insert(document_key); break; - case DocumentViewChangeType::kModified: + case DocumentViewChange::Type::kModified: modified_documents = modified_documents.insert(document_key); break; - case DocumentViewChangeType::kRemoved: + case DocumentViewChange::Type::kRemoved: removed_documents = removed_documents.insert(document_key); break; default: @@ -69,12 +78,9 @@ } } - return [[FSTTargetChange alloc] - initWithResumeToken:resume_token() - current:Current() - addedDocuments:std::move(added_documents) - modifiedDocuments:std::move(modified_documents) - removedDocuments:std::move(removed_documents)]; + return TargetChange{resume_token(), current(), std::move(added_documents), + std::move(modified_documents), + std::move(removed_documents)}; } void TargetState::ClearPendingChanges() { @@ -96,7 +102,7 @@ } void TargetState::AddDocumentChange(const DocumentKey& document_key, - DocumentViewChangeType type) { + DocumentViewChange::Type type) { has_pending_changes_ = true; document_changes_[document_key] = type; } @@ -108,6 +114,11 @@ // WatchChangeAggregator +WatchChangeAggregator::WatchChangeAggregator( + TargetMetadataProvider* target_metadata_provider) + : target_metadata_provider_{NOT_NULL(target_metadata_provider)} { +} + void WatchChangeAggregator::HandleDocumentChange( const DocumentWatchChange& document_change) { for (TargetId target_id : document_change.updated_target_ids()) { @@ -235,9 +246,9 @@ } } -FSTRemoteEvent* WatchChangeAggregator::CreateRemoteEvent( +RemoteEvent WatchChangeAggregator::CreateRemoteEvent( const SnapshotVersion& snapshot_version) { - std::unordered_map target_changes; + std::unordered_map target_changes; for (auto& entry : target_states_) { TargetId target_id = entry.first; @@ -245,7 +256,7 @@ FSTQueryData* query_data = QueryDataForActiveTarget(target_id); if (query_data) { - if (target_state.Current() && [query_data.query isDocumentQuery]) { + if (target_state.current() && [query_data.query isDocumentQuery]) { // Document queries for document that don't exist can produce an empty // result set. To update our local cache, we synthesize a document // delete if we have not previously received the document. This resolves @@ -291,12 +302,10 @@ } } - FSTRemoteEvent* remote_event = - [[FSTRemoteEvent alloc] initWithSnapshotVersion:snapshot_version - targetChanges:target_changes - targetMismatches:pending_target_resets_ - documentUpdates:pending_document_updates_ - limboDocuments:resolved_limbo_documents]; + RemoteEvent remote_event{snapshot_version, std::move(target_changes), + std::move(pending_target_resets_), + std::move(pending_document_updates_), + std::move(resolved_limbo_documents)}; // Re-initialize the current state to ensure that we do not modify the // generated `RemoteEvent`. @@ -313,10 +322,10 @@ return; } - DocumentViewChangeType change_type = + DocumentViewChange::Type change_type = TargetContainsDocument(target_id, document.key) - ? DocumentViewChangeType::kModified - : DocumentViewChangeType::kAdded; + ? DocumentViewChange::Type::kModified + : DocumentViewChange::Type::kAdded; TargetState& target_state = EnsureTargetState(target_id); target_state.AddDocumentChange(document.key, change_type); @@ -335,7 +344,7 @@ TargetState& target_state = EnsureTargetState(target_id); if (TargetContainsDocument(target_id, key)) { - target_state.AddDocumentChange(key, DocumentViewChangeType::kRemoved); + target_state.AddDocumentChange(key, DocumentViewChange::Type::kRemoved); } else { // The document may have entered and left the target before we raised a // snapshot, so we can just ignore the change. @@ -355,10 +364,10 @@ int WatchChangeAggregator::GetCurrentDocumentCountForTarget( TargetId target_id) { TargetState& target_state = EnsureTargetState(target_id); - FSTTargetChange* target_change = target_state.ToTargetChange(); - return ([target_metadata_provider_ remoteKeysForTarget:target_id].size() + - target_change.addedDocuments.size() - - target_change.removedDocuments.size()); + TargetChange target_change = target_state.ToTargetChange(); + return target_metadata_provider_->GetRemoteKeysForTarget(target_id).size() + + target_change.added_documents().size() - + target_change.removed_documents().size(); } void WatchChangeAggregator::RecordPendingTargetRequest(TargetId target_id) { @@ -381,7 +390,7 @@ return target_state != target_states_.end() && target_state->second.IsPending() ? nil - : [target_metadata_provider_ queryDataForTarget:target_id]; + : target_metadata_provider_->GetQueryDataForTarget(target_id); } void WatchChangeAggregator::ResetTarget(TargetId target_id) { @@ -396,7 +405,7 @@ // removals will be part of the initial snapshot if Watch does not resend // these documents. DocumentKeySet existingKeys = - [target_metadata_provider_ remoteKeysForTarget:target_id]; + target_metadata_provider_->GetRemoteKeysForTarget(target_id); for (const DocumentKey& key : existingKeys) { RemoveDocumentFromTarget(target_id, key, nil); @@ -406,7 +415,7 @@ bool WatchChangeAggregator::TargetContainsDocument(TargetId target_id, const DocumentKey& key) { const DocumentKeySet& existing_keys = - [target_metadata_provider_ remoteKeysForTarget:target_id]; + target_metadata_provider_->GetRemoteKeysForTarget(target_id); return existing_keys.contains(key); } diff --git a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h index c655063c4b0..e77b0627caa 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h +++ b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h @@ -39,7 +39,6 @@ #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" -#import "Firestore/Source/Remote/FSTStream.h" namespace firebase { namespace firestore { @@ -111,9 +110,9 @@ class WriteStreamSerializer { GCFSWriteRequest* CreateHandshake() const; GCFSWriteRequest* CreateWriteMutationsRequest( - NSArray* mutations) const; + const std::vector& mutations) const; GCFSWriteRequest* CreateEmptyMutationsList() { - return CreateWriteMutationsRequest(@[]); + return CreateWriteMutationsRequest({}); } static grpc::ByteBuffer ToByteBuffer(GCFSWriteRequest* request); @@ -125,7 +124,7 @@ class WriteStreamSerializer { GCFSWriteResponse* ParseResponse(const grpc::ByteBuffer& message, util::Status* out_status) const; model::SnapshotVersion ToCommitVersion(GCFSWriteResponse* proto) const; - NSArray* ToMutationResults( + std::vector ToMutationResults( GCFSWriteResponse* proto) const; /** Creates a pretty-printed description of the proto for debugging. */ @@ -147,7 +146,7 @@ class DatastoreSerializer { explicit DatastoreSerializer(const core::DatabaseInfo& database_info); GCFSCommitRequest* CreateCommitRequest( - NSArray* mutations) const; + const std::vector& mutations) const; static grpc::ByteBuffer ToByteBuffer(GCFSCommitRequest* request); GCFSBatchGetDocumentsRequest* CreateLookupRequest( @@ -158,7 +157,7 @@ class DatastoreSerializer { * Merges results of the streaming read together. The array is sorted by the * document key. */ - NSArray* MergeLookupResponses( + std::vector MergeLookupResponses( const std::vector& responses, util::Status* out_status) const; FSTMaybeDocument* ToMaybeDocument( @@ -172,39 +171,6 @@ class DatastoreSerializer { FSTSerializerBeta* serializer_; }; -/** A C++ bridge that invokes methods on an `FSTWatchStreamDelegate`. */ -class WatchStreamDelegate { - public: - explicit WatchStreamDelegate(id delegate) - : delegate_{delegate} { - } - - void NotifyDelegateOnOpen(); - void NotifyDelegateOnChange(const WatchChange& change, - const model::SnapshotVersion& snapshot_version); - void NotifyDelegateOnClose(const util::Status& status); - - private: - __weak id delegate_; -}; - -/** A C++ bridge that invokes methods on an `FSTWriteStreamDelegate`. */ -class WriteStreamDelegate { - public: - explicit WriteStreamDelegate(id delegate) - : delegate_{delegate} { - } - - void NotifyDelegateOnOpen(); - void NotifyDelegateOnHandshakeComplete(); - void NotifyDelegateOnCommit(const model::SnapshotVersion& commit_version, - NSArray* results); - void NotifyDelegateOnClose(const util::Status& status); - - private: - __weak id delegate_; -}; - } // namespace bridge } // namespace remote } // namespace firestore diff --git a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm index 8159fc96806..a3f3f65062e 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm +++ b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm @@ -179,10 +179,10 @@ bool IsLoggingEnabled() { } GCFSWriteRequest* WriteStreamSerializer::CreateWriteMutationsRequest( - NSArray* mutations) const { + const std::vector& mutations) const { NSMutableArray* protos = - [NSMutableArray arrayWithCapacity:mutations.count]; - for (FSTMutation* mutation in mutations) { + [NSMutableArray arrayWithCapacity:mutations.size()]; + for (FSTMutation* mutation : mutations) { [protos addObject:[serializer_ encodedMutation:mutation]]; }; @@ -208,16 +208,16 @@ bool IsLoggingEnabled() { return [serializer_ decodedVersion:proto.commitTime]; } -NSArray* WriteStreamSerializer::ToMutationResults( +std::vector WriteStreamSerializer::ToMutationResults( GCFSWriteResponse* response) const { NSMutableArray* responses = response.writeResultsArray; - NSMutableArray* results = - [NSMutableArray arrayWithCapacity:responses.count]; + std::vector results; + results.reserve(responses.count); const model::SnapshotVersion commitVersion = ToCommitVersion(response); for (GCFSWriteResult* proto in responses) { - [results addObject:[serializer_ decodedMutationResult:proto - commitVersion:commitVersion]]; + results.push_back([serializer_ decodedMutationResult:proto + commitVersion:commitVersion]); }; return results; } @@ -238,12 +238,12 @@ bool IsLoggingEnabled() { } GCFSCommitRequest* DatastoreSerializer::CreateCommitRequest( - NSArray* mutations) const { + const std::vector& mutations) const { GCFSCommitRequest* request = [GCFSCommitRequest message]; request.database = [serializer_ encodedDatabaseID]; NSMutableArray* mutationProtos = [NSMutableArray array]; - for (FSTMutation* mutation in mutations) { + for (FSTMutation* mutation : mutations) { [mutationProtos addObject:[serializer_ encodedMutation:mutation]]; } request.writesArray = mutationProtos; @@ -273,7 +273,7 @@ bool IsLoggingEnabled() { return ConvertToByteBuffer([request data]); } -NSArray* DatastoreSerializer::MergeLookupResponses( +std::vector DatastoreSerializer::MergeLookupResponses( const std::vector& responses, Status* out_status) const { // Sort by key. std::map results; @@ -281,17 +281,17 @@ bool IsLoggingEnabled() { for (const auto& response : responses) { auto* proto = ToProto(response, out_status); if (!out_status->ok()) { - return nil; + return {}; } FSTMaybeDocument* doc = [serializer_ decodedMaybeDocumentFromBatch:proto]; results[doc.key] = doc; } - NSMutableArray* docs = - [NSMutableArray arrayWithCapacity:results.size()]; + + std::vector docs; + docs.reserve(results.size()); for (const auto& kv : results) { - [docs addObject:kv.second]; + docs.push_back(kv.second); } - return docs; } @@ -300,42 +300,6 @@ bool IsLoggingEnabled() { return [serializer_ decodedMaybeDocumentFromBatch:response]; } -// WatchStreamDelegate - -void WatchStreamDelegate::NotifyDelegateOnOpen() { - [delegate_ watchStreamDidOpen]; -} - -void WatchStreamDelegate::NotifyDelegateOnChange( - const WatchChange& change, const SnapshotVersion& snapshot_version) { - [delegate_ watchStreamDidChange:change snapshotVersion:snapshot_version]; -} - -void WatchStreamDelegate::NotifyDelegateOnClose(const Status& status) { - [delegate_ watchStreamWasInterruptedWithError:status]; -} - -// WriteStreamDelegate - -void WriteStreamDelegate::NotifyDelegateOnOpen() { - [delegate_ writeStreamDidOpen]; -} - -void WriteStreamDelegate::NotifyDelegateOnHandshakeComplete() { - [delegate_ writeStreamDidCompleteHandshake]; -} - -void WriteStreamDelegate::NotifyDelegateOnCommit( - const SnapshotVersion& commit_version, - NSArray* results) { - [delegate_ writeStreamDidReceiveResponseWithVersion:commit_version - mutationResults:results]; -} - -void WriteStreamDelegate::NotifyDelegateOnClose(const Status& status) { - [delegate_ writeStreamWasInterruptedWithError:status]; -} - } // namespace bridge } // namespace remote } // namespace firestore diff --git a/Firestore/core/src/firebase/firestore/remote/remote_store.h b/Firestore/core/src/firebase/firestore/remote/remote_store.h new file mode 100644 index 00000000000..6489f8891dd --- /dev/null +++ b/Firestore/core/src/firebase/firestore/remote/remote_store.h @@ -0,0 +1,306 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_REMOTE_STORE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_REMOTE_STORE_H_ + +#if !defined(__OBJC__) +#error "This header only supports Objective-C++" +#endif // !defined(__OBJC__) + +#import + +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/core/transaction.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/remote/datastore.h" +#include "Firestore/core/src/firebase/firestore/remote/online_state_tracker.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" +#include "Firestore/core/src/firebase/firestore/remote/watch_change.h" +#include "Firestore/core/src/firebase/firestore/remote/watch_stream.h" +#include "Firestore/core/src/firebase/firestore/remote/write_stream.h" +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" + +@class FSTLocalStore; +@class FSTMutationBatch; +@class FSTMutationBatchResult; +@class FSTQueryData; +@class FSTTransaction; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A protocol that describes the actions the `RemoteStore` needs to perform on + * a cooperating synchronization engine. + */ +@protocol FSTRemoteSyncer + +/** + * Applies one remote event to the sync engine, notifying any views of the + * changes, and releasing any pending mutation batches that would become visible + * because of the snapshot version the remote event contains. + */ +- (void)applyRemoteEvent: + (const firebase::firestore::remote::RemoteEvent&)remoteEvent; + +/** + * Rejects the listen for the given targetID. This can be triggered by the + * backend for any active target. + * + * @param targetID The targetID corresponding to a listen initiated via + * `RemoteStore::Listen`. + * @param error A description of the condition that has forced the rejection. + * Nearly always this will be an indication that the user is no longer + * authorized to see the data matching the target. + */ +- (void)rejectListenWithTargetID: + (const firebase::firestore::model::TargetId)targetID + error: + (NSError*)error; // NOLINT(readability/casting) + +/** + * Applies the result of a successful write of a mutation batch to the sync + * engine, emitting snapshots in any views that the mutation applies to, and + * removing the batch from the mutation queue. + */ +- (void)applySuccessfulWriteWithResult: + (FSTMutationBatchResult*)batchResult; // NOLINT(readability/casting) + +/** + * Rejects the batch, removing the batch from the mutation queue, recomputing + * the local view of any documents affected by the batch and then, emitting + * snapshots with the reverted value. + */ +- (void) + rejectFailedWriteWithBatchID:(firebase::firestore::model::BatchId)batchID + error: + (NSError*)error; // NOLINT(readability/casting) + +/** + * Returns the set of remote document keys for the given target ID. This list + * includes the documents that were assigned to the target when we received the + * last snapshot. + */ +- (firebase::firestore::model::DocumentKeySet)remoteKeysForTarget: + (firebase::firestore::model::TargetId)targetId; + +@end + +namespace firebase { +namespace firestore { +namespace remote { + +class RemoteStore : public TargetMetadataProvider, + public WatchStreamCallback, + public WriteStreamCallback { + public: + RemoteStore(FSTLocalStore* local_store, + std::shared_ptr datastore, + util::AsyncQueue* worker_queue, + std::function online_state_handler); + + void set_sync_engine(id sync_engine) { + sync_engine_ = sync_engine; + } + + /** + * Starts up the remote store, creating streams, restoring state from + * `FSTLocalStore`, etc. + */ + void Start(); + + /** + * Shuts down the remote store, tearing down connections and otherwise + * cleaning up. + */ + void Shutdown(); + + /** + * Temporarily disables the network. The network can be re-enabled using + * 'EnableNetwork'. + */ + void DisableNetwork(); + + /** + * Re-enables the network. Only to be called as the counterpart to + * 'DisableNetwork'. + */ + void EnableNetwork(); + + /** + * Tells the `RemoteStore` that the currently authenticated user has changed. + * + * In response the remote store tears down streams and clears up any tracked + * operations that should not persist across users. Restarts the streams if + * appropriate. + */ + void HandleCredentialChange(); + + /** Listens to the target identified by the given `FSTQueryData`. */ + void Listen(FSTQueryData* query_data); + + /** Stops listening to the target with the given target ID. */ + void StopListening(model::TargetId target_id); + + /** + * Attempts to fill our write pipeline with writes from the `FSTLocalStore`. + * + * Called internally to bootstrap or refill the write pipeline and by + * `FSTSyncEngine` whenever there are new mutations to process. + * + * Starts the write stream if necessary. + */ + void FillWritePipeline(); + + /** + * Queues additional writes to be sent to the write stream, sending them + * immediately if the write stream is established. + */ + void AddToWritePipeline(FSTMutationBatch* batch); + + /** Returns a new transaction backed by this remote store. */ + // TODO(c++14): return a plain value when it becomes possible to move + // `Transaction` into lambdas. + std::shared_ptr CreateTransaction(); + + model::DocumentKeySet GetRemoteKeysForTarget( + model::TargetId target_id) const override; + FSTQueryData* GetQueryDataForTarget(model::TargetId target_id) const override; + + void OnWatchStreamOpen() override; + void OnWatchStreamChange( + const WatchChange& change, + const model::SnapshotVersion& snapshot_version) override; + void OnWatchStreamClose(const util::Status& status) override; + + void OnWriteStreamOpen() override; + void OnWriteStreamHandshakeComplete() override; + void OnWriteStreamClose(const util::Status& status) override; + void OnWriteStreamMutationResult( + model::SnapshotVersion commit_version, + std::vector mutation_results) override; + + private: + void DisableNetworkInternal(); + + void SendWatchRequest(FSTQueryData* query_data); + void SendUnwatchRequest(model::TargetId target_id); + + /** + * Takes a batch of changes from the `Datastore`, repackages them as a + * `RemoteEvent`, and passes that on to the `SyncEngine`. + */ + void RaiseWatchSnapshot(const model::SnapshotVersion& snapshot_version); + + /** Process a target error and passes the error along to `SyncEngine`. */ + void ProcessTargetError(const WatchTargetChange& change); + + /** + * Returns true if we can add to the write pipeline (i.e. it is not full and + * the network is enabled). + */ + bool CanAddToWritePipeline() const; + + void StartWriteStream(); + + /** + * Returns true if the network is enabled, the write stream has not yet been + * started and there are pending writes. + */ + bool ShouldStartWriteStream() const; + + void HandleHandshakeError(const util::Status& status); + void HandleWriteError(const util::Status& status); + + bool CanUseNetwork() const; + + void StartWatchStream(); + + /** + * Returns true if the network is enabled, the watch stream has not yet been + * started and there are active watch targets. + */ + bool ShouldStartWatchStream() const; + + void CleanUpWatchStreamState(); + + id sync_engine_ = nil; + + /** + * The local store, used to fill the write pipeline with outbound mutations + * and resolve existence filter mismatches. + */ + FSTLocalStore* local_store_ = nil; + + /** The client-side proxy for interacting with the backend. */ + std::shared_ptr datastore_; + + /** + * A mapping of watched targets that the client cares about tracking and the + * user has explicitly called a 'listen' for this target. + * + * These targets may or may not have been sent to or acknowledged by the + * server. On re-establishing the listen stream, these targets should be sent + * to the server. The targets removed with unlistens are removed eagerly + * without waiting for confirmation from the listen stream. + */ + std::unordered_map listen_targets_; + + OnlineStateTracker online_state_tracker_; + + /** + * Set to true by `EnableNetwork` and false by `DisableNetwork` and indicates + * the user-preferred network state. + */ + bool is_network_enabled_ = false; + + std::shared_ptr watch_stream_; + std::shared_ptr write_stream_; + std::unique_ptr watch_change_aggregator_; + + /** + * A list of up to `kMaxPendingWrites` writes that we have fetched from the + * `LocalStore` via `FillWritePipeline` and have or will send to the write + * stream. + * + * Whenever `write_pipeline_` is not empty, the `RemoteStore` will attempt to + * start or restart the write stream. When the stream is established, the + * writes in the pipeline will be sent in order. + * + * Writes remain in `write_pipeline_` until they are acknowledged by the + * backend and thus will automatically be re-sent if the stream is interrupted + * / restarted before they're acknowledged. + * + * Write responses from the backend are linked to their originating request + * purely based on order, and so we can just remove writes from the front of + * the `write_pipeline_` as we receive responses. + */ + std::vector write_pipeline_; +}; + +} // namespace remote +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_REMOTE_STORE_H_ diff --git a/Firestore/core/src/firebase/firestore/remote/remote_store.mm b/Firestore/core/src/firebase/firestore/remote/remote_store.mm new file mode 100644 index 00000000000..ce46f27d573 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/remote/remote_store.mm @@ -0,0 +1,552 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/remote/remote_store.h" + +#include + +#import "Firestore/Source/Local/FSTLocalStore.h" +#import "Firestore/Source/Local/FSTQueryData.h" +#import "Firestore/Source/Model/FSTMutationBatch.h" + +#include "Firestore/core/src/firebase/firestore/core/transaction.h" +#include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" +#include "Firestore/core/src/firebase/firestore/util/error_apple.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/log.h" +#include "absl/memory/memory.h" + +using firebase::firestore::core::Transaction; +using firebase::firestore::model::BatchId; +using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::OnlineState; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::TargetId; +using firebase::firestore::model::kBatchIdUnknown; +using firebase::firestore::remote::Datastore; +using firebase::firestore::remote::WatchStream; +using firebase::firestore::remote::DocumentWatchChange; +using firebase::firestore::remote::ExistenceFilterWatchChange; +using firebase::firestore::remote::OnlineStateTracker; +using firebase::firestore::remote::RemoteEvent; +using firebase::firestore::remote::TargetChange; +using firebase::firestore::remote::WatchChange; +using firebase::firestore::remote::WatchChangeAggregator; +using firebase::firestore::remote::WatchTargetChange; +using firebase::firestore::remote::WatchTargetChangeState; +using firebase::firestore::util::AsyncQueue; +using firebase::firestore::util::Status; + +namespace firebase { +namespace firestore { +namespace remote { + +/** + * The maximum number of pending writes to allow. + * TODO(b/35853402): Negotiate this value with the backend. + */ +constexpr int kMaxPendingWrites = 10; + +RemoteStore::RemoteStore( + FSTLocalStore* local_store, + std::shared_ptr datastore, + AsyncQueue* worker_queue, + std::function online_state_handler) + : local_store_{local_store}, + datastore_{std::move(datastore)}, + online_state_tracker_{worker_queue, std::move(online_state_handler)} { + datastore_->Start(); + + // Create streams (but note they're not started yet) + watch_stream_ = datastore_->CreateWatchStream(this); + write_stream_ = datastore_->CreateWriteStream(this); +} + +void RemoteStore::Start() { + // For now, all setup is handled by `EnableNetwork`. We might expand on this + // in the future. + EnableNetwork(); +} + +void RemoteStore::EnableNetwork() { + is_network_enabled_ = true; + + if (CanUseNetwork()) { + // Load any saved stream token from persistent storage + write_stream_->SetLastStreamToken([local_store_ lastStreamToken]); + + if (ShouldStartWatchStream()) { + StartWatchStream(); + } else { + online_state_tracker_.UpdateState(OnlineState::Unknown); + } + + // This will start the write stream if necessary. + FillWritePipeline(); + } +} + +void RemoteStore::DisableNetwork() { + is_network_enabled_ = false; + DisableNetworkInternal(); + + // Set the OnlineState to Offline so get()s return from cache, etc. + online_state_tracker_.UpdateState(OnlineState::Offline); +} + +void RemoteStore::DisableNetworkInternal() { + watch_stream_->Stop(); + write_stream_->Stop(); + + if (!write_pipeline_.empty()) { + LOG_DEBUG("Stopping write stream with %s pending writes", + write_pipeline_.size()); + write_pipeline_.clear(); + } + + CleanUpWatchStreamState(); +} + +void RemoteStore::Shutdown() { + LOG_DEBUG("RemoteStore %s shutting down", this); + is_network_enabled_ = false; + DisableNetworkInternal(); + + // Set the `OnlineState` to `Unknown` (rather than `Offline`) to avoid + // potentially triggering spurious listener events with cached data, etc. + online_state_tracker_.UpdateState(OnlineState::Unknown); + + datastore_->Shutdown(); +} + +// Watch Stream + +void RemoteStore::Listen(FSTQueryData* query_data) { + TargetId targetKey = query_data.targetID; + HARD_ASSERT(listen_targets_.find(targetKey) == listen_targets_.end(), + "Listen called with duplicate target id: %s", targetKey); + + // Mark this as something the client is currently listening for. + listen_targets_[targetKey] = query_data; + + if (ShouldStartWatchStream()) { + // The listen will be sent in `OnWatchStreamOpen` + StartWatchStream(); + } else if (watch_stream_->IsOpen()) { + SendWatchRequest(query_data); + } +} + +void RemoteStore::StopListening(TargetId target_id) { + size_t num_erased = listen_targets_.erase(target_id); + HARD_ASSERT(num_erased == 1, + "StopListening: target not currently watched: %s", target_id); + + // The watch stream might not be started if we're in a disconnected state + if (watch_stream_->IsOpen()) { + SendUnwatchRequest(target_id); + } + if (listen_targets_.empty()) { + if (watch_stream_->IsOpen()) { + watch_stream_->MarkIdle(); + } else if (CanUseNetwork()) { + // Revert to `OnlineState::Unknown` if the watch stream is not open and we + // have no listeners, since without any listens to send we cannot confirm + // if the stream is healthy and upgrade to `OnlineState::Online`. + online_state_tracker_.UpdateState(OnlineState::Unknown); + } + } +} + +void RemoteStore::SendWatchRequest(FSTQueryData* query_data) { + // We need to increment the the expected number of pending responses we're due + // from watch so we wait for the ack to process any messages from this target. + watch_change_aggregator_->RecordPendingTargetRequest(query_data.targetID); + watch_stream_->WatchQuery(query_data); +} + +void RemoteStore::SendUnwatchRequest(TargetId target_id) { + // We need to increment the expected number of pending responses we're due + // from watch so we wait for the removal on the server before we process any + // messages from this target. + watch_change_aggregator_->RecordPendingTargetRequest(target_id); + watch_stream_->UnwatchTargetId(target_id); +} + +bool RemoteStore::ShouldStartWatchStream() const { + return CanUseNetwork() && !watch_stream_->IsStarted() && + !listen_targets_.empty(); +} + +void RemoteStore::StartWatchStream() { + HARD_ASSERT(ShouldStartWatchStream(), + "StartWatchStream called when ShouldStartWatchStream is false."); + watch_change_aggregator_ = absl::make_unique(this); + watch_stream_->Start(); + + online_state_tracker_.HandleWatchStreamStart(); +} + +void RemoteStore::CleanUpWatchStreamState() { + watch_change_aggregator_.reset(); +} + +void RemoteStore::OnWatchStreamOpen() { + // Restore any existing watches. + for (const auto& kv : listen_targets_) { + SendWatchRequest(kv.second); + } +} + +void RemoteStore::OnWatchStreamClose(const Status& status) { + if (status.ok()) { + // Graceful stop (due to Stop() or idle timeout). Make sure that's + // desirable. + HARD_ASSERT(!ShouldStartWatchStream(), + "Watch stream was stopped gracefully while still needed."); + } + + CleanUpWatchStreamState(); + + // If we still need the watch stream, retry the connection. + if (ShouldStartWatchStream()) { + online_state_tracker_.HandleWatchStreamFailure(status); + + StartWatchStream(); + } else { + // We don't need to restart the watch stream because there are no active + // targets. The online state is set to unknown because there is no active + // attempt at establishing a connection. + online_state_tracker_.UpdateState(OnlineState::Unknown); + } +} + +void RemoteStore::OnWatchStreamChange(const WatchChange& change, + const SnapshotVersion& snapshot_version) { + // Mark the connection as Online because we got a message from the server. + online_state_tracker_.UpdateState(OnlineState::Online); + + if (change.type() == WatchChange::Type::TargetChange) { + const WatchTargetChange& watch_target_change = + static_cast(change); + if (watch_target_change.state() == WatchTargetChangeState::Removed && + !watch_target_change.cause().ok()) { + // There was an error on a target, don't wait for a consistent snapshot to + // raise events + return ProcessTargetError(watch_target_change); + } else { + watch_change_aggregator_->HandleTargetChange(watch_target_change); + } + } else if (change.type() == WatchChange::Type::Document) { + watch_change_aggregator_->HandleDocumentChange( + static_cast(change)); + } else { + HARD_ASSERT( + change.type() == WatchChange::Type::ExistenceFilter, + "Expected watchChange to be an instance of ExistenceFilterWatchChange"); + watch_change_aggregator_->HandleExistenceFilter( + static_cast(change)); + } + + if (snapshot_version != SnapshotVersion::None() && + snapshot_version >= [local_store_ lastRemoteSnapshotVersion]) { + // We have received a target change with a global snapshot if the snapshot + // version is not equal to `SnapshotVersion::None()`. + RaiseWatchSnapshot(snapshot_version); + } +} + +void RemoteStore::RaiseWatchSnapshot(const SnapshotVersion& snapshot_version) { + HARD_ASSERT(snapshot_version != SnapshotVersion::None(), + "Can't raise event for unknown SnapshotVersion"); + + RemoteEvent remote_event = + watch_change_aggregator_->CreateRemoteEvent(snapshot_version); + + // Update in-memory resume tokens. `FSTLocalStore` will update the persistent + // view of these when applying the completed `RemoteEvent`. + for (const auto& entry : remote_event.target_changes()) { + const TargetChange& target_change = entry.second; + NSData* resumeToken = target_change.resume_token(); + + if (resumeToken.length > 0) { + TargetId target_id = entry.first; + auto found = listen_targets_.find(target_id); + FSTQueryData* query_data = + found != listen_targets_.end() ? found->second : nil; + + // A watched target might have been removed already. + if (query_data) { + listen_targets_[target_id] = [query_data + queryDataByReplacingSnapshotVersion:snapshot_version + resumeToken:resumeToken + sequenceNumber:query_data.sequenceNumber]; + } + } + } + + // Re-establish listens for the targets that have been invalidated by + // existence filter mismatches. + for (TargetId target_id : remote_event.target_mismatches()) { + auto found = listen_targets_.find(target_id); + if (found == listen_targets_.end()) { + // A watched target might have been removed already. + continue; + } + FSTQueryData* query_data = found->second; + + // Clear the resume token for the query, since we're in a known mismatch + // state. + query_data = [[FSTQueryData alloc] initWithQuery:query_data.query + targetID:target_id + listenSequenceNumber:query_data.sequenceNumber + purpose:query_data.purpose]; + listen_targets_[target_id] = query_data; + + // Cause a hard reset by unwatching and rewatching immediately, but + // deliberately don't send a resume token so that we get a full update. + SendUnwatchRequest(target_id); + + // Mark the query we send as being on behalf of an existence filter + // mismatch, but don't actually retain that in listen_targets_. This ensures + // that we flag the first re-listen this way without impacting future + // listens of this target (that might happen e.g. on reconnect). + FSTQueryData* request_query_data = [[FSTQueryData alloc] + initWithQuery:query_data.query + targetID:target_id + listenSequenceNumber:query_data.sequenceNumber + purpose:FSTQueryPurposeExistenceFilterMismatch]; + SendWatchRequest(request_query_data); + } + + // Finally handle remote event + [sync_engine_ applyRemoteEvent:remote_event]; +} + +void RemoteStore::ProcessTargetError(const WatchTargetChange& change) { + HARD_ASSERT(!change.cause().ok(), "Handling target error without a cause"); + + // Ignore targets that have been removed already. + for (TargetId target_id : change.target_ids()) { + auto found = listen_targets_.find(target_id); + if (found != listen_targets_.end()) { + listen_targets_.erase(found); + watch_change_aggregator_->RemoveTarget(target_id); + [sync_engine_ rejectListenWithTargetID:target_id + error:util::MakeNSError(change.cause())]; + } + } +} + +// Write Stream + +void RemoteStore::FillWritePipeline() { + BatchId last_batch_id_retrieved = write_pipeline_.empty() + ? kBatchIdUnknown + : write_pipeline_.back().batchID; + while (CanAddToWritePipeline()) { + FSTMutationBatch* batch = + [local_store_ nextMutationBatchAfterBatchID:last_batch_id_retrieved]; + if (!batch) { + if (write_pipeline_.empty()) { + write_stream_->MarkIdle(); + } + break; + } + AddToWritePipeline(batch); + last_batch_id_retrieved = batch.batchID; + } + + if (ShouldStartWriteStream()) { + StartWriteStream(); + } +} + +bool RemoteStore::CanAddToWritePipeline() const { + return CanUseNetwork() && write_pipeline_.size() < kMaxPendingWrites; +} + +void RemoteStore::AddToWritePipeline(FSTMutationBatch* batch) { + HARD_ASSERT(CanAddToWritePipeline(), + "AddToWritePipeline called when pipeline is full"); + + write_pipeline_.push_back(batch); + + if (write_stream_->IsOpen() && write_stream_->handshake_complete()) { + write_stream_->WriteMutations(batch.mutations); + } +} + +bool RemoteStore::ShouldStartWriteStream() const { + return CanUseNetwork() && !write_stream_->IsStarted() && + !write_pipeline_.empty(); +} + +void RemoteStore::StartWriteStream() { + HARD_ASSERT(ShouldStartWriteStream(), "StartWriteStream called when " + "ShouldStartWriteStream is false."); + write_stream_->Start(); +} + +void RemoteStore::OnWriteStreamOpen() { + write_stream_->WriteHandshake(); +} + +void RemoteStore::OnWriteStreamHandshakeComplete() { + // Record the stream token. + [local_store_ setLastStreamToken:write_stream_->GetLastStreamToken()]; + + // Send the write pipeline now that the stream is established. + for (FSTMutationBatch* write : write_pipeline_) { + write_stream_->WriteMutations(write.mutations); + } +} + +void RemoteStore::OnWriteStreamMutationResult( + SnapshotVersion commit_version, + std::vector mutation_results) { + // This is a response to a write containing mutations and should be correlated + // to the first write in our write pipeline. + HARD_ASSERT(!write_pipeline_.empty(), "Got result for empty write pipeline"); + + FSTMutationBatch* batch = write_pipeline_.front(); + write_pipeline_.erase(write_pipeline_.begin()); + + FSTMutationBatchResult* batchResult = [FSTMutationBatchResult + resultWithBatch:batch + commitVersion:commit_version + mutationResults:std::move(mutation_results) + streamToken:write_stream_->GetLastStreamToken()]; + [sync_engine_ applySuccessfulWriteWithResult:batchResult]; + + // It's possible that with the completion of this mutation another slot has + // freed up. + FillWritePipeline(); +} + +void RemoteStore::OnWriteStreamClose(const Status& status) { + if (status.ok()) { + // Graceful stop (due to Stop() or idle timeout). Make sure that's + // desirable. + HARD_ASSERT(!ShouldStartWriteStream(), + "Write stream was stopped gracefully while still needed."); + } + + // If the write stream closed due to an error, invoke the error callbacks if + // there are pending writes. + if (!status.ok() && !write_pipeline_.empty()) { + // TODO(varconst): handle UNAUTHENTICATED status, see + // go/firestore-client-errors + if (write_stream_->handshake_complete()) { + // This error affects the actual writes. + HandleWriteError(status); + } else { + // If there was an error before the handshake finished, it's possible that + // the server is unable to process the stream token we're sending. + // (Perhaps it's too old?) + HandleHandshakeError(status); + } + } + + // The write stream might have been started by refilling the write pipeline + // for failed writes + if (ShouldStartWriteStream()) { + StartWriteStream(); + } +} + +void RemoteStore::HandleHandshakeError(const Status& status) { + HARD_ASSERT(!status.ok(), "Handling write error with status OK."); + + // Reset the token if it's a permanent error, signaling the write stream is + // no longer valid. Note that the handshake does not count as a write: see + // comments on `Datastore::IsPermanentWriteError` for details. + if (Datastore::IsPermanentError(status)) { + NSString* token = + [write_stream_->GetLastStreamToken() base64EncodedStringWithOptions:0]; + LOG_DEBUG("RemoteStore %s error before completed handshake; resetting " + "stream token %s: " + "error code: '%s', details: '%s'", + this, token, status.code(), status.error_message()); + write_stream_->SetLastStreamToken(nil); + [local_store_ setLastStreamToken:nil]; + } else { + // Some other error, don't reset stream token. Our stream logic will just + // retry with exponential backoff. + } +} + +void RemoteStore::HandleWriteError(const Status& status) { + HARD_ASSERT(!status.ok(), "Handling write error with status OK."); + + // Only handle permanent errors here. If it's transient, just let the retry + // logic kick in. + if (!Datastore::IsPermanentWriteError(status)) { + return; + } + + // If this was a permanent error, the request itself was the problem so it's + // not going to succeed if we resend it. + FSTMutationBatch* batch = write_pipeline_.front(); + write_pipeline_.erase(write_pipeline_.begin()); + + // In this case it's also unlikely that the server itself is melting + // down--this was just a bad request so inhibit backoff on the next restart. + write_stream_->InhibitBackoff(); + + [sync_engine_ rejectFailedWriteWithBatchID:batch.batchID + error:util::MakeNSError(status)]; + + // It's possible that with the completion of this mutation another slot has + // freed up. + FillWritePipeline(); +} + +bool RemoteStore::CanUseNetwork() const { + // PORTING NOTE: This method exists mostly because web also has to take into + // account primary vs. secondary state. + return is_network_enabled_; +} + +std::shared_ptr RemoteStore::CreateTransaction() { + return std::make_shared(datastore_.get()); +} + +DocumentKeySet RemoteStore::GetRemoteKeysForTarget(TargetId target_id) const { + return [sync_engine_ remoteKeysForTarget:target_id]; +} + +FSTQueryData* RemoteStore::GetQueryDataForTarget(TargetId target_id) const { + auto found = listen_targets_.find(target_id); + return found != listen_targets_.end() ? found->second : nil; +} + +void RemoteStore::HandleCredentialChange() { + if (CanUseNetwork()) { + // Tear down and re-create our network streams. This will ensure we get a + // fresh auth token for the new user and re-fill the write pipeline with new + // mutations from the `FSTLocalStore` (since mutations are per-user). + LOG_DEBUG("RemoteStore %s restarting streams for new credential", this); + is_network_enabled_ = false; + DisableNetworkInternal(); + online_state_tracker_.UpdateState(OnlineState::Unknown); + EnableNetwork(); + } +} + +} // namespace remote +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/remote/serializer.cc b/Firestore/core/src/firebase/firestore/remote/serializer.cc index 335f7a04642..cd0b9a15c4f 100644 --- a/Firestore/core/src/firebase/firestore/remote/serializer.cc +++ b/Firestore/core/src/firebase/firestore/remote/serializer.cc @@ -32,8 +32,10 @@ #include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/firebase/firestore/model/document.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" +#include "Firestore/core/src/firebase/firestore/model/field_value.h" #include "Firestore/core/src/firebase/firestore/model/no_document.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h" #include "Firestore/core/src/firebase/firestore/nanopb/reader.h" #include "Firestore/core/src/firebase/firestore/nanopb/writer.h" #include "Firestore/core/src/firebase/firestore/timestamp_internal.h" @@ -66,13 +68,14 @@ using firebase::firestore::model::Precondition; using firebase::firestore::model::ResourcePath; using firebase::firestore::model::SetMutation; using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::nanopb::CheckedSize; using firebase::firestore::nanopb::Reader; using firebase::firestore::nanopb::Writer; using firebase::firestore::util::Status; using firebase::firestore::util::StringFormat; pb_bytes_array_t* Serializer::EncodeString(const std::string& str) { - auto size = static_cast(str.size()); + pb_size_t size = CheckedSize(str.size()); auto result = static_cast(malloc(PB_BYTES_ARRAY_T_ALLOCSIZE(size))); result->size = size; @@ -82,11 +85,12 @@ pb_bytes_array_t* Serializer::EncodeString(const std::string& str) { std::string Serializer::DecodeString(const pb_bytes_array_t* str) { if (str == nullptr) return ""; - return std::string{reinterpret_cast(str->bytes), str->size}; + size_t size = static_cast(str->size); + return std::string{reinterpret_cast(str->bytes), size}; } pb_bytes_array_t* Serializer::EncodeBytes(const std::vector& bytes) { - auto size = static_cast(bytes.size()); + pb_size_t size = CheckedSize(bytes.size()); auto result = static_cast(malloc(PB_BYTES_ARRAY_T_ALLOCSIZE(size))); result->size = size; @@ -101,8 +105,8 @@ std::vector Serializer::DecodeBytes(const pb_bytes_array_t* bytes) { namespace { -ObjectValue::Map DecodeMapValue(Reader* reader, - const google_firestore_v1_MapValue& map_value); +FieldValue::Map DecodeMapValue(Reader* reader, + const google_firestore_v1_MapValue& map_value); // There's no f:f::model equivalent of StructuredQuery, so we'll create our // own struct for decoding. We could use nanopb's struct, but it's slightly @@ -119,7 +123,7 @@ struct StructuredQuery { // TODO(rsgowman): other fields }; -ObjectValue::Map::value_type DecodeFieldsEntry( +FieldValue::Map::value_type DecodeFieldsEntry( Reader* reader, const google_firestore_v1_Document_FieldsEntry& fields) { std::string key = Serializer::DecodeString(fields.key); FieldValue value = Serializer::DecodeFieldValue(reader, fields.value); @@ -130,32 +134,32 @@ ObjectValue::Map::value_type DecodeFieldsEntry( return {}; } - return ObjectValue::Map::value_type{std::move(key), std::move(value)}; + return FieldValue::Map::value_type{std::move(key), std::move(value)}; } -ObjectValue::Map DecodeFields( +FieldValue::Map DecodeFields( Reader* reader, size_t count, const google_firestore_v1_Document_FieldsEntry* fields) { - ObjectValue::Map result; + FieldValue::Map result; for (size_t i = 0; i < count; i++) { - result.emplace(DecodeFieldsEntry(reader, fields[i])); + FieldValue::Map::value_type kv = DecodeFieldsEntry(reader, fields[i]); + result = result.insert(std::move(kv.first), std::move(kv.second)); } return result; } -google_firestore_v1_MapValue EncodeMapValue( - const ObjectValue::Map& object_value_map) { +google_firestore_v1_MapValue EncodeMapValue(const ObjectValue& object_value) { google_firestore_v1_MapValue result{}; - size_t count = object_value_map.size(); + pb_size_t count = CheckedSize(object_value.GetInternalValue().size()); - result.fields_count = static_cast(count); + result.fields_count = count; result.fields = MakeArray(count); int i = 0; - for (const auto& kv : object_value_map) { + for (const auto& kv : object_value.GetInternalValue()) { result.fields[i].key = Serializer::EncodeString(kv.first); result.fields[i].value = Serializer::EncodeFieldValue(kv.second); i++; @@ -164,16 +168,16 @@ google_firestore_v1_MapValue EncodeMapValue( return result; } -ObjectValue::Map DecodeMapValue(Reader* reader, - const google_firestore_v1_MapValue& map_value) { - ObjectValue::Map result; +FieldValue::Map DecodeMapValue(Reader* reader, + const google_firestore_v1_MapValue& map_value) { + FieldValue::Map result; for (size_t i = 0; i < map_value.fields_count; i++) { std::string key = Serializer::DecodeString(map_value.fields[i].key); FieldValue value = Serializer::DecodeFieldValue(reader, map_value.fields[i].value); - result[key] = value; + result = result.insert(key, value); } return result; @@ -285,39 +289,62 @@ google_firestore_v1_Value Serializer::EncodeFieldValue( case FieldValue::Type::Null: result.which_value_type = google_firestore_v1_Value_null_value_tag; result.null_value = google_protobuf_NullValue_NULL_VALUE; - break; + return result; case FieldValue::Type::Boolean: result.which_value_type = google_firestore_v1_Value_boolean_value_tag; result.boolean_value = field_value.boolean_value(); - break; + return result; case FieldValue::Type::Integer: result.which_value_type = google_firestore_v1_Value_integer_value_tag; result.integer_value = field_value.integer_value(); - break; + return result; - case FieldValue::Type::String: - result.which_value_type = google_firestore_v1_Value_string_value_tag; - result.string_value = EncodeString(field_value.string_value()); - break; + case FieldValue::Type::Double: + result.which_value_type = google_firestore_v1_Value_double_value_tag; + result.double_value = field_value.double_value(); + return result; case FieldValue::Type::Timestamp: result.which_value_type = google_firestore_v1_Value_timestamp_value_tag; result.timestamp_value = EncodeTimestamp(field_value.timestamp_value()); - break; + return result; - case FieldValue::Type::Object: - result.which_value_type = google_firestore_v1_Value_map_value_tag; - result.map_value = - EncodeMapValue(field_value.object_value().internal_value); - break; + case FieldValue::Type::ServerTimestamp: + // TODO(rsgowman): Implement + abort(); - default: - // TODO(rsgowman): implement the other types + case FieldValue::Type::String: + result.which_value_type = google_firestore_v1_Value_string_value_tag; + result.string_value = EncodeString(field_value.string_value()); + return result; + + case FieldValue::Type::Blob: + result.which_value_type = google_firestore_v1_Value_bytes_value_tag; + result.bytes_value = EncodeBytes(field_value.blob_value()); + return result; + + case FieldValue::Type::Reference: + // TODO(rsgowman): Implement abort(); + + case FieldValue::Type::GeoPoint: + result.which_value_type = google_firestore_v1_Value_geo_point_value_tag; + result.geo_point_value = EncodeGeoPoint(field_value.geo_point_value()); + return result; + + case FieldValue::Type::Array: + result.which_value_type = google_firestore_v1_Value_array_value_tag; + result.array_value = EncodeArray(field_value.array_value()); + return result; + + case FieldValue::Type::Object: + result.which_value_type = google_firestore_v1_Value_map_value_tag; + result.map_value = EncodeMapValue(ObjectValue(field_value)); + return result; } - return result; + UNREACHABLE(); } FieldValue Serializer::DecodeFieldValue(Reader* reader, @@ -342,27 +369,38 @@ FieldValue Serializer::DecodeFieldValue(Reader* reader, case google_firestore_v1_Value_integer_value_tag: return FieldValue::FromInteger(msg.integer_value); - case google_firestore_v1_Value_string_value_tag: - return FieldValue::FromString(DecodeString(msg.string_value)); + case google_firestore_v1_Value_double_value_tag: + return FieldValue::FromDouble(msg.double_value); case google_firestore_v1_Value_timestamp_value_tag: { return FieldValue::FromTimestamp( DecodeTimestamp(reader, msg.timestamp_value)); } - case google_firestore_v1_Value_map_value_tag: { - return FieldValue::FromMap(DecodeMapValue(reader, msg.map_value)); + case google_firestore_v1_Value_string_value_tag: + return FieldValue::FromString(DecodeString(msg.string_value)); + + case google_firestore_v1_Value_bytes_value_tag: { + std::vector bytes = DecodeBytes(msg.bytes_value); + return FieldValue::FromBlob(bytes.data(), bytes.size()); } - case google_firestore_v1_Value_double_value_tag: - case google_firestore_v1_Value_bytes_value_tag: case google_firestore_v1_Value_reference_value_tag: - case google_firestore_v1_Value_geo_point_value_tag: - case google_firestore_v1_Value_array_value_tag: // TODO(b/74243929): Implement remaining types. HARD_FAIL("Unhandled message field number (tag): %i.", msg.which_value_type); + case google_firestore_v1_Value_geo_point_value_tag: + return FieldValue::FromGeoPoint( + DecodeGeoPoint(reader, msg.geo_point_value)); + + case google_firestore_v1_Value_array_value_tag: + return FieldValue::FromArray(DecodeArray(reader, msg.array_value)); + + case google_firestore_v1_Value_map_value_tag: { + return FieldValue::FromMap(DecodeMapValue(reader, msg.map_value)); + } + default: reader->Fail(StringFormat("Invalid type while decoding FieldValue: %s", msg.which_value_type)); @@ -415,11 +453,11 @@ google_firestore_v1_Document Serializer::EncodeDocument( result.name = EncodeString(EncodeKey(key)); // Encode Document.fields (unless it's empty) - size_t count = object_value.internal_value.size(); - result.fields_count = static_cast(count); + pb_size_t count = CheckedSize(object_value.GetInternalValue().size()); + result.fields_count = count; result.fields = MakeArray(count); int i = 0; - for (const auto& kv : object_value.internal_value) { + for (const auto& kv : object_value.GetInternalValue()) { result.fields[i].key = EncodeString(kv.first); result.fields[i].value = EncodeFieldValue(kv.second); i++; @@ -456,7 +494,7 @@ std::unique_ptr Serializer::DecodeFoundDocument( "Tried to deserialize a found document from a missing document."); DocumentKey key = DecodeKey(reader, DecodeString(response.found.name)); - ObjectValue::Map value = + FieldValue::Map value = DecodeFields(reader, response.found.fields_count, response.found.fields); SnapshotVersion version = DecodeSnapshotVersion(reader, response.found.update_time); @@ -465,7 +503,7 @@ std::unique_ptr Serializer::DecodeFoundDocument( reader->Fail("Got a document response with no snapshot version"); } - return absl::make_unique(FieldValue::FromMap(std::move(value)), + return absl::make_unique(ObjectValue::FromMap(std::move(value)), std::move(key), std::move(version), DocumentState::kSynced); } @@ -491,12 +529,12 @@ std::unique_ptr Serializer::DecodeMissingDocument( std::unique_ptr Serializer::DecodeDocument( Reader* reader, const google_firestore_v1_Document& proto) const { - ObjectValue::Map fields_internal = + FieldValue::Map fields_internal = DecodeFields(reader, proto.fields_count, proto.fields); SnapshotVersion version = DecodeSnapshotVersion(reader, proto.update_time); return absl::make_unique( - FieldValue::FromMap(std::move(fields_internal)), + ObjectValue::FromMap(std::move(fields_internal)), DecodeKey(reader, DecodeString(proto.name)), std::move(version), DocumentState::kSynced); } @@ -513,16 +551,14 @@ google_firestore_v1_Write Serializer::EncodeMutation( case Mutation::Type::kSet: { result.which_operation = google_firestore_v1_Write_update_tag; result.update = EncodeDocument( - mutation.key(), - static_cast(mutation).value().object_value()); + mutation.key(), static_cast(mutation).value()); return result; } case Mutation::Type::kPatch: { result.which_operation = google_firestore_v1_Write_update_tag; auto patch_mutation = static_cast(mutation); - result.update = - EncodeDocument(mutation.key(), patch_mutation.value().object_value()); + result.update = EncodeDocument(mutation.key(), patch_mutation.value()); result.update_mask = EncodeDocumentMask(patch_mutation.mask()); return result; } @@ -564,7 +600,7 @@ std::unique_ptr Serializer::DecodeMutation( switch (mutation.which_operation) { case google_firestore_v1_Write_update_tag: { DocumentKey key = DecodeKey(reader, DecodeString(mutation.update.name)); - FieldValue value = FieldValue::FromMap(DecodeFields( + ObjectValue value = ObjectValue::FromMap(DecodeFields( reader, mutation.update.fields_count, mutation.update.fields)); FieldMask mask = DecodeDocumentMask(mutation.update_mask); if (mask.size() > 0) { @@ -668,10 +704,8 @@ google_firestore_v1_DocumentMask Serializer::EncodeDocumentMask( const FieldMask& mask) { google_firestore_v1_DocumentMask result{}; - size_t count = mask.size(); - HARD_ASSERT(count <= std::numeric_limits::max(), - "Unable to encode specified document mask. Too many fields."); - result.field_paths_count = static_cast(count); + pb_size_t count = CheckedSize(mask.size()); + result.field_paths_count = count; result.field_paths = MakeArray(count); int i = 0; @@ -700,6 +734,7 @@ google_firestore_v1_Target_QueryTarget Serializer::EncodeQueryTarget( // Dissect the path into parent, collection_id and optional key filter. std::string collection_id; + // TODO(rsgowman): Port Collection Group Queries logic. if (query.path().empty()) { result.parent = EncodeString(EncodeQueryPath(ResourcePath::Empty())); } else { @@ -714,8 +749,8 @@ google_firestore_v1_Target_QueryTarget Serializer::EncodeQueryTarget( google_firestore_v1_Target_QueryTarget_structured_query_tag; if (!collection_id.empty()) { - size_t count = 1; - result.structured_query.from_count = static_cast(count); + pb_size_t count = 1; + result.structured_query.from_count = count; result.structured_query.from = MakeArray( count); @@ -817,7 +852,7 @@ Timestamp Serializer::DecodeTimestamp( reader->Fail( "Invalid message: timestamp beyond the earliest supported date"); } else if (TimestampInternal::Max().seconds() < timestamp_proto.seconds) { - reader->Fail("Invalid message: timestamp behond the latest supported date"); + reader->Fail("Invalid message: timestamp beyond the latest supported date"); } else if (timestamp_proto.nanos < 0 || timestamp_proto.nanos > 999999999) { reader->Fail( "Invalid message: timestamp nanos must be between 0 and 999999999"); @@ -827,6 +862,64 @@ Timestamp Serializer::DecodeTimestamp( return Timestamp{timestamp_proto.seconds, timestamp_proto.nanos}; } +/* static */ +google_type_LatLng Serializer::EncodeGeoPoint(const GeoPoint& geo_point_value) { + google_type_LatLng result{}; + result.latitude = geo_point_value.latitude(); + result.longitude = geo_point_value.longitude(); + return result; +} + +/* static */ +GeoPoint Serializer::DecodeGeoPoint(nanopb::Reader* reader, + const google_type_LatLng& latlng_proto) { + // The GeoPoint ctor will assert if we provide values outside the valid range. + // However, since we're decoding, a single corrupt byte could cause this to + // occur, so we'll verify the ranges before passing them in since we'd rather + // not abort in these situations. + double latitude = latlng_proto.latitude; + double longitude = latlng_proto.longitude; + if (std::isnan(latitude) || latitude < -90 || 90 < latitude) { + reader->Fail("Invalid message: Latitude must be in the range of [-90, 90]"); + } else if (std::isnan(longitude) || longitude < -180 || 180 < longitude) { + reader->Fail( + "Invalid message: Latitude must be in the range of [-180, 180]"); + } + + if (!reader->status().ok()) return GeoPoint(); + return GeoPoint(latitude, longitude); +} + +/* static */ +google_firestore_v1_ArrayValue Serializer::EncodeArray( + const std::vector& array_value) { + google_firestore_v1_ArrayValue result{}; + + pb_size_t count = CheckedSize(array_value.size()); + result.values_count = count; + result.values = MakeArray(count); + + size_t i = 0; + for (const FieldValue& fv : array_value) { + result.values[i++] = EncodeFieldValue(fv); + } + + return result; +} + +/* static */ +std::vector Serializer::DecodeArray( + nanopb::Reader* reader, const google_firestore_v1_ArrayValue& array_proto) { + std::vector result; + result.reserve(array_proto.values_count); + + for (size_t i = 0; i < array_proto.values_count; i++) { + result.push_back(DecodeFieldValue(reader, array_proto.values[i])); + } + + return result; +} + } // namespace remote } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/remote/serializer.h b/Firestore/core/src/firebase/firestore/remote/serializer.h index 086db7d191f..ae011b3defa 100644 --- a/Firestore/core/src/firebase/firestore/remote/serializer.h +++ b/Firestore/core/src/firebase/firestore/remote/serializer.h @@ -25,6 +25,7 @@ #include "Firestore/Protos/nanopb/google/firestore/v1/document.nanopb.h" #include "Firestore/Protos/nanopb/google/firestore/v1/firestore.nanopb.h" +#include "Firestore/Protos/nanopb/google/type/latlng.nanopb.h" #include "Firestore/core/src/firebase/firestore/core/query.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/document.h" @@ -52,7 +53,7 @@ class LocalSerializer; namespace remote { template -T* MakeArray(size_t count) { +T* MakeArray(pb_size_t count) { return reinterpret_cast(calloc(count, sizeof(T))); } @@ -194,11 +195,6 @@ class Serializer { std::unique_ptr DecodeDocument( nanopb::Reader* reader, const google_firestore_v1_Document& proto) const; - static void EncodeObjectMap(const model::ObjectValue::Map& object_value_map, - uint32_t map_tag, - uint32_t key_tag, - uint32_t value_tag); - static google_protobuf_Timestamp EncodeVersion( const model::SnapshotVersion& version); @@ -215,6 +211,16 @@ class Serializer { nanopb::Reader* reader, const google_firestore_v1_Target_QueryTarget& proto); + static google_type_LatLng EncodeGeoPoint(const GeoPoint& geo_point_value); + static GeoPoint DecodeGeoPoint(nanopb::Reader* reader, + const google_type_LatLng& latlng_proto); + + static google_firestore_v1_ArrayValue EncodeArray( + const std::vector& array_value); + static std::vector DecodeArray( + nanopb::Reader* reader, + const google_firestore_v1_ArrayValue& array_proto); + private: std::unique_ptr DecodeFoundDocument( nanopb::Reader* reader, @@ -223,10 +229,6 @@ class Serializer { nanopb::Reader* reader, const google_firestore_v1_BatchGetDocumentsResponse& response) const; - static void EncodeFieldsEntry(const model::ObjectValue::Map::value_type& kv, - uint32_t key_tag, - uint32_t value_tag); - std::string EncodeQueryPath(const model::ResourcePath& path) const; const model::DatabaseId& database_id_; diff --git a/Firestore/core/src/firebase/firestore/remote/watch_change.mm b/Firestore/core/src/firebase/firestore/remote/watch_change.mm index 7f76202f100..e56d9741e08 100644 --- a/Firestore/core/src/firebase/firestore/remote/watch_change.mm +++ b/Firestore/core/src/firebase/firestore/remote/watch_change.mm @@ -18,26 +18,20 @@ #import "Firestore/Source/Model/FSTDocument.h" +#include "Firestore/core/src/firebase/firestore/util/objc_compatibility.h" + +namespace objc = firebase::firestore::util::objc; + namespace firebase { namespace firestore { namespace remote { -namespace { - -template -bool objc_equals(T* lhs, T* rhs) { - // `isEqual:` will return false if both objects are nil. - return (lhs == nil && rhs == nil) || [lhs isEqual:rhs]; -} - -} // namespace - bool operator==(const DocumentWatchChange& lhs, const DocumentWatchChange& rhs) { return lhs.updated_target_ids() == rhs.updated_target_ids() && lhs.removed_target_ids() == rhs.removed_target_ids() && lhs.document_key() == rhs.document_key() && - objc_equals(lhs.new_document(), rhs.new_document()); + objc::Equals(lhs.new_document(), rhs.new_document()); } bool operator==(const ExistenceFilterWatchChange& lhs, @@ -47,7 +41,7 @@ bool objc_equals(T* lhs, T* rhs) { bool operator==(const WatchTargetChange& lhs, const WatchTargetChange& rhs) { return lhs.state() == rhs.state() && lhs.target_ids() == rhs.target_ids() && - objc_equals(lhs.resume_token(), rhs.resume_token()) && + objc::Equals(lhs.resume_token(), rhs.resume_token()) && lhs.cause() == rhs.cause(); } diff --git a/Firestore/core/src/firebase/firestore/remote/watch_stream.h b/Firestore/core/src/firebase/firestore/remote/watch_stream.h index 43b1ef32f7f..b6136cfbb4d 100644 --- a/Firestore/core/src/firebase/firestore/remote/watch_stream.h +++ b/Firestore/core/src/firebase/firestore/remote/watch_stream.h @@ -24,10 +24,12 @@ #include #include +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_connection.h" #include "Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h" #include "Firestore/core/src/firebase/firestore/remote/stream.h" +#include "Firestore/core/src/firebase/firestore/remote/watch_change.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "absl/strings/string_view.h" @@ -41,12 +43,41 @@ namespace firebase { namespace firestore { namespace remote { +/** + * An interface defining the events that can be emitted by the `WatchStream`. + */ +class WatchStreamCallback { + public: + /** Called by the `WatchStream` when it is ready to accept outbound request + * messages. */ + virtual void OnWatchStreamOpen() = 0; + + /** + * Called by the `WatchStream` with changes and the snapshot versions + * included in in the `WatchChange` responses sent back by the server. + */ + virtual void OnWatchStreamChange( + const WatchChange& change, + const model::SnapshotVersion& snapshot_version) = 0; + + /** + * Called by the `WatchStream` when the underlying streaming RPC is + * interrupted for whatever reason, usually because of an error, but possibly + * due to an idle timeout. The status passed to this method may be ok, in + * which case the stream was closed without attributable fault. + * + * NOTE: This will not be called after `Stop` is called on the stream. See + * "Starting and Stopping" on `Stream` for details. + */ + virtual void OnWatchStreamClose(const util::Status& status) = 0; +}; + /** * A `Stream` that implements the StreamingWatch RPC. * - * Once the `WatchStream` has called the `streamDidOpen` method on the delegate, - * any number of `WatchQuery` and `UnwatchTargetId` calls can be sent to control - * what changes will be sent from the server for WatchChanges. + * Once the `WatchStream` has called the `OnWatchStreamOpen` method on the + * callback, any number of `WatchQuery` and `UnwatchTargetId` calls can be sent + * to control what changes will be sent from the server for WatchChanges. */ class WatchStream : public Stream { public: @@ -54,7 +85,7 @@ class WatchStream : public Stream { auth::CredentialsProvider* credentials_provider, FSTSerializerBeta* serializer, GrpcConnection* grpc_connection, - id delegate); + WatchStreamCallback* callback); /** * Registers interest in the results of the given query. If the query includes @@ -85,7 +116,7 @@ class WatchStream : public Stream { } bridge::WatchStreamSerializer serializer_bridge_; - bridge::WatchStreamDelegate delegate_bridge_; + WatchStreamCallback* callback_; }; } // namespace remote diff --git a/Firestore/core/src/firebase/firestore/remote/watch_stream.mm b/Firestore/core/src/firebase/firestore/remote/watch_stream.mm index 7a487d75664..0066809fae3 100644 --- a/Firestore/core/src/firebase/firestore/remote/watch_stream.mm +++ b/Firestore/core/src/firebase/firestore/remote/watch_stream.mm @@ -16,6 +16,7 @@ #include "Firestore/core/src/firebase/firestore/remote/watch_stream.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" #include "Firestore/core/src/firebase/firestore/util/status.h" @@ -36,11 +37,11 @@ CredentialsProvider* credentials_provider, FSTSerializerBeta* serializer, GrpcConnection* grpc_connection, - id delegate) + WatchStreamCallback* callback) : Stream{async_queue, credentials_provider, grpc_connection, TimerId::ListenStreamConnectionBackoff, TimerId::ListenStreamIdle}, serializer_bridge_{serializer}, - delegate_bridge_{delegate} { + callback_{NOT_NULL(callback)} { } void WatchStream::WatchQuery(FSTQueryData* query) { @@ -73,7 +74,7 @@ } void WatchStream::NotifyStreamOpen() { - delegate_bridge_.NotifyDelegateOnOpen(); + callback_->OnWatchStreamOpen(); } Status WatchStream::NotifyStreamResponse(const grpc::ByteBuffer& message) { @@ -92,14 +93,14 @@ // A successful response means the stream is healthy. backoff_.Reset(); - delegate_bridge_.NotifyDelegateOnChange( + callback_->OnWatchStreamChange( *serializer_bridge_.ToWatchChange(response), serializer_bridge_.ToSnapshotVersion(response)); return Status::OK(); } void WatchStream::NotifyStreamClose(const Status& status) { - delegate_bridge_.NotifyDelegateOnClose(status); + callback_->OnWatchStreamClose(status); } } // namespace remote diff --git a/Firestore/core/src/firebase/firestore/remote/write_stream.h b/Firestore/core/src/firebase/firestore/remote/write_stream.h index 8e34e3eac25..bfa75304ab5 100644 --- a/Firestore/core/src/firebase/firestore/remote/write_stream.h +++ b/Firestore/core/src/firebase/firestore/remote/write_stream.h @@ -24,7 +24,9 @@ #import #include #include +#include +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_connection.h" #include "Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h" #include "Firestore/core/src/firebase/firestore/remote/stream.h" @@ -37,10 +39,46 @@ #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" +@class FSTMutationResult; + namespace firebase { namespace firestore { namespace remote { +class WriteStreamCallback { + public: + /** + * Called by the `WriteStream` when it is ready to accept outbound request + * messages. + */ + virtual void OnWriteStreamOpen() = 0; + + /** + * Called by the `WriteStream` upon a successful handshake response from the + * server, which is the receiver's cue to send any pending writes. + */ + virtual void OnWriteStreamHandshakeComplete() = 0; + + /** + * Called by the `WriteStream` upon receiving a StreamingWriteResponse from + * the server that contains mutation results. + */ + virtual void OnWriteStreamMutationResult( + model::SnapshotVersion commit_version, + std::vector results) = 0; + + /** + * Called when the `WriteStream`'s underlying RPC is interrupted for whatever + * reason, usually because of an error, but possibly due to an idle timeout. + * The status passed to this method may be "ok", in which case the stream was + * closed without attributable fault. + * + * NOTE: This will not be called after `Stop` is called on the stream. See + * "Starting and Stopping" on `Stream` for details. + */ + virtual void OnWriteStreamClose(const util::Status& status) = 0; +}; + /** * A Stream that implements the Write RPC. * @@ -65,7 +103,7 @@ class WriteStream : public Stream { auth::CredentialsProvider* credentials_provider, FSTSerializerBeta* serializer, GrpcConnection* grpc_connection, - id delegate); + WriteStreamCallback* callback); void SetLastStreamToken(NSData* token); /** @@ -93,7 +131,7 @@ class WriteStream : public Stream { virtual void WriteHandshake(); /** Sends a group of mutations to the Firestore backend to apply. */ - virtual void WriteMutations(NSArray* mutations); + virtual void WriteMutations(const std::vector& mutations); protected: // For tests only @@ -115,7 +153,7 @@ class WriteStream : public Stream { } bridge::WriteStreamSerializer serializer_bridge_; - bridge::WriteStreamDelegate delegate_bridge_; + WriteStreamCallback* callback_ = nullptr; bool handshake_complete_ = false; }; diff --git a/Firestore/core/src/firebase/firestore/remote/write_stream.mm b/Firestore/core/src/firebase/firestore/remote/write_stream.mm index 17f46a53e04..7e0513a5b47 100644 --- a/Firestore/core/src/firebase/firestore/remote/write_stream.mm +++ b/Firestore/core/src/firebase/firestore/remote/write_stream.mm @@ -36,11 +36,11 @@ CredentialsProvider* credentials_provider, FSTSerializerBeta* serializer, GrpcConnection* grpc_connection, - id delegate) + WriteStreamCallback* callback) : Stream{async_queue, credentials_provider, grpc_connection, TimerId::WriteStreamConnectionBackoff, TimerId::WriteStreamIdle}, serializer_bridge_{serializer}, - delegate_bridge_{delegate} { + callback_{NOT_NULL(callback)} { } void WriteStream::SetLastStreamToken(NSData* token) { @@ -65,7 +65,7 @@ // stream token on the handshake, ignoring any stream token we might have. } -void WriteStream::WriteMutations(NSArray* mutations) { +void WriteStream::WriteMutations(const std::vector& mutations) { EnsureOnQueue(); HARD_ASSERT(IsOpen(), "Writing mutations requires an opened stream"); HARD_ASSERT(handshake_complete(), @@ -97,11 +97,11 @@ } void WriteStream::NotifyStreamOpen() { - delegate_bridge_.NotifyDelegateOnOpen(); + callback_->OnWriteStreamOpen(); } void WriteStream::NotifyStreamClose(const Status& status) { - delegate_bridge_.NotifyDelegateOnClose(status); + callback_->OnWriteStreamClose(status); // Delegate's logic might depend on whether handshake was completed, so only // reset it after notifying. handshake_complete_ = false; @@ -124,14 +124,14 @@ if (!handshake_complete()) { // The first response is the handshake response handshake_complete_ = true; - delegate_bridge_.NotifyDelegateOnHandshakeComplete(); + callback_->OnWriteStreamHandshakeComplete(); } else { // A successful first write response means the stream is healthy. // Note that we could consider a successful handshake healthy, however, the // write itself might be causing an error we want to back off from. backoff_.Reset(); - delegate_bridge_.NotifyDelegateOnCommit( + callback_->OnWriteStreamMutationResult( serializer_bridge_.ToCommitVersion(response), serializer_bridge_.ToMutationResults(response)); } diff --git a/Firestore/core/src/firebase/firestore/timestamp.cc b/Firestore/core/src/firebase/firestore/timestamp.cc index c35fca10ed1..50ddec65ad3 100644 --- a/Firestore/core/src/firebase/firestore/timestamp.cc +++ b/Firestore/core/src/firebase/firestore/timestamp.cc @@ -16,7 +16,10 @@ #include "Firestore/core/include/firebase/firestore/timestamp.h" +#include + #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "absl/strings/str_cat.h" namespace firebase { @@ -84,8 +87,12 @@ Timestamp Timestamp::FromTimePoint( #endif // !defined(_STLPORT_VERSION) std::string Timestamp::ToString() const { - return std::string("Timestamp(seconds=") + std::to_string(seconds_) + - ", nanoseconds=" + std::to_string(nanoseconds_) + ")"; + return absl::StrCat("Timestamp(seconds=", seconds_, + ", nanoseconds=", nanoseconds_, ")"); +} + +std::ostream& operator<<(std::ostream& out, const Timestamp& timestamp) { + return out << timestamp.ToString(); } void Timestamp::ValidateBounds() const { diff --git a/Firestore/core/src/firebase/firestore/util/CMakeLists.txt b/Firestore/core/src/firebase/firestore/util/CMakeLists.txt index 21180afa26d..28b29ec6aa9 100644 --- a/Firestore/core/src/firebase/firestore/util/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/util/CMakeLists.txt @@ -245,14 +245,18 @@ cc_library( comparison.cc comparison.h config.h + delayed_constructor.h hashing.h iterator_adaptors.h + objc_compatibility.h ordered_code.cc ordered_code.h range.h string_util.cc string_util.h + to_string.h type_traits.h + warnings.h DEPENDS absl_base firebase_firestore_util_async diff --git a/Firestore/core/src/firebase/firestore/util/delayed_constructor.h b/Firestore/core/src/firebase/firestore/util/delayed_constructor.h new file mode 100644 index 00000000000..9c00e515158 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/delayed_constructor.h @@ -0,0 +1,131 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_DELAYED_CONSTRUCTOR_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_DELAYED_CONSTRUCTOR_H_ + +#include +#include + +namespace firebase { +namespace firestore { +namespace util { + +// DelayedConstructor is a wrapper around an object of type T that +// +// * stores the object of type T inline inside DelayedConstructor; +// * initially does not call T's constructor, leaving storage uninitialized; +// * calls the constructor when you call Init(); +// * provides access to the object of type T like a pointer via ->, *, and +// get(); and +// * calls T's destructor as usual. +// +// This is useful for embedding objects of type T inside Objective-C objects +// when T has no default constructor. +// +// Objective-C separates allocation from initialization which is different from +// the way C++ does it. A C++ object embedded in an Objective-C object is +// normally default constructed then assigned a value later. This doesn't work +// for classes that have no default constructor. +// +// DelayedConstructor does not count or otherwise check that Init is only +// called once. For best results call Init() from the Objective-C class's +// designated initializer. +// +// Note that DelayedConstructor makes no guarantees about the state of the +// storage backing it before Init() is called. However, Objective-C objects are +// zero filled during allocation, so as a member of an Objective-C object, the +// default state will be zero-filled. +// +// Normally this doesn't matter, but DelayedConstructor unconditionally invokes +// T's destructor, even if you don't call Init(). This may cause problems in +// Objective-C classes where the initializer is designed to return an instance +// other than self. It's best to avoid such instance switching techniques in +// combination with DelayedConstructor, but it is possible: either ensure that +// T's destructor handles the zero-filled case correctly, or call Init() before +// switching instances. +template +class DelayedConstructor { + public: + typedef T element_type; + + /** + * Default constructor does nothing. + */ + DelayedConstructor() { + } + + /** + * Forwards arguments to the T's constructor: calls T(args...). + * + * This overload is disabled when it might collide with copy/move. + */ + template ::type...), + void(DelayedConstructor)>::value, + int>::type = 0> + void Init(Ts&&... args) { + new (&space_) T(std::forward(args)...); + } + + /** + * Forwards copy and move construction for T. + */ + void Init(const T& x) { + new (&space_) T(x); + } + void Init(T&& x) { + new (&space_) T(std::move(x)); + } + + // No copying. + DelayedConstructor(const DelayedConstructor&) = delete; + DelayedConstructor& operator=(const DelayedConstructor&) = delete; + + ~DelayedConstructor() { + get()->~T(); + } + + // Pretend to be a smart pointer to T. + T& operator*() { + return *get(); + } + T* operator->() { + return get(); + } + T* get() { + return reinterpret_cast(&space_); + } + const T& operator*() const { + return *get(); + } + const T* operator->() const { + return get(); + } + const T* get() const { + return reinterpret_cast(&space_); + } + + private: + typename std::aligned_storage::type space_; +}; + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_DELAYED_CONSTRUCTOR_H_ diff --git a/Firestore/core/src/firebase/firestore/util/error_apple.h b/Firestore/core/src/firebase/firestore/util/error_apple.h index 66080ddc740..a091840dcaf 100644 --- a/Firestore/core/src/firebase/firestore/util/error_apple.h +++ b/Firestore/core/src/firebase/firestore/util/error_apple.h @@ -22,7 +22,8 @@ #import -#include "Firestore/Source/Public/FIRFirestoreErrors.h" // for FIRFirestoreErrorDomain +#import // for FIRFirestoreErrorDomain + #include "Firestore/core/include/firebase/firestore/firestore_errors.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" @@ -48,6 +49,21 @@ inline NSError* MakeNSError(const util::Status& status) { return MakeNSError(status.code(), status.error_message()); } +inline Status MakeStatus(NSError* error) { + if (!error) { + return Status::OK(); + } + + HARD_ASSERT(error.domain == FIRFirestoreErrorDomain, + "Can only translate a Firestore error to a status"); + auto error_code = static_cast(error.code); + HARD_ASSERT(error_code >= FirestoreErrorCode::Cancelled && + error_code <= FirestoreErrorCode::Unauthenticated, + "Unknown error code"); + return Status{static_cast(error_code), + MakeString(error.localizedDescription)}; +} + } // namespace util } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/util/hashing.h b/Firestore/core/src/firebase/firestore/util/hashing.h index 2ad0c933960..4d8462ab250 100644 --- a/Firestore/core/src/firebase/firestore/util/hashing.h +++ b/Firestore/core/src/firebase/firestore/util/hashing.h @@ -22,6 +22,9 @@ #include #include +#include "Firestore/core/src/firebase/firestore/util/type_traits.h" +#include "absl/meta/type_traits.h" + namespace firebase { namespace firestore { namespace util { @@ -84,7 +87,7 @@ struct has_std_hash { */ template using std_hash_type = - typename std::enable_if::value, size_t>::type; + typename absl::enable_if_t::value, size_t>; /** * Combines a hash_value with whatever accumulated state there is so far. @@ -99,6 +102,7 @@ inline size_t Combine(size_t state, size_t hash_value) { * * In order we try: * * A Hash() member, if defined and the return type is an integral type + * * A `-hash` method, if dealing with an Objective-C class * * A std::hash specialization, if available * * A range-based specialization, valid if either of the above hold on the * members of the range. @@ -117,7 +121,7 @@ template struct HashChoice : HashChoice {}; template <> -struct HashChoice<2> {}; +struct HashChoice<3> {}; template size_t InvokeHash(const K& value); @@ -132,13 +136,29 @@ auto RankedInvokeHash(const K& value, HashChoice<0>) -> decltype(value.Hash()) { return value.Hash(); } +#if __OBJC__ + +/** + * Hashes the given value if it's of an Objective-C class (and thus defines + * `-hash`. + * + * @return The result of `[value hash]`, converted to `size_t`. + */ +template ::value>> +size_t RankedInvokeHash(const K& value, HashChoice<1>) { + return static_cast([value hash]); +} + +#endif + /** * Hashes the given value if it has a specialization of std::hash. * * @return The result of `std::hash{}(value)` */ template -std_hash_type RankedInvokeHash(const K& value, HashChoice<1>) { +std_hash_type RankedInvokeHash(const K& value, HashChoice<2>) { return std::hash{}(value); } @@ -147,7 +167,7 @@ std_hash_type RankedInvokeHash(const K& value, HashChoice<1>) { * range can be hashed. */ template -auto RankedInvokeHash(const Range& range, HashChoice<2>) +auto RankedInvokeHash(const Range& range, HashChoice<3>) -> decltype(impl::InvokeHash(*std::begin(range))) { size_t result = 0; size_t size = 0; diff --git a/Firestore/core/src/firebase/firestore/util/objc_compatibility.h b/Firestore/core/src/firebase/firestore/util/objc_compatibility.h new file mode 100644 index 00000000000..74a4d44f89a --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/objc_compatibility.h @@ -0,0 +1,119 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_OBJC_COMPATIBILITY_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_OBJC_COMPATIBILITY_H_ + +#if !defined(__OBJC__) +#error "This header only supports Objective-C++" +#endif // !defined(__OBJC__) + +#import + +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/util/string_apple.h" +#include "Firestore/core/src/firebase/firestore/util/to_string.h" +#include "Firestore/core/src/firebase/firestore/util/type_traits.h" +#include "absl/meta/type_traits.h" + +/** + * Utility functions that help C++ code interoperate with Objective-C while + * migration is in progress + */ + +namespace firebase { +namespace firestore { +namespace util { +namespace objc { + +namespace internal { + +template +using is_container_of_objc = + absl::conjunction, + is_objective_c_pointer>; + +} + +/** + * Checks two Objective-C objects for equality using `isEqual`. Two nil objects + * are considered equal, unlike the behavior of `isEqual`. + */ +template ::value>> +bool Equals(T* lhs, T* rhs) { + return (lhs == nil && rhs == nil) || [lhs isEqual:rhs]; +} + +/** Checks two C++ containers of Objective-C objects for "deep" equality. */ +template < + typename T, + typename = absl::enable_if_t::value>> +bool Equals(const T& lhs, const T& rhs) { + using Ptr = typename T::value_type; + + return lhs.size() == rhs.size() && + std::equal(lhs.begin(), lhs.end(), rhs.begin(), + [](Ptr o1, Ptr o2) { return Equals(o1, o2); }); +} + +/** + * A function object that implements equality for an Objective-C pointer by + * delegating to -isEqual:. This is useful for using Objective-C objects as + * keys in STL associative containers. + */ +template ::value>> +class EqualTo { + public: + bool operator()(T lhs, T rhs) const { + return [lhs isEqual:rhs]; + } +}; + +/** + * A function object that implements STL-compatible hash code for an Objective-C + * pointer by delegating to -hash. This is useful for using Objective-C objects + * as keys in std::unordered_map. + */ +template ::value>> +class Hash { + public: + size_t operator()(T value) const { + return static_cast([value hash]); + } +}; + +/** + * Creates a debug description of the given `value` by calling `ToString` on it, + * converting the result to an `NSString`. Exists mainly to simplify writing + * `description:` methods for Objective-C classes. + */ +template +NSString* Description(const T& value) { + return WrapNSString(ToString(value)); +} + +} // namespace objc +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_OBJC_COMPATIBILITY_H_ diff --git a/Firestore/core/src/firebase/firestore/util/status.cc b/Firestore/core/src/firebase/firestore/util/status.cc index 838d9fc605c..06a643daa7a 100644 --- a/Firestore/core/src/firebase/firestore/util/status.cc +++ b/Firestore/core/src/firebase/firestore/util/status.cc @@ -16,6 +16,8 @@ #include "Firestore/core/src/firebase/firestore/util/status.h" +#include + #include "Firestore/core/src/firebase/firestore/util/string_format.h" #include "absl/memory/memory.h" @@ -127,6 +129,11 @@ std::string Status::ToString() const { } } +std::ostream& operator<<(std::ostream& out, const Status& status) { + out << status.ToString(); + return out; +} + void Status::IgnoreError() const { // no-op } diff --git a/Firestore/core/src/firebase/firestore/util/status.h b/Firestore/core/src/firebase/firestore/util/status.h index 2d1e32904c6..d49089ee0d6 100644 --- a/Firestore/core/src/firebase/firestore/util/status.h +++ b/Firestore/core/src/firebase/firestore/util/status.h @@ -100,6 +100,7 @@ class ABSL_MUST_USE_RESULT Status { /// \brief Return a string representation of this status suitable for /// printing. Returns the string `"OK"` for success. std::string ToString() const; + friend std::ostream& operator<<(std::ostream& out, const Status& status); // Ignores any errors. This method does nothing except potentially suppress // complaints from any tools that are checking that errors are not dropped on diff --git a/Firestore/core/src/firebase/firestore/util/statusor_callback.h b/Firestore/core/src/firebase/firestore/util/statusor_callback.h new file mode 100644 index 00000000000..ff39b5df2ae --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/statusor_callback.h @@ -0,0 +1,35 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_STATUSOR_CALLBACK_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_STATUSOR_CALLBACK_H_ + +#include + +#include "Firestore/core/src/firebase/firestore/util/statusor.h" + +namespace firebase { +namespace firestore { +namespace util { + +template +using StatusOrCallback = std::function)>; + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_STATUSOR_CALLBACK_H_ diff --git a/Firestore/core/src/firebase/firestore/util/string_format.h b/Firestore/core/src/firebase/firestore/util/string_format.h index 01776a9de19..e76aeaeafee 100644 --- a/Firestore/core/src/firebase/firestore/util/string_format.h +++ b/Firestore/core/src/firebase/firestore/util/string_format.h @@ -53,7 +53,7 @@ struct FormatChoice<5> {}; * * Chooses a conversion to a text form in this order: * * If the value is exactly of `bool` type (without implicit conversions) - * the text will the "true" or "false". + * the text will be "true" or "false". * * If the value is of type `const char*`, the text will be the value * interpreted as a C string. To show the address of a single char or to * show the `const char*` as an address, cast to `void*`. diff --git a/Firestore/core/src/firebase/firestore/util/string_util.h b/Firestore/core/src/firebase/firestore/util/string_util.h index 46462d89c68..86acc56b241 100644 --- a/Firestore/core/src/firebase/firestore/util/string_util.h +++ b/Firestore/core/src/firebase/firestore/util/string_util.h @@ -65,23 +65,6 @@ std::string PrefixSuccessor(absl::string_view prefix); */ std::string ImmediateSuccessor(absl::string_view s); -/** - * Returns a string description of the contents of the given collection. - */ -template -std::string ToString(const Container& container) { - std::string result; - result.append("["); - const char* sep = ""; - for (auto&& item : container) { - result.append(sep); - result.append(item); - sep = ", "; - } - result.append("]"); - return result; -} - } // namespace util } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/util/to_string.h b/Firestore/core/src/firebase/firestore/util/to_string.h new file mode 100644 index 00000000000..32f767ff88d --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/to_string.h @@ -0,0 +1,197 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_TO_STRING_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_TO_STRING_H_ + +#if __OBJC__ +#import +#endif // __OBJC__ + +#include +#include +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/util/string_apple.h" +#include "Firestore/core/src/firebase/firestore/util/string_format.h" +#include "Firestore/core/src/firebase/firestore/util/type_traits.h" +#include "absl/meta/type_traits.h" +#include "absl/strings/str_join.h" +#include "absl/types/optional.h" + +namespace firebase { +namespace firestore { +namespace util { + +/** + * Creates a human-readable description of the given `value`. The representation + * is loosely inspired by Python. + * + * The general idea is to create the description by using the most specific + * available function that creates a string representation of the class; for + * containers, do this recursively, adding some minimal container formatting to + * the output. + * + * Example: + * + * std::vector v{ + * DocumentKey({"foo/bar"}), + * DocumentKey({"this/that"}) + * }; + * assert(ToString(v) == "[foo/bar, this/that]"); + * + * std::map m{ + {1, "foo"}, + {2, "bar"} + * }; + * assert(ToString(m) == "{1: foo, 2: bar}"); + * + * The following algorithm is used: + * + * - if `value` defines a member function called `ToString`, the description is + * created by invoking the function; + * + * - (in Objective-C++ only) otherwise, if `value` is an Objective-C object, + * the description is created by calling `[value description]`and converting + * the result to an `std::string`; + * + * - otherwise, if `value` is an `std::string`, it's used as is; + * + * - otherwise, if `value` is an associative container (`std::map`, + * `std::unordered_map`, `f:f:immutable::SortedMap`, etc.), the description is + * of the form: + * + * {key1: value1, key2: value2} + * + * where the description of each key and value is created by running + * `ToString` recursively; + * + * - otherwise, if `value` is a container, the description is of the form: + * + * [element1, element2] + * + * where the description of each element is created by running `ToString` + * recursively; + * + * - otherwise, `std::to_string` is used as a fallback. If `std::to_string` is + * not defined for the class, a compilation error will be produced. + * + * Implementation notes: to rank different choices and avoid clashes (e.g., + * a type that is an associative container is also a (simple) container), tag + * dispatch is used. Each function in the chain either is tagged by + * `std::true_type` and can process the value, or is tagged by `std::false_type` + * and passes the value to the next function by the rank. When passing to the + * next function, some trait corresponding to the function is given in place of + * the tag; for example, `StringToString`, which can handle `std::string`s, is + * invoked with `std::is_same` as the tag. + */ +template +std::string ToString(const T& value); + +namespace impl { + +// Checks whether the given type `T` defines a member function `ToString` + +template > +struct has_to_string : std::false_type {}; + +template +struct has_to_string().ToString())>> + : std::true_type {}; + +template +struct ToStringChoice : ToStringChoice {}; + +template <> +struct ToStringChoice<6> {}; + +#if __OBJC__ + +// Objective-C class +template ::value>> +std::string ToStringImpl(T value, ToStringChoice<0>) { + return MakeString([value description]); +} + +#endif // __OBJC__ + +// Has `ToString` member function +template ::value>> +std::string ToStringImpl(const T& value, ToStringChoice<1>) { + return value.ToString(); +} + +// `std::string` +template ::value>> +std::string ToStringImpl(const T& value, ToStringChoice<2>) { + return value; +} + +// `absl::optional` +template , T>::value>> +std::string ToStringImpl(const T& maybe_value, ToStringChoice<3>) { + return maybe_value.has_value() ? ToString(maybe_value.value()) + : std::string{"nullopt"}; +} + +// Associative container +template ::value>> +std::string ToStringImpl(const T& value, ToStringChoice<4>) { + std::string contents = absl::StrJoin( + value, ", ", [](std::string* out, const typename T::value_type& kv) { + out->append(ToString(kv.first)); + out->append(": "); + out->append(ToString(kv.second)); + }); + return std::string{"{"} + contents + "}"; // NOLINT(whitespace/braces) +} + +// Container +template ::value>> +std::string ToStringImpl(const T& value, ToStringChoice<5>) { + std::string contents = absl::StrJoin( + value, ", ", [](std::string* out, const typename T::value_type& element) { + out->append(ToString(element)); + }); + return std::string{"["} + contents + "]"; // NOLINT(whitespace/braces) +} + +// Fallback +template +std::string ToStringImpl(const T& value, ToStringChoice<6>) { + FormatArg arg{value}; + return std::string{arg.data(), arg.data() + arg.size()}; +} + +} // namespace impl + +template +std::string ToString(const T& value) { + return impl::ToStringImpl(value, impl::ToStringChoice<0>{}); +} + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_TO_STRING_H_ diff --git a/Firestore/core/src/firebase/firestore/util/type_traits.h b/Firestore/core/src/firebase/firestore/util/type_traits.h index 52feb6be377..fbec4179aaf 100644 --- a/Firestore/core/src/firebase/firestore/util/type_traits.h +++ b/Firestore/core/src/firebase/firestore/util/type_traits.h @@ -22,6 +22,9 @@ #endif #include +#include + +#include "absl/meta/type_traits.h" namespace firebase { namespace firestore { @@ -45,44 +48,32 @@ namespace util { * is_objective_c_pointer>::value == false */ template -struct is_objective_c_pointer { - private: - using yes_type = char (&)[10]; - using no_type = char (&)[1]; - - /** - * A non-existent function declared to produce a pointer to type T (which is - * consistent with the way Objective-C objects are referenced). - * - * Note that there is no definition for this function but that's okay because - * we only need it to reason about the function's type at compile type. - */ - static T Pointer(); - - static yes_type Choose(id value); - static no_type Choose(...); - - public: - using value_type = bool; - - enum { value = sizeof(Choose(Pointer())) == sizeof(yes_type) }; - - constexpr operator bool() const { - return value; - } - - constexpr bool operator()() const { - return value; - } -}; - -// Hard-code the answer for `void` because you can't pass arguments of type -// `void` to another function. -template <> -struct is_objective_c_pointer : public std::false_type {}; +using is_objective_c_pointer = std::is_convertible; #endif // __OBJC__ +// is_iterable + +template > +struct is_iterable : std::false_type {}; + +template +struct is_iterable< + T, + absl::void_t().begin(), std::declval().end())>> + : std::true_type {}; + +// is_associative_container + +template > +struct is_associative_container : std::false_type {}; + +template +struct is_associative_container< + T, + absl::void_t())>> + : std::true_type {}; + } // namespace util } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/util/warnings.h b/Firestore/core/src/firebase/firestore/util/warnings.h new file mode 100644 index 00000000000..6ce6017da06 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/warnings.h @@ -0,0 +1,89 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_WARNINGS_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_WARNINGS_H_ + +/** + * Macros for suppressing various warnings. Use like this: + * + * SUPPRESS_DOCUMENTATION_WARNINGS_BEGIN() + * #include "header/that/has/warnings.h" + * SUPPRESS_END() + * + * SUPPRESS_DEPRECATED_DECLARATIONS_BEGIN() + * #include "header/that/uses/deprecated/stuff.h" + * SUPPRESS_END() + * + * SUPPRESS_DOCUMENTATION_WARNINGS_BEGIN() + * SUPPRESS_DEPRECATED_DECLARATIONS_BEGIN() + * #include "something/awful.h" + * SUPPRESS_END() + * SUPPRESS_END() + */ + +// Define some primitives used by the 'public' macros below. + +#if defined(__clang__) || defined(__GNUC__) +#define SUPPRESS_BEGIN_UNIMPLEMENTED_() _Pragma("GCC diagnostic push") + +#define SUPPRESS_BEGIN_(name) _Pragma("GCC diagnostic push") _Pragma(name) + +#elif defined(_MSC_VER) +#define SUPPRESS_BEGIN_UNIMPLEMENTED_() __pragma(warning(push)) + +#define SUPPRESS_BEGIN_(name) \ + __pragma(warning(push)) __pragma(warning(disable : name)) + +#endif + +// 'Public' macros. + +#if defined(__clang__) +#define SUPPRESS_DOCUMENTATION_WARNINGS_BEGIN() \ + SUPPRESS_BEGIN_("GCC diagnostic ignored \"-Wdocumentation\"") + +#define SUPPRESS_DEPRECATED_DECLARATIONS_BEGIN() \ + SUPPRESS_BEGIN_("GCC diagnostic ignored \"-Wdeprecated-declarations\"") + +#define SUPPRESS_END() _Pragma("GCC diagnostic pop") + +#elif defined(__GNUC__) +#define SUPPRESS_DOCUMENTATION_WARNINGS_BEGIN() SUPPRESS_BEGIN_UNIMPLEMENTED_() + +#define SUPPRESS_DEPRECATED_DECLARATIONS_BEGIN() \ + SUPPRESS_BEGIN_("GCC diagnostic ignored \"-Wdeprecated-declarations\"") + +#define SUPPRESS_END() _Pragma("GCC diagnostic pop") + +#elif defined(_MSC_VER) +// MSVC compiler warnings can be found here: (Look at the navbar on the left +// and select the appropriate range): +// https://docs.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warnings-by-compiler-version?view=vs-2017 +#define SUPPRESS_DOCUMENTATION_WARNINGS_BEGIN() SUPPRESS_BEGIN_UNIMPLEMENTED_() + +#define SUPPRESS_DEPRECATED_DECLARATIONS_BEGIN() SUPPRESS_BEGIN_(4995) + +#define SUPPRESS_END() __pragma(warning(pop)) + +#else +#define SUPPRESS_DOCUMENTATION_WARNINGS_BEGIN() +#define SUPPRESS_DEPRECATED_DECLARATIONS_BEGIN() +#define SUPPRESS_END() + +#endif + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_WARNINGS_H_ diff --git a/Firestore/core/test/firebase/firestore/core/query_test.cc b/Firestore/core/test/firebase/firestore/core/query_test.cc index a6c810ce74f..ed58236f9d1 100644 --- a/Firestore/core/test/firebase/firestore/core/query_test.cc +++ b/Firestore/core/test/firebase/firestore/core/query_test.cc @@ -36,9 +36,9 @@ using testutil::Doc; using testutil::Filter; TEST(QueryTest, MatchesBasedOnDocumentKey) { - Document doc1 = Doc("rooms/eros/messages/1"); - Document doc2 = Doc("rooms/eros/messages/2"); - Document doc3 = Doc("rooms/other/messages/1"); + Document doc1 = *Doc("rooms/eros/messages/1"); + Document doc2 = *Doc("rooms/eros/messages/2"); + Document doc3 = *Doc("rooms/other/messages/1"); Query query = Query::AtPath({"rooms", "eros", "messages", "1"}); EXPECT_TRUE(query.Matches(doc1)); @@ -47,10 +47,10 @@ TEST(QueryTest, MatchesBasedOnDocumentKey) { } TEST(QueryTest, MatchesShallowAncestorQuery) { - Document doc1 = Doc("rooms/eros/messages/1"); - Document doc1_meta = Doc("rooms/eros/messages/1/meta/1"); - Document doc2 = Doc("rooms/eros/messages/2"); - Document doc3 = Doc("rooms/other/messages/1"); + Document doc1 = *Doc("rooms/eros/messages/1"); + Document doc1_meta = *Doc("rooms/eros/messages/1/meta/1"); + Document doc2 = *Doc("rooms/eros/messages/2"); + Document doc3 = *Doc("rooms/other/messages/1"); Query query = Query::AtPath({"rooms", "eros", "messages"}); EXPECT_TRUE(query.Matches(doc1)); @@ -60,9 +60,9 @@ TEST(QueryTest, MatchesShallowAncestorQuery) { } TEST(QueryTest, EmptyFieldsAreAllowedForQueries) { - Document doc1 = Doc("rooms/eros/messages/1", 0, - {{"text", FieldValue::FromString("msg1")}}); - Document doc2 = Doc("rooms/eros/messages/2"); + Document doc1 = *Doc("rooms/eros/messages/1", 0, + {{"text", FieldValue::FromString("msg1")}}); + Document doc2 = *Doc("rooms/eros/messages/2"); Query query = Query::AtPath({"rooms", "eros", "messages"}) .Filter(Filter("text", "==", "msg1")); @@ -77,14 +77,14 @@ TEST(QueryTest, PrimitiveValueFilter) { .Filter(Filter("sort", "<=", 2)); Document doc1 = - Doc("collection/1", 0, {{"sort", FieldValue::FromInteger(1)}}); + *Doc("collection/1", 0, {{"sort", FieldValue::FromInteger(1)}}); Document doc2 = - Doc("collection/2", 0, {{"sort", FieldValue::FromInteger(2)}}); + *Doc("collection/2", 0, {{"sort", FieldValue::FromInteger(2)}}); Document doc3 = - Doc("collection/3", 0, {{"sort", FieldValue::FromInteger(3)}}); - Document doc4 = Doc("collection/4", 0, {{"sort", FieldValue::False()}}); + *Doc("collection/3", 0, {{"sort", FieldValue::FromInteger(3)}}); + Document doc4 = *Doc("collection/4", 0, {{"sort", FieldValue::False()}}); Document doc5 = - Doc("collection/5", 0, {{"sort", FieldValue::FromString("string")}}); + *Doc("collection/5", 0, {{"sort", FieldValue::FromString("string")}}); EXPECT_FALSE(query1.Matches(doc1)); EXPECT_TRUE(query1.Matches(doc2)); @@ -103,14 +103,14 @@ TEST(QueryTest, NanFilter) { Query query = Query::AtPath(ResourcePath::FromString("collection")) .Filter(Filter("sort", "==", NAN)); - Document doc1 = Doc("collection/1", 0, {{"sort", FieldValue::Nan()}}); + Document doc1 = *Doc("collection/1", 0, {{"sort", FieldValue::Nan()}}); Document doc2 = - Doc("collection/2", 0, {{"sort", FieldValue::FromInteger(2)}}); + *Doc("collection/2", 0, {{"sort", FieldValue::FromInteger(2)}}); Document doc3 = - Doc("collection/3", 0, {{"sort", FieldValue::FromDouble(3.1)}}); - Document doc4 = Doc("collection/4", 0, {{"sort", FieldValue::False()}}); + *Doc("collection/3", 0, {{"sort", FieldValue::FromDouble(3.1)}}); + Document doc4 = *Doc("collection/4", 0, {{"sort", FieldValue::False()}}); Document doc5 = - Doc("collection/5", 0, {{"sort", FieldValue::FromString("string")}}); + *Doc("collection/5", 0, {{"sort", FieldValue::FromString("string")}}); EXPECT_TRUE(query.Matches(doc1)); EXPECT_FALSE(query.Matches(doc2)); diff --git a/Firestore/core/test/firebase/firestore/immutable/array_sorted_map_test.cc b/Firestore/core/test/firebase/firestore/immutable/array_sorted_map_test.cc index 9a23df5bcdc..956c866ee44 100644 --- a/Firestore/core/test/firebase/firestore/immutable/array_sorted_map_test.cc +++ b/Firestore/core/test/firebase/firestore/immutable/array_sorted_map_test.cc @@ -16,6 +16,7 @@ #include "Firestore/core/src/firebase/firestore/immutable/array_sorted_map.h" +#include #include #include @@ -43,6 +44,12 @@ TEST(ArraySortedMap, ChecksSize) { ASSERT_ANY_THROW(map.insert(next, next)); } +TEST(ArraySortedMap, InitializerIsSorted) { + IntMap map{{3, 0}, {2, 0}, {1, 0}}; + + EXPECT_TRUE(std::is_sorted(map.begin(), map.end())); +} + } // namespace impl } // namespace immutable } // namespace firestore diff --git a/Firestore/core/test/firebase/firestore/immutable/sorted_map_test.cc b/Firestore/core/test/firebase/firestore/immutable/sorted_map_test.cc index acd06429a47..2c26c45b403 100644 --- a/Firestore/core/test/firebase/firestore/immutable/sorted_map_test.cc +++ b/Firestore/core/test/firebase/firestore/immutable/sorted_map_test.cc @@ -36,8 +36,6 @@ #include "Firestore/core/test/firebase/firestore/immutable/testing.h" #include "gtest/gtest.h" -using firebase::firestore::immutable::impl::SortedMapBase; - namespace firebase { namespace firestore { namespace immutable { diff --git a/Firestore/core/test/firebase/firestore/immutable/sorted_set_test.cc b/Firestore/core/test/firebase/firestore/immutable/sorted_set_test.cc index a4b337cb166..3239fdafe8b 100644 --- a/Firestore/core/test/firebase/firestore/immutable/sorted_set_test.cc +++ b/Firestore/core/test/firebase/firestore/immutable/sorted_set_test.cc @@ -21,13 +21,12 @@ #include "Firestore/core/test/firebase/firestore/immutable/testing.h" -using firebase::firestore::immutable::impl::SortedMapBase; -using SizeType = SortedMapBase::size_type; - namespace firebase { namespace firestore { namespace immutable { +using SizeType = SortedContainer::size_type; + template SortedSet ToSet(const std::vector& container) { SortedSet result; diff --git a/Firestore/core/test/firebase/firestore/immutable/tree_sorted_map_test.cc b/Firestore/core/test/firebase/firestore/immutable/tree_sorted_map_test.cc index 4f9ad3eb067..05c600c10b7 100644 --- a/Firestore/core/test/firebase/firestore/immutable/tree_sorted_map_test.cc +++ b/Firestore/core/test/firebase/firestore/immutable/tree_sorted_map_test.cc @@ -16,6 +16,8 @@ #include "Firestore/core/src/firebase/firestore/immutable/tree_sorted_map.h" +#include + #include "Firestore/core/src/firebase/firestore/util/secure_random.h" #include "Firestore/core/test/firebase/firestore/immutable/testing.h" #include "gtest/gtest.h" @@ -221,6 +223,13 @@ TEST(TreeSortedMap, InsertIsImmutable) { EXPECT_TRUE(original.root().right().empty()); } +TEST(TreeSortedMap, InitializerIsSorted) { + IntMap map = IntMap::Create( + std::vector{{3, 0}, {2, 0}, {1, 0}}, {}); + + EXPECT_TRUE(std::is_sorted(map.begin(), map.end())); +} + } // namespace impl } // namespace immutable } // namespace firestore diff --git a/Firestore/core/test/firebase/firestore/local/CMakeLists.txt b/Firestore/core/test/firebase/firestore/local/CMakeLists.txt index 8a97514fa46..f178b2baed6 100644 --- a/Firestore/core/test/firebase/firestore/local/CMakeLists.txt +++ b/Firestore/core/test/firebase/firestore/local/CMakeLists.txt @@ -26,7 +26,10 @@ endif() cc_test( firebase_firestore_local_test SOURCES + #index_manager_test.mm + #leveldb_index_manager_test.mm local_serializer_test.cc + #memory_index_manager_test.mm DEPENDS firebase_firestore_local firebase_firestore_model diff --git a/Firestore/core/test/firebase/firestore/local/index_manager_test.h b/Firestore/core/test/firebase/firestore/local/index_manager_test.h new file mode 100644 index 00000000000..5c51424625b --- /dev/null +++ b/Firestore/core/test/firebase/firestore/local/index_manager_test.h @@ -0,0 +1,62 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_LOCAL_INDEX_MANAGER_TEST_H_ +#define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_LOCAL_INDEX_MANAGER_TEST_H_ + +#if !defined(__OBJC__) +#error "For now, this file must only be included by ObjC source files." +#endif // !defined(__OBJC__) + +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/local/index_manager.h" +#include "gtest/gtest.h" + +#import "Firestore/Source/Local/FSTPersistence.h" + +NS_ASSUME_NONNULL_BEGIN + +namespace firebase { +namespace firestore { +namespace local { + +using FactoryFunc = id _Nonnull (*)(); + +class IndexManagerTest : public ::testing::TestWithParam { + public: + // `GetParam()` must return a factory function. + IndexManagerTest() : persistence{GetParam()()} { + } + + id persistence; + + virtual ~IndexManagerTest(); + + protected: + void AssertParents(const std::string &collection_id, + std::vector expected); +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +NS_ASSUME_NONNULL_END + +#endif // FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_LOCAL_INDEX_MANAGER_TEST_H_ diff --git a/Firestore/core/test/firebase/firestore/local/index_manager_test.mm b/Firestore/core/test/firebase/firestore/local/index_manager_test.mm new file mode 100644 index 00000000000..a3f88631ab1 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/local/index_manager_test.mm @@ -0,0 +1,75 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include +#include +#include +#include + +#include "Firestore/core/test/firebase/firestore/local/index_manager_test.h" + +#include "Firestore/core/src/firebase/firestore/local/index_manager.h" +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace local { + +using model::ResourcePath; + +void IndexManagerTest::AssertParents(const std::string &collection_id, + std::vector expected) { + IndexManager *index_manager = persistence.indexManager; + std::vector actual_paths = + index_manager->GetCollectionParents(collection_id); + std::vector actual; + for (const ResourcePath &actual_path : actual_paths) { + actual.push_back(actual_path.CanonicalString()); + } + std::sort(expected.begin(), expected.end()); + std::sort(actual.begin(), actual.end()); + + SCOPED_TRACE("AssertParents(\"" + collection_id + "\", ...)"); + EXPECT_EQ(actual, expected); +} + +IndexManagerTest::~IndexManagerTest() { + [persistence shutdown]; +} + +TEST_P(IndexManagerTest, AddAndReadCollectionParentIndexEntries) { + IndexManager *index_manager = persistence.indexManager; + persistence.run("AddAndReadCollectionParentIndexEntries", [&]() { + index_manager->AddToCollectionParentIndex(ResourcePath{"messages"}); + index_manager->AddToCollectionParentIndex(ResourcePath{"messages"}); + index_manager->AddToCollectionParentIndex( + ResourcePath{"rooms", "foo", "messages"}); + index_manager->AddToCollectionParentIndex( + ResourcePath{"rooms", "bar", "messages"}); + index_manager->AddToCollectionParentIndex( + ResourcePath{"rooms", "foo", "messages2"}); + + AssertParents("messages", + std::vector{"", "rooms/bar", "rooms/foo"}); + AssertParents("messages2", std::vector{"rooms/foo"}); + AssertParents("messages3", std::vector{}); + }); +} + +} // namespace local +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/local/leveldb_index_manager_test.mm b/Firestore/core/test/firebase/firestore/local/leveldb_index_manager_test.mm new file mode 100644 index 00000000000..a68581b17b1 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/local/leveldb_index_manager_test.mm @@ -0,0 +1,49 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/test/firebase/firestore/local/index_manager_test.h" + +#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" +#import "Firestore/Source/Local/FSTPersistence.h" + +#include "Firestore/core/src/firebase/firestore/local/leveldb_index_manager.h" +#include "absl/memory/memory.h" +#include "gtest/gtest.h" + +NS_ASSUME_NONNULL_BEGIN + +namespace firebase { +namespace firestore { +namespace local { + +namespace { + +id PersistenceFactory() { + return static_cast>( + [FSTPersistenceTestHelpers levelDBPersistence]); +} + +} // namespace + +INSTANTIATE_TEST_CASE_P(LevelDbIndexManagerTest, + IndexManagerTest, + ::testing::Values(PersistenceFactory)); + +NS_ASSUME_NONNULL_END + +} // namespace local +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/local/leveldb_key_test.cc b/Firestore/core/test/firebase/firestore/local/leveldb_key_test.cc index c2b1ea622e8..c6843f76943 100644 --- a/Firestore/core/test/firebase/firestore/local/leveldb_key_test.cc +++ b/Firestore/core/test/firebase/firestore/local/leveldb_key_test.cc @@ -201,11 +201,11 @@ TEST(LevelDbDocumentMutationKeyTest, Description) { auto key = LevelDbDocumentMutationKey::KeyPrefix( "user1", testutil::Resource("foo/bar")); AssertExpectedKeyDescription( - "[document_mutation: user_id=user1 key=foo/bar incomplete key]", key); + "[document_mutation: user_id=user1 path=foo/bar incomplete key]", key); key = LevelDbDocumentMutationKey::Key("user1", testutil::Key("foo/bar"), 42); AssertExpectedKeyDescription( - "[document_mutation: user_id=user1 key=foo/bar batch_id=42]", key); + "[document_mutation: user_id=user1 path=foo/bar batch_id=42]", key); } TEST(LevelDbTargetGlobalKeyTest, EncodeDecodeCycle) { @@ -279,7 +279,7 @@ TEST(TargetDocumentKeyTest, Ordering) { TEST(TargetDocumentKeyTest, Description) { auto key = LevelDbTargetDocumentKey::Key(42, testutil::Key("foo/bar")); - ASSERT_EQ("[target_document: target_id=42 key=foo/bar]", DescribeKey(key)); + ASSERT_EQ("[target_document: target_id=42 path=foo/bar]", DescribeKey(key)); } TEST(DocumentTargetKeyTest, EncodeDecodeCycle) { @@ -294,7 +294,7 @@ TEST(DocumentTargetKeyTest, EncodeDecodeCycle) { TEST(DocumentTargetKeyTest, Description) { auto key = LevelDbDocumentTargetKey::Key(testutil::Key("foo/bar"), 42); - ASSERT_EQ("[document_target: key=foo/bar target_id=42]", DescribeKey(key)); + ASSERT_EQ("[document_target: path=foo/bar target_id=42]", DescribeKey(key)); } TEST(DocumentTargetKeyTest, Ordering) { @@ -352,7 +352,7 @@ TEST(RemoteDocumentKeyTest, EncodeDecodeCycle) { TEST(RemoteDocumentKeyTest, Description) { AssertExpectedKeyDescription( - "[remote_document: key=foo/bar/baz/quux]", + "[remote_document: path=foo/bar/baz/quux]", LevelDbRemoteDocumentKey::Key(testutil::Key("foo/bar/baz/quux"))); } diff --git a/Firestore/core/test/firebase/firestore/local/local_serializer_test.cc b/Firestore/core/test/firebase/firestore/local/local_serializer_test.cc index f7d739c461f..fa045134696 100644 --- a/Firestore/core/test/firebase/firestore/local/local_serializer_test.cc +++ b/Firestore/core/test/firebase/firestore/local/local_serializer_test.cc @@ -58,6 +58,7 @@ using model::MaybeDocument; using model::Mutation; using model::MutationBatch; using model::NoDocument; +using model::ObjectValue; using model::PatchMutation; using model::Precondition; using model::SetMutation; @@ -258,8 +259,8 @@ TEST_F(LocalSerializerTest, EncodesMutationBatch) { {"num", FieldValue::FromInteger(1)}}); std::unique_ptr patch = absl::make_unique( Key("bar/baz"), - FieldValue::FromMap({{"a", FieldValue::FromString("b")}, - {"num", FieldValue::FromInteger(1)}}), + ObjectValue::FromMap({{"a", FieldValue::FromString("b")}, + {"num", FieldValue::FromInteger(1)}}), FieldMask({FieldPath({"a"})}), Precondition::Exists(true)); std::unique_ptr del = testutil::DeleteMutation("baz/quux"); @@ -309,8 +310,8 @@ TEST_F(LocalSerializerTest, EncodesMutationBatch) { } TEST_F(LocalSerializerTest, EncodesDocumentAsMaybeDocument) { - Document doc = Doc("some/path", /*version=*/42, - {{"foo", FieldValue::FromString("bar")}}); + Document doc = *Doc("some/path", /*version=*/42, + {{"foo", FieldValue::FromString("bar")}}); ::firestore::client::MaybeDocument maybe_doc_proto; maybe_doc_proto.mutable_document()->set_name( @@ -326,7 +327,7 @@ TEST_F(LocalSerializerTest, EncodesDocumentAsMaybeDocument) { } TEST_F(LocalSerializerTest, EncodesNoDocumentAsMaybeDocument) { - NoDocument no_doc = DeletedDoc("some/path", /*version=*/42); + NoDocument no_doc = *DeletedDoc("some/path", /*version=*/42); ::firestore::client::MaybeDocument maybe_doc_proto; maybe_doc_proto.mutable_no_document()->set_name( @@ -338,7 +339,7 @@ TEST_F(LocalSerializerTest, EncodesNoDocumentAsMaybeDocument) { } TEST_F(LocalSerializerTest, EncodesUnknownDocumentAsMaybeDocument) { - UnknownDocument unknown_doc = UnknownDoc("some/path", /*version=*/42); + UnknownDocument unknown_doc = *UnknownDoc("some/path", /*version=*/42); ::firestore::client::MaybeDocument maybe_doc_proto; maybe_doc_proto.mutable_unknown_document()->set_name( diff --git a/Firestore/core/test/firebase/firestore/local/memory_index_manager_test.mm b/Firestore/core/test/firebase/firestore/local/memory_index_manager_test.mm new file mode 100644 index 00000000000..28039b37243 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/local/memory_index_manager_test.mm @@ -0,0 +1,49 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/test/firebase/firestore/local/index_manager_test.h" + +#include "Firestore/core/src/firebase/firestore/local/memory_index_manager.h" +#include "absl/memory/memory.h" +#include "gtest/gtest.h" + +#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" +#import "Firestore/Source/Local/FSTPersistence.h" + +NS_ASSUME_NONNULL_BEGIN + +namespace firebase { +namespace firestore { +namespace local { + +namespace { + +id PersistenceFactory() { + return static_cast>( + [FSTPersistenceTestHelpers lruMemoryPersistence]); +} + +} // namespace + +INSTANTIATE_TEST_CASE_P(MemoryIndexManagerTest, + IndexManagerTest, + ::testing::Values(PersistenceFactory)); + +NS_ASSUME_NONNULL_END + +} // namespace local +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/model/document_test.cc b/Firestore/core/test/firebase/firestore/model/document_test.cc index ba62782889b..ae09b75825d 100644 --- a/Firestore/core/test/firebase/firestore/model/document_test.cc +++ b/Firestore/core/test/firebase/firestore/model/document_test.cc @@ -16,6 +16,7 @@ #include "Firestore/core/src/firebase/firestore/model/document.h" +#include "Firestore/core/src/firebase/firestore/model/field_value.h" #include "Firestore/core/src/firebase/firestore/model/unknown_document.h" #include "absl/strings/string_view.h" @@ -32,7 +33,7 @@ inline Document MakeDocument(const absl::string_view data, const Timestamp& timestamp, DocumentState document_state) { return Document( - FieldValue::FromMap({{"field", FieldValue::FromString(data.data())}}), + ObjectValue::FromMap({{"field", FieldValue::FromString(data.data())}}), DocumentKey::FromPathString(path.data()), SnapshotVersion(timestamp), document_state); } @@ -43,7 +44,7 @@ TEST(Document, Getter) { const Document& doc = MakeDocument("foo", "i/am/a/path", Timestamp(123, 456), DocumentState::kLocalMutations); EXPECT_EQ(MaybeDocument::Type::Document, doc.type()); - EXPECT_EQ(FieldValue::FromMap({{"field", FieldValue::FromString("foo")}}), + EXPECT_EQ(ObjectValue::FromMap({{"field", FieldValue::FromString("foo")}}), doc.data()); EXPECT_EQ(DocumentKey::FromPathString("i/am/a/path"), doc.key()); EXPECT_EQ(SnapshotVersion(Timestamp(123, 456)), doc.version()); @@ -74,11 +75,11 @@ TEST(Document, Comparison) { // Document and MaybeDocument will not equal. In particular, Document and // NoDocument will not equal, which I won't test here. - EXPECT_NE(Document(FieldValue::FromMap({}), - DocumentKey::FromPathString("same/path"), - SnapshotVersion(Timestamp()), DocumentState::kSynced), - UnknownDocument(DocumentKey::FromPathString("same/path"), - SnapshotVersion(Timestamp()))); + EXPECT_NE( + Document(ObjectValue::Empty(), DocumentKey::FromPathString("same/path"), + SnapshotVersion(Timestamp()), DocumentState::kSynced), + UnknownDocument(DocumentKey::FromPathString("same/path"), + SnapshotVersion(Timestamp()))); } } // namespace model diff --git a/Firestore/core/test/firebase/firestore/model/field_value_test.cc b/Firestore/core/test/firebase/firestore/model/field_value_test.cc index 69593aa46f8..b1d1f8b70b5 100644 --- a/Firestore/core/test/firebase/firestore/model/field_value_test.cc +++ b/Firestore/core/test/firebase/firestore/model/field_value_test.cc @@ -189,19 +189,16 @@ TEST(FieldValue, ArrayType) { } TEST(FieldValue, ObjectType) { - const FieldValue empty = FieldValue::FromMap({}); - ObjectValue::Map object{{"null", FieldValue::Null()}, - {"true", FieldValue::True()}, - {"false", FieldValue::False()}}; + const ObjectValue empty = ObjectValue::Empty(); + FieldValue::Map object{{"null", FieldValue::Null()}, + {"true", FieldValue::True()}, + {"false", FieldValue::False()}}; // copy the map - const FieldValue small = FieldValue::FromMap(object); - ObjectValue::Map another_object{{"null", FieldValue::Null()}, - {"true", FieldValue::False()}}; + const ObjectValue small = ObjectValue::FromMap(object); + FieldValue::Map another_object{{"null", FieldValue::Null()}, + {"true", FieldValue::False()}}; // move the array - const FieldValue large = FieldValue::FromMap(std::move(another_object)); - EXPECT_EQ(Type::Object, empty.type()); - EXPECT_EQ(Type::Object, small.type()); - EXPECT_EQ(Type::Object, large.type()); + const ObjectValue large = ObjectValue::FromMap(std::move(another_object)); EXPECT_TRUE(empty < small); EXPECT_FALSE(small < empty); EXPECT_FALSE(small < small); @@ -334,18 +331,18 @@ TEST(FieldValue, Copy) { clone = null_value; EXPECT_EQ(FieldValue::Null(), clone); - const FieldValue object_value = FieldValue::FromMap(ObjectValue::Map{ - {"true", FieldValue::True()}, {"false", FieldValue::False()}}); + const FieldValue object_value = FieldValue::FromMap( + {{"true", FieldValue::True()}, {"false", FieldValue::False()}}); clone = object_value; - EXPECT_EQ(FieldValue::FromMap(ObjectValue::Map{ - {"true", FieldValue::True()}, {"false", FieldValue::False()}}), + EXPECT_EQ(FieldValue::FromMap( + {{"true", FieldValue::True()}, {"false", FieldValue::False()}}), clone); - EXPECT_EQ(FieldValue::FromMap(ObjectValue::Map{ - {"true", FieldValue::True()}, {"false", FieldValue::False()}}), + EXPECT_EQ(FieldValue::FromMap( + {{"true", FieldValue::True()}, {"false", FieldValue::False()}}), object_value); clone = *&clone; - EXPECT_EQ(FieldValue::FromMap(ObjectValue::Map{ - {"true", FieldValue::True()}, {"false", FieldValue::False()}}), + EXPECT_EQ(FieldValue::FromMap( + {{"true", FieldValue::True()}, {"false", FieldValue::False()}}), clone); clone = null_value; EXPECT_EQ(FieldValue::Null(), clone); @@ -425,11 +422,11 @@ TEST(FieldValue, Move) { clone = FieldValue::Null(); EXPECT_EQ(FieldValue::Null(), clone); - FieldValue object_value = FieldValue::FromMap(ObjectValue::Map{ - {"true", FieldValue::True()}, {"false", FieldValue::False()}}); + FieldValue object_value = FieldValue::FromMap( + {{"true", FieldValue::True()}, {"false", FieldValue::False()}}); clone = std::move(object_value); - EXPECT_EQ(FieldValue::FromMap(ObjectValue::Map{ - {"true", FieldValue::True()}, {"false", FieldValue::False()}}), + EXPECT_EQ(FieldValue::FromMap( + {{"true", FieldValue::True()}, {"false", FieldValue::False()}}), clone); clone = FieldValue::Null(); EXPECT_EQ(FieldValue::Null(), clone); @@ -448,7 +445,7 @@ TEST(FieldValue, CompareMixedType) { const FieldValue geo_point_value = FieldValue::FromGeoPoint({1, 2}); const FieldValue array_value = FieldValue::FromArray(std::vector()); - const FieldValue object_value = FieldValue::FromMap({}); + const FieldValue object_value = FieldValue::EmptyObject(); EXPECT_TRUE(null_value < true_value); EXPECT_TRUE(true_value < number_value); EXPECT_TRUE(number_value < timestamp_value); @@ -489,13 +486,13 @@ TEST(FieldValue, CompareWithOperator) { TEST(FieldValue, Set) { // Set a field in an object. - const FieldValue value = FieldValue::FromMap({ + const ObjectValue value = ObjectValue::FromMap({ {"a", FieldValue::FromString("A")}, {"b", FieldValue::FromMap({ {"ba", FieldValue::FromString("BA")}, })}, }); - const FieldValue expected = FieldValue::FromMap({ + const ObjectValue expected = ObjectValue::FromMap({ {"a", FieldValue::FromString("A")}, {"b", FieldValue::FromMap({ {"ba", FieldValue::FromString("BA")}, @@ -508,10 +505,10 @@ TEST(FieldValue, Set) { TEST(FieldValue, SetRecursive) { // Set a field in a new object. - const FieldValue value = FieldValue::FromMap({ + const ObjectValue value = ObjectValue::FromMap({ {"a", FieldValue::FromString("A")}, }); - const FieldValue expected = FieldValue::FromMap({ + const ObjectValue expected = ObjectValue::FromMap({ {"a", FieldValue::FromString("A")}, {"b", FieldValue::FromMap({ {"bb", FieldValue::FromString("BB")}, @@ -522,14 +519,14 @@ TEST(FieldValue, SetRecursive) { } TEST(FieldValue, Delete) { - const FieldValue value = FieldValue::FromMap({ + const ObjectValue value = ObjectValue::FromMap({ {"a", FieldValue::FromString("A")}, {"b", FieldValue::FromMap({ {"ba", FieldValue::FromString("BA")}, {"bb", FieldValue::FromString("BB")}, })}, }); - const FieldValue expected = FieldValue::FromMap({ + const ObjectValue expected = ObjectValue::FromMap({ {"a", FieldValue::FromString("A")}, {"b", FieldValue::FromMap({ {"ba", FieldValue::FromString("BA")}, @@ -539,7 +536,7 @@ TEST(FieldValue, Delete) { } TEST(FieldValue, DeleteNothing) { - const FieldValue value = FieldValue::FromMap({ + const ObjectValue value = ObjectValue::FromMap({ {"a", FieldValue::FromString("A")}, {"b", FieldValue::FromMap({ {"ba", FieldValue::FromString("BA")}, @@ -550,7 +547,7 @@ TEST(FieldValue, DeleteNothing) { } TEST(FieldValue, Get) { - const FieldValue value = FieldValue::FromMap({ + const ObjectValue value = ObjectValue::FromMap({ {"a", FieldValue::FromString("A")}, {"b", FieldValue::FromMap({ {"ba", FieldValue::FromString("BA")}, @@ -563,7 +560,7 @@ TEST(FieldValue, Get) { } TEST(FieldValue, GetNothing) { - const FieldValue value = FieldValue::FromMap({ + const ObjectValue value = ObjectValue::FromMap({ {"a", FieldValue::FromString("A")}, {"b", FieldValue::FromMap({ {"ba", FieldValue::FromString("BA")}, @@ -578,7 +575,7 @@ TEST(FieldValue, IsSmallish) { // We expect the FV to use 4 bytes to track the type of the union, plus 8 // bytes for the union contents themselves. The other 4 is for padding. We // want to keep FV as small as possible. - EXPECT_LE(sizeof(FieldValue), 2 * sizeof(void*)); + EXPECT_LE(sizeof(FieldValue), 2 * sizeof(int64_t)); } } // namespace model diff --git a/Firestore/core/test/firebase/firestore/model/mutation_test.cc b/Firestore/core/test/firebase/firestore/model/mutation_test.cc index 9e57b11a635..8e3ad04ac1a 100644 --- a/Firestore/core/test/firebase/firestore/model/mutation_test.cc +++ b/Firestore/core/test/firebase/firestore/model/mutation_test.cc @@ -20,6 +20,7 @@ #include "Firestore/core/src/firebase/firestore/model/document.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" +#include "Firestore/core/src/firebase/firestore/model/maybe_document.h" #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" #include "gtest/gtest.h" @@ -35,10 +36,10 @@ using testutil::PatchMutation; using testutil::SetMutation; TEST(Mutation, AppliesSetsToDocuments) { - auto base_doc = std::make_shared( + MaybeDocumentPtr base_doc = Doc("collection/key", 0, {{"foo", FieldValue::FromString("foo-value")}, - {"baz", FieldValue::FromString("baz-value")}})); + {"baz", FieldValue::FromString("baz-value")}}); std::unique_ptr set = SetMutation( "collection/key", {{"bar", FieldValue::FromString("bar-value")}}); @@ -46,17 +47,17 @@ TEST(Mutation, AppliesSetsToDocuments) { set->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now()); ASSERT_NE(set_doc, nullptr); ASSERT_EQ(set_doc->type(), MaybeDocument::Type::Document); - EXPECT_EQ(*set_doc.get(), Doc("collection/key", 0, - {{"bar", FieldValue::FromString("bar-value")}}, - DocumentState::kLocalMutations)); + EXPECT_EQ(*set_doc, *Doc("collection/key", 0, + {{"bar", FieldValue::FromString("bar-value")}}, + DocumentState::kLocalMutations)); } TEST(Mutation, AppliesPatchToDocuments) { - auto base_doc = std::make_shared(Doc( + MaybeDocumentPtr base_doc = Doc( "collection/key", 0, {{"foo", FieldValue::FromMap({{"bar", FieldValue::FromString("bar-value")}})}, - {"baz", FieldValue::FromString("baz-value")}})); + {"baz", FieldValue::FromString("baz-value")}}); std::unique_ptr patch = PatchMutation( "collection/key", {{"foo.bar", FieldValue::FromString("new-bar-value")}}); @@ -64,16 +65,16 @@ TEST(Mutation, AppliesPatchToDocuments) { patch->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now()); ASSERT_NE(local, nullptr); EXPECT_EQ( - *local.get(), - Doc("collection/key", 0, - {{"foo", FieldValue::FromMap( - {{"bar", FieldValue::FromString("new-bar-value")}})}, - {"baz", FieldValue::FromString("baz-value")}}, - DocumentState::kLocalMutations)); + *local, + *Doc("collection/key", 0, + {{"foo", FieldValue::FromMap( + {{"bar", FieldValue::FromString("new-bar-value")}})}, + {"baz", FieldValue::FromString("baz-value")}}, + DocumentState::kLocalMutations)); } TEST(Mutation, AppliesPatchWithMergeToDocuments) { - auto base_doc = std::make_shared(DeletedDoc("collection/key", 0)); + MaybeDocumentPtr base_doc = DeletedDoc("collection/key", 0); std::unique_ptr upsert = PatchMutation( "collection/key", {{"foo.bar", FieldValue::FromString("new-bar-value")}}, @@ -82,11 +83,11 @@ TEST(Mutation, AppliesPatchWithMergeToDocuments) { upsert->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now()); ASSERT_NE(new_doc, nullptr); EXPECT_EQ( - *new_doc.get(), - Doc("collection/key", 0, - {{"foo", FieldValue::FromMap( - {{"bar", FieldValue::FromString("new-bar-value")}})}}, - DocumentState::kLocalMutations)); + *new_doc, + *Doc("collection/key", 0, + {{"foo", FieldValue::FromMap( + {{"bar", FieldValue::FromString("new-bar-value")}})}}, + DocumentState::kLocalMutations)); } TEST(Mutation, AppliesPatchToNullDocWithMergeToDocuments) { @@ -99,38 +100,38 @@ TEST(Mutation, AppliesPatchToNullDocWithMergeToDocuments) { upsert->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now()); ASSERT_NE(new_doc, nullptr); EXPECT_EQ( - *new_doc.get(), - Doc("collection/key", 0, - {{"foo", FieldValue::FromMap( - {{"bar", FieldValue::FromString("new-bar-value")}})}}, - DocumentState::kLocalMutations)); + *new_doc, + *Doc("collection/key", 0, + {{"foo", FieldValue::FromMap( + {{"bar", FieldValue::FromString("new-bar-value")}})}}, + DocumentState::kLocalMutations)); } TEST(Mutation, DeletesValuesFromTheFieldMask) { - auto base_doc = std::make_shared(Doc( + MaybeDocumentPtr base_doc = Doc( "collection/key", 0, {{"foo", FieldValue::FromMap({{"bar", FieldValue::FromString("bar-value")}, - {"baz", FieldValue::FromString("baz-value")}})}})); + {"baz", FieldValue::FromString("baz-value")}})}}); std::unique_ptr patch = - PatchMutation("collection/key", {}, {Field("foo.bar")}); + PatchMutation("collection/key", FieldValue::Map(), {Field("foo.bar")}); MaybeDocumentPtr patch_doc = patch->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now()); ASSERT_NE(patch_doc, nullptr); - EXPECT_EQ(*patch_doc.get(), - Doc("collection/key", 0, - {{"foo", FieldValue::FromMap( - {{"baz", FieldValue::FromString("baz-value")}})}}, - DocumentState::kLocalMutations)); + EXPECT_EQ(*patch_doc, + *Doc("collection/key", 0, + {{"foo", FieldValue::FromMap( + {{"baz", FieldValue::FromString("baz-value")}})}}, + DocumentState::kLocalMutations)); } TEST(Mutation, PatchesPrimitiveValue) { - auto base_doc = std::make_shared( + MaybeDocumentPtr base_doc = Doc("collection/key", 0, {{"foo", FieldValue::FromString("foo-value")}, - {"baz", FieldValue::FromString("baz-value")}})); + {"baz", FieldValue::FromString("baz-value")}}); std::unique_ptr patch = PatchMutation( "collection/key", {{"foo.bar", FieldValue::FromString("new-bar-value")}}); @@ -139,22 +140,51 @@ TEST(Mutation, PatchesPrimitiveValue) { patch->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now()); ASSERT_NE(patched_doc, nullptr); EXPECT_EQ( - *patched_doc.get(), - Doc("collection/key", 0, - {{"foo", FieldValue::FromMap( - {{"bar", FieldValue::FromString("new-bar-value")}})}, - {"baz", FieldValue::FromString("baz-value")}}, - DocumentState::kLocalMutations)); + *patched_doc, + *Doc("collection/key", 0, + {{"foo", FieldValue::FromMap( + {{"bar", FieldValue::FromString("new-bar-value")}})}, + {"baz", FieldValue::FromString("baz-value")}}, + DocumentState::kLocalMutations)); } TEST(Mutation, PatchingDeletedDocumentsDoesNothing) { - // TODO(rsgowman) + MaybeDocumentPtr base_doc = testutil::DeletedDoc("collection/key", 0); + std::unique_ptr patch = + PatchMutation("collection/key", {{"foo", FieldValue::FromString("bar")}}); + MaybeDocumentPtr patched_doc = + patch->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now()); + EXPECT_EQ(base_doc, patched_doc); } TEST(Mutation, AppliesLocalServerTimestampTransformsToDocuments) { // TODO(rsgowman) } +TEST(Mutation, AppliesIncrementTransformToDocument) { + // TODO(rsgowman) +} + +TEST(Mutation, AppliesIncrementTransformToUnexpectedType) { + // TODO(rsgowman) +} + +TEST(Mutation, AppliesIncrementTransformToMissingField) { + // TODO(rsgowman) +} + +TEST(Mutation, AppliesIncrementTransformsConsecutively) { + // TODO(rsgowman) +} + +TEST(Mutation, AppliesIncrementWithoutOverflow) { + // TODO(rsgowman) +} + +TEST(Mutation, AppliesIncrementWithoutUnderflow) { + // TODO(rsgowman) +} + TEST(Mutation, CreatesArrayUnionTransform) { // TODO(rsgowman) } @@ -221,20 +251,20 @@ TEST(Mutation, AppliesServerAckedArrayTransformsToDocuments) { } TEST(Mutation, DeleteDeletes) { - auto base_doc = std::make_shared( - Doc("collection/key", 0, {{"foo", FieldValue::FromString("bar")}})); + MaybeDocumentPtr base_doc = + Doc("collection/key", 0, {{"foo", FieldValue::FromString("bar")}}); std::unique_ptr del = testutil::DeleteMutation("collection/key"); MaybeDocumentPtr deleted_doc = del->ApplyToLocalView(base_doc, base_doc.get(), Timestamp::Now()); ASSERT_NE(deleted_doc, nullptr); - EXPECT_EQ(*deleted_doc.get(), testutil::DeletedDoc("collection/key", 0)); + EXPECT_EQ(*deleted_doc, *testutil::DeletedDoc("collection/key", 0)); } TEST(Mutation, SetWithMutationResult) { - auto base_doc = std::make_shared( - Doc("collection/key", 0, {{"foo", FieldValue::FromString("bar")}})); + MaybeDocumentPtr base_doc = + Doc("collection/key", 0, {{"foo", FieldValue::FromString("bar")}}); std::unique_ptr set = SetMutation( "collection/key", {{"foo", FieldValue::FromString("new-bar")}}); @@ -242,14 +272,14 @@ TEST(Mutation, SetWithMutationResult) { set->ApplyToRemoteDocument(base_doc, MutationResult(4)); ASSERT_NE(set_doc, nullptr); - EXPECT_EQ(*set_doc.get(), Doc("collection/key", 4, - {{"foo", FieldValue::FromString("new-bar")}}, - DocumentState::kCommittedMutations)); + EXPECT_EQ(*set_doc, *Doc("collection/key", 4, + {{"foo", FieldValue::FromString("new-bar")}}, + DocumentState::kCommittedMutations)); } TEST(Mutation, PatchWithMutationResult) { - auto base_doc = std::make_shared( - Doc("collection/key", 0, {{"foo", FieldValue::FromString("bar")}})); + MaybeDocumentPtr base_doc = + Doc("collection/key", 0, {{"foo", FieldValue::FromString("bar")}}); std::unique_ptr patch = PatchMutation( "collection/key", {{"foo", FieldValue::FromString("new-bar")}}); @@ -257,9 +287,9 @@ TEST(Mutation, PatchWithMutationResult) { patch->ApplyToRemoteDocument(base_doc, MutationResult(4)); ASSERT_NE(patch_doc, nullptr); - EXPECT_EQ(*patch_doc.get(), Doc("collection/key", 4, - {{"foo", FieldValue::FromString("new-bar")}}, - DocumentState::kCommittedMutations)); + EXPECT_EQ(*patch_doc, *Doc("collection/key", 4, + {{"foo", FieldValue::FromString("new-bar")}}, + DocumentState::kCommittedMutations)); } TEST(Mutation, Transitions) { diff --git a/Firestore/core/test/firebase/firestore/model/precondition_test.cc b/Firestore/core/test/firebase/firestore/model/precondition_test.cc index a755e29c14f..a610461da34 100644 --- a/Firestore/core/test/firebase/firestore/model/precondition_test.cc +++ b/Firestore/core/test/firebase/firestore/model/precondition_test.cc @@ -28,21 +28,21 @@ namespace firestore { namespace model { TEST(Precondition, None) { - const Precondition none = Precondition::None(); + Precondition none = Precondition::None(); EXPECT_EQ(Precondition::Type::None, none.type()); EXPECT_TRUE(none.IsNone()); EXPECT_EQ(SnapshotVersion::None(), none.update_time()); - const NoDocument deleted_doc = testutil::DeletedDoc("foo/doc", 1234567); - const Document doc = testutil::Doc("bar/doc", 7654321); + NoDocument deleted_doc = *testutil::DeletedDoc("foo/doc", 1234567); + Document doc = *testutil::Doc("bar/doc", 7654321); EXPECT_TRUE(none.IsValidFor(&deleted_doc)); EXPECT_TRUE(none.IsValidFor(&doc)); EXPECT_TRUE(none.IsValidFor(nullptr)); } TEST(Precondition, Exists) { - const Precondition exists = Precondition::Exists(true); - const Precondition no_exists = Precondition::Exists(false); + Precondition exists = Precondition::Exists(true); + Precondition no_exists = Precondition::Exists(false); EXPECT_EQ(Precondition::Type::Exists, exists.type()); EXPECT_EQ(Precondition::Type::Exists, no_exists.type()); EXPECT_FALSE(exists.IsNone()); @@ -50,8 +50,8 @@ TEST(Precondition, Exists) { EXPECT_EQ(SnapshotVersion::None(), exists.update_time()); EXPECT_EQ(SnapshotVersion::None(), no_exists.update_time()); - const NoDocument deleted_doc = testutil::DeletedDoc("foo/doc", 1234567); - const Document doc = testutil::Doc("bar/doc", 7654321); + NoDocument deleted_doc = *testutil::DeletedDoc("foo/doc", 1234567); + Document doc = *testutil::Doc("bar/doc", 7654321); EXPECT_FALSE(exists.IsValidFor(&deleted_doc)); EXPECT_TRUE(exists.IsValidFor(&doc)); EXPECT_FALSE(exists.IsValidFor(nullptr)); @@ -61,15 +61,15 @@ TEST(Precondition, Exists) { } TEST(Precondition, UpdateTime) { - const Precondition update_time = + Precondition update_time = Precondition::UpdateTime(testutil::Version(1234567)); EXPECT_EQ(Precondition::Type::UpdateTime, update_time.type()); EXPECT_FALSE(update_time.IsNone()); EXPECT_EQ(testutil::Version(1234567), update_time.update_time()); - const NoDocument deleted_doc = testutil::DeletedDoc("foo/doc", 1234567); - const Document not_match = testutil::Doc("bar/doc", 7654321); - const Document match = testutil::Doc("baz/doc", 1234567); + NoDocument deleted_doc = *testutil::DeletedDoc("foo/doc", 1234567); + Document not_match = *testutil::Doc("bar/doc", 7654321); + Document match = *testutil::Doc("baz/doc", 1234567); EXPECT_FALSE(update_time.IsValidFor(&deleted_doc)); EXPECT_FALSE(update_time.IsValidFor(¬_match)); EXPECT_TRUE(update_time.IsValidFor(&match)); diff --git a/Firestore/core/test/firebase/firestore/remote/datastore_test.mm b/Firestore/core/test/firebase/firestore/remote/datastore_test.mm index b8d376959c2..1719c476dab 100644 --- a/Firestore/core/test/firebase/firestore/remote/datastore_test.mm +++ b/Firestore/core/test/firebase/firestore/remote/datastore_test.mm @@ -16,6 +16,7 @@ #include #include +#include #include "Firestore/core/src/firebase/firestore/remote/datastore.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" @@ -172,11 +173,11 @@ void ForceFinishAnyTypeOrder( // Normal operation TEST_F(DatastoreTest, CommitMutationsSuccess) { - __block bool done = false; - __block NSError* resulting_error = nullptr; - datastore->CommitMutations(@[], ^(NSError* _Nullable error) { + bool done = false; + Status resulting_status; + datastore->CommitMutations({}, [&](const Status& status) { done = true; - resulting_error = error; + resulting_status = status; }); // Make sure Auth has a chance to run. worker_queue.EnqueueBlocking([] {}); @@ -184,20 +185,20 @@ void ForceFinishAnyTypeOrder( ForceFinish({{Type::Finish, grpc::Status::OK}}); EXPECT_TRUE(done); - EXPECT_EQ(resulting_error, nullptr); + EXPECT_TRUE(resulting_status.ok()); } TEST_F(DatastoreTest, LookupDocumentsOneSuccessfulRead) { - __block bool done = false; - __block NSArray* resulting_docs = nullptr; - __block NSError* resulting_error = nullptr; - datastore->LookupDocuments({}, - ^(NSArray* _Nullable documents, - NSError* _Nullable error) { - done = true; - resulting_docs = documents; - resulting_error = error; - }); + bool done = false; + std::vector resulting_docs; + Status resulting_status; + datastore->LookupDocuments( + {}, [&](const std::vector& documents, + const Status& status) { + done = true; + resulting_docs = documents; + resulting_status = status; + }); // Make sure Auth has a chance to run. worker_queue.EnqueueBlocking([] {}); @@ -207,23 +208,22 @@ void ForceFinishAnyTypeOrder( ForceFinish({{Type::Finish, grpc::Status::OK}}); EXPECT_TRUE(done); - ASSERT_NE(resulting_docs, nullptr); - EXPECT_EQ(resulting_docs.count, 1); - EXPECT_EQ([[resulting_docs objectAtIndex:0] key].ToString(), "foo/1"); - EXPECT_EQ(resulting_error, nullptr); + EXPECT_EQ(resulting_docs.size(), 1); + EXPECT_EQ(resulting_docs[0].key.ToString(), "foo/1"); + EXPECT_TRUE(resulting_status.ok()); } TEST_F(DatastoreTest, LookupDocumentsTwoSuccessfulReads) { - __block bool done = false; - __block NSArray* resulting_docs = nullptr; - __block NSError* resulting_error = nullptr; - datastore->LookupDocuments({}, - ^(NSArray* _Nullable documents, - NSError* _Nullable error) { - done = true; - resulting_docs = documents; - resulting_error = error; - }); + bool done = false; + std::vector resulting_docs; + Status resulting_status; + datastore->LookupDocuments( + {}, [&](const std::vector& documents, + const Status& status) { + done = true; + resulting_docs = documents; + resulting_status = status; + }); // Make sure Auth has a chance to run. worker_queue.EnqueueBlocking([] {}); @@ -234,21 +234,20 @@ void ForceFinishAnyTypeOrder( ForceFinish({{Type::Finish, grpc::Status::OK}}); EXPECT_TRUE(done); - ASSERT_NE(resulting_docs, nullptr); - EXPECT_EQ(resulting_docs.count, 2); - EXPECT_EQ([[resulting_docs objectAtIndex:0] key].ToString(), "foo/1"); - EXPECT_EQ([[resulting_docs objectAtIndex:1] key].ToString(), "foo/2"); - EXPECT_EQ(resulting_error, nullptr); + EXPECT_EQ(resulting_docs.size(), 2); + EXPECT_EQ(resulting_docs[0].key.ToString(), "foo/1"); + EXPECT_EQ(resulting_docs[1].key.ToString(), "foo/2"); + EXPECT_TRUE(resulting_status.ok()); } // gRPC errors TEST_F(DatastoreTest, CommitMutationsError) { - __block bool done = false; - __block NSError* resulting_error = nullptr; - datastore->CommitMutations(@[], ^(NSError* _Nullable error) { + bool done = false; + Status resulting_status; + datastore->CommitMutations({}, [&](const Status& status) { done = true; - resulting_error = error; + resulting_status = status; }); // Make sure Auth has a chance to run. worker_queue.EnqueueBlocking([] {}); @@ -256,18 +255,19 @@ void ForceFinishAnyTypeOrder( ForceFinish({{Type::Finish, grpc::Status{grpc::UNAVAILABLE, ""}}}); EXPECT_TRUE(done); - EXPECT_NE(resulting_error, nullptr); + EXPECT_FALSE(resulting_status.ok()); + EXPECT_EQ(resulting_status.code(), FirestoreErrorCode::Unavailable); } TEST_F(DatastoreTest, LookupDocumentsErrorBeforeFirstRead) { - __block bool done = false; - __block NSError* resulting_error = nullptr; - datastore->LookupDocuments({}, - ^(NSArray* _Nullable documents, - NSError* _Nullable error) { - done = true; - resulting_error = error; - }); + bool done = false; + Status resulting_status; + datastore->LookupDocuments( + {}, [&](const std::vector& documents, + const Status& status) { + done = true; + resulting_status = status; + }); // Make sure Auth has a chance to run. worker_queue.EnqueueBlocking([] {}); @@ -275,19 +275,20 @@ void ForceFinishAnyTypeOrder( ForceFinish({{Type::Finish, grpc::Status{grpc::UNAVAILABLE, ""}}}); EXPECT_TRUE(done); - EXPECT_NE(resulting_error, nullptr); + EXPECT_FALSE(resulting_status.ok()); + EXPECT_EQ(resulting_status.code(), FirestoreErrorCode::Unavailable); } TEST_F(DatastoreTest, LookupDocumentsErrorAfterFirstRead) { - __block bool done = false; - __block NSArray* resulting_docs = nullptr; - __block NSError* resulting_error = nullptr; - datastore->LookupDocuments({}, - ^(NSArray* _Nullable documents, - NSError* _Nullable error) { - done = true; - resulting_error = error; - }); + bool done = false; + std::vector resulting_docs; + Status resulting_status; + datastore->LookupDocuments( + {}, [&](const std::vector& documents, + const Status& status) { + done = true; + resulting_status = status; + }); // Make sure Auth has a chance to run. worker_queue.EnqueueBlocking([] {}); @@ -297,8 +298,9 @@ void ForceFinishAnyTypeOrder( ForceFinish({{Type::Finish, grpc::Status{grpc::UNAVAILABLE, ""}}}); EXPECT_TRUE(done); - EXPECT_EQ(resulting_docs, nullptr); - EXPECT_NE(resulting_error, nullptr); + EXPECT_TRUE(resulting_docs.empty()); + EXPECT_FALSE(resulting_status.ok()); + EXPECT_EQ(resulting_status.code(), FirestoreErrorCode::Unavailable); } // Auth errors @@ -306,31 +308,30 @@ void ForceFinishAnyTypeOrder( TEST_F(DatastoreTest, CommitMutationsAuthFailure) { credentials.FailGetToken(); - __block NSError* resulting_error = nullptr; - datastore->CommitMutations(@[], ^(NSError* _Nullable error) { - resulting_error = error; - }); + Status resulting_status; + datastore->CommitMutations( + {}, [&](const Status& status) { resulting_status = status; }); worker_queue.EnqueueBlocking([] {}); - EXPECT_NE(resulting_error, nullptr); + EXPECT_FALSE(resulting_status.ok()); } TEST_F(DatastoreTest, LookupDocumentsAuthFailure) { credentials.FailGetToken(); - __block NSError* resulting_error = nullptr; + Status resulting_status; datastore->LookupDocuments( - {}, ^(NSArray* docs, NSError* _Nullable error) { - resulting_error = error; + {}, [&](const std::vector&, const Status& status) { + resulting_status = status; }); worker_queue.EnqueueBlocking([] {}); - EXPECT_NE(resulting_error, nullptr); + EXPECT_FALSE(resulting_status.ok()); } TEST_F(DatastoreTest, AuthAfterDatastoreHasBeenShutDown) { credentials.DelayGetToken(); worker_queue.EnqueueBlocking([&] { - datastore->CommitMutations(@[], ^(NSError* _Nullable error) { + datastore->CommitMutations({}, [](const Status& status) { FAIL() << "Callback shouldn't be invoked"; }); }); @@ -343,7 +344,7 @@ void ForceFinishAnyTypeOrder( credentials.DelayGetToken(); worker_queue.EnqueueBlocking([&] { - datastore->CommitMutations(@[], ^(NSError* _Nullable error) { + datastore->CommitMutations({}, [](const Status& status) { FAIL() << "Callback shouldn't be invoked"; }); }); diff --git a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc index 96ce5052742..ec1c410a636 100644 --- a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc +++ b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc @@ -36,6 +36,7 @@ #include "Firestore/Protos/cpp/google/firestore/v1/firestore.pb.h" #include "Firestore/core/include/firebase/firestore/firestore_errors.h" #include "Firestore/core/include/firebase/firestore/timestamp.h" +#include "Firestore/core/src/firebase/firestore/model/field_path.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/nanopb/reader.h" @@ -195,11 +196,10 @@ class SerializerTest : public ::testing::Test { std::vector EncodeDocument(Serializer* serializer, const DocumentKey& key, - const FieldValue& value) { + const ObjectValue& value) { std::vector bytes; Writer writer = Writer::Wrap(&bytes); - google_firestore_v1_Document proto = - serializer->EncodeDocument(key, value.object_value()); + google_firestore_v1_Document proto = serializer->EncodeDocument(key, value); writer.WriteNanopbMessage(google_firestore_v1_Document_fields, &proto); serializer->FreeNanopbMessage(google_firestore_v1_Document_fields, &proto); return bytes; @@ -232,6 +232,16 @@ class SerializerTest : public ::testing::Test { return proto; } + v1::Value ValueProto(double d) { + std::vector bytes = + EncodeFieldValue(&serializer, FieldValue::FromDouble(d)); + v1::Value proto; + bool ok = + proto.ParseFromArray(bytes.data(), static_cast(bytes.size())); + EXPECT_TRUE(ok); + return proto; + } + v1::Value ValueProto(const char* s) { return ValueProto(std::string(s)); } @@ -256,6 +266,36 @@ class SerializerTest : public ::testing::Test { return proto; } + v1::Value ValueProto(const std::vector& blob) { + std::vector bytes = EncodeFieldValue( + &serializer, FieldValue::FromBlob(blob.data(), blob.size())); + v1::Value proto; + bool ok = + proto.ParseFromArray(bytes.data(), static_cast(bytes.size())); + EXPECT_TRUE(ok); + return proto; + } + + v1::Value ValueProto(const GeoPoint& geo_point) { + std::vector bytes = + EncodeFieldValue(&serializer, FieldValue::FromGeoPoint(geo_point)); + v1::Value proto; + bool ok = + proto.ParseFromArray(bytes.data(), static_cast(bytes.size())); + EXPECT_TRUE(ok); + return proto; + } + + v1::Value ValueProto(const std::vector& array) { + std::vector bytes = + EncodeFieldValue(&serializer, FieldValue::FromArray(array)); + v1::Value proto; + bool ok = + proto.ParseFromArray(bytes.data(), static_cast(bytes.size())); + EXPECT_TRUE(ok); + return proto; + } + /** * Creates entries in the proto that we don't care about. * @@ -316,7 +356,7 @@ class SerializerTest : public ::testing::Test { void ExpectSerializationRoundTrip( const DocumentKey& key, - const FieldValue& value, + const ObjectValue& value, const SnapshotVersion& update_time, const v1::BatchGetDocumentsResponse& proto) { std::vector bytes = EncodeDocument(&serializer, key, value); @@ -349,7 +389,7 @@ class SerializerTest : public ::testing::Test { void ExpectDeserializationRoundTrip( const DocumentKey& key, - const absl::optional value, + const absl::optional value, const SnapshotVersion& version, // either update_time or read_time const v1::BatchGetDocumentsResponse& proto) { size_t size = proto.ByteSizeLong(); @@ -424,6 +464,40 @@ TEST_F(SerializerTest, EncodesIntegers) { } } +TEST_F(SerializerTest, EncodesDoubles) { + // Not technically required at all. But if we run into a platform where this + // is false, then we'll have to eliminate a few of our test cases in this + // test. + static_assert(std::numeric_limits::is_iec559, + "IEC559/IEEE764 floating point required"); + + std::vector cases{-std::numeric_limits::infinity(), + std::numeric_limits::lowest(), + std::numeric_limits::min() - 1.0, + -2.0, + -1.1, + -1.0, + -std::numeric_limits::epsilon(), + -std::numeric_limits::min(), + -std::numeric_limits::denorm_min(), + -0.0, + 0.0, + std::numeric_limits::denorm_min(), + std::numeric_limits::min(), + std::numeric_limits::epsilon(), + 1.0, + 1.1, + 2.0, + std::numeric_limits::max() + 1.0, + std::numeric_limits::max(), + std::numeric_limits::infinity()}; + + for (double double_value : cases) { + FieldValue model = FieldValue::FromDouble(double_value); + ExpectRoundTrip(model, ValueProto(double_value), FieldValue::Type::Double); + } +} + TEST_F(SerializerTest, EncodesString) { std::vector cases{ "", @@ -464,8 +538,56 @@ TEST_F(SerializerTest, EncodesTimestamps) { } } +TEST_F(SerializerTest, EncodesBlobs) { + std::vector> cases{ + {}, + {0, 1, 2, 3}, + {0xff, 0x00, 0xff, 0x00}, + }; + + for (const std::vector& blob_value : cases) { + FieldValue model = + FieldValue::FromBlob(blob_value.data(), blob_value.size()); + ExpectRoundTrip(model, ValueProto(blob_value), FieldValue::Type::Blob); + } +} + +TEST_F(SerializerTest, EncodesGeoPoint) { + std::vector cases{ + {1.23, 4.56}, + }; + + for (const GeoPoint& geo_value : cases) { + FieldValue model = FieldValue::FromGeoPoint(geo_value); + ExpectRoundTrip(model, ValueProto(geo_value), FieldValue::Type::GeoPoint); + } +} + +TEST_F(SerializerTest, EncodesArray) { + std::vector> cases{ + // Empty Array. + {}, + // Typical Array. + {FieldValue::FromBoolean(true), FieldValue::FromString("foo")}, + // Nested Array. NB: the protos explicitly state that directly nested + // arrays are not allowed, however arrays *can* contain a map which + // contains another array. + {FieldValue::FromString("foo"), + FieldValue::FromMap( + {{"nested array", + FieldValue::FromArray( + {FieldValue::FromString("nested array value 1"), + FieldValue::FromString("nested array value 2")})}}), + FieldValue::FromString("bar")}}; + + for (const std::vector& array_value : cases) { + FieldValue model = FieldValue::FromArray(array_value); + ExpectRoundTrip(model, ValueProto(array_value), FieldValue::Type::Array); + } +} + TEST_F(SerializerTest, EncodesEmptyMap) { - FieldValue model = FieldValue::FromMap({}); + FieldValue model = FieldValue::EmptyObject(); v1::Value proto; proto.mutable_map_value(); @@ -476,13 +598,13 @@ TEST_F(SerializerTest, EncodesEmptyMap) { TEST_F(SerializerTest, EncodesNestedObjects) { FieldValue model = FieldValue::FromMap({ {"b", FieldValue::True()}, - // TODO(rsgowman): add doubles (once they're supported) - // {"d", FieldValue::DoubleValue(std::numeric_limits::max())}, + {"d", FieldValue::FromDouble(std::numeric_limits::max())}, {"i", FieldValue::FromInteger(1)}, {"n", FieldValue::Null()}, {"s", FieldValue::FromString("foo")}, - // TODO(rsgowman): add arrays (once they're supported) - // {"a", [2, "bar", {"b", false}]}, + {"a", FieldValue::FromArray( + {FieldValue::FromInteger(2), FieldValue::FromString("bar"), + FieldValue::FromMap({{"b", FieldValue::False()}})})}, {"o", FieldValue::FromMap({ {"d", FieldValue::FromInteger(100)}, {"nested", FieldValue::FromMap({ @@ -506,13 +628,24 @@ TEST_F(SerializerTest, EncodesNestedObjects) { (*middle_fields)["d"] = ValueProto(int64_t{100}); (*middle_fields)["nested"] = inner_proto; + v1::Value array_proto; + *array_proto.mutable_array_value()->add_values() = ValueProto(int64_t{2}); + *array_proto.mutable_array_value()->add_values() = ValueProto("bar"); + v1::Value array_inner_proto; + google::protobuf::Map* array_inner_fields = + array_inner_proto.mutable_map_value()->mutable_fields(); + (*array_inner_fields)["b"] = ValueProto(false); + *array_proto.mutable_array_value()->add_values() = array_inner_proto; + v1::Value proto; google::protobuf::Map* fields = proto.mutable_map_value()->mutable_fields(); (*fields)["b"] = ValueProto(true); + (*fields)["d"] = ValueProto(std::numeric_limits::max()); (*fields)["i"] = ValueProto(int64_t{1}); (*fields)["n"] = ValueProto(nullptr); (*fields)["s"] = ValueProto("foo"); + (*fields)["a"] = array_proto; (*fields)["o"] = middle_proto; ExpectRoundTrip(model, proto, FieldValue::Type::Object); @@ -797,7 +930,7 @@ TEST_F(SerializerTest, BadKey) { TEST_F(SerializerTest, EncodesEmptyDocument) { DocumentKey key = DocumentKey::FromPathString("path/to/the/doc"); - FieldValue empty_value = FieldValue::FromMap({}); + ObjectValue empty_value = ObjectValue::Empty(); SnapshotVersion update_time = SnapshotVersion{{1234, 5678}}; v1::BatchGetDocumentsResponse proto; @@ -817,7 +950,7 @@ TEST_F(SerializerTest, EncodesEmptyDocument) { TEST_F(SerializerTest, EncodesNonEmptyDocument) { DocumentKey key = DocumentKey::FromPathString("path/to/the/doc"); - FieldValue fields = FieldValue::FromMap({ + ObjectValue fields = ObjectValue::FromMap({ {"foo", FieldValue::FromString("bar")}, {"two", FieldValue::FromInteger(2)}, {"nested", FieldValue::FromMap({ diff --git a/Firestore/core/test/firebase/firestore/testutil/CMakeLists.txt b/Firestore/core/test/firebase/firestore/testutil/CMakeLists.txt index 9d6e4a72515..6d548d943ac 100644 --- a/Firestore/core/test/firebase/firestore/testutil/CMakeLists.txt +++ b/Firestore/core/test/firebase/firestore/testutil/CMakeLists.txt @@ -17,6 +17,7 @@ cc_library( SOURCES app_testing.h app_testing.mm + xcgmock.h DEPENDS FirebaseCore GoogleUtilities diff --git a/Firestore/core/test/firebase/firestore/testutil/testutil.cc b/Firestore/core/test/firebase/firestore/testutil/testutil.cc index 2910df0892a..e08678b932b 100644 --- a/Firestore/core/test/firebase/firestore/testutil/testutil.cc +++ b/Firestore/core/test/firebase/firestore/testutil/testutil.cc @@ -24,15 +24,17 @@ namespace testutil { std::unique_ptr PatchMutation( absl::string_view path, - const model::ObjectValue::Map& values, + const model::FieldValue::Map& values, // TODO(rsgowman): Investigate changing update_mask to a set. const std::vector* update_mask) { - model::FieldValue object_value = model::FieldValue::FromMap({}); + model::ObjectValue object_value = model::ObjectValue::Empty(); std::set object_mask; for (const auto& kv : values) { model::FieldPath field_path = Field(kv.first); object_mask.insert(field_path); + // TODO(rsgowman): This will abort if kv.second.string_value.type() != + // String if (kv.second.string_value() != kDeleteSentinel) { object_value = object_value.Set(field_path, kv.second); } diff --git a/Firestore/core/test/firebase/firestore/testutil/testutil.h b/Firestore/core/test/firebase/firestore/testutil/testutil.h index 3f19c777580..6d7375b41b1 100644 --- a/Firestore/core/test/firebase/firestore/testutil/testutil.h +++ b/Firestore/core/test/firebase/firestore/testutil/testutil.h @@ -77,23 +77,25 @@ inline model::SnapshotVersion Version(int64_t version) { return model::SnapshotVersion{Timestamp::FromTimePoint(timepoint)}; } -inline model::Document Doc( +inline std::shared_ptr Doc( absl::string_view key, int64_t version = 0, - const model::ObjectValue::Map& data = {}, + const model::FieldValue::Map& data = model::FieldValue::Map(), model::DocumentState document_state = model::DocumentState::kSynced) { - return model::Document{model::FieldValue::FromMap(data), Key(key), - Version(version), document_state}; + return std::make_shared(model::ObjectValue::FromMap(data), + Key(key), Version(version), + document_state); } -inline model::NoDocument DeletedDoc(absl::string_view key, int64_t version) { - return model::NoDocument{Key(key), Version(version), - /*has_committed_mutations=*/false}; +inline std::shared_ptr DeletedDoc(absl::string_view key, + int64_t version) { + return std::make_shared(Key(key), Version(version), + /*has_committed_mutations=*/false); } -inline model::UnknownDocument UnknownDoc(absl::string_view key, - int64_t version) { - return model::UnknownDocument{Key(key), Version(version)}; +inline std::shared_ptr UnknownDoc(absl::string_view key, + int64_t version) { + return std::make_shared(Key(key), Version(version)); } inline core::RelationFilter::Operator OperatorFromString(absl::string_view s) { @@ -140,20 +142,21 @@ inline core::Query Query(absl::string_view path) { } inline std::unique_ptr SetMutation( - absl::string_view path, const model::ObjectValue::Map& values = {}) { + absl::string_view path, + const model::FieldValue::Map& values = model::FieldValue::Map()) { return absl::make_unique( - Key(path), model::FieldValue::FromMap(values), + Key(path), model::ObjectValue::FromMap(values), model::Precondition::None()); } std::unique_ptr PatchMutation( absl::string_view path, - const model::ObjectValue::Map& values = {}, + const model::FieldValue::Map& values = model::FieldValue::Map(), const std::vector* update_mask = nullptr); inline std::unique_ptr PatchMutation( absl::string_view path, - const model::ObjectValue::Map& values, + const model::FieldValue::Map& values, const std::vector& update_mask) { return PatchMutation(path, values, &update_mask); } diff --git a/Firestore/core/test/firebase/firestore/testutil/xcgmock.h b/Firestore/core/test/firebase/firestore/testutil/xcgmock.h new file mode 100644 index 00000000000..96336684c38 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/testutil/xcgmock.h @@ -0,0 +1,212 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#ifndef FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_TESTUTIL_XCGMOCK_H_ +#define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_TESTUTIL_XCGMOCK_H_ + +#if !defined(__OBJC__) +#error "This header only supports Objective-C++" +#endif // !defined(__OBJC__) + +#import + +#include +#include +#include +#include + +#import "Firestore/Source/Model/FSTDocument.h" + +#include "Firestore/core/src/firebase/firestore/util/string_apple.h" +#include "gmock/gmock.h" + +namespace firebase { +namespace firestore { +namespace testutil { + +template +class XcTestRecorder { + public: + XcTestRecorder(M matcher, XCTestCase* test_case, const char* file, int line) + : formatter_(std::move(matcher)), + test_case_(test_case), + file_(file), + line_(line) { + } + + template + void Match(const char* text, const T& value) const { + testing::AssertionResult result = formatter_(text, value); + if (!result) { + RecordFailure(result.message()); + } + } + + void RecordFailure(const char* message) const { + [test_case_ + recordFailureWithDescription:[NSString stringWithUTF8String:message] + inFile:[NSString stringWithUTF8String:file_] + atLine:line_ + expected:YES]; + } + + private: + testing::internal::PredicateFormatterFromMatcher formatter_; + + XCTestCase* test_case_; + const char* file_; + int line_; +}; + +template +XcTestRecorder MakeXcTestRecorder(M matcher, + XCTestCase* test_case, + const char* file, + int line) { + return XcTestRecorder(std::move(matcher), test_case, file, line); +} + +#define XC_ASSERT_THAT(actual, matcher) \ + do { \ + auto recorder = firebase::firestore::testutil::MakeXcTestRecorder( \ + matcher, self, __FILE__, __LINE__); \ + recorder.Match(#actual, actual); \ + } while (0) + +/** + * Prints the -description of an Objective-C object to the given ostream. + */ +inline void ObjcPrintTo(id value, std::ostream* os) { + // Force the result type to NSString* or we can't resolve MakeString. + NSString* description = [value description]; + *os << util::MakeString(description); +} + +} // namespace testutil +} // namespace firestore +} // namespace firebase + +#define OBJC_PRINT_TO(objc_class) \ + @class objc_class; \ + inline void PrintTo(objc_class* value, std::ostream* os) { \ + firebase::firestore::testutil::ObjcPrintTo(value, os); \ + } + +// Define overloads for Objective-C types. Note that each type must be +// explicitly overloaded here because `id` cannot be implicitly converted to +// void* under ARC. If `id` could be converted to void*, then a single overload +// of `operator<<` would be sufficient. + +// Select Foundation types +OBJC_PRINT_TO(NSObject); +OBJC_PRINT_TO(NSArray); +OBJC_PRINT_TO(NSDictionary); +OBJC_PRINT_TO(NSNumber); +OBJC_PRINT_TO(NSString); + +// Declare all Firestore Objective-C classes printable. +// +// Regenerate with: +// find Firestore/Source -name \*.h \ +// | xargs sed -n '/@interface/{ s/<.*//; p; }' \ +// | awk '{ print "OBJC_PRINT_TO(" $2 ");" }' \ +// | sort -u + +OBJC_PRINT_TO(FIRCollectionReference); +OBJC_PRINT_TO(FIRDocumentChange); +OBJC_PRINT_TO(FIRDocumentReference); +OBJC_PRINT_TO(FIRDocumentSnapshot); +OBJC_PRINT_TO(FIRFieldPath); +OBJC_PRINT_TO(FIRFieldValue); +OBJC_PRINT_TO(FIRFirestore); +OBJC_PRINT_TO(FIRFirestoreSettings); +OBJC_PRINT_TO(FIRGeoPoint); +OBJC_PRINT_TO(FIRQuery); +OBJC_PRINT_TO(FIRQueryDocumentSnapshot); +OBJC_PRINT_TO(FIRQuerySnapshot); +OBJC_PRINT_TO(FIRSnapshotMetadata); +OBJC_PRINT_TO(FIRTimestamp); +OBJC_PRINT_TO(FIRTransaction); +OBJC_PRINT_TO(FIRWriteBatch); +OBJC_PRINT_TO(FSTArrayRemoveFieldValue); +OBJC_PRINT_TO(FSTArrayUnionFieldValue); +OBJC_PRINT_TO(FSTArrayValue); +OBJC_PRINT_TO(FSTAsyncQueryListener); +OBJC_PRINT_TO(FSTBlobValue); +OBJC_PRINT_TO(FSTBooleanValue); +OBJC_PRINT_TO(FSTBound); +OBJC_PRINT_TO(FSTDeleteFieldValue); +OBJC_PRINT_TO(FSTDeleteMutation); +OBJC_PRINT_TO(FSTDeletedDocument); +OBJC_PRINT_TO(FSTDocument); +OBJC_PRINT_TO(FSTDocumentKey); +OBJC_PRINT_TO(FSTDocumentKeyReference); +OBJC_PRINT_TO(FSTDocumentSet); +OBJC_PRINT_TO(FSTDoubleValue); +OBJC_PRINT_TO(FSTEventManager); +OBJC_PRINT_TO(FSTFieldValue); +OBJC_PRINT_TO(FSTFieldValueOptions); +OBJC_PRINT_TO(FSTFilter); +OBJC_PRINT_TO(FSTFirestoreClient); +OBJC_PRINT_TO(FSTFirestoreComponent); +OBJC_PRINT_TO(FSTGeoPointValue); +OBJC_PRINT_TO(FSTIntegerValue); +OBJC_PRINT_TO(FSTLRUGarbageCollector); +OBJC_PRINT_TO(FSTLevelDB); +OBJC_PRINT_TO(FSTLevelDBLRUDelegate); +OBJC_PRINT_TO(FSTLimboDocumentChange); +OBJC_PRINT_TO(FSTListenerRegistration); +OBJC_PRINT_TO(FSTLocalDocumentsView); +OBJC_PRINT_TO(FSTLocalSerializer); +OBJC_PRINT_TO(FSTLocalStore); +OBJC_PRINT_TO(FSTLocalViewChanges); +OBJC_PRINT_TO(FSTLocalWriteResult); +OBJC_PRINT_TO(FSTMaybeDocument); +OBJC_PRINT_TO(FSTMemoryEagerReferenceDelegate); +OBJC_PRINT_TO(FSTMemoryLRUReferenceDelegate); +OBJC_PRINT_TO(FSTMemoryPersistence); +OBJC_PRINT_TO(FSTMutation); +OBJC_PRINT_TO(FSTMutationBatch); +OBJC_PRINT_TO(FSTMutationBatchResult); +OBJC_PRINT_TO(FSTMutationResult); +OBJC_PRINT_TO(FSTNanFilter); +OBJC_PRINT_TO(FSTNullFilter); +OBJC_PRINT_TO(FSTNullValue); +OBJC_PRINT_TO(FSTNumberValue); +OBJC_PRINT_TO(FSTNumericIncrementFieldValue); +OBJC_PRINT_TO(FSTObjectValue); +OBJC_PRINT_TO(FSTPatchMutation); +OBJC_PRINT_TO(FSTQuery); +OBJC_PRINT_TO(FSTQueryData); +OBJC_PRINT_TO(FSTQueryListener); +OBJC_PRINT_TO(FSTReferenceValue); +OBJC_PRINT_TO(FSTRelationFilter); +OBJC_PRINT_TO(FSTSerializerBeta); +OBJC_PRINT_TO(FSTServerTimestampFieldValue); +OBJC_PRINT_TO(FSTServerTimestampValue); +OBJC_PRINT_TO(FSTSetMutation); +OBJC_PRINT_TO(FSTSortOrder); +OBJC_PRINT_TO(FSTStringValue); +OBJC_PRINT_TO(FSTSyncEngine); +OBJC_PRINT_TO(FSTTimestampValue); +OBJC_PRINT_TO(FSTTransformMutation); +OBJC_PRINT_TO(FSTUnknownDocument); +OBJC_PRINT_TO(FSTUserDataConverter); +OBJC_PRINT_TO(FSTView); +OBJC_PRINT_TO(FSTViewChange); +OBJC_PRINT_TO(FSTViewDocumentChanges); + +#endif // FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_TESTUTIL_XCGMOCK_H_ diff --git a/Firestore/core/test/firebase/firestore/testutil/xcgmock_test.mm b/Firestore/core/test/firebase/firestore/testutil/xcgmock_test.mm new file mode 100644 index 00000000000..5bac8b38d0d --- /dev/null +++ b/Firestore/core/test/firebase/firestore/testutil/xcgmock_test.mm @@ -0,0 +1,64 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/test/firebase/firestore/testutil/xcgmock.h" + +#import "Firestore/Source/Core/FSTQuery.h" + +#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/to_string.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace testutil { + +TEST(XcGmockTest, NSArrayPrints) { + std::string expected = util::ToString(@[ @"value" ]); + + EXPECT_EQ(expected, testing::PrintToString(@[ @"value" ])); +} + +TEST(XcGmockTest, NSNumberPrints) { + EXPECT_EQ("1", testing::PrintToString(@1)); +} + +// TODO(wilhuff): make this actually work! +// For whatever reason, this prints like a pointer. +TEST(XcGmockTest, DISABLED_NSStringPrints) { + EXPECT_EQ("value", testing::PrintToString(@"value")); +} + +TEST(XcGmockTest, FSTNullFilterPrints) { + FSTNullFilter* filter = + [[FSTNullFilter alloc] initWithField:model::FieldPath({"field"})]; + EXPECT_EQ("field IS NULL", testing::PrintToString(filter)); +} + +TEST(XcGmockTest, StatusPrints) { + util::Status status(FirestoreErrorCode::NotFound, "missing foo"); + EXPECT_EQ("Not found: missing foo", testing::PrintToString(status)); +} + +TEST(XcGmockTest, TimestampPrints) { + Timestamp timestamp(32, 42); + EXPECT_EQ("Timestamp(seconds=32, nanoseconds=42)", + testing::PrintToString(timestamp)); +} + +} // namespace testutil +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/CMakeLists.txt b/Firestore/core/test/firebase/firestore/util/CMakeLists.txt index 92698a74938..860578f4895 100644 --- a/Firestore/core/test/firebase/firestore/util/CMakeLists.txt +++ b/Firestore/core/test/firebase/firestore/util/CMakeLists.txt @@ -150,6 +150,7 @@ cc_test( autoid_test.cc bits_test.cc comparison_test.cc + delayed_constructor_test.cc hashing_test.cc iterator_adaptors_test.cc ordered_code_test.cc diff --git a/Firestore/core/test/firebase/firestore/util/delayed_constructor_test.cc b/Firestore/core/test/firebase/firestore/util/delayed_constructor_test.cc new file mode 100644 index 00000000000..56d0ee09442 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/delayed_constructor_test.cc @@ -0,0 +1,149 @@ +/* + * Copyright 2017 Google + * + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/util/delayed_constructor.h" + +#include + +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +TEST(DelayedConstructorTest, NoDefaultConstructor) { + static int constructed = 0; + + struct NoDefault { + NoDefault() = delete; + NoDefault(const NoDefault&) = delete; + + explicit NoDefault(int) { + constructed += 1; + } + }; + + DelayedConstructor value; + EXPECT_EQ(0, constructed); + + value.Init(0); + EXPECT_EQ(1, constructed); +} + +TEST(DelayedConstructorTest, NonCopyableType) { + static int constructed = 0; + + struct NonCopyable { + NonCopyable() { + constructed += 1; + } + NonCopyable(const NonCopyable&) = delete; + }; + + DelayedConstructor value; + EXPECT_EQ(0, constructed); + + value.Init(); + EXPECT_EQ(1, constructed); +} + +TEST(DelayedConstructorTest, CopyableType) { + static int constructed = 0; + + struct Copyable { + Copyable() = delete; + Copyable(const Copyable&) { + constructed += 1; + } + + // Backdoor to construct a value without exposing a default constructor + explicit Copyable(int) { + } + }; + + DelayedConstructor value; + EXPECT_EQ(0, constructed); + + value.Init(Copyable(0)); + EXPECT_EQ(1, constructed); +} + +TEST(DelayedConstructorTest, MoveOnlyType) { + static int constructed = 0; + + struct MoveOnly { + MoveOnly() = delete; + MoveOnly(MoveOnly&&) { + constructed += 1; + } + + // Backdoor to construct a value without exposing a default constructor + explicit MoveOnly(int) { + } + }; + + DelayedConstructor value; + EXPECT_EQ(0, constructed); + + value.Init(MoveOnly(0)); + EXPECT_EQ(1, constructed); +} + +TEST(DelayedConstructorTest, CallsDestructor) { + static int constructed = 0; + static int destructed = 0; + + struct Counter { + Counter() { + constructed += 1; + } + + ~Counter() { + destructed += 1; + } + }; + + { + DelayedConstructor value; + EXPECT_EQ(0, constructed); + EXPECT_EQ(0, destructed); + + value.Init(); + EXPECT_EQ(1, constructed); + EXPECT_EQ(0, destructed); + } + + EXPECT_EQ(1, constructed); + EXPECT_EQ(1, destructed); +} + +TEST(DelayedConstructorTest, SingleConstructorArg) { + DelayedConstructor str; + str.Init("foo"); + + EXPECT_EQ(*str, std::string("foo")); +} + +TEST(DelayedConstructorTest, MultipleConstructorArgs) { + DelayedConstructor str; + str.Init(3, 'a'); + + EXPECT_EQ(*str, std::string("aaa")); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/hashing_test_apple.mm b/Firestore/core/test/firebase/firestore/util/hashing_test_apple.mm new file mode 100644 index 00000000000..121557d39f0 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/hashing_test_apple.mm @@ -0,0 +1,37 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/util/hashing.h" + +#import + +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +TEST(HashingAppleTest, ObjectiveCHash) { + NSString* foo = @"foobar"; + EXPECT_EQ(Hash(foo), static_cast([foo hash])); + + NSArray* bar = @[ @1, @2, @3 ]; + EXPECT_EQ(Hash(bar), static_cast([bar hash])); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/objc_compatibility_apple_test.mm b/Firestore/core/test/firebase/firestore/util/objc_compatibility_apple_test.mm new file mode 100644 index 00000000000..8454eaf12ff --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/objc_compatibility_apple_test.mm @@ -0,0 +1,131 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/util/objc_compatibility.h" + +#import + +#include +#include +#include + +#import "Firestore/Example/Tests/Util/FSTHelpers.h" +#import "Firestore/Source/Model/FSTDocument.h" + +#include "Firestore/core/src/firebase/firestore/immutable/sorted_map.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_map.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { +namespace objc { + +TEST(ObjCCompatibilityTest, Equals) { + FSTDocument* doc1a = FSTTestDoc("a/b", 0, @{}, FSTDocumentStateSynced); + FSTDocument* doc1b = FSTTestDoc("a/b", 0, @{}, FSTDocumentStateSynced); + FSTDocument* doc2 = FSTTestDoc("b/c", 1, @{}, FSTDocumentStateSynced); + + EXPECT_TRUE(Equals(doc1a, doc1b)); + EXPECT_FALSE(Equals(doc1a, doc2)); + EXPECT_FALSE(Equals(doc1b, doc2)); +} + +TEST(ObjCCompatibilityTest, ContainerEquals) { + FSTDocument* doc1a = FSTTestDoc("a/b", 0, @{}, FSTDocumentStateSynced); + FSTDocument* doc2a = FSTTestDoc("b/c", 1, @{}, FSTDocumentStateSynced); + FSTDocument* doc1b = FSTTestDoc("a/b", 0, @{}, FSTDocumentStateSynced); + FSTDocument* doc2b = FSTTestDoc("b/c", 1, @{}, FSTDocumentStateSynced); + + std::vector v1{doc1a, doc2a}; + std::vector v2{doc1b, doc2b}; + std::vector v3{doc1a, doc1b}; + EXPECT_TRUE(Equals(v1, v2)); + EXPECT_FALSE(Equals(v1, v3)); + EXPECT_FALSE(Equals(v2, v3)); +} + +TEST(ObjCCompatibilityTest, NilEquals) { + FSTDocument* doc1 = nil; + FSTDocument* doc2 = nil; + EXPECT_FALSE([doc1 isEqual:doc2]); + EXPECT_TRUE(Equals(doc1, doc2)); +} + +TEST(ObjCCompatibilityTest, Description) { + std::vector v{"foo", "bar"}; + EXPECT_TRUE([Description(v) isEqual:@"[foo, bar]"]); +} + +TEST(ObjCCompatibilityTest, EqualToAndHash) { + EqualTo equals; + Hash hash; + + NSMutableString* source = [NSMutableString stringWithUTF8String:"value"]; + NSString* value = [source copy]; + NSString* copy = [source copy]; + + EXPECT_TRUE(equals(value, value)); + EXPECT_EQ(hash(value), hash(value)); + + // Same type, different instance + EXPECT_TRUE(equals(value, copy)); + EXPECT_EQ(hash(value), hash(copy)); + + // Different type, same value + EXPECT_TRUE(equals(source, value)); + EXPECT_EQ(hash(source), hash(value)); + + NSString* other = @"other"; + EXPECT_FALSE(equals(value, other)); + EXPECT_FALSE(equals(value, nil)); + EXPECT_FALSE(equals(nil, value)); + EXPECT_FALSE(equals(nil, nil)); +} + +TEST(ObjCCompatibilityTest, UnorderedMap) { + using MapType = std::unordered_map, + EqualTo>; + MapType map; + + auto inserted = map.insert({ @"foo", @1 }); + ASSERT_TRUE(inserted.second); + + inserted = map.insert({ @"bar", @2 }); + ASSERT_TRUE(inserted.second); + ASSERT_EQ(map.size(), 2); + + auto foo_iter = map.find(@"foo"); + ASSERT_NE(foo_iter, map.end()); + ASSERT_EQ(foo_iter->first, @"foo"); + + auto bar_iter = map.find(@"bar"); + ASSERT_NE(bar_iter, map.end()); + ASSERT_EQ(bar_iter->first, @"bar"); + + auto result = map.insert({ @"foo", @3 }); + ASSERT_FALSE(result.second); // not inserted + ASSERT_TRUE(Equals(result.first->second, @1)); // old value preserved + + map.erase(@"foo"); + ASSERT_EQ(map.size(), 1); +} + +} // namespace objc +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/to_string_apple_test.mm b/Firestore/core/test/firebase/firestore/util/to_string_apple_test.mm new file mode 100644 index 00000000000..9d047825157 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/to_string_apple_test.mm @@ -0,0 +1,70 @@ +/* + * Copyright 2019 Google + * + * 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 + +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/util/to_string.h" + +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +TEST(ToStringAppleTest, ObjCTypes) { + EXPECT_EQ(ToString(@123), "123"); + EXPECT_EQ(ToString(@"foo"), "foo"); + + NSArray* objc_array = @[ @1, @2, @3 ]; + EXPECT_EQ(ToString(objc_array), "(\n 1,\n 2,\n 3\n)"); +} + +TEST(ToStringAppleTest, Nested) { + using Nested = std::map*>; + Nested foo1{ + {100, @[ @1, @2, @3 ]}, + {200, @[ @4, @5, @6 ]}, + }; + Nested foo2{ + {300, @[ @3, @2, @1 ]}, + }; + std::map> nested{ + {"bar", std::vector{foo1}}, + {"baz", std::vector{foo2}}, + }; + std::string expected = R"!({bar: [{100: ( + 1, + 2, + 3 +), 200: ( + 4, + 5, + 6 +)}], baz: [{300: ( + 3, + 2, + 1 +)}]})!"; + EXPECT_EQ(ToString(nested), expected); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/to_string_test.cc b/Firestore/core/test/firebase/firestore/util/to_string_test.cc new file mode 100644 index 00000000000..7a9a0b36eec --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/to_string_test.cc @@ -0,0 +1,167 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/immutable/sorted_map.h" +#include "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/util/to_string.h" +#include "absl/types/optional.h" + +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +using immutable::SortedMap; +using immutable::SortedSet; +using model::DocumentKey; + +TEST(ToStringTest, SimpleTypes) { + EXPECT_EQ(ToString(123), "123"); + EXPECT_EQ(ToString(1.5), "1.5"); + + EXPECT_EQ(ToString("foo"), "foo"); + EXPECT_EQ(ToString(std::string{"foo"}), "foo"); + + EXPECT_EQ(ToString(true), "true"); + + EXPECT_EQ(ToString(nullptr), "null"); + + void* ptr = reinterpret_cast(0xBAAAAAAD); + EXPECT_EQ(ToString(ptr), "baaaaaad"); +} + +TEST(ToStringTest, CustomToString) { + DocumentKey key({"rooms", "firestore"}); + EXPECT_EQ(ToString(key), "rooms/firestore"); +} + +TEST(ToStringTest, Optional) { + absl::optional foo; + EXPECT_EQ(ToString(foo), "nullopt"); + + absl::optional bar = 1; + EXPECT_EQ(ToString(bar), "1"); +} + +TEST(ToStringTest, Container) { + std::vector keys{ + DocumentKey({"foo", "bar"}), + DocumentKey({"foo", "baz"}), + }; + EXPECT_EQ(ToString(keys), "[foo/bar, foo/baz]"); +} + +TEST(ToStringTest, StdMap) { + std::map key_map{ + {1, DocumentKey({"foo", "bar"})}, + {2, DocumentKey({"foo", "baz"})}, + }; + EXPECT_EQ(ToString(key_map), "{1: foo/bar, 2: foo/baz}"); +} + +TEST(ToStringTest, CustomMap) { + using MapT = SortedMap; + MapT sorted_map = MapT{}.insert(1, "foo").insert(2, "bar"); + EXPECT_EQ(ToString(sorted_map), "{1: foo, 2: bar}"); +} + +TEST(ToStringTest, CustomSet) { + using SetT = SortedSet; + SetT sorted_set = SetT{}.insert("foo").insert("bar"); + EXPECT_EQ(ToString(sorted_set), "[bar, foo]"); +} + +TEST(ToStringTest, MoreStdContainers) { + std::deque d{1, 2, 3, 4}; + EXPECT_EQ(ToString(d), "[1, 2, 3, 4]"); + + std::set s{5, 6, 7}; + EXPECT_EQ(ToString(s), "[5, 6, 7]"); + + // Multimap with the same duplicate element twice to avoid dealing with order. + std::unordered_multimap mm{{3, "abc"}, {3, "abc"}}; + EXPECT_EQ(ToString(mm), "{3: abc, 3: abc}"); +} + +TEST(ToStringTest, Nested) { + using Nested = std::map>; + Nested foo1{ + {100, {1, 2, 3}}, + {200, {4, 5, 6}}, + }; + Nested foo2{ + {300, {3, 2, 1}}, + }; + std::map> nested{ + {"bar", std::vector{foo1}}, + {"baz", std::vector{foo2}}, + }; + std::string expected = + "{bar: [{100: [1, 2, 3], 200: [4, 5, 6]}], " + "baz: [{300: [3, 2, 1]}]}"; + EXPECT_EQ(ToString(nested), expected); +} + +class Foo {}; +std::string ToString(const Foo&) { + return "Foo"; +} + +TEST(ToStringTest, FreeFunctionToStringIsConsidered) { + EXPECT_EQ(ToString(Foo{}), "Foo"); +} + +TEST(ToStringTest, Ordering) { + struct Container { + using value_type = int; + + explicit Container(std::vector&& v) : v{std::move(v)} { + } + + std::vector::const_iterator begin() const { + return v.begin(); + } + std::vector::const_iterator end() const { + return v.end(); + } + + std::vector v; + }; + + struct CustomToString : public Container { + using Container::Container; + std::string ToString() const { + return "CustomToString"; + } + }; + + EXPECT_EQ(ToString(Container{{1, 2, 3}}), "[1, 2, 3]"); + EXPECT_EQ(ToString(CustomToString{{1, 2, 3}}), "CustomToString"); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Functions/Backend/index.js b/Functions/Backend/index.js index 1b3ade62518..85524d81a44 100644 --- a/Functions/Backend/index.js +++ b/Functions/Backend/index.js @@ -21,7 +21,7 @@ exports.dataTest = functions.https.onRequest((request, response) => { bool: true, int: 2, long: { - value: '3', + value: '9876543210', '@type': 'type.googleapis.com/google.protobuf.Int64Value', }, string: 'four', @@ -107,3 +107,9 @@ exports.httpErrorTest = functions.https.onRequest((request, response) => { // Send an http error with no body. response.status(400).send(); }); + +exports.timeoutTest = functions.https.onRequest((request, response) => { + // Wait for longer than 500ms. + setTimeout(() => response.send({data: true}), 500); +}); + diff --git a/Functions/Backend/start.sh b/Functions/Backend/start.sh index b09675a90ed..8e82c1658e7 100755 --- a/Functions/Backend/start.sh +++ b/Functions/Backend/start.sh @@ -17,6 +17,10 @@ # Sets up a project with the functions CLI and starts a backend to run # integration tests against. +# Adding the "synchronous" parameter will cause the script to exit +# with the server still running so that other scripts can invoke this +# script followed by subsequent dependent commands. + set -e # Get the absolute path to the directory containing this script. @@ -32,7 +36,9 @@ npm install # Start the server. FUNCTIONS_BIN="./node_modules/.bin/functions" -"${FUNCTIONS_BIN}" config set projectId functions-integration-test +"${FUNCTIONS_BIN}" config set projectId functions-integration-test <<-! + myproject +! "${FUNCTIONS_BIN}" config set supervisorPort 5005 "${FUNCTIONS_BIN}" config set region us-central1 "${FUNCTIONS_BIN}" config set verbose true @@ -47,9 +53,12 @@ FUNCTIONS_BIN="./node_modules/.bin/functions" "${FUNCTIONS_BIN}" deploy unknownErrorTest --trigger-http "${FUNCTIONS_BIN}" deploy explicitErrorTest --trigger-http "${FUNCTIONS_BIN}" deploy httpErrorTest --trigger-http +"${FUNCTIONS_BIN}" deploy timeoutTest --trigger-http -# Wait for the user to tell us to stop the server. -echo "Functions emulator now running in ${TEMP_DIR}." -read -n 1 -p "*** Press any key to stop the server. ***" -echo "\nStopping the emulator..." -"${FUNCTIONS_BIN}" stop +if [ "$1" != "synchronous" ]; then + # Wait for the user to tell us to stop the server. + echo "Functions emulator now running in ${TEMP_DIR}." + read -n 1 -p "*** Press any key to stop the server. ***" + echo "\nStopping the emulator..." + "${FUNCTIONS_BIN}" stop +fi diff --git a/Functions/CHANGELOG.md b/Functions/CHANGELOG.md index a8e61c84649..3321f498727 100644 --- a/Functions/CHANGELOG.md +++ b/Functions/CHANGELOG.md @@ -1,3 +1,10 @@ +# v2.4.0 +- Introduce community support for tvOS and macOS (#2506). + +# v2.3.0 +- Change the default timeout for callable functions to 70s (#2329). +- Add a method to change the timeout for a callable (#2329). + # v2.1.0 - Add a constructor to set the region. - Add a method to set a Cloud Functions emulator origin to use, for testing. diff --git a/Functions/Example/IntegrationTests/FIRIntegrationTests.m b/Functions/Example/IntegrationTests/FIRIntegrationTests.m index ce7e1215484..8def02b720f 100644 --- a/Functions/Example/IntegrationTests/FIRIntegrationTests.m +++ b/Functions/Example/IntegrationTests/FIRIntegrationTests.m @@ -48,7 +48,7 @@ - (void)testData { NSDictionary *data = @{ @"bool" : @YES, @"int" : @2, - @"long" : @3L, + @"long" : @9876543210L, @"string" : @"four", @"array" : @[ @5, @6 ], @"null" : [NSNull null], @@ -196,4 +196,19 @@ - (void)testHttpError { [self waitForExpectations:@[ expectation ] timeout:10]; } +- (void)testTimeout { + XCTestExpectation *expectation = [[XCTestExpectation alloc] init]; + FIRHTTPSCallable *function = [_functions HTTPSCallableWithName:@"timeoutTest"]; + function.timeoutInterval = 0.05; + [function + callWithCompletion:^(FIRHTTPSCallableResult *_Nullable result, NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqual(FIRFunctionsErrorCodeDeadlineExceeded, error.code); + XCTAssertEqualObjects(@"DEADLINE EXCEEDED", error.userInfo[NSLocalizedDescriptionKey]); + XCTAssertNil(error.userInfo[FIRFunctionsErrorDetailsKey]); + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:10]; +} + @end diff --git a/Functions/Example/Podfile b/Functions/Example/Podfile index 5c8bcef7b32..aa1ed376317 100644 --- a/Functions/Example/Podfile +++ b/Functions/Example/Podfile @@ -5,7 +5,7 @@ target 'FirebaseFunctions_Example' do pod 'FirebaseAuthInterop', :path => '../../' pod 'FirebaseCore', :path => '../../' - pod 'FirebaseFunctions', :path => '../../' + pod 'FirebaseFunctions', :path => '../../', :testspecs => ['unit'] pod 'GoogleUtilities', :path => '../../' target 'FirebaseFunctions_Tests' do diff --git a/Functions/Example/Tests/FUNSerializerTests.m b/Functions/Example/Tests/FUNSerializerTests.m index 1acbf2c3f24..0d12e74937a 100644 --- a/Functions/Example/Tests/FUNSerializerTests.m +++ b/Functions/Example/Tests/FUNSerializerTests.m @@ -153,13 +153,13 @@ - (void)testDecodeString { } - (void)testEncodeArray { - NSArray *input = @[ @1, @"two", @[ @3, @4L ] ]; + NSArray *input = @[ @1, @"two", @[ @3, @9876543210LL ] ]; NSArray *expected = @[ @1, @"two", @[ @3, @{ @"@type" : @"type.googleapis.com/google.protobuf.Int64Value", - @"value" : @"4", + @"value" : @"9876543210", } ] ]; @@ -173,11 +173,11 @@ - (void)testDecodeArray { @[ @3, @{ @"@type" : @"type.googleapis.com/google.protobuf.Int64Value", - @"value" : @"4", + @"value" : @"9876543210", } ] ]; - NSArray *expected = @[ @1, @"two", @[ @3, @4L ] ]; + NSArray *expected = @[ @1, @"two", @[ @3, @9876543210LL ] ]; FUNSerializer *serializer = [[FUNSerializer alloc] init]; NSError *error = nil; @@ -186,14 +186,14 @@ - (void)testDecodeArray { } - (void)testEncodeMap { - NSDictionary *input = @{@"foo" : @1, @"bar" : @"hello", @"baz" : @[ @3, @4L ]}; + NSDictionary *input = @{@"foo" : @1, @"bar" : @"hello", @"baz" : @[ @3, @9876543210LL ]}; NSDictionary *expected = @{ @"foo" : @1, @"bar" : @"hello", @"baz" : @[ @3, @{ @"@type" : @"type.googleapis.com/google.protobuf.Int64Value", - @"value" : @"4", + @"value" : @"9876543210", } ] }; @@ -208,11 +208,11 @@ - (void)testDecodeMap { @"baz" : @[ @3, @{ @"@type" : @"type.googleapis.com/google.protobuf.Int64Value", - @"value" : @"4", + @"value" : @"9876543210", } ] }; - NSDictionary *expected = @{@"foo" : @1, @"bar" : @"hello", @"baz" : @[ @3, @4L ]}; + NSDictionary *expected = @{@"foo" : @1, @"bar" : @"hello", @"baz" : @[ @3, @9876543210LL ]}; FUNSerializer *serializer = [[FUNSerializer alloc] init]; NSError *error = nil; XCTAssertEqualObjects(expected, [serializer decode:input error:&error]); diff --git a/Functions/FirebaseFunctions/FIRFunctions+Internal.h b/Functions/FirebaseFunctions/FIRFunctions+Internal.h index cd375da41f0..9a88cd5429f 100644 --- a/Functions/FirebaseFunctions/FIRFunctions+Internal.h +++ b/Functions/FirebaseFunctions/FIRFunctions+Internal.h @@ -31,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)callFunction:(NSString *)name withObject:(nullable id)data + timeout:(NSTimeInterval)timeout completion:(void (^)(FIRHTTPSCallableResult *_Nullable result, NSError *_Nullable error))completion; diff --git a/Functions/FirebaseFunctions/FIRFunctions.m b/Functions/FirebaseFunctions/FIRFunctions.m index 115ebc88539..61a594c9eb1 100644 --- a/Functions/FirebaseFunctions/FIRFunctions.m +++ b/Functions/FirebaseFunctions/FIRFunctions.m @@ -159,6 +159,7 @@ - (NSString *)URLWithName:(NSString *)name { - (void)callFunction:(NSString *)name withObject:(nullable id)data + timeout:(NSTimeInterval)timeout completion:(void (^)(FIRHTTPSCallableResult *_Nullable result, NSError *_Nullable error))completion { [_contextProvider getContext:^(FUNContext *_Nullable context, NSError *_Nullable error) { @@ -168,16 +169,25 @@ - (void)callFunction:(NSString *)name } return; } - return [self callFunction:name withObject:data context:context completion:completion]; + return [self callFunction:name + withObject:data + timeout:timeout + context:context + completion:completion]; }]; } - (void)callFunction:(NSString *)name withObject:(nullable id)data + timeout:(NSTimeInterval)timeout context:(FUNContext *)context completion:(void (^)(FIRHTTPSCallableResult *_Nullable result, NSError *_Nullable error))completion { - GTMSessionFetcher *fetcher = [_fetcherService fetcherWithURLString:[self URLWithName:name]]; + NSURL *url = [NSURL URLWithString:[self URLWithName:name]]; + NSURLRequest *request = [NSURLRequest requestWithURL:url + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:timeout]; + GTMSessionFetcher *fetcher = [_fetcherService fetcherWithRequest:request]; NSMutableDictionary *body = [NSMutableDictionary dictionary]; // Encode the data in the body. @@ -225,6 +235,11 @@ - (void)callFunction:(NSString *)name if ([error.domain isEqualToString:kGTMSessionFetcherStatusDomain]) { error = FUNErrorForResponse(error.code, data, serializer); } + if ([error.domain isEqualToString:NSURLErrorDomain]) { + if (error.code == NSURLErrorTimedOut) { + error = FUNErrorForCode(FIRFunctionsErrorCodeDeadlineExceeded); + } + } } else { // If there wasn't an HTTP error, see if there was an error in the body. error = FUNErrorForResponse(200, data, serializer); diff --git a/Functions/FirebaseFunctions/FIRHTTPSCallable.m b/Functions/FirebaseFunctions/FIRHTTPSCallable.m index 2979ca59bdb..74d5058a18d 100644 --- a/Functions/FirebaseFunctions/FIRHTTPSCallable.m +++ b/Functions/FirebaseFunctions/FIRHTTPSCallable.m @@ -51,6 +51,7 @@ - (instancetype)initWithFunctions:(FIRFunctions *)functions name:(NSString *)nam } _name = [name copy]; _functions = functions; + _timeoutInterval = 70.0; } return self; } @@ -63,7 +64,10 @@ - (void)callWithCompletion:(void (^)(FIRHTTPSCallableResult *_Nullable result, - (void)callWithObject:(nullable id)data completion:(void (^)(FIRHTTPSCallableResult *_Nullable result, NSError *_Nullable error))completion { - [_functions callFunction:_name withObject:data completion:completion]; + [_functions callFunction:_name + withObject:data + timeout:self.timeoutInterval + completion:completion]; } @end diff --git a/Functions/FirebaseFunctions/FUNError.h b/Functions/FirebaseFunctions/FUNError.h index 18d9de3ed2c..2193aa0b70a 100644 --- a/Functions/FirebaseFunctions/FUNError.h +++ b/Functions/FirebaseFunctions/FUNError.h @@ -14,11 +14,19 @@ // limitations under the License. #import +#import "FIRError.h" @class FUNSerializer; NS_ASSUME_NONNULL_BEGIN +/** + * Takes an error code and returns a corresponding NSError. + * @param code The eror code. + * @return The corresponding NSError. + */ +NSError *_Nullable FUNErrorForCode(FIRFunctionsErrorCode code); + /** * Takes an HTTP status code and optional body and returns a corresponding NSError. * If an explicit error is encoded in the JSON body, it will be used. diff --git a/Functions/FirebaseFunctions/FUNError.m b/Functions/FirebaseFunctions/FUNError.m index da0dcad2720..b747021b495 100644 --- a/Functions/FirebaseFunctions/FUNError.m +++ b/Functions/FirebaseFunctions/FUNError.m @@ -14,7 +14,6 @@ // limitations under the License. #import "FUNError.h" -#import "FIRError.h" #import "FUNSerializer.h" @@ -141,6 +140,11 @@ FIRFunctionsErrorCode FIRFunctionsErrorCodeForName(NSString *name) { return @"UNKNOWN"; } +NSError *_Nullable FUNErrorForCode(FIRFunctionsErrorCode code) { + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : FUNDescriptionForErrorCode(code)}; + return [NSError errorWithDomain:FIRFunctionsErrorDomain code:code userInfo:userInfo]; +} + NSError *_Nullable FUNErrorForResponse(NSInteger status, NSData *_Nullable body, FUNSerializer *serializer) { diff --git a/Functions/FirebaseFunctions/Public/FIRHTTPSCallable.h b/Functions/FirebaseFunctions/Public/FIRHTTPSCallable.h index 473a9448fb9..04f53bcb9db 100644 --- a/Functions/FirebaseFunctions/Public/FIRHTTPSCallable.h +++ b/Functions/FirebaseFunctions/Public/FIRHTTPSCallable.h @@ -89,6 +89,11 @@ NS_SWIFT_NAME(HTTPSCallable) NS_SWIFT_NAME(call(_:completion:)); // clang-format on +/** + * The timeout to use when calling the function. Defaults to 60 seconds. + */ +@property(nonatomic, assign) NSTimeInterval timeoutInterval; + @end NS_ASSUME_NONNULL_END diff --git a/Functions/README.md b/Functions/README.md index b35f5076734..c5f8c90cbea 100644 --- a/Functions/README.md +++ b/Functions/README.md @@ -5,16 +5,26 @@ Follow the subsequent instructions to develop, debug, unit test, and integration test FirebaseFunctions: -``` -$ git clone git@github.com:firebase/firebase-ios-sdk.git -$ cd firebase-ios-sdk/Functions/Example -$ pod update -$ open FirebaseFunctions.xcworkspace -``` +### Prereqs + +- At least CocoaPods 1.6.0 +- Install [cocoapods-generate](https://github.com/square/cocoapods-generate) + +### To Develop + +- Run `pod gen FirebaseFunctions.podspec` +- `open gen/FirebaseFunctions/FirebaseFunctions.xcworkspace` + +OR these two commands can be combined with + +- `pod gen FirebaseFunctions.podspec --auto-open --gen-directory="gen" --clean` + +You're now in an Xcode workspace generate for building, debugging and +testing the FirebaseFunctions CocoaPod. ### Running Unit Tests -Choose the FirebaseFunctions_Tests scheme and press Command-u. +Choose the FirebaseFunctions-Unit-unit scheme and press Command-u. ## Running Integration Tests @@ -29,5 +39,5 @@ for them to talk to. You can put anything you like. It will be ignored. 3. Create the workspace in Functions/Example with `pod install`. 4. `open FirebaseFunctions.xcworkspace` -5. Choose the FirebaseFunctions_IntegrationTests scheme and press Command-u. +5. Choose the FirebaseFunctions-Unit-integration scheme and press Command-u. 6. When you are finished, you can press any key to stop the backend. diff --git a/Gemfile b/Gemfile index 14b043aabba..9e8aa3afbc5 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ -# use 'bundle update' to update to latest gems +# To update, change version below, run bundle install, test, +# commit Gemfile and Gemfile.lock. source 'https://rubygems.org' -gem 'cocoapods', :git => 'https://github.com/CocoaPods/CocoaPods.git' -gem 'cocoapods-core', :git => 'https://github.com/CocoaPods/Core.git' -gem 'xcodeproj', :git => 'https://github.com/CocoaPods/Xcodeproj.git' +gem 'cocoapods', "=1.6.1" +gem 'cocoapods-generate' diff --git a/Gemfile.lock b/Gemfile.lock index 4a6b22f88d3..19a8e920992 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,93 +1,78 @@ -GIT - remote: https://github.com/CocoaPods/CocoaPods.git - revision: 10b69dbd9b991442646944f118f569d855652b26 +GEM + remote: https://rubygems.org/ specs: - cocoapods (1.5.3) + CFPropertyList (3.0.0) + activesupport (4.2.11) + i18n (~> 0.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + atomos (0.1.3) + claide (1.0.2) + cocoapods (1.6.1) activesupport (>= 4.0.2, < 5) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.5.3) + cocoapods-core (= 1.6.1) cocoapods-deintegrate (>= 1.0.2, < 2.0) - cocoapods-downloader (>= 1.2.1, < 2.0) + cocoapods-downloader (>= 1.2.2, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-stats (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.3.0, < 2.0) + cocoapods-trunk (>= 1.3.1, < 2.0) cocoapods-try (>= 1.1.0, < 2.0) colored2 (~> 3.1) escape (~> 0.0.4) - fourflusher (~> 2.0.1) + fourflusher (>= 2.2.0, < 3.0) gh_inspector (~> 1.0) - molinillo (~> 0.6.5) + molinillo (~> 0.6.6) nap (~> 1.0) - ruby-macho (~> 1.2) - xcodeproj (>= 1.5.8, < 2.0) - -GIT - remote: https://github.com/CocoaPods/Core.git - revision: 577c69f38fdb56cbdb883b44681ca2b224cad746 - specs: - cocoapods-core (1.5.3) + ruby-macho (~> 1.4) + xcodeproj (>= 1.8.1, < 2.0) + cocoapods-core (1.6.1) activesupport (>= 4.0.2, < 6) fuzzy_match (~> 2.0.4) nap (~> 1.0) - -GIT - remote: https://github.com/CocoaPods/Xcodeproj.git - revision: cadb238767d09942a1c5eba90a3112034438ba23 - specs: - xcodeproj (1.5.9) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.2) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.2.5) - -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.0) - activesupport (4.2.10) - i18n (~> 0.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - atomos (0.1.2) - claide (1.0.2) - cocoapods-deintegrate (1.0.2) - cocoapods-downloader (1.2.1) + cocoapods-deintegrate (1.0.3) + cocoapods-downloader (1.2.2) + cocoapods-generate (1.3.1) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.0) - cocoapods-stats (1.0.0) - cocoapods-trunk (1.3.0) + cocoapods-stats (1.1.0) + cocoapods-trunk (1.3.1) nap (>= 0.8, < 2.0) netrc (~> 0.11) cocoapods-try (1.1.0) colored2 (3.1.2) - concurrent-ruby (1.0.5) + concurrent-ruby (1.1.4) escape (0.0.4) - fourflusher (2.0.1) + fourflusher (2.2.0) fuzzy_match (2.0.4) gh_inspector (1.1.3) i18n (0.9.5) concurrent-ruby (~> 1.0) minitest (5.11.3) - molinillo (0.6.5) - nanaimo (0.2.5) + molinillo (0.6.6) + nanaimo (0.2.6) nap (1.1.0) netrc (0.11.0) - ruby-macho (1.2.0) + ruby-macho (1.4.0) thread_safe (0.3.6) tzinfo (1.2.5) thread_safe (~> 0.1) + xcodeproj (1.8.1) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.2.6) PLATFORMS ruby DEPENDENCIES - cocoapods! - cocoapods-core! - xcodeproj! + cocoapods (= 1.6.1) + cocoapods-generate BUNDLED WITH - 1.16.1 + 1.16.6 diff --git a/GoogleUtilities.podspec b/GoogleUtilities.podspec index 4c40a1b5012..b3dac0c0bdf 100644 --- a/GoogleUtilities.podspec +++ b/GoogleUtilities.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleUtilities' - s.version = '5.3.7' + s.version = '5.5.0' s.summary = 'Google Utilities for iOS (plus community support for macOS and tvOS)' s.description = <<-DESC diff --git a/GoogleUtilities/CHANGELOG.md b/GoogleUtilities/CHANGELOG.md index 564ac5a12cc..2bf1c603e2b 100644 --- a/GoogleUtilities/CHANGELOG.md +++ b/GoogleUtilities/CHANGELOG.md @@ -1,5 +1,14 @@ # Unreleased +# 5.5.0 +- Revert 5.4.x changes restoring 5.3.7 version. + +# 5.4.1 +- Fix GULResetLogger API breakage. (#2551) + +# 5.4.0 +- Update GULLogger to use os_log instead of asl_log on iOS 9 and later. (#2374, #2504) + # 5.3.7 - Fixed `pod lib lint GoogleUtilities.podspec --use-libraries` regression. (#2130) - Fixed macOS conditional check in UserDefaults. (#2245) diff --git a/GoogleUtilities/Example/Tests/Logger/GULLoggerTest.m b/GoogleUtilities/Example/Tests/Logger/GULLoggerTest.m index 9483bbcb5f9..35109e71518 100644 --- a/GoogleUtilities/Example/Tests/Logger/GULLoggerTest.m +++ b/GoogleUtilities/Example/Tests/Logger/GULLoggerTest.m @@ -32,13 +32,15 @@ extern BOOL getGULLoggerDebugMode(void); +extern CFStringRef getGULLoggerUsetDefaultsSuiteName(void); +extern dispatch_queue_t getGULLoggerCounterQueue(void); + static NSString *const kMessageCode = @"I-COR000001"; @interface GULLoggerTest : XCTestCase @property(nonatomic) NSString *randomLogString; - -@property(nonatomic, strong) NSUserDefaults *defaults; +@property(nonatomic) NSUserDefaults *loggerDefaults; @end @@ -48,14 +50,18 @@ - (void)setUp { [super setUp]; GULResetLogger(); - // Stub NSUserDefaults for cleaner testing. - _defaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.google.logger_test"]; + self.loggerDefaults = [[NSUserDefaults alloc] + initWithSuiteName:CFBridgingRelease(getGULLoggerUsetDefaultsSuiteName())]; } - (void)tearDown { - [super tearDown]; + // Make sure all async operations have finished before starting a new test. + [self drainQueue:getGULClientQueue()]; + [self drainQueue:getGULLoggerCounterQueue()]; + + self.loggerDefaults = nil; - _defaults = nil; + [super tearDown]; } - (void)testMessageCodeFormat { @@ -153,17 +159,61 @@ - (void)testGULLoggerLevelValues { XCTAssertEqual(GULLoggerLevelDebug, ASL_LEVEL_DEBUG); } +- (void)testGetErrorWarningNumberBeforeLogDontCrash { + GULResetLogger(); + + XCTAssertNoThrow(GULNumberOfErrorsLogged()); + XCTAssertNoThrow(GULNumberOfWarningsLogged()); +} + +- (void)testErrorNumberIncrement { + [self.loggerDefaults setInteger:10 forKey:kGULLoggerErrorCountKey]; + + GULLogError(@"my service", NO, kMessageCode, @"Message."); + + [self drainQueue:getGULLoggerCounterQueue()]; + XCTAssertEqual(GULNumberOfErrorsLogged(), 11); +} + +- (void)testWarningNumberIncrement { + [self.loggerDefaults setInteger:5 forKey:kGULLoggerWarningCountKey]; + + GULLogWarning(@"my service", NO, kMessageCode, @"Message."); + + [self drainQueue:getGULLoggerCounterQueue()]; + XCTAssertEqual(GULNumberOfWarningsLogged(), 6); +} + +- (void)testResetIssuesCount { + [self.loggerDefaults setInteger:3 forKey:kGULLoggerErrorCountKey]; + [self.loggerDefaults setInteger:4 forKey:kGULLoggerWarningCountKey]; + + GULResetNumberOfIssuesLogged(); + + XCTAssertEqual(GULNumberOfErrorsLogged(), 0); + XCTAssertEqual(GULNumberOfWarningsLogged(), 0); +} + +- (void)testNumberOfIssuesLoggedNoDeadlock { + [self dispatchSyncNestedDispatchCount:100 + queue:getGULLoggerCounterQueue() + block:^{ + XCTAssertNoThrow(GULNumberOfErrorsLogged()); + XCTAssertNoThrow(GULNumberOfWarningsLogged()); + }]; +} + // Helper functions. - (BOOL)logExists { - [self drainGULClientQueue]; + [self drainQueue:getGULClientQueue()]; NSString *correctMsg = [NSString stringWithFormat:@"%@[%@] %@", @"my service", kMessageCode, self.randomLogString]; return [self messageWasLogged:correctMsg]; } -- (void)drainGULClientQueue { +- (void)drainQueue:(dispatch_queue_t)queue { dispatch_semaphore_t workerSemaphore = dispatch_semaphore_create(0); - dispatch_async(getGULClientQueue(), ^{ + dispatch_barrier_async(queue, ^{ dispatch_semaphore_signal(workerSemaphore); }); dispatch_semaphore_wait(workerSemaphore, DISPATCH_TIME_FOREVER); @@ -191,5 +241,19 @@ - (BOOL)messageWasLogged:(NSString *)message { #pragma clang pop } +- (void)dispatchSyncNestedDispatchCount:(NSInteger)count + queue:(dispatch_queue_t)queue + block:(dispatch_block_t)block { + if (count < 0) { + return; + } + + dispatch_sync(queue, ^{ + [self dispatchSyncNestedDispatchCount:count - 1 queue:queue block:block]; + block(); + NSLog(@"%@, depth: %ld", NSStringFromSelector(_cmd), (long)count); + }); +} + @end #endif diff --git a/GoogleUtilities/Logger/GULLogger.m b/GoogleUtilities/Logger/GULLogger.m index 495e5830bb0..7a88af8cf5e 100644 --- a/GoogleUtilities/Logger/GULLogger.m +++ b/GoogleUtilities/Logger/GULLogger.m @@ -19,6 +19,9 @@ #import #import "Public/GULLoggerLevel.h" +NSString *const kGULLoggerErrorCountKey = @"kGULLoggerErrorCountKey"; +NSString *const kGULLoggerWarningCountKey = @"kGULLoggerWarningCountKey"; + /// ASL client facility name used by GULLogger. const char *kGULLoggerASLClientFacilityName = "com.google.utilities.logger"; @@ -43,6 +46,8 @@ static NSRegularExpression *sMessageCodeRegex; #endif +void GULIncrementLogCountForLevel(GULLoggerLevel level); + void GULLoggerInitializeASL(void) { dispatch_once(&sGULLoggerOnceToken, ^{ NSInteger majorOSVersion = [[GULAppEnvironmentUtil systemVersion] integerValue]; @@ -149,6 +154,10 @@ void GULLogBasic(GULLoggerLevel level, NSString *message, va_list args_ptr) { GULLoggerInitializeASL(); + + // Keep count of how many errors and warnings are triggered. + GULIncrementLogCountForLevel(level); + if (!(level <= sGULLoggerMaximumLevel || sGULLoggerDebugMode || forceLog)) { return; } @@ -194,6 +203,87 @@ void GULLogBasic(GULLoggerLevel level, #undef GUL_MAKE_LOGGER +#pragma mark - User defaults + +// NSUserDefaults cannot be used due to a bug described in GULUserDefaults +// GULUserDefaults cannot be used because GULLogger is a dependency for GULUserDefaults +// We have to use C API deireclty here + +CFStringRef getGULLoggerUsetDefaultsSuiteName(void) { + return (__bridge CFStringRef) @"GoogleUtilities.Logger.GULLogger"; +} + +NSInteger GULGetUserDefaultsIntegerForKey(NSString *key) { + id value = (__bridge_transfer id)CFPreferencesCopyAppValue((__bridge CFStringRef)key, + getGULLoggerUsetDefaultsSuiteName()); + if (![value isKindOfClass:[NSNumber class]]) { + return 0; + } + + return [(NSNumber *)value integerValue]; +} + +void GULLoggerUserDefaultsSetIntegerForKey(NSInteger count, NSString *key) { + NSNumber *countNumber = @(count); + CFPreferencesSetAppValue((__bridge CFStringRef)key, (__bridge CFNumberRef)countNumber, + getGULLoggerUsetDefaultsSuiteName()); + CFPreferencesAppSynchronize(getGULLoggerUsetDefaultsSuiteName()); +} + +#pragma mark - Number of errors and warnings + +dispatch_queue_t getGULLoggerCounterQueue(void) { + static dispatch_queue_t queue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = + dispatch_queue_create("GoogleUtilities.GULLogger.counterQueue", DISPATCH_QUEUE_CONCURRENT); + dispatch_set_target_queue(queue, + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)); + }); + + return queue; +} + +NSInteger GULSyncGetUserDefaultsIntegerForKey(NSString *key) { + __block NSInteger integerValue = 0; + dispatch_sync(getGULLoggerCounterQueue(), ^{ + integerValue = GULGetUserDefaultsIntegerForKey(key); + }); + + return integerValue; +} + +NSInteger GULNumberOfErrorsLogged(void) { + return GULSyncGetUserDefaultsIntegerForKey(kGULLoggerErrorCountKey); +} + +NSInteger GULNumberOfWarningsLogged(void) { + return GULSyncGetUserDefaultsIntegerForKey(kGULLoggerWarningCountKey); +} + +void GULResetNumberOfIssuesLogged(void) { + dispatch_barrier_async(getGULLoggerCounterQueue(), ^{ + GULLoggerUserDefaultsSetIntegerForKey(0, kGULLoggerErrorCountKey); + GULLoggerUserDefaultsSetIntegerForKey(0, kGULLoggerWarningCountKey); + }); +} + +void GULIncrementUserDefaultsIntegerForKey(NSString *key) { + NSInteger value = GULGetUserDefaultsIntegerForKey(key); + GULLoggerUserDefaultsSetIntegerForKey(value + 1, key); +} + +void GULIncrementLogCountForLevel(GULLoggerLevel level) { + dispatch_barrier_async(getGULLoggerCounterQueue(), ^{ + if (level == GULLoggerLevelError) { + GULIncrementUserDefaultsIntegerForKey(kGULLoggerErrorCountKey); + } else if (level == GULLoggerLevelWarning) { + GULIncrementUserDefaultsIntegerForKey(kGULLoggerWarningCountKey); + } + }); +} + #pragma mark - GULLoggerWrapper @implementation GULLoggerWrapper diff --git a/GoogleUtilities/Logger/Private/GULLogger.h b/GoogleUtilities/Logger/Private/GULLogger.h index ff425768681..167f24c4caa 100644 --- a/GoogleUtilities/Logger/Private/GULLogger.h +++ b/GoogleUtilities/Logger/Private/GULLogger.h @@ -25,6 +25,16 @@ NS_ASSUME_NONNULL_BEGIN */ typedef NSString *const GULLoggerService; +/** + * The key used to store the logger's error count. + */ +extern NSString *const kGULLoggerErrorCountKey; + +/** + * The key used to store the logger's warning count. + */ +extern NSString *const kGULLoggerWarningCountKey; + #ifdef __cplusplus extern "C" { #endif // __cplusplus @@ -129,6 +139,23 @@ extern void GULLogDebug(GULLoggerService service, NSString *message, ...) NS_FORMAT_FUNCTION(4, 5); +/** + * Retrieve the number of errors that have been logged since the stat was last reset. + * Calling this method can be comparably expensive, so it should not be called from main thread. + */ +extern NSInteger GULNumberOfErrorsLogged(void); + +/** + * Retrieve the number of warnings that have been logged since the stat was last reset. + * Calling this method can be comparably expensive, so it should not be called from main thread. + */ +extern NSInteger GULNumberOfWarningsLogged(void); + +/** + * Reset number of errors and warnings that have been logged to 0. + */ +extern void GULResetNumberOfIssuesLogged(void); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.h b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.h new file mode 100644 index 00000000000..fbd9afa1834 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.h @@ -0,0 +1,36 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMBookKeeper.h" +#import "FIRIAMMsgFetcherUsingRestful.h" + +NS_ASSUME_NONNULL_BEGIN +@interface AppDelegate : UIResponder +@property(strong, nonatomic) UIWindow *window; +@property(strong, nonatomic) FIRIAMActivityLogger *activityLogger; +@property(nonatomic, nullable) FIRIAMBookKeeperViaUserDefaults *bookKeeper; + +@property(nonatomic, nullable) FIRIAMMsgFetcherUsingRestful *restfulFetcher; +@property(nonatomic, copy) NSString *backendServer; +@property(nonatomic, copy) NSString *projectId; +@property(nonatomic, copy) NSString *apiKey; + +@property(nonatomic) NSInteger displayIntervalInSeconds; +@property(nonatomic) NSInteger fetchIntervalInSeconds; +@end +NS_ASSUME_NONNULL_END diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.m b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.m new file mode 100644 index 00000000000..79ab41c8d2b --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.m @@ -0,0 +1,117 @@ +/* + * Copyright 2017 Google + * + * 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 "AppDelegate.h" +#import "FIRIAMClearcutUploader.h" +#import "FIRIAMRuntimeManager.h" +#import "FIRInAppMessaging+Bootstrap.h" +#import "NSString+FIRInterlaceStrings.h" + +#import +#import + +@interface FIRInAppMessaging (Testing) ++ (void)disableAutoBootstrapWithFIRApp; +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + NSLog(@"application started"); + + [FIRInAppMessaging disableAutoBootstrapWithFIRApp]; + [FIROptions defaultOptions].deepLinkURLScheme = @"fiam-testing"; + [FIRApp configure]; + + FIRIAMSDKSettings *sdkSetting = [[FIRIAMSDKSettings alloc] init]; + + sdkSetting.apiServerHost = @"firebaseinappmessaging.googleapis.com"; + + NSString *serverHostNameFirstComponent = @"pa.ogepscm"; + NSString *serverHostNameSecondComponent = @"lygolai.o"; + + sdkSetting.clearcutServerHost = [NSString fir_interlaceString:serverHostNameFirstComponent + withString:serverHostNameSecondComponent]; + sdkSetting.apiHttpProtocol = @"https"; + sdkSetting.fetchMinIntervalInMinutes = 0.1; // ok to refetch every 6 seconds + sdkSetting.loggerMaxCountBeforeReduce = 800; + sdkSetting.loggerSizeAfterReduce = 600; + sdkSetting.appFGRenderMinIntervalInMinutes = 0.1; + sdkSetting.loggerInVerboseMode = YES; + sdkSetting.firebaseAutoDataCollectionEnabled = NO; + + sdkSetting.clearcutStrategy = + [[FIRIAMClearcutStrategy alloc] initWithMinWaitTimeInMills:5 * 1000 // 5 seconds + maxWaitTimeInMills:30 * 1000 // 30 seconds + failureBackoffTimeInMills:60 * 1000 // 60 seconds + batchSendSize:50]; + + [FIRInAppMessaging bootstrapIAMWithSettings:sdkSetting]; + return YES; +} + +- (BOOL)application:(UIApplication *)application + continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void (^)(NSArray *))restorationHandler { + NSLog(@"handle page url %@", userActivity.webpageURL); + BOOL handled = [[FIRDynamicLinks dynamicLinks] + handleUniversalLink:userActivity.webpageURL + completion:^(FIRDynamicLink *_Nullable dynamicLink, NSError *_Nullable error) { + if (dynamicLink) { + NSLog(@"dynamic link recogized with url as %@", dynamicLink.url.absoluteString); + [self showDeepLink:dynamicLink.url.absoluteString forUrlType:@"universal link"]; + } else { + NSLog(@"error happened %@", error); + } + }]; + return handled; +} + +- (void)showDeepLink:(NSString *)url forUrlType:(NSString *)urlType { + NSString *message = [NSString stringWithFormat:@"App wants to open a %@ : %@", urlType, url]; + UIAlertController *alert = + [UIAlertController alertControllerWithTitle:@"Deep link recognized" + message:message + preferredStyle:UIAlertControllerStyleAlert]; + + UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action){ + }]; + + [alert addAction:defaultAction]; + [UIApplication.sharedApplication.keyWindow.rootViewController presentViewController:alert + animated:YES + completion:nil]; +} + +- (BOOL)application:(UIApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options { + return [self application:app openURL:url sourceApplication:@"source app" annotation:@{}]; +} + +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + sourceApplication:(NSString *)sourceApplication + annotation:(id)annotation { + NSLog(@"handle link with custom scheme: %@", url.absoluteString); + [self showDeepLink:url.absoluteString forUrlType:@"custom scheme url"]; + return YES; +} +@end diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..1d060ed2882 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,93 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.h b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.h new file mode 100644 index 00000000000..78cc22bc3c2 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.h @@ -0,0 +1,20 @@ +/* + * Copyright 2017 Google + * + * 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 + +@interface AutoDisplayFlowViewController : UIViewController + +@end diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.m b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.m new file mode 100644 index 00000000000..6ed900259c2 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.m @@ -0,0 +1,170 @@ +/* + * Copyright 2017 Google + * + * 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 "AppDelegate.h" + +#import "AutoDisplayFlowViewController.h" +#import "AutoDisplayMesagesTableVC.h" + +#import "FIRIAMDisplayCheckOnAppForegroundFlow.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMMessageContentDataWithImageURL.h" +#import "FIRIAMMessageDefinition.h" + +#import "FIRIAMActivityLogger.h" +#import "FIRIAMDisplayCheckOnAnalyticEventsFlow.h" +#import "FIRIAMFetchOnAppForegroundFlow.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMMsgFetcherUsingRestful.h" + +#import "FIRIAMRuntimeManager.h" +#import "FIRInAppMessaging.h" + +#import + +@interface AutoDisplayFlowViewController () +@property(weak, nonatomic) IBOutlet UISwitch *autoDisplayFlowSwitch; + +@property(nonatomic, weak) AutoDisplayMesagesTableVC *messageTableVC; +@property(weak, nonatomic) IBOutlet UITextField *autoDisplayIntervalText; +@property(weak, nonatomic) IBOutlet UITextField *autoFetchIntervalText; +@property(weak, nonatomic) IBOutlet UITextField *eventNameText; +@property(nonatomic) FIRIAMRuntimeManager *sdkRuntime; +@property(weak, nonatomic) IBOutlet UIButton *disableEnableSDKBtn; +@property(weak, nonatomic) IBOutlet UIButton *changeDataCollectionBtn; +@end + +@implementation AutoDisplayFlowViewController +- (IBAction)clearClientStorage:(id)sender { + [self.sdkRuntime.fetchResultStorage + saveResponseDictionary:@{} + withCompletion:^(BOOL success) { + [self.sdkRuntime.messageCache + loadMessageDataFromServerFetchStorage:self.sdkRuntime.fetchResultStorage + withCompletion:^(BOOL success) { + NSLog(@"load from storage result is %d", success); + }]; + }]; +} +- (IBAction)disableEnableClicked:(id)sender { + FIRInAppMessaging *sdk = [FIRInAppMessaging inAppMessaging]; + sdk.messageDisplaySuppressed = !sdk.messageDisplaySuppressed; + [self setupDisableEnableButtonLabel]; +} + +- (void)setupDisableEnableButtonLabel { + FIRInAppMessaging *sdk = [FIRInAppMessaging inAppMessaging]; + NSString *title = sdk.messageDisplaySuppressed ? @"allow rendering" : @"disallow rendering"; + [self.disableEnableSDKBtn setTitle:title forState:UIControlStateNormal]; +} + +- (IBAction)triggerAnalyticEventTapped:(id)sender { + NSLog(@"triggering an analytics event: %@", self.eventNameText.text); + + [FIRAnalytics logEventWithName:self.eventNameText.text parameters:@{}]; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + UITouch *touch = [touches anyObject]; + if (![touch.view isMemberOfClass:[UITextField class]]) { + [touch.view endEditing:YES]; + } +} +- (IBAction)changeAutoDataCollection:(id)sender { + FIRInAppMessaging *sdk = [FIRInAppMessaging inAppMessaging]; + sdk.automaticDataCollectionEnabled = !sdk.automaticDataCollectionEnabled; + [self setupChangeAutoDataCollectionButtonLabel]; +} + +- (void)setupChangeAutoDataCollectionButtonLabel { + FIRInAppMessaging *sdk = [FIRInAppMessaging inAppMessaging]; + NSString *title = sdk.automaticDataCollectionEnabled ? @"disable data-col" : @"enable data-col"; + [self.changeDataCollectionBtn setTitle:title forState:UIControlStateNormal]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + double delayInSeconds = 2.0; + dispatch_time_t setupTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); + dispatch_after(setupTime, dispatch_get_main_queue(), ^(void) { + // code to be executed on the main queue after delay + self.sdkRuntime = [FIRIAMRuntimeManager getSDKRuntimeInstance]; + self.messageTableVC.messageCache = self.sdkRuntime.messageCache; + [self.sdkRuntime.messageCache setDataObserver:self.messageTableVC]; + [self.messageTableVC.tableView reloadData]; + [self setupDisableEnableButtonLabel]; + [self setupChangeAutoDataCollectionButtonLabel]; + }); + + NSLog(@"done with set data observer"); + + self.autoFetchIntervalText.text = [[NSNumber + numberWithDouble:self.sdkRuntime.currentSetting.fetchMinIntervalInMinutes * 60] stringValue]; + self.autoDisplayIntervalText.text = + [[NSNumber numberWithDouble:self.sdkRuntime.currentSetting.appFGRenderMinIntervalInMinutes * + 60] stringValue]; +} + +- (IBAction)dumpImpressionsToConsole:(id)sender { + NSArray *impressions = [self.sdkRuntime.bookKeeper getImpressions]; + NSLog(@"impressions are %@", [impressions componentsJoinedByString:@","]); +} +- (IBAction)clearImpressionRecord:(id)sender { + [self.sdkRuntime.bookKeeper cleanupImpressions]; +} + +- (IBAction)changeAutoFetchDisplaySettings:(id)sender { + FIRIAMSDKSettings *setting = self.sdkRuntime.currentSetting; + + // set fetch interval + double intervalValue = self.autoFetchIntervalText.text.doubleValue / 60; + if (intervalValue < 0.0001) { + intervalValue = 1; + self.autoFetchIntervalText.text = [[NSNumber numberWithDouble:intervalValue * 60] stringValue]; + } + setting.fetchMinIntervalInMinutes = intervalValue; + + // set app foreground display interval + double displayIntervalValue = self.autoDisplayIntervalText.text.doubleValue / 60; + + if (displayIntervalValue < 0.0001) { + displayIntervalValue = 1; + self.autoDisplayIntervalText.text = + [[NSNumber numberWithDouble:displayIntervalValue * 60] stringValue]; + } + setting.appFGRenderMinIntervalInMinutes = displayIntervalValue; + + [self.sdkRuntime startRuntimeWithSDKSettings:setting]; +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +#pragma mark - Navigation +// In a storyboard-based application, you will often want to do a little preparation before +// navigation +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + // Get the new view controller using [segue destinationViewController]. + // Pass the selected object to the new view controller. + + if ([segue.identifier isEqualToString:@"message-table-segue"]) { + self.messageTableVC = (AutoDisplayMesagesTableVC *)[segue destinationViewController]; + } +} +@end diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.h b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.h new file mode 100644 index 00000000000..df529553d60 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.h @@ -0,0 +1,23 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMMessageClientCache.h" + +@interface AutoDisplayMesagesTableVC : UITableViewController +@property(nonatomic) FIRIAMMessageClientCache *messageCache; +@end diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.m b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.m new file mode 100644 index 00000000000..27e5c667a9a --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.m @@ -0,0 +1,137 @@ +/* + * Copyright 2017 Google + * + * 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 "AutoDisplayMesagesTableVC.h" +#import "FIRIAMDisplayTriggerDefinition.h" +#import "FIRIAMMessageContentData.h" + +@interface AutoDisplayMesagesTableVC () +@end + +@implementation AutoDisplayMesagesTableVC + +- (void)dataChanged { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.tableView reloadData]; + }); +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + // Uncomment the following line to preserve selection between presentations. + // self.clearsSelectionOnViewWillAppear = NO; + + // Uncomment the following line to display an Edit button in the navigation bar for this view + // controller. self.navigationItem.rightBarButtonItem = self.editButtonItem; +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +#pragma mark - Table view data source +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + NSArray *messages = self.messageCache.allRegularMessages; + if (messages) { + return messages.count; + } else { + return 0; + } +} + +static NSString *CellIdentifier = @"CellIdentifier"; + +- (NSString *)viewModeDisplayString:(FIRIAMRenderingMode)viewMode { + switch (viewMode) { + case FIRIAMRenderAsBannerView: + return @"Banner"; + case FIRIAMRenderAsModalView: + return @"Modal"; + case FIRIAMRenderAsImageOnlyView: + return @"Image"; + default: + return @"Unknown"; + } +} + +- (NSString *)triggerDisplayString:(NSArray *)triggers { + NSMutableString *s = [[NSMutableString alloc] init]; + for (FIRIAMDisplayTriggerDefinition *trigger in triggers) { + [s appendString:[self triggerDisplayStringForOneTrigger:trigger]]; + [s appendString:@","]; + } + return [s copy]; +} + +- (NSString *)triggerDisplayStringForOneTrigger: + (FIRIAMDisplayTriggerDefinition *)triggerDefinition { + switch (triggerDefinition.triggerType) { + case FIRIAMRenderTriggerOnAppForeground: + return @"app_foreground"; + case FIRIAMRenderTriggerOnFirebaseAnalyticsEvent: + return triggerDefinition.firebaseEventName; + default: + return @"Unknown"; + } +} +- (UITableViewCell *)tableView:(UITableView *)tableView + cellForRowAtIndexPath:(NSIndexPath *)indexPath { + NSArray *messageDefs = self.messageCache.allRegularMessages; + + NSInteger rowIndex = [indexPath row]; + if (messageDefs.count > rowIndex) { + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; + + if (cell == nil) { + cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle + reuseIdentifier:CellIdentifier]; + } + + UILabel *titleLabel = (UILabel *)[cell.contentView viewWithTag:10]; + UILabel *modeLabel = (UILabel *)[cell.contentView viewWithTag:20]; + UIImageView *imageView = (UIImageView *)[cell.contentView viewWithTag:30]; + UILabel *triggerLabel = (UILabel *)[cell.contentView viewWithTag:40]; + + titleLabel.text = messageDefs[rowIndex].renderData.contentData.titleText; + modeLabel.text = [self + viewModeDisplayString:messageDefs[rowIndex].renderData.renderingEffectSettings.viewMode]; + + triggerLabel.text = [self triggerDisplayString:messageDefs[rowIndex].renderTriggers]; + + [messageDefs[rowIndex].renderData.contentData + loadImageDataWithBlock:^(NSData *_Nullable imageData, NSError *error) { + if (error) { + NSLog(@"error in loading image: %@", error.localizedDescription); + } else { + UIImage *image = [UIImage imageWithData:imageData]; + dispatch_async(dispatch_get_main_queue(), ^{ + [imageView setImage:image]; + }); + } + }]; + return cell; + } else { + return nil; + } +} + +@end diff --git a/Example/Auth/SwiftSample/LaunchScreen.storyboard b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/LaunchScreen.storyboard similarity index 70% rename from Example/Auth/SwiftSample/LaunchScreen.storyboard rename to InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/LaunchScreen.storyboard index 8326657f7a5..fdf3f97d1b6 100644 --- a/Example/Auth/SwiftSample/LaunchScreen.storyboard +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/LaunchScreen.storyboard @@ -1,8 +1,8 @@ - + - - + + @@ -14,9 +14,9 @@ - + - + diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/Main.storyboard b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/Main.storyboard new file mode 100644 index 00000000000..0aa7f4cb7ca --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/Main.storyboard @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/GoogleService-Info.plist b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/GoogleService-Info.plist new file mode 100644 index 00000000000..3f7547fb48d --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/GoogleService-Info.plist @@ -0,0 +1,28 @@ + + + + + API_KEY + correct_api_key + TRACKING_ID + correct_tracking_id + CLIENT_ID + correct_client_id + REVERSED_CLIENT_ID + correct_reversed_client_id + GOOGLE_APP_ID + 1:123:ios:123abc + GCM_SENDER_ID + correct_gcm_sender_id + PLIST_VERSION + 1 + BUNDLE_ID + com.google.FirebaseSDKTests + PROJECT_ID + abc-xyz-123 + DATABASE_URL + https://abc-xyz-123.firebaseio.com + STORAGE_BUCKET + project-id-123.storage.firebase.com + + diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Info.plist b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Info.plist new file mode 100644 index 00000000000..f49d10287cd --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Info.plist @@ -0,0 +1,67 @@ + + + + + FirebaseInAppMessagingAutomaticDataCollectionEnabled + + FirebaseAutomaticDataCollectionEnabled + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0.3 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + mytesting + CFBundleURLSchemes + + fiam-testing + + + + CFBundleVersion + 1.0.3-1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.h b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.h new file mode 100644 index 00000000000..270c6cfb520 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.h @@ -0,0 +1,21 @@ +/* + * Copyright 2017 Google + * + * 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 + +@interface LogDumpViewController : UIViewController + +@end diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.m b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.m new file mode 100644 index 00000000000..c050cf40763 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.m @@ -0,0 +1,73 @@ +/* + * Copyright 2017 Google + * + * 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 "LogDumpViewController.h" +#import "AppDelegate.h" +#import "FIRIAMRuntimeManager.h" + +@interface LogDumpViewController () +@property(weak, nonatomic) IBOutlet UITextView *logTextView; +@end + +@implementation LogDumpViewController +- (IBAction)dumpImpressList:(id)sender { + NSArray *impressions = [[FIRIAMRuntimeManager getSDKRuntimeInstance].bookKeeper getImpressions]; + NSString *text = [NSString stringWithFormat:@"Message Impression History are :\n%@", + [impressions componentsJoinedByString:@"\n"]]; + self.logTextView.text = text; +} + +- (IBAction)dumActivityLogs:(id)sender { + NSArray *records = + [[FIRIAMRuntimeManager getSDKRuntimeInstance].activityLogger readRecords]; + + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + dateFormatter.dateStyle = NSDateFormatterShortStyle; + dateFormatter.timeStyle = NSDateFormatterMediumStyle; + + static NSString *appBuildVersion = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + appBuildVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; + }); + + NSMutableString *dumpContent = [[NSString + stringWithFormat:@"App Build Version -- %@\n\n" + "SDK Settings -- %@\n\n" + "Activity Logs: %lu records\n\n", + appBuildVersion, [FIRIAMRuntimeManager getSDKRuntimeInstance].currentSetting, + (unsigned long)records.count] mutableCopy]; + + for (FIRIAMActivityRecord *next in records) { + NSString *nextRecordLog = [NSString + stringWithFormat:@"%@, %@, %@, %@\n", [dateFormatter stringFromDate:next.timestamp], + [next displayStringForActivityType], next.success ? @"Success" : @"Failed", + next.detail]; + [dumpContent appendString:nextRecordLog]; + } + self.logTextView.text = dumpContent; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view. +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} +@end diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Podfile b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Podfile new file mode 100644 index 00000000000..f4396a22c41 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/Podfile @@ -0,0 +1,34 @@ + +use_frameworks! + + +target 'InAppMessaging_Example_iOS' do + platform :ios, '8.0' + + pod 'FirebaseCommunity/InAppMessaging', :path => '../..' + # Lock to the 1.0.9 version of InstanceID since 1.0.10 added a dependency + # to FirebaseCore + # pod 'FirebaseInstanceID', '1.0.9' + + #target 'InAppMessaging_Tests_iOS' do + # inherit! :search_paths + # pod 'FirebaseCommunity/InAppMessaging', :path => '../..' + # pod 'OCMock' + #end +end + + +#target 'InAppMessaging_Example_Swift_iOS' do +# platform :ios, '8.0' + +# pod 'FirebaseCommunity/InAppMessaging', :path => '../' + # Lock to the 1.0.9 version of InstanceID since 1.0.10 added a dependency + # to FirebaseCore +# pod 'FirebaseInstanceID', '1.0.9' + + #target 'Messaging_Tests_iOS' do + # inherit! :search_paths + # pod 'OCMock' + #end +#end + diff --git a/InAppMessaging/Example/App/InAppMessaging-Example-iOS/main.m b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/main.m new file mode 100644 index 00000000000..3b3b323ef02 --- /dev/null +++ b/InAppMessaging/Example/App/InAppMessaging-Example-iOS/main.m @@ -0,0 +1,24 @@ +/* + * Copyright 2017 Google + * + * 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 "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/GoogleService-Info.plist b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/GoogleService-Info.plist new file mode 100644 index 00000000000..cb4d6f4ac0f --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/GoogleService-Info.plist @@ -0,0 +1,28 @@ + + + + + API_KEY + correct_api_key + TRACKING_ID + correct_tracking_id + CLIENT_ID + correct_client_id + REVERSED_CLIENT_ID + correct_reversed_client_id + GOOGLE_APP_ID + 1:123:ios:123abc + GCM_SENDER_ID + correct_gcm_sender_id + PLIST_VERSION + 1 + BUNDLE_ID + com.google.FirebaseSDKTests + PROJECT_ID + abc-xyz-123 + DATABASE_URL + https://abc-xyz-123.firebaseio.com + STORAGE_BUCKET + project-id-123.storage.firebase.com + + diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/Podfile b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/Podfile new file mode 100644 index 00000000000..6e716cb976c --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/Podfile @@ -0,0 +1,18 @@ +# Uncomment the next line to define a global platform for your project +platform :ios, '8.4' + +# uncomment the follow two lines if you are trying to test internal releases +#source 'sso://cpdc-internal/spec.git' +#source 'https://github.com/CocoaPods/Specs.git' + +use_frameworks! + +target 'fiam-external-ios-testing-app' do + # Uncomment the next line if you're using Swift or would like to use dynamic frameworks + # use_frameworks! + + # Pods for fiam-external-ios-testing-app + pod 'Firebase/Core' + pod 'Firebase/InAppMessagingDisplay' + pod 'Firebase/DynamicLinks' +end diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app.xcodeproj/project.pbxproj b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..3f40036081f --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app.xcodeproj/project.pbxproj @@ -0,0 +1,423 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 7D31D8493943DCD77743922A /* Pods_fiam_external_ios_testing_app.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 12F51E141FEC71BA1CE57DC4 /* Pods_fiam_external_ios_testing_app.framework */; }; + AD7649C71FE1B0A800378AE0 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AD7649C61FE1B0A800378AE0 /* AppDelegate.m */; }; + AD7649CA1FE1B0A800378AE0 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AD7649C91FE1B0A800378AE0 /* ViewController.m */; }; + AD7649CD1FE1B0A800378AE0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD7649CB1FE1B0A800378AE0 /* Main.storyboard */; }; + AD7649CF1FE1B0A800378AE0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AD7649CE1FE1B0A800378AE0 /* Assets.xcassets */; }; + AD7649D21FE1B0A800378AE0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD7649D01FE1B0A800378AE0 /* LaunchScreen.storyboard */; }; + AD7649D51FE1B0A800378AE0 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AD7649D41FE1B0A800378AE0 /* main.m */; }; + AD7649DC1FE1B57A00378AE0 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = AD7649DB1FE1B57A00378AE0 /* GoogleService-Info.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 12F51E141FEC71BA1CE57DC4 /* Pods_fiam_external_ios_testing_app.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_fiam_external_ios_testing_app.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 61D649CA05E938083C88FC6D /* Pods-fiam-external-ios-testing-app.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fiam-external-ios-testing-app.debug.xcconfig"; path = "Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app.debug.xcconfig"; sourceTree = ""; }; + AD7649C21FE1B0A800378AE0 /* fiam-external-ios-testing-app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "fiam-external-ios-testing-app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + AD7649C51FE1B0A800378AE0 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + AD7649C61FE1B0A800378AE0 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + AD7649C81FE1B0A800378AE0 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + AD7649C91FE1B0A800378AE0 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + AD7649CC1FE1B0A800378AE0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + AD7649CE1FE1B0A800378AE0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + AD7649D11FE1B0A800378AE0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + AD7649D31FE1B0A800378AE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AD7649D41FE1B0A800378AE0 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + AD7649DB1FE1B57A00378AE0 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = SOURCE_ROOT; }; + EE0B5FD5B23F372E4894C799 /* Pods-fiam-external-ios-testing-app.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fiam-external-ios-testing-app.release.xcconfig"; path = "Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AD7649BF1FE1B0A800378AE0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7D31D8493943DCD77743922A /* Pods_fiam_external_ios_testing_app.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2F924E232047E700385C2AFA /* Frameworks */ = { + isa = PBXGroup; + children = ( + 12F51E141FEC71BA1CE57DC4 /* Pods_fiam_external_ios_testing_app.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 4DC924B9E0562D822E3E68F3 /* Pods */ = { + isa = PBXGroup; + children = ( + 61D649CA05E938083C88FC6D /* Pods-fiam-external-ios-testing-app.debug.xcconfig */, + EE0B5FD5B23F372E4894C799 /* Pods-fiam-external-ios-testing-app.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + AD7649B91FE1B0A800378AE0 = { + isa = PBXGroup; + children = ( + AD7649C41FE1B0A800378AE0 /* fiam-external-ios-testing-app */, + AD7649C31FE1B0A800378AE0 /* Products */, + 4DC924B9E0562D822E3E68F3 /* Pods */, + 2F924E232047E700385C2AFA /* Frameworks */, + ); + sourceTree = ""; + }; + AD7649C31FE1B0A800378AE0 /* Products */ = { + isa = PBXGroup; + children = ( + AD7649C21FE1B0A800378AE0 /* fiam-external-ios-testing-app.app */, + ); + name = Products; + sourceTree = ""; + }; + AD7649C41FE1B0A800378AE0 /* fiam-external-ios-testing-app */ = { + isa = PBXGroup; + children = ( + AD7649DB1FE1B57A00378AE0 /* GoogleService-Info.plist */, + AD7649C51FE1B0A800378AE0 /* AppDelegate.h */, + AD7649C61FE1B0A800378AE0 /* AppDelegate.m */, + AD7649C81FE1B0A800378AE0 /* ViewController.h */, + AD7649C91FE1B0A800378AE0 /* ViewController.m */, + AD7649CB1FE1B0A800378AE0 /* Main.storyboard */, + AD7649CE1FE1B0A800378AE0 /* Assets.xcassets */, + AD7649D01FE1B0A800378AE0 /* LaunchScreen.storyboard */, + AD7649D31FE1B0A800378AE0 /* Info.plist */, + AD7649D41FE1B0A800378AE0 /* main.m */, + ); + path = "fiam-external-ios-testing-app"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AD7649C11FE1B0A800378AE0 /* fiam-external-ios-testing-app */ = { + isa = PBXNativeTarget; + buildConfigurationList = AD7649D81FE1B0A800378AE0 /* Build configuration list for PBXNativeTarget "fiam-external-ios-testing-app" */; + buildPhases = ( + 89F39EB5CA1632B8B86E938F /* [CP] Check Pods Manifest.lock */, + AD7649BE1FE1B0A800378AE0 /* Sources */, + AD7649BF1FE1B0A800378AE0 /* Frameworks */, + AD7649C01FE1B0A800378AE0 /* Resources */, + AF2C898A9A08823BD10E1650 /* [CP] Embed Pods Frameworks */, + 638CE7E9369C7DEB067D82E7 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "fiam-external-ios-testing-app"; + productName = "fiam-external-ios-testing-app"; + productReference = AD7649C21FE1B0A800378AE0 /* fiam-external-ios-testing-app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AD7649BA1FE1B0A800378AE0 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0910; + ORGANIZATIONNAME = "Yong Mao"; + TargetAttributes = { + AD7649C11FE1B0A800378AE0 = { + CreatedOnToolsVersion = 9.1; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = AD7649BD1FE1B0A800378AE0 /* Build configuration list for PBXProject "fiam-external-ios-testing-app" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AD7649B91FE1B0A800378AE0; + productRefGroup = AD7649C31FE1B0A800378AE0 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AD7649C11FE1B0A800378AE0 /* fiam-external-ios-testing-app */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AD7649C01FE1B0A800378AE0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD7649D21FE1B0A800378AE0 /* LaunchScreen.storyboard in Resources */, + AD7649DC1FE1B57A00378AE0 /* GoogleService-Info.plist in Resources */, + AD7649CF1FE1B0A800378AE0 /* Assets.xcassets in Resources */, + AD7649CD1FE1B0A800378AE0 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 638CE7E9369C7DEB067D82E7 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInAppMessagingDisplay/InAppMessagingDisplayResources.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/InAppMessagingDisplayResources.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 89F39EB5CA1632B8B86E938F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-fiam-external-ios-testing-app-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + AF2C898A9A08823BD10E1650 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AD7649BE1FE1B0A800378AE0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD7649CA1FE1B0A800378AE0 /* ViewController.m in Sources */, + AD7649D51FE1B0A800378AE0 /* main.m in Sources */, + AD7649C71FE1B0A800378AE0 /* AppDelegate.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + AD7649CB1FE1B0A800378AE0 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AD7649CC1FE1B0A800378AE0 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + AD7649D01FE1B0A800378AE0 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AD7649D11FE1B0A800378AE0 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + AD7649D61FE1B0A800378AE0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + AD7649D71FE1B0A800378AE0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.1; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AD7649D91FE1B0A800378AE0 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 61D649CA05E938083C88FC6D /* Pods-fiam-external-ios-testing-app.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + INFOPLIST_FILE = "fiam-external-ios-testing-app/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.4; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.google.fiam-external-ios-testing"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AD7649DA1FE1B0A800378AE0 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EE0B5FD5B23F372E4894C799 /* Pods-fiam-external-ios-testing-app.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + INFOPLIST_FILE = "fiam-external-ios-testing-app/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.4; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.google.fiam-external-ios-testing"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AD7649BD1FE1B0A800378AE0 /* Build configuration list for PBXProject "fiam-external-ios-testing-app" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD7649D61FE1B0A800378AE0 /* Debug */, + AD7649D71FE1B0A800378AE0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AD7649D81FE1B0A800378AE0 /* Build configuration list for PBXNativeTarget "fiam-external-ios-testing-app" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD7649D91FE1B0A800378AE0 /* Debug */, + AD7649DA1FE1B0A800378AE0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AD7649BA1FE1B0A800378AE0 /* Project object */; +} diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.h b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.h new file mode 100644 index 00000000000..013891c90b6 --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.h @@ -0,0 +1,21 @@ +// Copyright 2017 Google +// +// 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 + +@interface AppDelegate : UIResponder + +@property(strong, nonatomic) UIWindow *window; + +@end diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.m b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.m new file mode 100644 index 00000000000..f645a71b700 --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.m @@ -0,0 +1,70 @@ +// Copyright 2017 Google +// +// 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 "AppDelegate.h" + +@import Firebase; + +@interface AppDelegate () + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + // uncomment the following line for disabling the auto startup + // of the sdk + // [FIRInAppMessaging inAppMessaging].automaticDataCollectionEnabled = @NO; + + [FIROptions defaultOptions].deepLinkURLScheme = @"com.google.InAppMessagingExampleiOS"; + [FIRApp configure]; + return YES; +} + +- (BOOL)application:(UIApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options { + NSLog(@"called here 1"); + return [self application:app + openURL:url + sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey] + annotation:options[UIApplicationOpenURLOptionsAnnotationKey]]; +} + +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + sourceApplication:(NSString *)sourceApplication + annotation:(id)annotation { + FIRDynamicLink *dynamicLink = [[FIRDynamicLinks dynamicLinks] dynamicLinkFromCustomSchemeURL:url]; + + NSLog(@"called here with %@", dynamicLink); + if (dynamicLink) { + if (dynamicLink.url) { + // Handle the deep link. For example, show the deep-linked content, + // apply a promotional offer to the user's account or show customized onboarding view. + // ... + + } else { + // Dynamic link has empty deep link. This situation will happens if + // Firebase Dynamic Links iOS SDK tried to retrieve pending dynamic link, + // but pending link is not available for this device/App combination. + // At this point you may display default onboarding view. + } + return YES; + } + return NO; +} +@end diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Assets.xcassets/AppIcon.appiconset/Contents.json b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..1d060ed2882 --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,93 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/LaunchScreen.storyboard b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000000..acde84d5a56 --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/Main.storyboard b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/Main.storyboard new file mode 100644 index 00000000000..67cb7ed4c58 --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/Main.storyboard @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Auth/SwiftSample/InfoTemplate.plist b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Info.plist similarity index 71% rename from Example/Auth/SwiftSample/InfoTemplate.plist rename to InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Info.plist index e51622bc7c1..15f461a4e6a 100644 --- a/Example/Auth/SwiftSample/InfoTemplate.plist +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Info.plist @@ -1,14 +1,11 @@ - CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + fiam-external-ios-testing CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -21,28 +18,16 @@ APPL CFBundleShortVersionString 1.0 - CFBundleSignature - ???? CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLName - com.google.swiftbear + my-url CFBundleURLSchemes - com.google.swiftbear - - - - CFBundleTypeRole - Editor - CFBundleURLName - $REVERSE_CLIENT_ID - CFBundleURLSchemes - - $REVERSE_CLIENT_ID + com.google.InAppMessagingExampleiOS @@ -71,9 +56,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - LSApplicationQueriesSchemes - - fbauth2 - diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/ViewController.h b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/ViewController.h new file mode 100644 index 00000000000..b6115b80707 --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/ViewController.h @@ -0,0 +1,19 @@ +// Copyright 2017 Google +// +// 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 + +@interface ViewController : UIViewController + +@end diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/ViewController.m b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/ViewController.m new file mode 100644 index 00000000000..44335d8292b --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/ViewController.m @@ -0,0 +1,39 @@ +// Copyright 2017 Google +// +// 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 "ViewController.h" + +#import + +@interface ViewController () +@property(weak, nonatomic) IBOutlet UITextField *urlText; + +@end + +@implementation ViewController +- (IBAction)triggerEvent:(id)sender { + [FIRAnalytics logEventWithName:self.urlText.text parameters:@{}]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +@end diff --git a/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/main.m b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/main.m new file mode 100644 index 00000000000..3b3b323ef02 --- /dev/null +++ b/InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/main.m @@ -0,0 +1,24 @@ +/* + * Copyright 2017 Google + * + * 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 "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/InAppMessaging/Example/FIRIAMSDKModeManagerTests.m b/InAppMessaging/Example/FIRIAMSDKModeManagerTests.m new file mode 100644 index 00000000000..eed680b3136 --- /dev/null +++ b/InAppMessaging/Example/FIRIAMSDKModeManagerTests.m @@ -0,0 +1,137 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import "FIRIAMSDKModeManager.h" + +@interface FIRIAMSDKModeManagerTests : XCTestCase +@property(nonatomic) NSUserDefaults *mockUserDefaults; +@property(nonatomic) id mockTestingModeListener; +@end + +@implementation FIRIAMSDKModeManagerTests + +- (void)setUp { + [super setUp]; + self.mockUserDefaults = OCMClassMock(NSUserDefaults.class); + self.mockTestingModeListener = OCMStrictProtocolMock(@protocol(FIRIAMTestingModeListener)); +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testFirstRunFromInstall_ok { + // mode entry not existing from a fresh install + OCMStub([self.mockUserDefaults objectForKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForSDKMode]]) + .andReturn(nil); + FIRIAMSDKModeManager *sdkManager = + [[FIRIAMSDKModeManager alloc] initWithUserDefaults:self.mockUserDefaults + testingModeListener:self.mockTestingModeListener]; + + XCTAssertEqual(FIRIAMSDKModeNewlyInstalled, [sdkManager currentMode]); + + // verify that we setting the mode into use defaults + OCMVerify([self.mockUserDefaults + setObject:[OCMArg isEqual:[NSNumber numberWithInt:FIRIAMSDKModeNewlyInstalled]] + forKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForSDKMode]]); + + // verify that we are initializing fetch count as 0 by writing into user defaults + OCMVerify([self.mockUserDefaults + setInteger:0 + forKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForServerFetchCount]]); +} + +- (void)testGoingIntoRegularFromNewlyInstalledMode { + // mode entry not existing from a fresh install + OCMStub([self.mockUserDefaults objectForKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForSDKMode]]) + .andReturn(nil); + FIRIAMSDKModeManager *sdkManager = + [[FIRIAMSDKModeManager alloc] initWithUserDefaults:self.mockUserDefaults + testingModeListener:self.mockTestingModeListener]; + XCTAssertEqual(FIRIAMSDKModeNewlyInstalled, [sdkManager currentMode]); + + // now we register up to kFIRIAMMaxFetchInNewlyInstalledMode - 1 fetches and it still stay + // in Newly Installed mode + for (int i = 0; i < kFIRIAMMaxFetchInNewlyInstalledMode - 1; i++) { + [sdkManager registerOneMoreFetch]; + } + XCTAssertEqual(FIRIAMSDKModeNewlyInstalled, [sdkManager currentMode]); + + // now one more fetch would turn it into regular mode + [sdkManager registerOneMoreFetch]; + XCTAssertEqual(FIRIAMSDKModeRegular, [sdkManager currentMode]); +} + +- (void)testIncrementCountForFetchRegistrationFromNewlyInstalledMode { + // put sdk into regular mode + OCMStub([self.mockUserDefaults objectForKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForSDKMode]]) + .andReturn([NSNumber numberWithInt:FIRIAMSDKModeNewlyInstalled]); + + int currentFetchCount = 3; + OCMStub([self.mockUserDefaults + integerForKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForServerFetchCount]]) + .andReturn(currentFetchCount); + FIRIAMSDKModeManager *sdkManager = + [[FIRIAMSDKModeManager alloc] initWithUserDefaults:self.mockUserDefaults + testingModeListener:self.mockTestingModeListener]; + + // now we do new fetch registeration + [sdkManager registerOneMoreFetch]; + + // verify that we are writing currentFetchCount+1 into user defaults + OCMVerify([self.mockUserDefaults + setInteger:currentFetchCount + 1 + forKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForServerFetchCount]]); +} + +- (void)testNoUpdateForFetchRegistrationFromRegularMode { + // put sdk into regular mode + OCMStub([self.mockUserDefaults objectForKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForSDKMode]]) + .andReturn([NSNumber numberWithInt:FIRIAMSDKModeRegular]); + + int currentFetchCount = 3; + OCMStub([self.mockUserDefaults + integerForKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForServerFetchCount]]) + .andReturn(currentFetchCount); + FIRIAMSDKModeManager *sdkManager = + [[FIRIAMSDKModeManager alloc] initWithUserDefaults:self.mockUserDefaults + testingModeListener:self.mockTestingModeListener]; + + // now we do new fetch registeration, but no more fetch count or mode updates in user defaults + [sdkManager registerOneMoreFetch]; + XCTAssertEqual(FIRIAMSDKModeRegular, [sdkManager currentMode]); + OCMReject([self.mockUserDefaults setInteger:currentFetchCount + 1 forKey:[OCMArg any]]); +} + +- (void)testGoingIntoTestingDeviceMode { + // mode entry not existing from a fresh install + OCMStub([self.mockUserDefaults objectForKey:[OCMArg isEqual:kFIRIAMUserDefaultKeyForSDKMode]]) + .andReturn(nil); + OCMExpect([self.mockTestingModeListener testingModeSwitchedOn]); + FIRIAMSDKModeManager *sdkManager = + [[FIRIAMSDKModeManager alloc] initWithUserDefaults:self.mockUserDefaults + testingModeListener:self.mockTestingModeListener]; + + [sdkManager becomeTestingInstance]; + XCTAssertEqual(FIRIAMSDKModeTesting, [sdkManager currentMode]); + OCMVerify([self.mockTestingModeListener testingModeSwitchedOn]); +} +@end diff --git a/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/project.pbxproj b/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..a2d174fd00f --- /dev/null +++ b/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/project.pbxproj @@ -0,0 +1,828 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 25C6745121EEC868005A4C23 /* NSString+InterlaceStringsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 25C6745021EEC868005A4C23 /* NSString+InterlaceStringsTests.m */; }; + 5BDD2462D9E03C4678945D2B /* Pods_InAppMessaging_Example_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 16E2417C62DE07F79F99BC3A /* Pods_InAppMessaging_Example_iOS.framework */; }; + AD0E370C1F7C63BC00BBF23C /* FIRIAMMsgFetcherUsingRestfulTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD0E370B1F7C63BC00BBF23C /* FIRIAMMsgFetcherUsingRestfulTests.m */; }; + AD0E37101F7DB3D500BBF23C /* FIRIAMFetchResponseParserTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD0E370F1F7DB3D500BBF23C /* FIRIAMFetchResponseParserTests.m */; }; + AD241B0F1F4F582A009A3C22 /* FIRIAMElapsedTimeTrackerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD241B0E1F4F582A009A3C22 /* FIRIAMElapsedTimeTrackerTests.m */; }; + AD3868531F95300E00E36BC5 /* FIRIAMMessageClientCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD3868521F95300E00E36BC5 /* FIRIAMMessageClientCacheTests.m */; }; + AD39268920AB55B700FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt in Resources */ = {isa = PBXBuildFile; fileRef = AD39268820AB55B700FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt */; }; + AD39268A20AB56B900FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt in Resources */ = {isa = PBXBuildFile; fileRef = AD39268820AB55B700FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt */; }; + AD3EE82A1F4CED0B006829AE /* FIRIAMDisplayExecutorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD3EE8291F4CED0B006829AE /* FIRIAMDisplayExecutorTests.m */; }; + AD5D25CB1FA91C9900F1B0EB /* FIRIAMActivityLoggerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD5D25CA1FA91C9900F1B0EB /* FIRIAMActivityLoggerTests.m */; }; + AD6A6CB11F56093700A6DFA1 /* FIRIAMBookKeeperViaUserDefaultsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD6A6CAE1F5606CD00A6DFA1 /* FIRIAMBookKeeperViaUserDefaultsTests.m */; }; + AD764A341FE4856400378AE0 /* AutoDisplayFlowViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AD764A2E1FE4856300378AE0 /* AutoDisplayFlowViewController.m */; }; + AD764A361FE4856400378AE0 /* LogDumpViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AD764A301FE4856300378AE0 /* LogDumpViewController.m */; }; + AD764A371FE4856400378AE0 /* AutoDisplayMesagesTableVC.m in Sources */ = {isa = PBXBuildFile; fileRef = AD764A331FE4856300378AE0 /* AutoDisplayMesagesTableVC.m */; }; + AD811A321F13F88800BF632A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AD811A311F13F88800BF632A /* main.m */; }; + AD811A351F13F88800BF632A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AD811A341F13F88800BF632A /* AppDelegate.m */; }; + AD811A3D1F13F88800BF632A /* Shared.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AD811A3C1F13F88800BF632A /* Shared.xcassets */; }; + AD81223D1F14100700BF632A /* FIRIAMMessageContentDataWithImageURLTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD8122391F140FFC00BF632A /* FIRIAMMessageContentDataWithImageURLTests.m */; }; + AD8122431F14148100BF632A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD81221B1F14064800BF632A /* LaunchScreen.storyboard */; }; + AD8122441F14148100BF632A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD8122181F14052100BF632A /* Main.storyboard */; }; + AD95D8EE1FFEFCA700780607 /* FIRIAMActionUrlFollowerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AD95D8ED1FFEFCA700780607 /* FIRIAMActionUrlFollowerTests.m */; }; + ADA10F98202CC1B0000F4425 /* AdSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ADA10F97202CC1B0000F4425 /* AdSupport.framework */; }; + ADA10F9A202CCBFC000F4425 /* FIRIAMAnalyticsEventLoggerImplTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADA10F99202CCBFC000F4425 /* FIRIAMAnalyticsEventLoggerImplTests.m */; }; + ADA72B9320282F3B0087E131 /* FIRIAMClearcutUploaderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADA72B9220282F3B0087E131 /* FIRIAMClearcutUploaderTests.m */; }; + ADB47ADF20A107C6002D52E9 /* TestJsonDataFromFetch.txt in Resources */ = {isa = PBXBuildFile; fileRef = ADB47ADE20A107C6002D52E9 /* TestJsonDataFromFetch.txt */; }; + ADB47AE020A107F3002D52E9 /* TestJsonDataFromFetch.txt in Resources */ = {isa = PBXBuildFile; fileRef = ADB47ADE20A107C6002D52E9 /* TestJsonDataFromFetch.txt */; }; + ADB47AE220A110F4002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt in Resources */ = {isa = PBXBuildFile; fileRef = ADB47AE120A110F4002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt */; }; + ADB47AE320A111D5002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt in Resources */ = {isa = PBXBuildFile; fileRef = ADB47AE120A110F4002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt */; }; + ADBC527A1F4CD18300A4BEF9 /* FIRIAMFetchFlowTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADBC52791F4CD18300A4BEF9 /* FIRIAMFetchFlowTests.m */; }; + ADC4298A1F8BEAFD00027599 /* UIColor+FIRIAMHexStringTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADC429891F8BEAFD00027599 /* UIColor+FIRIAMHexStringTests.m */; }; + ADC4298F1F8D3DD500027599 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = ADC4298E1F8D3DD500027599 /* GoogleService-Info.plist */; }; + ADCC091020782E2D009BFC2F /* FIRIAMSDKModeManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADCC090F20782E2D009BFC2F /* FIRIAMSDKModeManagerTests.m */; }; + ADD981962006D1C500944751 /* FIRIAMClearcutLoggerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADD981952006D1C500944751 /* FIRIAMClearcutLoggerTests.m */; }; + ADE5BB60200988A1001A1395 /* FIRIAMClearcutRetryLocalStorageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADE5BB5F200988A1001A1395 /* FIRIAMClearcutRetryLocalStorageTests.m */; }; + FA7A6267360499E94F504669 /* Pods_InAppMessaging_Tests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B403D5D166E193EAAB4C3B25 /* Pods_InAppMessaging_Tests_iOS.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + AD8122311F140F9700BF632A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AD811A251F13F88800BF632A /* Project object */; + proxyType = 1; + remoteGlobalIDString = AD811A2C1F13F88800BF632A; + remoteInfo = InAppMessaging_Example_iOS; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0AC166835CF567A8E912BD91 /* Pods-InAppMessaging_Example_iOS_Swift.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessaging_Example_iOS_Swift.release.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessaging_Example_iOS_Swift/Pods-InAppMessaging_Example_iOS_Swift.release.xcconfig"; sourceTree = ""; }; + 16E2417C62DE07F79F99BC3A /* Pods_InAppMessaging_Example_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InAppMessaging_Example_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 25C6745021EEC868005A4C23 /* NSString+InterlaceStringsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "NSString+InterlaceStringsTests.m"; path = "Tests/NSString+InterlaceStringsTests.m"; sourceTree = ""; }; + 4A218F594D01B3E92842DF08 /* Pods-InAppMessaging_Tests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessaging_Tests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessaging_Tests_iOS/Pods-InAppMessaging_Tests_iOS.release.xcconfig"; sourceTree = ""; }; + 92F3AF0CAD88D57986578C52 /* Pods-InAppMessaging_Tests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessaging_Tests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessaging_Tests_iOS/Pods-InAppMessaging_Tests_iOS.debug.xcconfig"; sourceTree = ""; }; + 9E53A331D50419C13BE9189E /* Pods_InAppMessaging_Example_iOS_Swift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InAppMessaging_Example_iOS_Swift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AD00D6E61F26655B00DB4967 /* InAppMessaging_Example_iOS_SwiftUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppMessaging_Example_iOS_SwiftUITests.swift; sourceTree = ""; }; + AD00D6E81F26655B00DB4967 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AD0E370B1F7C63BC00BBF23C /* FIRIAMMsgFetcherUsingRestfulTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMMsgFetcherUsingRestfulTests.m; path = Tests/FIRIAMMsgFetcherUsingRestfulTests.m; sourceTree = ""; }; + AD0E370F1F7DB3D500BBF23C /* FIRIAMFetchResponseParserTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMFetchResponseParserTests.m; path = Tests/FIRIAMFetchResponseParserTests.m; sourceTree = ""; }; + AD1469821FEC7DD5002051BF /* InAppMessaging_Example_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = InAppMessaging_Example_iOS.entitlements; sourceTree = ""; }; + AD241B0E1F4F582A009A3C22 /* FIRIAMElapsedTimeTrackerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMElapsedTimeTrackerTests.m; path = Tests/FIRIAMElapsedTimeTrackerTests.m; sourceTree = ""; }; + AD3868521F95300E00E36BC5 /* FIRIAMMessageClientCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMMessageClientCacheTests.m; path = Tests/FIRIAMMessageClientCacheTests.m; sourceTree = ""; }; + AD39268820AB55B700FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt */ = {isa = PBXFileReference; lastKnownFileType = text; name = JsonDataWithInvalidMessagesFromFetch.txt; path = Tests/JsonDataWithInvalidMessagesFromFetch.txt; sourceTree = ""; }; + AD3EE8291F4CED0B006829AE /* FIRIAMDisplayExecutorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMDisplayExecutorTests.m; path = Tests/FIRIAMDisplayExecutorTests.m; sourceTree = ""; }; + AD40FB071F38CCB700AB8C14 /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SnapshotHelper.swift; path = ../App/InAppMessaging_Example_iOS_Swift/SnapshotHelper.swift; sourceTree = ""; }; + AD5D25CA1FA91C9900F1B0EB /* FIRIAMActivityLoggerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMActivityLoggerTests.m; path = Tests/FIRIAMActivityLoggerTests.m; sourceTree = ""; }; + AD6A6CAE1F5606CD00A6DFA1 /* FIRIAMBookKeeperViaUserDefaultsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRIAMBookKeeperViaUserDefaultsTests.m; path = Tests/FIRIAMBookKeeperViaUserDefaultsTests.m; sourceTree = ""; }; + AD764A2C1FE4856300378AE0 /* AutoDisplayMesagesTableVC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AutoDisplayMesagesTableVC.h; path = "App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.h"; sourceTree = ""; }; + AD764A2D1FE4856300378AE0 /* AutoDisplayFlowViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AutoDisplayFlowViewController.h; path = "App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.h"; sourceTree = ""; }; + AD764A2E1FE4856300378AE0 /* AutoDisplayFlowViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AutoDisplayFlowViewController.m; path = "App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.m"; sourceTree = ""; }; + AD764A301FE4856300378AE0 /* LogDumpViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = LogDumpViewController.m; path = "App/InAppMessaging-Example-iOS/LogDumpViewController.m"; sourceTree = ""; }; + AD764A311FE4856300378AE0 /* LogDumpViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LogDumpViewController.h; path = "App/InAppMessaging-Example-iOS/LogDumpViewController.h"; sourceTree = ""; }; + AD764A331FE4856300378AE0 /* AutoDisplayMesagesTableVC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AutoDisplayMesagesTableVC.m; path = "App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.m"; sourceTree = ""; }; + AD811A2D1F13F88800BF632A /* InAppMessaging_Example_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InAppMessaging_Example_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AD811A311F13F88800BF632A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = main.m; path = "App/InAppMessaging-Example-iOS/main.m"; sourceTree = ""; }; + AD811A331F13F88800BF632A /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = "App/InAppMessaging-Example-iOS/AppDelegate.h"; sourceTree = ""; }; + AD811A341F13F88800BF632A /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = "App/InAppMessaging-Example-iOS/AppDelegate.m"; sourceTree = ""; }; + AD811A3C1F13F88800BF632A /* Shared.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Shared.xcassets; path = ../../Example/Shared/Shared.xcassets; sourceTree = ""; }; + AD811A411F13F88800BF632A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = "App/InAppMessaging-Example-iOS/Info.plist"; sourceTree = ""; }; + AD8122191F14052100BF632A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = "App/InAppMessaging-Example-iOS/Base.lproj/Main.storyboard"; sourceTree = ""; }; + AD81221C1F14064800BF632A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = "App/InAppMessaging-Example-iOS/Base.lproj/LaunchScreen.storyboard"; sourceTree = ""; }; + AD81222C1F140F9700BF632A /* InAppMessaging_Tests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InAppMessaging_Tests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + AD8122391F140FFC00BF632A /* FIRIAMMessageContentDataWithImageURLTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRIAMMessageContentDataWithImageURLTests.m; path = Tests/FIRIAMMessageContentDataWithImageURLTests.m; sourceTree = ""; }; + AD8122411F1412C500BF632A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Tests/Info.plist; sourceTree = ""; }; + AD95D8ED1FFEFCA700780607 /* FIRIAMActionUrlFollowerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMActionUrlFollowerTests.m; path = Tests/FIRIAMActionUrlFollowerTests.m; sourceTree = ""; }; + ADA10F97202CC1B0000F4425 /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; }; + ADA10F99202CCBFC000F4425 /* FIRIAMAnalyticsEventLoggerImplTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMAnalyticsEventLoggerImplTests.m; path = Tests/FIRIAMAnalyticsEventLoggerImplTests.m; sourceTree = ""; }; + ADA72B9220282F3B0087E131 /* FIRIAMClearcutUploaderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMClearcutUploaderTests.m; path = Tests/FIRIAMClearcutUploaderTests.m; sourceTree = ""; }; + ADB47ADE20A107C6002D52E9 /* TestJsonDataFromFetch.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestJsonDataFromFetch.txt; sourceTree = ""; }; + ADB47AE120A110F4002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestJsonDataWithTestMessageFromFetch.txt; sourceTree = ""; }; + ADBC52791F4CD18300A4BEF9 /* FIRIAMFetchFlowTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FIRIAMFetchFlowTests.m; path = Tests/FIRIAMFetchFlowTests.m; sourceTree = ""; }; + ADC429891F8BEAFD00027599 /* UIColor+FIRIAMHexStringTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "UIColor+FIRIAMHexStringTests.m"; path = "Tests/UIColor+FIRIAMHexStringTests.m"; sourceTree = ""; }; + ADC4298E1F8D3DD500027599 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "App/InAppMessaging-Example-iOS/GoogleService-Info.plist"; sourceTree = ""; }; + ADCC090F20782E2D009BFC2F /* FIRIAMSDKModeManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRIAMSDKModeManagerTests.m; sourceTree = ""; }; + ADD981952006D1C500944751 /* FIRIAMClearcutLoggerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMClearcutLoggerTests.m; path = Tests/FIRIAMClearcutLoggerTests.m; sourceTree = ""; }; + ADE5BB5F200988A1001A1395 /* FIRIAMClearcutRetryLocalStorageTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = FIRIAMClearcutRetryLocalStorageTests.m; path = Tests/FIRIAMClearcutRetryLocalStorageTests.m; sourceTree = ""; }; + B08DD02B5CAEC2B9FF562FBC /* Pods_fiam_sample_consuming_app.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_fiam_sample_consuming_app.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B403D5D166E193EAAB4C3B25 /* Pods_InAppMessaging_Tests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InAppMessaging_Tests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BF4A969BA440546630E4C931 /* Pods-InAppMessaging_Example_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessaging_Example_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessaging_Example_iOS/Pods-InAppMessaging_Example_iOS.debug.xcconfig"; sourceTree = ""; }; + E96B40A85DA4991275EC4934 /* Pods-InAppMessaging_Example_iOS_Swift.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessaging_Example_iOS_Swift.debug.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessaging_Example_iOS_Swift/Pods-InAppMessaging_Example_iOS_Swift.debug.xcconfig"; sourceTree = ""; }; + F0C2B7614A637C08CBDB8FF1 /* Pods-InAppMessaging_Example_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessaging_Example_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessaging_Example_iOS/Pods-InAppMessaging_Example_iOS.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AD811A2A1F13F88800BF632A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ADA10F98202CC1B0000F4425 /* AdSupport.framework in Frameworks */, + 5BDD2462D9E03C4678945D2B /* Pods_InAppMessaging_Example_iOS.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD8122291F140F9700BF632A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FA7A6267360499E94F504669 /* Pods_InAppMessaging_Tests_iOS.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9F765C4CE4252F7162E20DB1 /* Pods */ = { + isa = PBXGroup; + children = ( + BF4A969BA440546630E4C931 /* Pods-InAppMessaging_Example_iOS.debug.xcconfig */, + F0C2B7614A637C08CBDB8FF1 /* Pods-InAppMessaging_Example_iOS.release.xcconfig */, + E96B40A85DA4991275EC4934 /* Pods-InAppMessaging_Example_iOS_Swift.debug.xcconfig */, + 0AC166835CF567A8E912BD91 /* Pods-InAppMessaging_Example_iOS_Swift.release.xcconfig */, + 92F3AF0CAD88D57986578C52 /* Pods-InAppMessaging_Tests_iOS.debug.xcconfig */, + 4A218F594D01B3E92842DF08 /* Pods-InAppMessaging_Tests_iOS.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + AD00D6E51F26655B00DB4967 /* InAppMessaging_Example_iOS_SwiftUITests */ = { + isa = PBXGroup; + children = ( + AD40FB071F38CCB700AB8C14 /* SnapshotHelper.swift */, + AD00D6E61F26655B00DB4967 /* InAppMessaging_Example_iOS_SwiftUITests.swift */, + AD00D6E81F26655B00DB4967 /* Info.plist */, + ); + path = InAppMessaging_Example_iOS_SwiftUITests; + sourceTree = ""; + }; + AD531EF91F3A5D9D00E899A5 /* UI-Tests */ = { + isa = PBXGroup; + children = ( + AD00D6E51F26655B00DB4967 /* InAppMessaging_Example_iOS_SwiftUITests */, + ); + name = "UI-Tests"; + sourceTree = ""; + }; + AD531EFA1F3A5DB400E899A5 /* Unit Tests */ = { + isa = PBXGroup; + children = ( + ADE5BB5E20098880001A1395 /* Analytics */, + AD95D8EC1FFEFC6000780607 /* Runtime */, + AD6A6CAB1F56030F00A6DFA1 /* Util */, + ADBC52781F4CD12C00A4BEF9 /* Flows */, + AD8122411F1412C500BF632A /* Info.plist */, + AD8122371F140FEA00BF632A /* Data */, + AD8122361F140FE500BF632A /* UI */, + ); + name = "Unit Tests"; + sourceTree = ""; + }; + AD6A6CAB1F56030F00A6DFA1 /* Util */ = { + isa = PBXGroup; + children = ( + AD241B0E1F4F582A009A3C22 /* FIRIAMElapsedTimeTrackerTests.m */, + ADC429891F8BEAFD00027599 /* UIColor+FIRIAMHexStringTests.m */, + 25C6745021EEC868005A4C23 /* NSString+InterlaceStringsTests.m */, + ); + name = Util; + sourceTree = ""; + }; + AD811A241F13F88800BF632A = { + isa = PBXGroup; + children = ( + AD1469821FEC7DD5002051BF /* InAppMessaging_Example_iOS.entitlements */, + AD81221F1F140DE800BF632A /* Tests */, + AD811A471F13FB3A00BF632A /* App */, + AD811A2E1F13F88800BF632A /* Products */, + 9F765C4CE4252F7162E20DB1 /* Pods */, + AE2B80A4702458F0BCEF595B /* Frameworks */, + ); + sourceTree = ""; + }; + AD811A2E1F13F88800BF632A /* Products */ = { + isa = PBXGroup; + children = ( + AD811A2D1F13F88800BF632A /* InAppMessaging_Example_iOS.app */, + AD81222C1F140F9700BF632A /* InAppMessaging_Tests_iOS.xctest */, + ); + name = Products; + sourceTree = ""; + }; + AD811A471F13FB3A00BF632A /* App */ = { + isa = PBXGroup; + children = ( + AD8121DF1F13FC2D00BF632A /* iOS */, + ); + name = App; + sourceTree = ""; + }; + AD8121DF1F13FC2D00BF632A /* iOS */ = { + isa = PBXGroup; + children = ( + AD8121E01F13FC3800BF632A /* objc */, + ); + name = iOS; + sourceTree = ""; + }; + AD8121E01F13FC3800BF632A /* objc */ = { + isa = PBXGroup; + children = ( + AD764A2D1FE4856300378AE0 /* AutoDisplayFlowViewController.h */, + AD764A2E1FE4856300378AE0 /* AutoDisplayFlowViewController.m */, + AD764A2C1FE4856300378AE0 /* AutoDisplayMesagesTableVC.h */, + AD764A331FE4856300378AE0 /* AutoDisplayMesagesTableVC.m */, + AD764A311FE4856300378AE0 /* LogDumpViewController.h */, + AD764A301FE4856300378AE0 /* LogDumpViewController.m */, + ADC4298E1F8D3DD500027599 /* GoogleService-Info.plist */, + AD81221B1F14064800BF632A /* LaunchScreen.storyboard */, + AD8122181F14052100BF632A /* Main.storyboard */, + AD811A331F13F88800BF632A /* AppDelegate.h */, + AD811A341F13F88800BF632A /* AppDelegate.m */, + AD811A3C1F13F88800BF632A /* Shared.xcassets */, + AD811A411F13F88800BF632A /* Info.plist */, + AD811A311F13F88800BF632A /* main.m */, + ); + name = objc; + sourceTree = ""; + }; + AD81221F1F140DE800BF632A /* Tests */ = { + isa = PBXGroup; + children = ( + AD531EFA1F3A5DB400E899A5 /* Unit Tests */, + AD531EF91F3A5D9D00E899A5 /* UI-Tests */, + ); + name = Tests; + sourceTree = ""; + }; + AD8122361F140FE500BF632A /* UI */ = { + isa = PBXGroup; + children = ( + ); + name = UI; + sourceTree = ""; + }; + AD8122371F140FEA00BF632A /* Data */ = { + isa = PBXGroup; + children = ( + AD8122391F140FFC00BF632A /* FIRIAMMessageContentDataWithImageURLTests.m */, + AD0E370F1F7DB3D500BBF23C /* FIRIAMFetchResponseParserTests.m */, + ADB47ADE20A107C6002D52E9 /* TestJsonDataFromFetch.txt */, + ADB47AE120A110F4002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt */, + AD39268820AB55B700FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt */, + ); + name = Data; + sourceTree = ""; + }; + AD95D8EC1FFEFC6000780607 /* Runtime */ = { + isa = PBXGroup; + children = ( + AD95D8ED1FFEFCA700780607 /* FIRIAMActionUrlFollowerTests.m */, + ); + name = Runtime; + sourceTree = ""; + }; + ADBC52781F4CD12C00A4BEF9 /* Flows */ = { + isa = PBXGroup; + children = ( + AD6A6CAE1F5606CD00A6DFA1 /* FIRIAMBookKeeperViaUserDefaultsTests.m */, + ADBC52791F4CD18300A4BEF9 /* FIRIAMFetchFlowTests.m */, + AD3EE8291F4CED0B006829AE /* FIRIAMDisplayExecutorTests.m */, + AD0E370B1F7C63BC00BBF23C /* FIRIAMMsgFetcherUsingRestfulTests.m */, + AD3868521F95300E00E36BC5 /* FIRIAMMessageClientCacheTests.m */, + AD5D25CA1FA91C9900F1B0EB /* FIRIAMActivityLoggerTests.m */, + ADCC090F20782E2D009BFC2F /* FIRIAMSDKModeManagerTests.m */, + ); + name = Flows; + sourceTree = ""; + }; + ADE5BB5E20098880001A1395 /* Analytics */ = { + isa = PBXGroup; + children = ( + ADD981952006D1C500944751 /* FIRIAMClearcutLoggerTests.m */, + ADE5BB5F200988A1001A1395 /* FIRIAMClearcutRetryLocalStorageTests.m */, + ADA10F99202CCBFC000F4425 /* FIRIAMAnalyticsEventLoggerImplTests.m */, + ADA72B9220282F3B0087E131 /* FIRIAMClearcutUploaderTests.m */, + ); + name = Analytics; + sourceTree = ""; + }; + AE2B80A4702458F0BCEF595B /* Frameworks */ = { + isa = PBXGroup; + children = ( + ADA10F97202CC1B0000F4425 /* AdSupport.framework */, + 16E2417C62DE07F79F99BC3A /* Pods_InAppMessaging_Example_iOS.framework */, + 9E53A331D50419C13BE9189E /* Pods_InAppMessaging_Example_iOS_Swift.framework */, + B403D5D166E193EAAB4C3B25 /* Pods_InAppMessaging_Tests_iOS.framework */, + B08DD02B5CAEC2B9FF562FBC /* Pods_fiam_sample_consuming_app.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AD811A2C1F13F88800BF632A /* InAppMessaging_Example_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = AD811A441F13F88800BF632A /* Build configuration list for PBXNativeTarget "InAppMessaging_Example_iOS" */; + buildPhases = ( + B3566055E1E8BBB4417DE800 /* [CP] Check Pods Manifest.lock */, + AD811A291F13F88800BF632A /* Sources */, + AD811A2A1F13F88800BF632A /* Frameworks */, + AD811A2B1F13F88800BF632A /* Resources */, + EB73238A9A185D6764A4067A /* [CP] Embed Pods Frameworks */, + E8F0203E0D9D89F08A0D0D30 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = InAppMessaging_Example_iOS; + productName = "InAppMessaging-Example-iOS"; + productReference = AD811A2D1F13F88800BF632A /* InAppMessaging_Example_iOS.app */; + productType = "com.apple.product-type.application"; + }; + AD81222B1F140F9700BF632A /* InAppMessaging_Tests_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = AD8122351F140F9700BF632A /* Build configuration list for PBXNativeTarget "InAppMessaging_Tests_iOS" */; + buildPhases = ( + 51B09CAB6AC5DD3E8DDA85E3 /* [CP] Check Pods Manifest.lock */, + AD8122281F140F9700BF632A /* Sources */, + AD8122291F140F9700BF632A /* Frameworks */, + AD81222A1F140F9700BF632A /* Resources */, + 8A096C95C7D03347A14348EE /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + AD8122321F140F9700BF632A /* PBXTargetDependency */, + ); + name = InAppMessaging_Tests_iOS; + productName = InAppMessaging_Tests_iOS; + productReference = AD81222C1F140F9700BF632A /* InAppMessaging_Tests_iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AD811A251F13F88800BF632A /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0830; + LastUpgradeCheck = 0900; + ORGANIZATIONNAME = "Yong Mao"; + TargetAttributes = { + AD811A2C1F13F88800BF632A = { + CreatedOnToolsVersion = 8.3.3; + DevelopmentTeam = EQHXZ8M8AV; + LastSwiftMigration = 0910; + ProvisioningStyle = Manual; + SystemCapabilities = { + com.apple.SafariKeychain = { + enabled = 1; + }; + }; + }; + AD81222B1F140F9700BF632A = { + CreatedOnToolsVersion = 8.3.3; + DevelopmentTeam = L8VKXC2S77; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = AD811A281F13F88800BF632A /* Build configuration list for PBXProject "InAppMessaging-Example-iOS" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AD811A241F13F88800BF632A; + productRefGroup = AD811A2E1F13F88800BF632A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AD811A2C1F13F88800BF632A /* InAppMessaging_Example_iOS */, + AD81222B1F140F9700BF632A /* InAppMessaging_Tests_iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AD811A2B1F13F88800BF632A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD8122431F14148100BF632A /* LaunchScreen.storyboard in Resources */, + AD8122441F14148100BF632A /* Main.storyboard in Resources */, + ADC4298F1F8D3DD500027599 /* GoogleService-Info.plist in Resources */, + AD39268920AB55B700FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt in Resources */, + ADB47ADF20A107C6002D52E9 /* TestJsonDataFromFetch.txt in Resources */, + AD811A3D1F13F88800BF632A /* Shared.xcassets in Resources */, + ADB47AE220A110F4002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD81222A1F140F9700BF632A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD39268A20AB56B900FA3FDF /* JsonDataWithInvalidMessagesFromFetch.txt in Resources */, + ADB47AE320A111D5002D52E9 /* TestJsonDataWithTestMessageFromFetch.txt in Resources */, + ADB47AE020A107F3002D52E9 /* TestJsonDataFromFetch.txt in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 51B09CAB6AC5DD3E8DDA85E3 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-InAppMessaging_Tests_iOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 8A096C95C7D03347A14348EE /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-InAppMessaging_Tests_iOS/Pods-InAppMessaging_Tests_iOS-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-InAppMessaging_Tests_iOS/Pods-InAppMessaging_Tests_iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + B3566055E1E8BBB4417DE800 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-InAppMessaging_Example_iOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + }; + E8F0203E0D9D89F08A0D0D30 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 12; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-InAppMessaging_Example_iOS/Pods-InAppMessaging_Example_iOS-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInAppMessagingDisplay/InAppMessagingDisplayResources.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/InAppMessagingDisplayResources.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-InAppMessaging_Example_iOS/Pods-InAppMessaging_Example_iOS-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + EB73238A9A185D6764A4067A /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-InAppMessaging_Example_iOS/Pods-InAppMessaging_Example_iOS-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-InAppMessaging_Example_iOS/Pods-InAppMessaging_Example_iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AD811A291F13F88800BF632A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD764A341FE4856400378AE0 /* AutoDisplayFlowViewController.m in Sources */, + AD764A371FE4856400378AE0 /* AutoDisplayMesagesTableVC.m in Sources */, + AD811A351F13F88800BF632A /* AppDelegate.m in Sources */, + AD764A361FE4856400378AE0 /* LogDumpViewController.m in Sources */, + AD811A321F13F88800BF632A /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD8122281F140F9700BF632A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 25C6745121EEC868005A4C23 /* NSString+InterlaceStringsTests.m in Sources */, + ADC4298A1F8BEAFD00027599 /* UIColor+FIRIAMHexStringTests.m in Sources */, + AD3EE82A1F4CED0B006829AE /* FIRIAMDisplayExecutorTests.m in Sources */, + ADA72B9320282F3B0087E131 /* FIRIAMClearcutUploaderTests.m in Sources */, + ADCC091020782E2D009BFC2F /* FIRIAMSDKModeManagerTests.m in Sources */, + ADD981962006D1C500944751 /* FIRIAMClearcutLoggerTests.m in Sources */, + ADE5BB60200988A1001A1395 /* FIRIAMClearcutRetryLocalStorageTests.m in Sources */, + AD81223D1F14100700BF632A /* FIRIAMMessageContentDataWithImageURLTests.m in Sources */, + AD95D8EE1FFEFCA700780607 /* FIRIAMActionUrlFollowerTests.m in Sources */, + ADA10F9A202CCBFC000F4425 /* FIRIAMAnalyticsEventLoggerImplTests.m in Sources */, + ADBC527A1F4CD18300A4BEF9 /* FIRIAMFetchFlowTests.m in Sources */, + AD3868531F95300E00E36BC5 /* FIRIAMMessageClientCacheTests.m in Sources */, + AD0E370C1F7C63BC00BBF23C /* FIRIAMMsgFetcherUsingRestfulTests.m in Sources */, + AD6A6CB11F56093700A6DFA1 /* FIRIAMBookKeeperViaUserDefaultsTests.m in Sources */, + AD0E37101F7DB3D500BBF23C /* FIRIAMFetchResponseParserTests.m in Sources */, + AD241B0F1F4F582A009A3C22 /* FIRIAMElapsedTimeTrackerTests.m in Sources */, + AD5D25CB1FA91C9900F1B0EB /* FIRIAMActivityLoggerTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + AD8122321F140F9700BF632A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AD811A2C1F13F88800BF632A /* InAppMessaging_Example_iOS */; + targetProxy = AD8122311F140F9700BF632A /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + AD8122181F14052100BF632A /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AD8122191F14052100BF632A /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + AD81221B1F14064800BF632A /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AD81221C1F14064800BF632A /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + AD811A421F13F88800BF632A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AD811A431F13F88800BF632A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.3; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AD811A451F13F88800BF632A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = BF4A969BA440546630E4C931 /* Pods-InAppMessaging_Example_iOS.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = InAppMessaging_Example_iOS.entitlements; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/../../../Firebase/InAppMessaging/\"/**", + ); + INFOPLIST_FILE = "$(SRCROOT)/App/InAppMessaging-Example-iOS/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.4; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental1.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "3aa1288b-7c4f-4d59-8975-546306cf00ae"; + PROVISIONING_PROFILE_SPECIFIER = "Experimental App 1 Dev"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + AD811A461F13F88800BF632A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F0C2B7614A637C08CBDB8FF1 /* Pods-InAppMessaging_Example_iOS.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = InAppMessaging_Example_iOS.entitlements; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = EQHXZ8M8AV; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/../../../Firebase/InAppMessaging/\"/**", + ); + INFOPLIST_FILE = "$(SRCROOT)/App/InAppMessaging-Example-iOS/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.4; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental1.dev; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = "3aa1288b-7c4f-4d59-8975-546306cf00ae"; + PROVISIONING_PROFILE_SPECIFIER = "Experimental App 1 Dev"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + AD8122331F140F9700BF632A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 92F3AF0CAD88D57986578C52 /* Pods-InAppMessaging_Tests_iOS.debug.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = L8VKXC2S77; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/../../../Firebase/InAppMessaging/\"/**", + ); + INFOPLIST_FILE = Tests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.google.InAppMessaging-Tests-iOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + AD8122341F140F9700BF632A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4A218F594D01B3E92842DF08 /* Pods-InAppMessaging_Tests_iOS.release.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = L8VKXC2S77; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/../../../Firebase/InAppMessaging/\"/**", + ); + INFOPLIST_FILE = Tests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.google.InAppMessaging-Tests-iOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AD811A281F13F88800BF632A /* Build configuration list for PBXProject "InAppMessaging-Example-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD811A421F13F88800BF632A /* Debug */, + AD811A431F13F88800BF632A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AD811A441F13F88800BF632A /* Build configuration list for PBXNativeTarget "InAppMessaging_Example_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD811A451F13F88800BF632A /* Debug */, + AD811A461F13F88800BF632A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AD8122351F140F9700BF632A /* Build configuration list for PBXNativeTarget "InAppMessaging_Tests_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD8122331F140F9700BF632A /* Debug */, + AD8122341F140F9700BF632A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AD811A251F13F88800BF632A /* Project object */; +} diff --git a/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/xcshareddata/xcschemes/InAppMessaging_Example_iOS.xcscheme b/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/xcshareddata/xcschemes/InAppMessaging_Example_iOS.xcscheme new file mode 100644 index 00000000000..735c3f01be4 --- /dev/null +++ b/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/xcshareddata/xcschemes/InAppMessaging_Example_iOS.xcscheme @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_EarlGreyTests.xcscheme b/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/xcshareddata/xcschemes/InAppMessaging_Tests_iOS.xcscheme similarity index 71% rename from Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_EarlGreyTests.xcscheme rename to InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/xcshareddata/xcschemes/InAppMessaging_Tests_iOS.xcscheme index fe6245d2559..d80dabb17c5 100644 --- a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/Auth_EarlGreyTests.xcscheme +++ b/InAppMessaging/Example/InAppMessaging-Example-iOS.xcodeproj/xcshareddata/xcschemes/InAppMessaging_Tests_iOS.xcscheme @@ -5,35 +5,33 @@ - - - - - - + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> + BlueprintIdentifier = "AD81222B1F140F9700BF632A" + BuildableName = "InAppMessaging_Tests_iOS.xctest" + BlueprintName = "InAppMessaging_Tests_iOS" + ReferencedContainer = "container:InAppMessaging-Example-iOS.xcodeproj"> + + + + diff --git a/Example/Auth/SwiftSample/Sample.entitlements b/InAppMessaging/Example/InAppMessaging_Example_iOS.entitlements similarity index 54% rename from Example/Auth/SwiftSample/Sample.entitlements rename to InAppMessaging/Example/InAppMessaging_Example_iOS.entitlements index 9199daef185..31304199ac4 100644 --- a/Example/Auth/SwiftSample/Sample.entitlements +++ b/InAppMessaging/Example/InAppMessaging_Example_iOS.entitlements @@ -2,9 +2,9 @@ - application-identifier - $(AppIdentifierPrefix)$(CFBundleIdentifier) - aps-environment - development + com.apple.developer.associated-domains + + applinks:da29k.app.goo.gl + diff --git a/InAppMessaging/Example/InAppMessaging_Example_iOS_SwiftUITests/InAppMessaging_Example_iOS_SwiftUITests.swift b/InAppMessaging/Example/InAppMessaging_Example_iOS_SwiftUITests/InAppMessaging_Example_iOS_SwiftUITests.swift new file mode 100644 index 00000000000..71c526137ee --- /dev/null +++ b/InAppMessaging/Example/InAppMessaging_Example_iOS_SwiftUITests/InAppMessaging_Example_iOS_SwiftUITests.swift @@ -0,0 +1,680 @@ +/* + * Copyright 2017 Google + * + * 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 + +class InAppMessaging_Example_iOS_SwiftUITests: XCTestCase { + override func setUp() { + super.setUp() + continueAfterFailure = false + let app = XCUIApplication() + setupSnapshot(app) + app.launch() + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + XCUIDevice.shared.orientation = .portrait + super.tearDown() + } + + func waitForElementToAppear(_ element: XCUIElement, _ timeoutInSeconds: TimeInterval = 5) { + let existsPredicate = NSPredicate(format: "exists == true") + expectation(for: existsPredicate, evaluatedWith: element, handler: nil) + waitForExpectations(timeout: timeoutInSeconds, handler: nil) + } + + func waitForElementToDisappear(_ element: XCUIElement, _ timeoutInSeconds: TimeInterval = 5) { + let existsPredicate = NSPredicate(format: "exists == false") + expectation(for: existsPredicate, evaluatedWith: element, handler: nil) + waitForExpectations(timeout: timeoutInSeconds, handler: nil) + } + + func childFrameWithinParentBound(parent: XCUIElement, child: XCUIElement) -> Bool { + return parent.frame.contains(child.frame) + } + + func isUIElementWithinUIWindow(_ uiElement: XCUIElement) -> Bool { + let app = XCUIApplication() + let window = app.windows.element(boundBy: 0) + return window.frame.contains(uiElement.frame) + } + + func isElementExistentAndHavingSize(_ uiElement: XCUIElement) -> Bool { + // on iOS 9.3 for a XCUIElement whose height or width <=0, uiElement.exists still returns true + // on iOS 10.3, for such an element uiElement.exists returns false + // this function is to handle the existence (in our semanatic visible) testing for both cases + return uiElement.exists && uiElement.frame.size.height > 0 && uiElement.frame.size.width > 0 + } + + func testNormalModalView() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Regular"].tap() + + waitForElementToAppear(closeButton) + + snapshot("in-app-regular-modal-view-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithWideImage() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Thin Image"].tap() + + waitForElementToAppear(closeButton) + + snapshot("in-app-regular-modal-view-with-wider-image-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithNarrowImage() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Wide Image"].tap() + + waitForElementToAppear(closeButton) + + snapshot("in-app-regular-modal-view-with-narrow-image-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + } + } + + func testNormalBannerView() { + let app = XCUIApplication() + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Show Regular Banner View"].tap() + + waitForElementToAppear(bannerUIView) + + snapshot("in-app-regular-banner-view-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + + waitForElementToDisappear(bannerUIView) + } + } + + func testBannerViewAutoDimiss() { + let app = XCUIApplication() + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Banner View With Short Auto Dismiss"].tap() + + waitForElementToAppear(bannerUIView) + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + // without user action, the banner is dismissed quickly in this test setup + waitForElementToDisappear(bannerUIView, 15) + } + } + + func testBannerViewWithoutImage() { + let app = XCUIApplication() + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Without Image"].tap() + waitForElementToAppear(bannerUIView) + + snapshot("in-app-banner-view-without-image-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + waitForElementToDisappear(bannerUIView) + } + } + + func testBannerViewWithLongTitle() { + let app = XCUIApplication() + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["With Long Title"].tap() + waitForElementToAppear(bannerUIView) + + snapshot("in-app-banner-view-with-long-title-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + waitForElementToDisappear(bannerUIView) + } + } + + func testBannerViewWithWideImage() { + let app = XCUIApplication() + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["With Wide Image"].tap() + waitForElementToAppear(bannerUIView) + + snapshot("in-app-banner-view-with-wide-image-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + waitForElementToDisappear(bannerUIView) + } + } + + func testBannerViewWithThinImage() { + let app = XCUIApplication() + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["With Thin Image"].tap() + waitForElementToAppear(bannerUIView) + + snapshot("in-app-banner-view-with-thing-image-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + waitForElementToDisappear(bannerUIView) + } + } + + func testBannerViewWithLargeBody() { + let app = XCUIApplication() + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["With Large Body Text"].tap() + waitForElementToAppear(bannerUIView) + + snapshot("in-app-banner-view-with-long-body-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + waitForElementToDisappear(bannerUIView) + } + } + + func testImageOnlyView() { + let app = XCUIApplication() + app.tabBars.buttons["Image Only Messages"].tap() + + let imageView = app.images["image-view-in-image-only-view"] + let closeButton = app.buttons["close-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Show Regular Image Only View"].tap() + + waitForElementToAppear(closeButton) + snapshot("in-app-regular-image-only-view-\(orientation.rawValue)") + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isUIElementWithinUIWindow(imageView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(imageView) + } + } + + func testImageOnlyViewWithLargeImageDimension() { + let app = XCUIApplication() + app.tabBars.buttons["Image Only Messages"].tap() + + let imageView = app.images["image-view-in-image-only-view"] + let closeButton = app.buttons["close-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["High Dimension Image"].tap() + + // wait time longer due to large image + waitForElementToAppear(closeButton, 10) + + snapshot("in-app-large-image-only-view-high-dimension-\(orientation.rawValue)") + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isUIElementWithinUIWindow(imageView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(imageView) + } + } + + func testImageOnlyViewWithLowImageDimension() { + let app = XCUIApplication() + app.tabBars.buttons["Image Only Messages"].tap() + + let imageView = app.images["image-view-in-image-only-view"] + let closeButton = app.buttons["close-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Low Dimension Image"].tap() + + // wait time longer due to large image + waitForElementToAppear(closeButton, 10) + + snapshot("in-app-large-image-only-view-low-dimension-\(orientation.rawValue)") + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isUIElementWithinUIWindow(imageView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(imageView) + } + } + + func testModalViewWithoutImage() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let actionButton = app.buttons["message-action-button"] + let imageView = app.images["modal-image-view"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Without Image"].tap() + waitForElementToAppear(closeButton) + + snapshot("in-app-no-image-modal-view-\(orientation.rawValue)") + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssertFalse(isElementExistentAndHavingSize(imageView)) + + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithoutImageOrActionButton() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Wthout Image or Action Button"].tap() + + waitForElementToAppear(closeButton) + + snapshot("in-app-no-image-no-button-modal-view-\(orientation.rawValue)") + XCTAssertFalse(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + XCUIDevice.shared.orientation = .portrait + } + } + + func testModalViewWithoutActionButton() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Without Action Button"].tap() + waitForElementToAppear(closeButton) + + snapshot("in-app-no-action-button-moal-view-\(orientation.rawValue)") + XCTAssertFalse(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithLongMessageTitle() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let bodyTextview = app.textViews["message-body-textview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Large Title Text"].tap() + waitForElementToAppear(closeButton) + + snapshot("in-app-long-title-modal-view-\(orientation.rawValue)") + let actionButton = app.buttons["message-action-button"] + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(bodyTextview)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: bodyTextview)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: imageView)) + + app.buttons["close-button"].tap() + + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithLongMessageBody() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let bodyTextview = app.textViews["message-body-textview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Large Title Text"].tap() + waitForElementToAppear(closeButton) + + snapshot("in-app-long-body-modal-view-\(orientation.rawValue)") + let actionButton = app.buttons["message-action-button"] + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(bodyTextview)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: bodyTextview)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: imageView)) + + app.buttons["close-button"].tap() + + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithLongMessageTitleAndMessageBody() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let bodyTextview = app.textViews["message-body-textview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["With Large Title and Body"].tap() + waitForElementToAppear(closeButton) + + snapshot("in-app-long-title-and-body-modal-view-\(orientation.rawValue)") + let actionButton = app.buttons["message-action-button"] + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(bodyTextview)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: bodyTextview)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: imageView)) + + app.buttons["close-button"].tap() + + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithLongMessageTitleAndMessageBodyWithoutImage() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let bodyTextview = app.textViews["message-body-textview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["With Large Title and Body Without Image"].tap() + waitForElementToAppear(closeButton) + + snapshot("in-app-long-title-and-body-no-image-modal-view-\(orientation.rawValue)") + let actionButton = app.buttons["message-action-button"] + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(bodyTextview)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(!isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: bodyTextview)) + + app.buttons["close-button"].tap() + + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithLongMessageTitleWithoutBodyWithoutImageWithoutButton() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let bodyTextview = app.textViews["message-body-textview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["With Large Title, No Image, No Body and No Button"].tap() + waitForElementToAppear(closeButton) + + snapshot("in-app-long-title-no-image-body-button-modal-view-\(orientation.rawValue)") + let actionButton = app.buttons["message-action-button"] + + XCTAssert(!isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(!isElementExistentAndHavingSize(bodyTextview)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(!isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + + app.buttons["close-button"].tap() + + waitForElementToDisappear(messageCardView) + } + } +} diff --git a/InAppMessaging/Example/InAppMessaging_Example_iOS_SwiftUITests/Info.plist b/InAppMessaging/Example/InAppMessaging_Example_iOS_SwiftUITests/Info.plist new file mode 100644 index 00000000000..6c6c23c43ad --- /dev/null +++ b/InAppMessaging/Example/InAppMessaging_Example_iOS_SwiftUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/InAppMessaging/Example/Podfile b/InAppMessaging/Example/Podfile new file mode 100644 index 00000000000..04a75ebcee4 --- /dev/null +++ b/InAppMessaging/Example/Podfile @@ -0,0 +1,26 @@ +use_frameworks! + +# Uncomment the next two lines for pre-release testing on public repo +# source 'https://github.com/Firebase/SpecsStaging.git' +# source 'https://github.com/CocoaPods/Specs.git' + +pod 'FirebaseCore', :path => '../..' + +target 'InAppMessaging_Example_iOS' do + platform :ios, '8.0' + + pod 'FirebaseInAppMessagingDisplay', :path => '../..' + pod 'FirebaseInAppMessaging', :path => '../..' + pod 'FirebaseAnalyticsInterop', :path => '../..' + pod 'FirebaseAnalytics' + pod 'FirebaseDynamicLinks', :path => '../..' +end + +target 'InAppMessaging_Tests_iOS' do + platform :ios, '8.0' + + pod 'FirebaseInAppMessaging', :path => '../..' + pod 'FirebaseInstanceID' + pod 'FirebaseAnalyticsInterop', :path => '../..' + pod 'OCMock' +end diff --git a/InAppMessaging/Example/Scanfile b/InAppMessaging/Example/Scanfile new file mode 100644 index 00000000000..43b96a958a3 --- /dev/null +++ b/InAppMessaging/Example/Scanfile @@ -0,0 +1,13 @@ +# For more information about this configuration visit +# https://github.com/fastlane/fastlane/tree/master/scan#scanfile + +# In general, you can use the options available +# fastlane scan --help + +# Remove the # in front of the line to enable the option + +workspace "InAppMessaging-Example-iOS.xcworkspace" +scheme "InAppMessaging_Example_iOS_Swift" +devices ["iPad Pro", "iPhone 6s", "iPhone 4s", "iPhone 7Plus"] +open_report true +clean true diff --git a/InAppMessaging/Example/Snapfile b/InAppMessaging/Example/Snapfile new file mode 100644 index 00000000000..9c3aed4cc48 --- /dev/null +++ b/InAppMessaging/Example/Snapfile @@ -0,0 +1,37 @@ +# Uncomment the lines below you want to change by removing the # in the beginning + +# A list of devices you want to take the screenshots from + devices([ +# "iPhone 4s", + "iPhone 6 Plus", + "iPhone 5s", + "iPhone 7", + "iPad Pro (12.9 inch)", +# "iPad Pro (9.7 inch)", +# "Apple TV 1080p" + ]) + + +languages([ + "en-US", +]) + +# The name of the scheme which contains the UI Tests +scheme "InAppMessaging_Example_iOS_Swift" + +# Where should the resulting screenshots be stored? +output_directory "./screenshots" + +clear_previous_screenshots true # remove the '#' to clear all previously generated screenshots before creating new ones + +# Choose which project/workspace to use +# project "./Project.xcodeproj" +# workspace "./Project.xcworkspace" + +workspace "InAppMessaging-Example-iOS.xcworkspace" + +# Arguments to pass to the app on launch. See https://github.com/fastlane/fastlane/tree/master/snapshot#launch-arguments +# launch_arguments(["-favColor red"]) + +# For more information about all available options run +# fastlane snapshot --help diff --git a/InAppMessaging/Example/TestJsonDataFromFetch.txt b/InAppMessaging/Example/TestJsonDataFromFetch.txt new file mode 100644 index 00000000000..ce305646e0f --- /dev/null +++ b/InAppMessaging/Example/TestJsonDataFromFetch.txt @@ -0,0 +1,169 @@ +{ + "messages": [ + { + "vanillaPayload": { + "campaignId": "13313766398414028800", + "campaignStartTimeMillis": "1523986039000", + "campaignEndTimeMillis": "1526986039000", + "campaignName": "first campaign" + }, + "content": { + "modal": { + "title": { + "text": "I heard you like In-App Messages", + "hexColor": "#000000" + }, + "body": { + "text": "This is message body", + "hexColor": "#000000" + }, + "imageUrl": "https://image.com/5GCaq8sWMgk", + "actionButton": { + "text": { + "text": "Learn More", + "hexColor": "#ffffff" + }, + "buttonHexColor": "#000000" + }, + "action": { + "actionUrl": "https://www.google.com" + }, + "backgroundHexColor": "#fffff8" + } + }, + "priority": { + "value": 1 + }, + "triggeringConditions": [ + { + "fiamTrigger": "ON_FOREGROUND" + }, + { + "event": { + "name": "jackpot" + } + } + ] + }, + { + "vanillaPayload": { + "campaignId": "9350598726327992320", + "campaignStartTimeMillis": "1523985333000", + "campaignEndTimeMillis": "9223372036854775807", + "campaignName": "Inception1" + }, + "content": { + "modal": { + "title": { + "text": "Test 2", + "hexColor": "#000000" + }, + "body": { + "hexColor": "#000000" + }, + "imageUrl": "https://image.com/5GCaq8sWMgk.jpg", + "actionButton": { + "text": { + "text": "Learn More", + "hexColor": "#ffffff" + }, + "buttonHexColor": "#000000" + }, + "action": { + "actionUrl": "https://www.google.com" + }, + "backgroundHexColor": "#ffffff" + } + }, + "priority": { + "value": 1 + }, + "triggeringConditions": [ + { + "fiamTrigger": "ON_FOREGROUND" + }, + { + "event": { + "name": "jackpot" + } + } + ] + }, + { + "vanillaPayload": { + "campaignId": "14819094573862617088", + "campaignStartTimeMillis": "1519934825000", + "campaignEndTimeMillis": "9223372036854775807", + "campaignName": "Top banner" + }, + "content": { + "banner": { + "title": { + "text": "Hey everybody!", + "hexColor": "#000000" + }, + "body": { + "text": "This is an in-app message! Now go to Screen 2!", + "hexColor": "#000000" + }, + "imageUrl": "https://image.com/5YYCaq8sWMgk.png", + "action": { + "actionUrl": "https://test-app.firebaseapp.com/Calculator/screen2" + }, + "backgroundHexColor": "#ffffff" + } + }, + "priority": { + "value": 1 + }, + "triggeringConditions": [ + { + "event": { + "name": "jackpot" + } + } + ] + }, + { + "vanillaPayload": { + "campaignId": "5595722537007841280", + "campaignStartTimeMillis": "1519934650000", + "campaignEndTimeMillis": "9223372036854775807", + "campaignName": "Ducks on foreground" + }, + "content": { + "modal": { + "title": { + "text": "Look, it's a duck!", + "hexColor": "#000000" + }, + "body": { + "text": "It's a very nice duck.", + "hexColor": "#000000" + }, + "imageUrl": "https://image.com/5YYCaq8sWMgkff.png", + "actionButton": { + "text": { + "text": "Go to Google.com", + "hexColor": "#ffffff" + }, + "buttonHexColor": "#000000" + }, + "action": { + "actionUrl": "https://www.google.com" + }, + "backgroundHexColor": "#ffffff" + } + }, + "priority": { + "value": 1 + }, + "triggeringConditions": [ + { + "fiamTrigger": "ON_FOREGROUND" + } + ] + } + ], + "expirationEpochTimestampMillis": "1537896430193" +} diff --git a/InAppMessaging/Example/TestJsonDataWithTestMessageFromFetch.txt b/InAppMessaging/Example/TestJsonDataWithTestMessageFromFetch.txt new file mode 100644 index 00000000000..637eb5cf68d --- /dev/null +++ b/InAppMessaging/Example/TestJsonDataWithTestMessageFromFetch.txt @@ -0,0 +1,71 @@ +{ + "messages": [ + { + "vanillaPayload": { + "campaignId": "2108810525516234752" + }, + "content": { + "modal": { + "title": { + "text": "FAST", + "hexColor": "#000000" + }, + "body": { + "hexColor": "#000000" + }, + "backgroundHexColor": "#ffffff" + } + }, + "triggeringConditions": [ + { + "fiamTrigger": "ON_FOREGROUND" + } + ], + "isTestCampaign": true + }, + { + "vanillaPayload": { + "campaignId": "13313766398414028800", + "campaignStartTimeMillis": "1523986039000", + "campaignEndTimeMillis": "9223372036854775807", + "campaignName": "copy of Inception1" + }, + "content": { + "modal": { + "title": { + "text": "I heard you like In-App Messages", + "hexColor": "#000000" + }, + "body": { + "hexColor": "#000000" + }, + "imageUrl": "https://google.com/an_image", + "actionButton": { + "text": { + "text": "Learn More", + "hexColor": "#ffffff" + }, + "buttonHexColor": "#000000" + }, + "action": { + "actionUrl": "https://www.google.com" + }, + "backgroundHexColor": "#ffffff" + } + }, + "priority": { + "value": 1 + }, + "triggeringConditions": [ + { + "fiamTrigger": "ON_FOREGROUND" + }, + { + "event": { + "name": "jackpot" + } + } + ] + } + ] +} diff --git a/InAppMessaging/Example/Tests/FIRIAMActionUrlFollowerTests.m b/InAppMessaging/Example/Tests/FIRIAMActionUrlFollowerTests.m new file mode 100644 index 00000000000..0f85028c68a --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMActionUrlFollowerTests.m @@ -0,0 +1,270 @@ +/* + * Copyright 2018 Google + * + * 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 + +#import "FIRIAMActionURLFollower.h" + +// since OCMock does support mocking respondsToSelector on mock object, we have to define +// different delegate classes with different coverages of certain delegate methods: +// FIRIAMActionURLFollower behavior depend on these method implementation coverages on the +// delegate + +// this delegate only implements application:continueUserActivity:restorationHandler +@interface Delegate1 : NSObject +- (BOOL)application:(UIApplication *)application + continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void (^)(NSArray *))restorationHandler; +@end +@implementation Delegate1 +- (BOOL)application:(UIApplication *)application + continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void (^)(NSArray *))restorationHandler { + return YES; +} +@end + +// this delegate only implements application:openURL:options which is suitable for custom url scheme +// link handling +@interface Delegate2 : NSObject +- (BOOL)application:(UIApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options; +@end +@implementation Delegate2 +- (BOOL)application:(UIApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options { + return YES; +} +@end + +@interface FIRIAMActionURLFollowerTests : XCTestCase +@property FIRIAMActionURLFollower *actionFollower; +@property UIApplication *mockApplication; +@property id mockAppDelegate; +@end + +@implementation FIRIAMActionURLFollowerTests + +- (void)setUp { + [super setUp]; + self.mockApplication = OCMClassMock([UIApplication class]); +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testUniversalLinkHandlingReturnYES { + self.mockAppDelegate = OCMClassMock([Delegate1 class]); + OCMStub([self.mockApplication delegate]).andReturn(self.mockAppDelegate); + + // In this test case, app delegate's application:continueUserActivity:restorationHandler + // handles the url and returns YES + + NSURL *url = [NSURL URLWithString:@"http://test.com"]; + OCMExpect([self.mockAppDelegate application:[OCMArg isKindOfClass:[UIApplication class]] + continueUserActivity:[OCMArg checkWithBlock:^BOOL(id userActivity) { + // verifying the type and url field for the userActivity object + NSUserActivity *activity = (NSUserActivity *)userActivity; + return [activity.activityType + isEqualToString:NSUserActivityTypeBrowsingWeb] && + [activity.webpageURL isEqual:url]; + }] + restorationHandler:[OCMArg any]]) + .andReturn(YES); + + FIRIAMActionURLFollower *follower = + [[FIRIAMActionURLFollower alloc] initWithCustomURLSchemeArray:@[] + withApplication:self.mockApplication]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Completion Callback Triggered"]; + [follower followActionURL:url + withCompletionBlock:^(BOOL success) { + XCTAssertTrue(success); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerifyAll((id)self.mockAppDelegate); +} + +- (void)setupOpenURLViaIOSForUIApplicationWithReturnValue:(BOOL)returnValue { + // it would fallback to either openURL:options:completionHandler: + // or openURL: on the UIApplication object to follow the url + if ([self.mockApplication respondsToSelector:@selector(openURL:options:completionHandler:)]) { + // id types is needed for calling invokeBlockWithArgs + id yesOrNo = returnValue ? @YES : @NO; + OCMStub([self.mockApplication openURL:[OCMArg any] + options:[OCMArg any] + completionHandler:([OCMArg invokeBlockWithArgs:yesOrNo, nil])]); + } else { + OCMStub([self.mockApplication openURL:[OCMArg any]]).andReturn(returnValue); + } +} + +- (void)testUniversalLinkHandlingReturnNo { + self.mockAppDelegate = OCMClassMock([Delegate1 class]); + OCMStub([self.mockApplication delegate]).andReturn(self.mockAppDelegate); + + // In this test case, app delegate's application:continueUserActivity:restorationHandler + // tries to handle the url but returns NO. We should fallback to the do iOS OpenURL for + // this case + NSURL *url = [NSURL URLWithString:@"http://test.com"]; + OCMExpect([self.mockAppDelegate application:[OCMArg isKindOfClass:[UIApplication class]] + continueUserActivity:[OCMArg any] + restorationHandler:[OCMArg any]]) + .andReturn(NO); + + [self setupOpenURLViaIOSForUIApplicationWithReturnValue:YES]; + + FIRIAMActionURLFollower *follower = + [[FIRIAMActionURLFollower alloc] initWithCustomURLSchemeArray:@[] + withApplication:self.mockApplication]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Completion Callback Triggered"]; + [follower followActionURL:url + withCompletionBlock:^(BOOL success) { + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerifyAll((id)self.mockAppDelegate); +} + +- (void)testCustomSchemeHandlingReturnYES { + self.mockAppDelegate = OCMClassMock([Delegate2 class]); + OCMStub([self.mockApplication delegate]).andReturn(self.mockAppDelegate); + + // we support custom url scheme 'scheme1' and 'scheme2' in this setup + FIRIAMActionURLFollower *follower = + [[FIRIAMActionURLFollower alloc] initWithCustomURLSchemeArray:@[ @"scheme1", @"scheme2" ] + withApplication:self.mockApplication]; + + NSURL *customURL = [NSURL URLWithString:@"scheme1://test.com"]; + OCMExpect([self.mockAppDelegate application:[OCMArg isKindOfClass:[UIApplication class]] + openURL:[OCMArg checkWithBlock:^BOOL(id urlId) { + // verifying url received by the app delegate is expected + NSURL *url = (NSURL *)urlId; + return [url isEqual:customURL]; + }] + options:[OCMArg any]]) + .andReturn(YES); + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Completion Callback Triggered"]; + [follower followActionURL:customURL + withCompletionBlock:^(BOOL success) { + XCTAssertTrue(success); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerifyAll((id)self.mockAppDelegate); +} + +- (void)testCustomSchemeHandlingReturnNO { + self.mockAppDelegate = OCMClassMock([Delegate2 class]); + OCMStub([self.mockApplication delegate]).andReturn(self.mockAppDelegate); + + // we support custom url scheme 'scheme1' and 'scheme2' in this setup + FIRIAMActionURLFollower *follower = + [[FIRIAMActionURLFollower alloc] initWithCustomURLSchemeArray:@[ @"scheme1", @"scheme2" ] + withApplication:self.mockApplication]; + + NSURL *customURL = [NSURL URLWithString:@"scheme1://test.com"]; + OCMExpect([self.mockAppDelegate application:[OCMArg isKindOfClass:[UIApplication class]] + openURL:[OCMArg checkWithBlock:^BOOL(id urlId) { + // verifying url received by the app delegate is expected + NSURL *url = (NSURL *)urlId; + return [url isEqual:customURL]; + }] + options:[OCMArg any]]) + .andReturn(NO); + + // it would fallback to Open URL with iOS System + [self setupOpenURLViaIOSForUIApplicationWithReturnValue:NO]; + XCTestExpectation *expectation = + [self expectationWithDescription:@"Completion Callback Triggered"]; + [follower followActionURL:customURL + withCompletionBlock:^(BOOL success) { + // since both custom scheme url open and fallback iOS url open returns NO, we expect + // to get a NO here + XCTAssertFalse(success); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerifyAll((id)self.mockAppDelegate); +} + +- (void)testCustomSchemeNotMatching { + self.mockAppDelegate = OCMClassMock([Delegate2 class]); + OCMStub([self.mockApplication delegate]).andReturn(self.mockAppDelegate); + + // we support custom url scheme 'scheme1' and 'scheme2' in this setup + FIRIAMActionURLFollower *follower = + [[FIRIAMActionURLFollower alloc] initWithCustomURLSchemeArray:@[ @"scheme1", @"scheme2" ] + withApplication:self.mockApplication]; + + NSURL *customURL = [NSURL URLWithString:@"unknown-scheme://test.com"]; + + // since custom scheme does not match, we should not expect app delegate's open URL method + // being triggered + OCMReject([self.mockAppDelegate application:[OCMArg any] + openURL:[OCMArg any] + options:[OCMArg any]]); + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Completion Callback Triggered"]; + [self setupOpenURLViaIOSForUIApplicationWithReturnValue:YES]; + + [follower followActionURL:customURL + withCompletionBlock:^(BOOL success) { + XCTAssertTrue(success); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerifyAll((id)self.mockAppDelegate); +} + +- (void)testUniversalLinkWithoutContinueUserActivityDefined { + // Delegate2 does not define application:continueUserActivity:restorationHandler + self.mockAppDelegate = OCMClassMock([Delegate2 class]); + OCMStub([self.mockApplication delegate]).andReturn(self.mockAppDelegate); + + FIRIAMActionURLFollower *follower = + [[FIRIAMActionURLFollower alloc] initWithCustomURLSchemeArray:@[] + withApplication:self.mockApplication]; + + // so for this url, even if it's a http or https link, we should fall back to openURL with + // iOS system + NSURL *url = [NSURL URLWithString:@"http://test.com"]; + + XCTestExpectation *expectation = + [self expectationWithDescription:@"Completion Callback Triggered"]; + [self setupOpenURLViaIOSForUIApplicationWithReturnValue:YES]; + + [follower followActionURL:url + withCompletionBlock:^(BOOL success) { + XCTAssertTrue(success); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMActivityLoggerTests.m b/InAppMessaging/Example/Tests/FIRIAMActivityLoggerTests.m new file mode 100644 index 00000000000..a82da439662 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMActivityLoggerTests.m @@ -0,0 +1,185 @@ +/* + * Copyright 2017 Google + * + * 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 "FIRIAMActivityLogger.h" +@interface FIRIAMActivityLogger () +- (void)loadFromCachePath:(NSString *)cacheFilePath; +- (BOOL)saveIntoCacheWithPath:(NSString *)cacheFilePath; +@end + +@interface FIRIAMActivityLoggerTests : XCTestCase + +@end + +@implementation FIRIAMActivityLoggerTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the + // class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testNormalFlow { + FIRIAMActivityLogger *logger = [[FIRIAMActivityLogger alloc] initWithMaxCountBeforeReduce:100 + withSizeAfterReduce:80 + verboseMode:YES + loadFromCache:NO]; + + FIRIAMActivityRecord *first = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeRenderMessage + isSuccessful:NO + withDetail:@"log detail" + timestamp:nil]; + [logger addLogRecord:first]; + NSDate *now = [[NSDate alloc] init]; + FIRIAMActivityRecord *second = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForFetch + isSuccessful:YES + withDetail:@"log detail2" + timestamp:now]; + [logger addLogRecord:second]; + + // now read them back + NSArray *records = [logger readRecords]; + XCTAssertEqual(2, [records count]); + + // notice that log records read out would be [second, first] in LIFO order + FIRIAMActivityRecord *firstFetched = records[0]; + XCTAssertEqualObjects(@"log detail2", firstFetched.detail); + XCTAssertEqual(YES, firstFetched.success); + XCTAssertEqual(FIRIAMActivityTypeCheckForFetch, firstFetched.activityType); + // second's timestamp should be equal to now since it's used to construct that log record + XCTAssertEqualWithAccuracy(now.timeIntervalSince1970, + firstFetched.timestamp.timeIntervalSince1970, 0.001); + + FIRIAMActivityRecord *secondFetched = records[1]; + XCTAssertEqualObjects(@"log detail", secondFetched.detail); + XCTAssertEqual(NO, secondFetched.success); + XCTAssertEqual(FIRIAMActivityTypeRenderMessage, secondFetched.activityType); + // 60 seconds is large enough buffer for the timestamp comparison + XCTAssertEqualWithAccuracy([[NSDate alloc] init].timeIntervalSince1970, + secondFetched.timestamp.timeIntervalSince1970, 60); +} + +- (void)testReduceAfterReachingMaxCount { + // expected behavior for logger regarding reducing is to come down to 1 after reaching size of 3 + FIRIAMActivityLogger *logger = [[FIRIAMActivityLogger alloc] initWithMaxCountBeforeReduce:3 + withSizeAfterReduce:1 + verboseMode:YES + loadFromCache:NO]; + + FIRIAMActivityRecord *first = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeRenderMessage + isSuccessful:NO + withDetail:@"log detail" + timestamp:nil]; + [logger addLogRecord:first]; + FIRIAMActivityRecord *second = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForFetch + isSuccessful:YES + withDetail:@"log detail2" + timestamp:nil]; + [logger addLogRecord:second]; + + FIRIAMActivityRecord *third = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForFetch + isSuccessful:YES + withDetail:@"log detail3" + timestamp:nil]; + [logger addLogRecord:third]; + NSArray *records = [logger readRecords]; + XCTAssertEqual(1, [records count]); + + // and the remaining one would be the last one being inserted + XCTAssertEqualObjects(@"log detail3", records[0].detail); +} + +- (void)testNonVerboseMode { + // certain types of messages would get dropped + FIRIAMActivityLogger *logger = [[FIRIAMActivityLogger alloc] initWithMaxCountBeforeReduce:100 + withSizeAfterReduce:50 + verboseMode:NO + loadFromCache:NO]; + + // this one would be added + FIRIAMActivityRecord *next = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeRenderMessage + isSuccessful:NO + withDetail:@"log detail" + timestamp:nil]; + [logger addLogRecord:next]; + + // this one would be dropped + next = [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForOnOpenMessage + isSuccessful:NO + withDetail:@"log detail" + timestamp:nil]; + [logger addLogRecord:next]; + + // this one would be added + next = [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeFetchMessage + isSuccessful:NO + withDetail:@"log detail" + timestamp:nil]; + [logger addLogRecord:next]; + NSArray *records = [logger readRecords]; + XCTAssertEqual(2, [records count]); +} + +- (void)testReadingAndWritingCache { + FIRIAMActivityLogger *logger = [[FIRIAMActivityLogger alloc] initWithMaxCountBeforeReduce:100 + withSizeAfterReduce:50 + verboseMode:YES + loadFromCache:NO]; + + FIRIAMActivityRecord *next = + [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeRenderMessage + isSuccessful:NO + withDetail:@"log detail" + timestamp:nil]; + [logger addLogRecord:next]; + next = [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForOnOpenMessage + isSuccessful:NO + withDetail:@"log detail2" + timestamp:nil]; + [logger addLogRecord:next]; + next = [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForOnOpenMessage + isSuccessful:NO + withDetail:@"log detail3" + timestamp:nil]; + [logger addLogRecord:next]; + + NSString *cacheFilePath = [NSString stringWithFormat:@"%@/temp-cache", NSTemporaryDirectory()]; + [logger saveIntoCacheWithPath:cacheFilePath]; + + // read it back + FIRIAMActivityLogger *logger2 = [[FIRIAMActivityLogger alloc] initWithMaxCountBeforeReduce:100 + withSizeAfterReduce:50 + verboseMode:YES + loadFromCache:NO]; + [logger2 loadFromCachePath:cacheFilePath]; + + XCTAssertEqual(3, [[logger2 readRecords] count]); +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMAnalyticsEventLoggerImplTests.m b/InAppMessaging/Example/Tests/FIRIAMAnalyticsEventLoggerImplTests.m new file mode 100644 index 00000000000..5816c652483 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMAnalyticsEventLoggerImplTests.m @@ -0,0 +1,224 @@ +/* + * Copyright 2018 Google + * + * 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 + +#import "FIRIAMAnalyticsEventLoggerImpl.h" +#import "FIRIAMClearcutLogger.h" + +#import +#import +#import + +@interface FIRIAMAnalyticsEventLoggerImplTests : XCTestCase +@property(nonatomic) FIRIAMClearcutLogger *mockClearcutLogger; +@property(nonatomic) id mockTimeFetcher; +@property(nonatomic) id mockFirebaseAnalytics; +@property(nonatomic) NSUserDefaults *mockUserDefaults; + +@end + +static NSString *campaignID = @"campaign id"; +static NSString *campaignName = @"campaign name"; + +typedef void (^FIRAUserPropertiesCallback)(NSDictionary *userProperties); + +typedef void (^FakeAnalyticsLogEventHandler)(NSString *origin, + NSString *name, + NSDictionary *parameters); +typedef void (^FakeAnalyticsUserPropertyHandler)(NSString *origin, NSString *name, id value); +typedef void (^LastNotificationCallback)(NSString *); +typedef void (^FakeAnalyticsLastNotificationHandler)(NSString *origin, LastNotificationCallback); + +@interface FakeAnalytics : NSObject + +@property FakeAnalyticsLogEventHandler eventHandler; +@property FakeAnalyticsLogEventHandler userPropertyHandler; +@property FakeAnalyticsLastNotificationHandler lastNotificationHandler; + +- (instancetype)initWithEventHandler:(FakeAnalyticsLogEventHandler)eventHandler; +- (instancetype)initWithUserPropertyHandler:(FakeAnalyticsUserPropertyHandler)userPropertyHandler; +@end + +@implementation FakeAnalytics + +- (instancetype)initWithEventHandler:(FakeAnalyticsLogEventHandler)eventHandler { + self = [super init]; + if (self) { + _eventHandler = eventHandler; + } + return self; +} + +- (instancetype)initWithUserPropertyHandler:(FakeAnalyticsUserPropertyHandler)userPropertyHandler { + self = [super init]; + if (self) { + _userPropertyHandler = userPropertyHandler; + } + return self; +} + +- (void)logEventWithOrigin:(nonnull NSString *)origin + name:(nonnull NSString *)name + parameters:(nullable NSDictionary *)parameters { + if (_eventHandler) { + _eventHandler(origin, name, parameters); + } +} + +- (void)setUserPropertyWithOrigin:(nonnull NSString *)origin + name:(nonnull NSString *)name + value:(nonnull id)value { + if (_userPropertyHandler) { + _userPropertyHandler(origin, name, value); + } +} + +- (void)checkLastNotificationForOrigin:(nonnull NSString *)origin + queue:(nonnull dispatch_queue_t)queue + callback:(nonnull void (^)(NSString *_Nullable)) + currentLastNotificationProperty { + if (_lastNotificationHandler) { + _lastNotificationHandler(origin, currentLastNotificationProperty); + } +} + +// Stubs +- (void)clearConditionalUserProperty:(nonnull NSString *)userPropertyName + clearEventName:(nonnull NSString *)clearEventName + clearEventParameters:(nonnull NSDictionary *)clearEventParameters { +} + +- (nonnull NSArray *) + conditionalUserProperties:(nonnull NSString *)origin + propertyNamePrefix:(nonnull NSString *)propertyNamePrefix { + return @[]; +} + +- (NSInteger)maxUserProperties:(nonnull NSString *)origin { + return -1; +} + +- (void)setConditionalUserProperty:(nonnull FIRAConditionalUserProperty *)conditionalUserProperty { +} + +- (void)registerAnalyticsListener:(nonnull id)listener + withOrigin:(nonnull NSString *)origin { +} + +- (void)unregisterAnalyticsListenerWithOrigin:(nonnull NSString *)origin { +} +@end + +@implementation FIRIAMAnalyticsEventLoggerImplTests + +- (void)setUp { + [super setUp]; + self.mockClearcutLogger = OCMClassMock(FIRIAMClearcutLogger.class); + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + self.mockUserDefaults = OCMClassMock(NSUserDefaults.class); +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testLogImpressionEvent { + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Log to Analytics"]; + FakeAnalytics *analytics = [[FakeAnalytics alloc] + initWithEventHandler:^(NSString *origin, NSString *name, NSDictionary *parameters) { + XCTAssertEqualObjects(origin, @"fiam"); + XCTAssertEqualObjects(name, @"firebase_in_app_message_impression"); + XCTAssertEqual([parameters count], 3); + XCTAssertNotNil(parameters); + XCTAssertEqual(parameters[@"_nmid"], campaignID); + XCTAssertEqual(parameters[@"_nmn"], campaignName); + [expectation1 fulfill]; + }]; + FIRIAMAnalyticsEventLoggerImpl *logger = + [[FIRIAMAnalyticsEventLoggerImpl alloc] initWithClearcutLogger:self.mockClearcutLogger + usingTimeFetcher:self.mockTimeFetcher + usingUserDefaults:nil + analytics:analytics]; + + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + + OCMExpect([self.mockClearcutLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression + forCampaignID:[OCMArg isEqual:campaignID] + withCampaignName:[OCMArg isEqual:campaignName] + eventTimeInMs:[OCMArg isNil] + completion:([OCMArg invokeBlockWithArgs:@YES, nil])]); + + XCTestExpectation *expectation2 = + [self expectationWithDescription:@"Completion Callback Triggered"]; + [logger logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression + forCampaignID:campaignID + withCampaignName:campaignName + eventTimeInMs:nil + completion:^(BOOL success) { + [expectation2 fulfill]; + }]; + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +- (void)testLogActionEvent { + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Log to Analytics"]; + FakeAnalytics *analytics = [[FakeAnalytics alloc] + initWithEventHandler:^(NSString *origin, NSString *name, NSDictionary *parameters) { + XCTAssertEqualObjects(origin, @"fiam"); + XCTAssertEqualObjects(name, @"firebase_in_app_message_action"); + XCTAssertEqual([parameters count], 3); + XCTAssertNotNil(parameters); + XCTAssertEqual(parameters[@"_nmid"], campaignID); + XCTAssertEqual(parameters[@"_nmn"], campaignName); + [expectation1 fulfill]; + }]; + + FIRIAMAnalyticsEventLoggerImpl *logger = + [[FIRIAMAnalyticsEventLoggerImpl alloc] initWithClearcutLogger:self.mockClearcutLogger + usingTimeFetcher:self.mockTimeFetcher + usingUserDefaults:self.mockUserDefaults + analytics:analytics]; + + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + + OCMExpect([self.mockClearcutLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow + forCampaignID:[OCMArg isEqual:campaignID] + withCampaignName:[OCMArg isEqual:campaignName] + eventTimeInMs:[OCMArg isNil] + completion:([OCMArg invokeBlockWithArgs:@YES, nil])]); + + XCTestExpectation *expectation2 = + [self expectationWithDescription:@"Completion Callback Triggered"]; + + [logger logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow + forCampaignID:campaignID + withCampaignName:campaignName + eventTimeInMs:nil + completion:^(BOOL success) { + [expectation2 fulfill]; + }]; + + [self waitForExpectationsWithTimeout:2.0 handler:nil]; + OCMVerifyAll((id)self.mockClearcutLogger); +} + +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMBookKeeperViaUserDefaultsTests.m b/InAppMessaging/Example/Tests/FIRIAMBookKeeperViaUserDefaultsTests.m new file mode 100644 index 00000000000..85b07845eb8 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMBookKeeperViaUserDefaultsTests.m @@ -0,0 +1,187 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import "FIRIAMBookKeeper.h" + +@interface FIRIAMBookKeeperViaUserDefaultsTests : XCTestCase +@property(nonatomic) NSUserDefaults *userDefaultsForTesting; +@end + +extern NSString *FIRIAM_UserDefaultsKeyForImpressions; +extern NSString *FIRIAM_UserDefaultsKeyForLastImpressionTimestamp; + +extern NSString *FIRIAM_ImpressionDictKeyForID; +extern NSString *FIRIAM_ImpressionDictKeyForTimestamp; + +@implementation FIRIAMBookKeeperViaUserDefaultsTests +- (void)setUp { + [super setUp]; + self.userDefaultsForTesting = + [[NSUserDefaults alloc] initWithSuiteName:@"FIRIAMBookKeeperViaUserDefaultsTests"]; +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; + [self.userDefaultsForTesting removeSuiteNamed:@"FIRIAMBookKeeperViaUserDefaultsTests"]; +} + +- (void)testRecordImpressionRecords { + FIRIAMBookKeeperViaUserDefaults *bookKeeper = + [[FIRIAMBookKeeperViaUserDefaults alloc] initWithUserDefaults:self.userDefaultsForTesting]; + [bookKeeper cleanupImpressions]; + + NSArray *impressions = [bookKeeper getImpressions]; + XCTAssertEqual(0, [impressions count]); + + double impression1_ts = 12345; + double impression2_ts = 34567; + + [bookKeeper recordNewImpressionForMessage:@"m1" withStartTimestampInSeconds:impression1_ts]; + [bookKeeper recordNewImpressionForMessage:@"m1" withStartTimestampInSeconds:impression2_ts]; + + impressions = [bookKeeper getImpressions]; + // For the same message, we only record the last impression record. + XCTAssertEqual(1, [impressions count]); + XCTAssertEqualWithAccuracy(impression2_ts, impressions[0].impressionTimeInSeconds, 0.1); + + // Verify the last display time. + XCTAssertEqualWithAccuracy(impression2_ts, [bookKeeper lastDisplayTime], 0.1); + + double impression3_ts = 45000; + + [bookKeeper recordNewImpressionForMessage:@"m2" withStartTimestampInSeconds:impression3_ts]; + impressions = [bookKeeper getImpressions]; + // Now we should see two different impression records for two different messages. + XCTAssertEqual(2, [impressions count]); + // Verify the last display time is updated again. + XCTAssertEqualWithAccuracy(impression3_ts, [bookKeeper lastDisplayTime], 0.1); +} + +- (void)testRecordFetchTimes { + FIRIAMBookKeeperViaUserDefaults *bookKeeper = + [[FIRIAMBookKeeperViaUserDefaults alloc] initWithUserDefaults:self.userDefaultsForTesting]; + [bookKeeper cleanupImpressions]; + + double fetch1_ts = 12345; + double fetch2_ts = 34567; + [bookKeeper recordNewFetchWithFetchCount:10 + withTimestampInSeconds:fetch1_ts + nextFetchWaitTime:nil]; + [bookKeeper recordNewFetchWithFetchCount:10 + withTimestampInSeconds:fetch2_ts + nextFetchWaitTime:nil]; + + XCTAssertEqualWithAccuracy(fetch2_ts, [bookKeeper lastFetchTime], 0.1); +} + +- (void)testRecordFetchTimesWithFetchWaitTime { + FIRIAMBookKeeperViaUserDefaults *bookKeeper = + [[FIRIAMBookKeeperViaUserDefaults alloc] initWithUserDefaults:self.userDefaultsForTesting]; + [bookKeeper cleanupImpressions]; + + double fetch1_ts = 12345; + NSNumber *fetchWaitTime = [NSNumber numberWithInt:30000]; + [bookKeeper recordNewFetchWithFetchCount:10 + withTimestampInSeconds:fetch1_ts + nextFetchWaitTime:fetchWaitTime]; + XCTAssertEqualWithAccuracy(fetchWaitTime.doubleValue, [bookKeeper nextFetchWaitTime], 0.1); +} + +- (void)testRecordFetchTimesWithFetchWaitTimeOverCap { + FIRIAMBookKeeperViaUserDefaults *bookKeeper = + [[FIRIAMBookKeeperViaUserDefaults alloc] initWithUserDefaults:self.userDefaultsForTesting]; + [bookKeeper cleanupImpressions]; + + double fetch1_ts = 12345; + NSNumber *fetchWaitTime = [NSNumber numberWithInt:30000]; + [bookKeeper recordNewFetchWithFetchCount:10 + withTimestampInSeconds:fetch1_ts + nextFetchWaitTime:fetchWaitTime]; + XCTAssertEqualWithAccuracy(fetchWaitTime.doubleValue, [bookKeeper nextFetchWaitTime], 0.1); + + // Second recording use a very large fetch wait time: 30000000 is to large to be accepted. + NSNumber *fetchWaitTime2 = [NSNumber numberWithInt:30000000]; + [bookKeeper recordNewFetchWithFetchCount:10 + withTimestampInSeconds:fetch1_ts + nextFetchWaitTime:fetchWaitTime2]; + // Next fetch wait time is still the same as from fetchWaitTime + XCTAssertEqualWithAccuracy(fetchWaitTime.doubleValue, [bookKeeper nextFetchWaitTime], 0.1); +} + +- (void)testFetchImpressions { + NSString *message1 = @"message1 id"; + double message1ImpressionTime = 1000.0; + + NSString *message2 = @"message2 id"; + double message2ImpressionTime = 2000.0; + + FIRIAMBookKeeperViaUserDefaults *bookKeeper = + [[FIRIAMBookKeeperViaUserDefaults alloc] initWithUserDefaults:self.userDefaultsForTesting]; + [bookKeeper cleanupImpressions]; + // Set up existing impressions. + [bookKeeper recordNewImpressionForMessage:message1 + withStartTimestampInSeconds:message1ImpressionTime]; + [bookKeeper recordNewImpressionForMessage:message2 + withStartTimestampInSeconds:message2ImpressionTime]; + + NSArray *fetchedImpressions = [bookKeeper getImpressions]; + + XCTAssertEqual(2, fetchedImpressions.count); + + FIRIAMImpressionRecord *first = fetchedImpressions[0]; + XCTAssertEqualObjects(first.messageID, message1); + XCTAssertEqualWithAccuracy((double)first.impressionTimeInSeconds, message1ImpressionTime, 0.1); + + FIRIAMImpressionRecord *second = fetchedImpressions[1]; + XCTAssertEqualObjects(second.messageID, message2); + XCTAssertEqualWithAccuracy((double)second.impressionTimeInSeconds, message2ImpressionTime, 0.1); + + NSArray *messageIDs = [bookKeeper getMessageIDsFromImpressions]; + XCTAssertEqualObjects(messageIDs[0], message1); + XCTAssertEqualObjects(messageIDs[1], message2); +} + +- (void)testClearImpressionsForMessageIDs { + FIRIAMBookKeeperViaUserDefaults *bookKeeper = + [[FIRIAMBookKeeperViaUserDefaults alloc] initWithUserDefaults:self.userDefaultsForTesting]; + [bookKeeper cleanupImpressions]; + + NSArray *impressions = [bookKeeper getImpressions]; + XCTAssertEqual(0, [impressions count]); + + double impression1_ts = 12345; + double impression2_ts = 34567; + double impression3_ts = 34567; + + [bookKeeper recordNewImpressionForMessage:@"m1" withStartTimestampInSeconds:impression1_ts]; + [bookKeeper recordNewImpressionForMessage:@"m2" withStartTimestampInSeconds:impression2_ts]; + [bookKeeper recordNewImpressionForMessage:@"m3" withStartTimestampInSeconds:impression3_ts]; + + [bookKeeper clearImpressionsWithMessageList:@[ @"m1", @"m3" ]]; + + impressions = [bookKeeper getImpressions]; + + // Only impressions about m2 remains. + XCTAssertEqual(1, [impressions count]); + XCTAssertEqualObjects(impressions[0].messageID, @"m2"); +} + +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMClearcutLoggerTests.m b/InAppMessaging/Example/Tests/FIRIAMClearcutLoggerTests.m new file mode 100644 index 00000000000..dfdc70c246e --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMClearcutLoggerTests.m @@ -0,0 +1,144 @@ +/* + * Copyright 2018 Google + * + * 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 + +#import "FIRIAMClearcutHttpRequestSender.h" +#import "FIRIAMClearcutLogStorage.h" +#import "FIRIAMClearcutLogger.h" +#import "FIRIAMClearcutUploader.h" + +@interface FIRIAMClearcutLoggerTests : XCTestCase +@property(nonatomic) FIRIAMClientInfoFetcher *mockClientInfoFetcher; +@property(nonatomic) id mockTimeFetcher; +@property(nonatomic) FIRIAMClearcutHttpRequestSender *mockRequestSender; +@property(nonatomic) FIRIAMClearcutUploader *mockCtUploader; + +@end + +NSString *iid = @"my iid"; +NSString *osVersion = @"iOS version"; +NSString *sdkVersion = @"SDK version"; + +// we need to access the some internal things in FIRIAMClearcutLogger in our unit tests +// verifications +@interface FIRIAMClearcutLogger (UnitTestAccess) +@property(readonly, nonatomic) FIRIAMClearcutLogStorage *retryStorage; +@property(nonatomic) FIRIAMClearcutHttpRequestSender *requestSender; +@property(nonatomic) id timeFetcher; +- (void)checkAndRetryClearcutLogs; +@end +@interface FIRIAMClearcutLogStorage (UnitTestAccess) +@property(nonatomic) NSMutableArray *records; +@end + +@implementation FIRIAMClearcutLoggerTests +- (void)setUp { + [super setUp]; + + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + self.mockClientInfoFetcher = OCMClassMock(FIRIAMClientInfoFetcher.class); + self.mockRequestSender = OCMClassMock(FIRIAMClearcutHttpRequestSender.class); + self.mockCtUploader = OCMClassMock(FIRIAMClearcutUploader.class); + + OCMStub([self.mockClientInfoFetcher + fetchFirebaseIIDDataWithProjectNumber:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:iid, @"token", + [NSNull null], nil])]); + + OCMStub([self.mockClientInfoFetcher getIAMSDKVersion]).andReturn(sdkVersion); + OCMStub([self.mockClientInfoFetcher getOSVersion]).andReturn(osVersion); +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +// verify that the produced FIRIAMClearcutLogRecord record has expected content for +// the event extension json string +- (void)testEventLogBodyContent_Expected { + NSString *fbProjectNumber = @"clearcutserver"; + NSString *fbAppId = @"test Firebase app"; + + FIRIAMClearcutLogger *logger = + [[FIRIAMClearcutLogger alloc] initWithFBProjectNumber:fbProjectNumber + fbAppId:fbAppId + clientInfoFetcher:self.mockClientInfoFetcher + usingTimeFetcher:self.mockTimeFetcher + usingUploader:self.mockCtUploader]; + + NSTimeInterval eventMoment = 10000; + __block NSDictionary *capturedEventDict; + + OCMExpect([self.mockCtUploader + addNewLogRecord:[OCMArg checkWithBlock:^BOOL(FIRIAMClearcutLogRecord *newLogRecord) { + NSString *jsonString = newLogRecord.eventExtensionJsonString; + + capturedEventDict = [NSJSONSerialization + JSONObjectWithData:[jsonString dataUsingEncoding:NSUTF8StringEncoding] + options:kNilOptions + error:nil]; + return (int)newLogRecord.eventTimestampInSeconds == (int)eventMoment; + }]]); + + NSString *campaignID = @"test campaign"; + [logger logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow + forCampaignID:campaignID + withCampaignName:@"name" + eventTimeInMs:[NSNumber numberWithInteger:eventMoment * 1000] + completion:^(BOOL success){ + }]; + + OCMVerifyAll((id)self.mockCtUploader); + + XCTAssertEqualObjects(@"CLICK_EVENT_TYPE", capturedEventDict[@"event_type"]); + XCTAssertEqualObjects(fbProjectNumber, capturedEventDict[@"project_number"]); + XCTAssertEqualObjects(campaignID, capturedEventDict[@"campaign_id"]); + XCTAssertEqualObjects(fbAppId, capturedEventDict[@"client_app"][@"google_app_id"]); + XCTAssertEqualObjects(iid, capturedEventDict[@"client_app"][@"firebase_instance_id"]); + XCTAssertEqualObjects(sdkVersion, capturedEventDict[@"fiam_sdk_version"]); +} + +// calling logAnalyticsEventForType with event time set to nil +- (void)testNilEventTimestamp { + FIRIAMClearcutLogger *logger = + [[FIRIAMClearcutLogger alloc] initWithFBProjectNumber:@"clearcutserver" + fbAppId:@"test Firebase app" + clientInfoFetcher:self.mockClientInfoFetcher + usingTimeFetcher:self.mockTimeFetcher + usingUploader:self.mockCtUploader]; + + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + + OCMExpect([self.mockCtUploader + addNewLogRecord:[OCMArg checkWithBlock:^BOOL(FIRIAMClearcutLogRecord *newLogRecord) { + return (int)newLogRecord.eventTimestampInSeconds == (int)currentMoment; + }]]); + + [logger logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow + forCampaignID:@"test campaign" + withCampaignName:@"name" + eventTimeInMs:nil + completion:^(BOOL success){ + }]; + + OCMVerifyAll((id)self.mockCtUploader); +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMClearcutRetryLocalStorageTests.m b/InAppMessaging/Example/Tests/FIRIAMClearcutRetryLocalStorageTests.m new file mode 100644 index 00000000000..95901f055a5 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMClearcutRetryLocalStorageTests.m @@ -0,0 +1,81 @@ +/* + * Copyright 2018 Google + * + * 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 + +#import "FIRIAMClearcutLogStorage.h" +#import "FIRIAMTimeFetcher.h" + +@interface FIRIAMClearcutLogStorage (UnitTestAccess) +@property(nonatomic) NSMutableArray *records; +@end + +@interface FIRIAMClearcutLogStorageTests : XCTestCase + +@end + +@implementation FIRIAMClearcutLogStorageTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the + // class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testExpiringOldLogs { + id mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + NSInteger logExpiresInSeconds = 20; + + FIRIAMClearcutLogStorage *storage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:logExpiresInSeconds + withTimeFetcher:mockTimeFetcher]; + + NSInteger eventTimestamp = 1000; + // insert 10 logs with event timestamp as eventTimestamp + for (int i = 0; i < 10; i++) { + FIRIAMClearcutLogRecord *nextRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"json string" + eventTimestampInSeconds:eventTimestamp]; + [storage pushRecords:@[ nextRecord ]]; + } + + // insert 2 logs with event timestamp as eventTimestamp + 10 + for (int i = 0; i < 2; i++) { + FIRIAMClearcutLogRecord *nextRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"json string" + eventTimestampInSeconds:eventTimestamp + 10]; + [storage pushRecords:@[ nextRecord ]]; + } + + // with this stub, 10 out of the 12 the retry logs are going expired + OCMStub([mockTimeFetcher currentTimestampInSeconds]) + .andReturn(eventTimestamp + logExpiresInSeconds + 1); + + NSArray *results = [storage popStillValidRecordsForUpTo:6]; + // only 2 out of 12 retry logs are still valid + XCTAssertEqual(2, results.count); + + // all the messages should be gone here + XCTAssertEqual(0, storage.records.count); +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMClearcutUploaderTests.m b/InAppMessaging/Example/Tests/FIRIAMClearcutUploaderTests.m new file mode 100644 index 00000000000..d26c37b4205 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMClearcutUploaderTests.m @@ -0,0 +1,373 @@ +/* + * Copyright 2018 Google + * + * 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 + +#import "FIRIAMClearcutHttpRequestSender.h" +#import "FIRIAMClearcutLogStorage.h" +#import "FIRIAMClearcutUploader.h" +#import "FIRIAMTimeFetcher.h" + +@interface FIRIAMClearcutUploaderTests : XCTestCase +@property(nonatomic) id mockTimeFetcher; +@property(nonatomic) FIRIAMClearcutHttpRequestSender *mockRequestSender; +@property(nonatomic) FIRIAMClearcutLogStorage *mockLogStorage; + +@property(nonatomic) FIRIAMClearcutStrategy *defaultStrategy; + +@property(nonatomic) NSUserDefaults *mockUserDefaults; +@end + +// expose certain internal things to help with unit testing +@interface FIRIAMClearcutUploader (UnitTest) +@property(nonatomic, assign) int64_t nextValidSendTimeInMills; +@end + +@implementation FIRIAMClearcutUploaderTests + +- (void)setUp { + [super setUp]; + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + self.mockRequestSender = OCMClassMock(FIRIAMClearcutHttpRequestSender.class); + self.mockLogStorage = OCMClassMock(FIRIAMClearcutLogStorage.class); + + self.defaultStrategy = [[FIRIAMClearcutStrategy alloc] initWithMinWaitTimeInMills:1000 + maxWaitTimeInMills:2000 + failureBackoffTimeInMills:1000 + batchSendSize:10]; + + self.mockUserDefaults = OCMClassMock(NSUserDefaults.class); + OCMStub([self.mockUserDefaults integerForKey:[OCMArg any]]).andReturn(0); +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testUploadTriggeredWhenWaitTimeConditionSatisfied { + // using a real storage in this case + FIRIAMClearcutLogStorage *logStorage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:1000 + withTimeFetcher:self.mockTimeFetcher]; + + FIRIAMClearcutUploader *uploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:self.mockRequestSender + timeFetcher:self.mockTimeFetcher + logStorage:logStorage + usingStrategy:self.defaultStrategy + usingUserDefaults:self.mockUserDefaults]; + + // so the following setup is to say that it's now ok to do upload right away + // nextValidSendTimeInMills < currnt time + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + uploader.nextValidSendTimeInMills = (int64_t)(currentMoment - 1) * 1000; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Triggers send on sender"]; + + OCMStub([self.mockRequestSender sendClearcutHttpRequestForLogs:[OCMArg any] + withCompletion:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + + FIRIAMClearcutLogRecord *newRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"string" + eventTimestampInSeconds:currentMoment]; + + [uploader addNewLogRecord:newRecord]; + + // we expect expectation to be fulfilled right away since the upload can be carried out without + // delay + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +- (void)disable_testUploadNotTriggeredWhenWaitTimeConditionNotSatisfied { + // using a real storage in this case + FIRIAMClearcutLogStorage *logStorage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:1000 + withTimeFetcher:self.mockTimeFetcher]; + + FIRIAMClearcutUploader *uploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:self.mockRequestSender + timeFetcher:self.mockTimeFetcher + logStorage:logStorage + usingStrategy:self.defaultStrategy + usingUserDefaults:self.mockUserDefaults]; + + // so the following setup is to say that we need at least 5 seconds from now to attempt the + // the uploading + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + uploader.nextValidSendTimeInMills = (int64_t)(currentMoment + 5) * 1000; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Triggers send on sender"]; + + FIRIAMClearcutLogRecord *newRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"string" + eventTimestampInSeconds:currentMoment]; + + __block BOOL sendingAttempted = NO; + // we don't expect sendClearcutHttpRequestForLogs:withCompletion: to be triggered + // after wait for 2.0 seconds below. We have a BOOL flag to be used for that kind verification + // checking + OCMStub([self.mockRequestSender sendClearcutHttpRequestForLogs:[OCMArg any] + withCompletion:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + sendingAttempted = YES; + }); + [uploader addNewLogRecord:newRecord]; + + // we wait for 2 seconds and we expect nothing should happen to self.mockRequestSender right after + // 2 seconds: the upload will eventually be attempted in after 10 seconds based on the setup + // in this unit test + double delayInSeconds = 2.0; + dispatch_time_t popTime = + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { + [expectation fulfill]; + }); + + // we expect expectation to be fulfilled right away since the upload can be carried out without + // delay + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + XCTAssertFalse(sendingAttempted); +} + +- (void)testUploadBatchSizeIsBasedOnStrategySetting { + int batchSendSize = 5; + + // using a strategy with batch send size as 5 + FIRIAMClearcutStrategy *strategy = + [[FIRIAMClearcutStrategy alloc] initWithMinWaitTimeInMills:1000 + maxWaitTimeInMills:2000 + failureBackoffTimeInMills:1000 + batchSendSize:batchSendSize]; + + FIRIAMClearcutUploader *uploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:self.mockRequestSender + timeFetcher:self.mockTimeFetcher + logStorage:self.mockLogStorage + usingStrategy:strategy + usingUserDefaults:self.mockUserDefaults]; + + // so the following setup is to say that next upload is going to be now + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + uploader.nextValidSendTimeInMills = (int64_t)currentMoment * 1000; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Triggers send on sender"]; + OCMExpect([self.mockLogStorage popStillValidRecordsForUpTo:batchSendSize]); + + FIRIAMClearcutLogRecord *newRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"string" + eventTimestampInSeconds:currentMoment]; + [uploader addNewLogRecord:newRecord]; + + // we wait for 2 seconds to ensure that the next send is attempted and then verify its + // interacton with the underlying storage + double delayInSeconds = 2.0; + dispatch_time_t popTime = + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { + [expectation fulfill]; + }); + + // we expect expectation to be fulfilled right away since the upload can be carried out without + // delay + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + OCMVerifyAll((id)self.mockLogStorage); +} + +- (void)testRespectingWaitTimeFromRequestSender { + // using a real storage in this case + FIRIAMClearcutLogStorage *logStorage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:1000 + withTimeFetcher:self.mockTimeFetcher]; + + FIRIAMClearcutUploader *uploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:self.mockRequestSender + timeFetcher:self.mockTimeFetcher + logStorage:logStorage + usingStrategy:self.defaultStrategy + usingUserDefaults:self.mockUserDefaults]; + + // so the following setup is to say that next upload is going to be now + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + uploader.nextValidSendTimeInMills = (int64_t)currentMoment * 1000; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Triggers send on sender"]; + + // notice that waitTime is between minWaitTimeInMills and maxWaitTimeInMills in the default + // strategy + NSNumber *waitTime = [NSNumber numberWithLongLong:1500]; + // set up request sender which triggers the callback with a wait time interval to be 1000 + // milliseconds + OCMStub( + [self.mockRequestSender + sendClearcutHttpRequestForLogs:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@YES, @NO, waitTime, nil])]) + .andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + + FIRIAMClearcutLogRecord *newRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"string" + eventTimestampInSeconds:currentMoment]; + [uploader addNewLogRecord:newRecord]; + + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + // verify the update to nextValidSendTimeInMills is expected + XCTAssertEqual(currentMoment * 1000 + 1500, uploader.nextValidSendTimeInMills); +} + +- (void)disable_testWaitTimeFromRequestSenderAdjustedByMinWaitTimeInStrategy { + // using a real storage in this case + FIRIAMClearcutLogStorage *logStorage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:1000 + withTimeFetcher:self.mockTimeFetcher]; + + FIRIAMClearcutUploader *uploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:self.mockRequestSender + timeFetcher:self.mockTimeFetcher + logStorage:logStorage + usingStrategy:self.defaultStrategy + usingUserDefaults:self.mockUserDefaults]; + + // so the following setup is to say that next upload is going to be now + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + uploader.nextValidSendTimeInMills = (int64_t)currentMoment * 1000; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Triggers send on sender"]; + + // notice that waitTime is below minWaitTimeInMills in the default strategy + NSNumber *waitTime = + [NSNumber numberWithLongLong:self.defaultStrategy.minimalWaitTimeInMills - 200]; + // set up request sender which triggers the callback with a wait time interval to be 1000 + // milliseconds + OCMStub( + [self.mockRequestSender + sendClearcutHttpRequestForLogs:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@YES, @NO, waitTime, nil])]) + .andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + + FIRIAMClearcutLogRecord *newRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"string" + eventTimestampInSeconds:currentMoment]; + [uploader addNewLogRecord:newRecord]; + + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + // verify the update to nextValidSendTimeInMills is expected + XCTAssertEqual(currentMoment * 1000 + self.defaultStrategy.minimalWaitTimeInMills, + uploader.nextValidSendTimeInMills); +} + +- (void)testWaitTimeFromRequestSenderAdjustedByMaxWaitTimeInStrategy { + // using a real storage in this case + FIRIAMClearcutLogStorage *logStorage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:1000 + withTimeFetcher:self.mockTimeFetcher]; + + FIRIAMClearcutUploader *uploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:self.mockRequestSender + timeFetcher:self.mockTimeFetcher + logStorage:logStorage + usingStrategy:self.defaultStrategy + usingUserDefaults:self.mockUserDefaults]; + + // so the following setup is to say that next upload is going to be now + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + uploader.nextValidSendTimeInMills = (int64_t)currentMoment * 1000; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Triggers send on sender"]; + + // notice that waitTime is larger than maximumWaitTimeInMills in the default strategy + NSNumber *waitTime = + [NSNumber numberWithLongLong:self.defaultStrategy.maximumWaitTimeInMills + 200]; + // set up request sender which triggers the callback with a wait time interval to be 1000 + // milliseconds + OCMStub( + [self.mockRequestSender + sendClearcutHttpRequestForLogs:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@YES, @NO, waitTime, nil])]) + .andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + + FIRIAMClearcutLogRecord *newRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"string" + eventTimestampInSeconds:currentMoment]; + [uploader addNewLogRecord:newRecord]; + + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + // verify the update to nextValidSendTimeInMills is expected + XCTAssertEqual(currentMoment * 1000 + self.defaultStrategy.maximumWaitTimeInMills, + uploader.nextValidSendTimeInMills); +} + +- (void)testRepushLogsIfRequestSenderSaysSo { + // using a real storage in this case + FIRIAMClearcutLogStorage *logStorage = + [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:1000 + withTimeFetcher:self.mockTimeFetcher]; + + FIRIAMClearcutUploader *uploader = + [[FIRIAMClearcutUploader alloc] initWithRequestSender:self.mockRequestSender + timeFetcher:self.mockTimeFetcher + logStorage:logStorage + usingStrategy:self.defaultStrategy + usingUserDefaults:self.mockUserDefaults]; + + // so the following setup is to say that next upload is going to be now + NSTimeInterval currentMoment = 10000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + uploader.nextValidSendTimeInMills = (int64_t)currentMoment * 1000; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Triggers send on sender"]; + + // notice that waitTime is larger than maximumWaitTimeInMills in the default strategy + NSNumber *waitTime = + [NSNumber numberWithLongLong:self.defaultStrategy.maximumWaitTimeInMills + 200]; + + // Notice that it's invoking completion with falure flag and a flag to re-push those logs + OCMStub( + [self.mockRequestSender + sendClearcutHttpRequestForLogs:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@NO, @YES, waitTime, nil])]) + .andDo(^(NSInvocation *invocation) { + [expectation fulfill]; + }); + + FIRIAMClearcutLogRecord *newRecord = + [[FIRIAMClearcutLogRecord alloc] initWithExtensionJsonString:@"string" + eventTimestampInSeconds:currentMoment]; + [uploader addNewLogRecord:newRecord]; + + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + + // we should still be able to fetch one log record from storage since it's re-pushed due + // to send failure + XCTAssertEqual([logStorage popStillValidRecordsForUpTo:10].count, 1); +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMDisplayExecutorTests.m b/InAppMessaging/Example/Tests/FIRIAMDisplayExecutorTests.m new file mode 100644 index 00000000000..79ab4681286 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMDisplayExecutorTests.m @@ -0,0 +1,870 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import "FIRIAMActionURLFollower.h" +#import "FIRIAMDisplayExecutor.h" +#import "FIRIAMDisplayTriggerDefinition.h" +#import "FIRIAMMessageContentData.h" +#import "FIRInAppMessaging.h" + +// A class implementing protocol FIRIAMMessageContentData to be used for unit testing +@interface FIRIAMMessageContentDataForTesting : NSObject +@property(nonatomic, readwrite, nonnull) NSString *titleText; +@property(nonatomic, readwrite, nonnull) NSString *bodyText; +@property(nonatomic, nullable) NSString *actionButtonText; +@property(nonatomic, nullable) NSURL *actionURL; +@property(nonatomic, nullable) NSURL *imageURL; +@property BOOL errorEncountered; + +- (instancetype)initWithMessageTitle:(NSString *)title + messageBody:(NSString *)body + actionButtonText:(nullable NSString *)actionButtonText + actionURL:(nullable NSURL *)actionURL + imageURL:(nullable NSURL *)imageURL + hasImageError:(BOOL)hasImageError; +@end + +@implementation FIRIAMMessageContentDataForTesting +- (instancetype)initWithMessageTitle:(NSString *)title + messageBody:(NSString *)body + actionButtonText:(nullable NSString *)actionButtonText + actionURL:(nullable NSURL *)actionURL + imageURL:(nullable NSURL *)imageURL + hasImageError:(BOOL)hasImageError { + if (self = [super init]) { + _titleText = title; + _bodyText = body; + _imageURL = imageURL; + _actionButtonText = actionButtonText; + _actionURL = actionURL; + _errorEncountered = hasImageError; + } + return self; +} + +- (void)loadImageDataWithBlock:(void (^)(NSData *_Nullable imageData, + NSError *_Nullable error))block { + if (self.errorEncountered) { + block(nil, [NSError errorWithDomain:@"image error" code:0 userInfo:nil]); + } else { + NSString *str = @"image data"; + NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding]; + block(data, nil); + } +} +@end + +// Defines how the message display component triggers the delegate in unit testing +typedef NS_ENUM(NSInteger, FIRInAppMessagingDelegateInteraction) { + FIRInAppMessagingDelegateInteractionDismiss, // message display component triggers + // messageDismissedWithType: + FIRInAppMessagingDelegateInteractionClick, // message display component triggers + // messageClicked: + FIRInAppMessagingDelegateInteractionError, // message display component triggers + // displayErrorEncountered: + FIRInAppMessagingDelegateInteractionImpressionDetected, // message has finished a valid + // impression, but it's not getting + // closed by the user. +}; + +// A class implementing protocol FIRInAppMessagingDisplay to be used for unit testing +@interface FIRIAMMessageDisplayForTesting : NSObject +@property FIRInAppMessagingDelegateInteraction delegateInteraction; + +// used for interaction verificatio +@property FIRInAppMessagingDisplayMessage *message; +- (instancetype)initWithDelegateInteraction:(FIRInAppMessagingDelegateInteraction)interaction; +@end + +@implementation FIRIAMMessageDisplayForTesting +- (instancetype)initWithDelegateInteraction:(FIRInAppMessagingDelegateInteraction)interaction { + if (self = [super init]) { + _delegateInteraction = interaction; + } + return self; +} + +- (void)displayMessage:(FIRInAppMessagingDisplayMessage *)messageForDisplay + displayDelegate:(id)displayDelegate { + self.message = messageForDisplay; + + switch (self.delegateInteraction) { + case FIRInAppMessagingDelegateInteractionClick: + [displayDelegate messageClicked:messageForDisplay]; + break; + case FIRInAppMessagingDelegateInteractionDismiss: + [displayDelegate messageDismissed:messageForDisplay + dismissType:FIRInAppMessagingDismissTypeAuto]; + break; + case FIRInAppMessagingDelegateInteractionError: + [displayDelegate displayErrorForMessage:messageForDisplay + error:[NSError errorWithDomain:NSURLErrorDomain + code:0 + userInfo:nil]]; + break; + case FIRInAppMessagingDelegateInteractionImpressionDetected: + [displayDelegate impressionDetectedForMessage:messageForDisplay]; + break; + } +} +@end + +@interface FIRInAppMessagingDisplayTestDelegate : NSObject + +@property(nonatomic) BOOL receivedMessageErrorCallback; +@property(nonatomic) BOOL receivedMessageImpressionCallback; +@property(nonatomic) BOOL receivedMessageClickedCallback; +@property(nonatomic) BOOL receivedMessageDismissedCallback; + +@end + +@implementation FIRInAppMessagingDisplayTestDelegate + +- (void)displayErrorForMessage:(nonnull FIRInAppMessagingDisplayMessage *)inAppMessage + error:(nonnull NSError *)error { + self.receivedMessageErrorCallback = YES; +} + +- (void)impressionDetectedForMessage:(nonnull FIRInAppMessagingDisplayMessage *)inAppMessage { + self.receivedMessageImpressionCallback = YES; +} + +- (void)messageClicked:(nonnull FIRInAppMessagingDisplayMessage *)inAppMessage { + self.receivedMessageClickedCallback = YES; +} + +- (void)messageDismissed:(nonnull FIRInAppMessagingDisplayMessage *)inAppMessage + dismissType:(FIRInAppMessagingDismissType)dismissType { + self.receivedMessageDismissedCallback = YES; +} + +@end + +@interface FIRIAMDisplayExecutorTests : XCTestCase + +@property(nonatomic) FIRIAMDisplaySetting *displaySetting; +@property FIRIAMMessageClientCache *clientMessageCache; +@property id mockBookkeeper; +@property id mockTimeFetcher; + +@property FIRIAMDisplayExecutor *displayExecutor; + +@property FIRIAMActivityLogger *mockActivityLogger; +@property FIRInAppMessaging *mockInAppMessaging; +@property id mockAnalyticsEventLogger; + +@property FIRIAMActionURLFollower *mockActionURLFollower; + +@property id mockMessageDisplayComponent; + +// three pre-defined messages +@property FIRIAMMessageDefinition *m1, *m2, *m3, *m4; +@end + +@implementation FIRIAMDisplayExecutorTests + +- (void)setupMessageTexture { + // startTime, endTime here ensures messages with them are active + NSTimeInterval activeStartTime = 0; + NSTimeInterval activeEndTime = [[NSDate date] timeIntervalSince1970] + 10000; + + // m1 & m3 will be of contextual trigger + FIRIAMDisplayTriggerDefinition *contextualTriggerDefinition = + [[FIRIAMDisplayTriggerDefinition alloc] initWithFirebaseAnalyticEvent:@"test_event"]; + + // m2 and m4 will be of app open trigger + FIRIAMDisplayTriggerDefinition *appOpentriggerDefinition = + [[FIRIAMDisplayTriggerDefinition alloc] initForAppForegroundTrigger]; + + FIRIAMMessageContentDataForTesting *m1ContentData = [[FIRIAMMessageContentDataForTesting alloc] + initWithMessageTitle:@"m1 title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://google.com/image"] + hasImageError:NO]; + + FIRIAMRenderingEffectSetting *renderSetting1 = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting1.viewMode = FIRIAMRenderAsBannerView; + + FIRIAMMessageRenderData *renderData1 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m1" + messageName:@"name" + contentData:m1ContentData + renderingEffect:renderSetting1]; + + self.m1 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData1 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ contextualTriggerDefinition ]]; + + FIRIAMMessageContentDataForTesting *m2ContentData = [[FIRIAMMessageContentDataForTesting alloc] + initWithMessageTitle:@"m2 title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://unsplash.it/300/400"] + hasImageError:NO]; + + FIRIAMRenderingEffectSetting *renderSetting2 = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting2.viewMode = FIRIAMRenderAsModalView; + + FIRIAMMessageRenderData *renderData2 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m2" + messageName:@"name" + contentData:m2ContentData + renderingEffect:renderSetting2]; + + self.m2 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData2 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ appOpentriggerDefinition ]]; + + FIRIAMMessageContentDataForTesting *m3ContentData = [[FIRIAMMessageContentDataForTesting alloc] + initWithMessageTitle:@"m3 title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://google.com/image"] + hasImageError:NO]; + + FIRIAMRenderingEffectSetting *renderSetting3 = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting3.viewMode = FIRIAMRenderAsImageOnlyView; + + FIRIAMMessageRenderData *renderData3 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m3" + messageName:@"name" + contentData:m3ContentData + renderingEffect:renderSetting3]; + + self.m3 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData3 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ contextualTriggerDefinition ]]; + + FIRIAMMessageContentDataForTesting *m4ContentData = [[FIRIAMMessageContentDataForTesting alloc] + initWithMessageTitle:@"m4 title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://google.com/image"] + hasImageError:NO]; + + FIRIAMRenderingEffectSetting *renderSetting4 = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting4.viewMode = FIRIAMRenderAsImageOnlyView; + + FIRIAMMessageRenderData *renderData4 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m4" + messageName:@"name" + contentData:m4ContentData + renderingEffect:renderSetting4]; + + self.m4 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData4 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ appOpentriggerDefinition ]]; +} + +NSTimeInterval DISPLAY_MIN_INTERVALS = 1; + +- (void)setUp { + [super setUp]; + [self setupMessageTexture]; + + self.displaySetting = [[FIRIAMDisplaySetting alloc] init]; + self.displaySetting.displayMinIntervalInMinutes = DISPLAY_MIN_INTERVALS; + self.mockBookkeeper = OCMProtocolMock(@protocol(FIRIAMBookKeeper)); + + FIRIAMFetchResponseParser *parser = + [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:[[FIRIAMTimerWithNSDate alloc] init]]; + + self.clientMessageCache = [[FIRIAMMessageClientCache alloc] initWithBookkeeper:self.mockBookkeeper + usingResponseParser:parser]; + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + self.mockActivityLogger = OCMClassMock([FIRIAMActivityLogger class]); + self.mockAnalyticsEventLogger = OCMProtocolMock(@protocol(FIRIAMAnalyticsEventLogger)); + self.mockInAppMessaging = OCMClassMock([FIRInAppMessaging class]); + self.mockActionURLFollower = OCMClassMock([FIRIAMActionURLFollower class]); + + self.displayExecutor = + [[FIRIAMDisplayExecutor alloc] initWithInAppMessaging:self.mockInAppMessaging + setting:self.displaySetting + messageCache:self.clientMessageCache + timeFetcher:self.mockTimeFetcher + bookKeeper:self.mockBookkeeper + actionURLFollower:self.mockActionURLFollower + activityLogger:self.mockActivityLogger + analyticsEventLogger:self.mockAnalyticsEventLogger]; + + OCMStub([self.mockBookkeeper recordNewImpressionForMessage:[OCMArg any] + withStartTimestampInSeconds:1000]); +} + +- (void)testRegularMessageAvailableCase { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]]; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + XCTAssertEqual(1, remainingMsgCount); + + // Verify that the message content handed to display component is expected + XCTAssertEqualObjects(self.m2.renderData.messageID, display.message.campaignInfo.messageID); +} + +- (void)testFollowingActionURL { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + [self.clientMessageCache setMessageData:@[ self.m2 ]]; + + // not expecting triggering analytics recording + OCMExpect([self.mockActionURLFollower + followActionURL:[OCMArg isEqual:self.m2.renderData.contentData.actionURL] + withCompletionBlock:[OCMArg any]]); + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + OCMVerifyAll((id)self.mockActionURLFollower); +} + +- (void)testFollowingActionURLForTestMessage { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m1.renderData]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + [self.clientMessageCache setMessageData:@[ testMessage ]]; + + // not expecting triggering analytics recording + OCMExpect([self.mockActionURLFollower + followActionURL:[OCMArg isEqual:testMessage.renderData.contentData.actionURL] + withCompletionBlock:[OCMArg any]]); + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + OCMVerifyAll((id)self.mockActionURLFollower); +} + +- (void)testClientTestMessageAvailableCase { + // When test message is present in cache, even if the display time interval has not been + // reached, we still render. + + // 10 seconds is less than DISPLAY_MIN_INTERVALS minutes, so we have not reached + // minimal display time interval yet. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(10); + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m1.renderData]; + + [self.clientMessageCache setMessageData:@[ self.m2, testMessage, self.m4 ]]; + + // We have test message in the cache now. + XCTAssertTrue([self.clientMessageCache hasTestMessage]); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + // No more test message in the cache now. + XCTAssertFalse([self.clientMessageCache hasTestMessage]); +} + +// If a message is still being displayed, we won't try to display a second one on top of it +- (void)testNoDualDisplay { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + // This display component only detects a valid impression, but does not end the renderig + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected]; + self.displayExecutor.messageDisplayComponent = display; + + [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]]; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + // m2 is being rendered + XCTAssertEqualObjects(self.m2.renderData.messageID, display.message.campaignInfo.messageID); + + NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + XCTAssertEqual(1, remainingMsgCount); + + // try to display again when the in-display flag is already turned on (and not turned off yet) + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + // Verify that the message in display component is still m2 + XCTAssertEqualObjects(self.m2.renderData.messageID, display.message.campaignInfo.messageID); + + // message in cache remain unchanged for the second checkAndDisplayNext call + remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + XCTAssertEqual(1, remainingMsgCount); +} + +// this test case contracts testNoAnalyticsTrackingOnTestMessage to cover both positive +// and negative cases +- (void)testDoesAnalyticsTrackingOnNonTestMessage { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + // not expecting triggering analytics recording + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + [self.clientMessageCache setMessageData:@[ self.m2 ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testDoesAnalyticsTrackingOnDisplayError { + // 1000 seconds is larger than DISPLAY_MIN_INTERVALS minutes + // last display time is set to 0 by default + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000); + + // not expecting triggering analytics recording + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventImageFetchError + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + [self.clientMessageCache setMessageData:@[ self.m2 ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionError]; + self.displayExecutor.messageDisplayComponent = display; + + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testAnalyticsTrackingOnMessageDismissCase { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + // not expecting triggering analytics recording + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventMessageDismissAuto + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + // Make sure we don't log the url follow event. + OCMReject([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + [self.clientMessageCache setMessageData:@[ self.m2 ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionDismiss]; + self.displayExecutor.messageDisplayComponent = display; + + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testAnalyticsTrackingOnMessageClickCase { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + // We expect two analytics events for a click action: + // An impression event and an action URL follow event + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + [self.clientMessageCache setMessageData:@[ self.m2 ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testAnalyticsTrackingOnTestMessageClickCase { + // 1000 seconds is larger than DISPLAY_MIN_INTERVALS minutes + // last display time is set to 0 by default + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000); + + // We expect two analytics events for a click action: + // An test message impression event and a test message click event + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageClick + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m2.renderData]; + + [self.clientMessageCache setMessageData:@[ testMessage ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testAnalyticsTrackingOnTestMessageDismissCase { + // 1000 seconds is larger than DISPLAY_MIN_INTERVALS minutes + // last display time is set to 0 by default + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000); + + // We expect a test message impression + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + // No click event + OCMReject([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageClick + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m2.renderData]; + + [self.clientMessageCache setMessageData:@[ testMessage ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionDismiss]; + self.displayExecutor.messageDisplayComponent = display; + + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testAnalyticsTrackingImpressionOnValidImpressionDetectedCase { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + // not expecting triggering analytics recording + OCMExpect([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression + forCampaignID:[OCMArg isEqual:self.m2.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + + [self.clientMessageCache setMessageData:@[ self.m2 ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected]; + self.displayExecutor.messageDisplayComponent = display; + + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testNoAnalyticsTrackingOnTestMessage { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m1.renderData]; + + // not expecting triggering analytics recording + OCMReject([self.mockAnalyticsEventLogger + logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression + forCampaignID:[OCMArg isEqual:self.m1.renderData.messageID] + withCampaignName:[OCMArg any] + eventTimeInMs:[OCMArg any] + completion:[OCMArg any]]); + [self.clientMessageCache setMessageData:@[ testMessage ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + OCMVerifyAll((id)self.mockAnalyticsEventLogger); +} + +- (void)testNoMessageAvailableCase { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + [self.clientMessageCache setMessageData:@[]]; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + // No display has happened so the message stored in the display component should be nil + XCTAssertNil(display.message); + NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + XCTAssertEqual(0, remainingMsgCount); +} + +- (void)testIntervalBetweenOnAppOpenDisplays { + self.displaySetting.displayMinIntervalInMinutes = 10; + + // last display time is set to 0 by default + // 10 seconds is not long enough for satisfying the 10-min internal requirement + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(10); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + + [self.clientMessageCache setMessageData:@[ self.m1 ]]; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + // No display has happened so the message stored in the display component should be nil + XCTAssertNil(display.message); + + // still got one in the queue + XCTAssertEqual(1, remainingMsgCount); +} + +// making sure that we match on the event names for analytics based events +- (void)testOnFirebaseAnalyticsEventDisplayMessages { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + + // m1 and m3 are messages triggered by 'test_event' analytics events + [self.clientMessageCache setMessageData:@[ self.m1, self.m3 ]]; + + [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"different event"]; + NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + + // No message matching event "different event", so no message is nil + XCTAssertNil(display.message); + // still got 2 in the queue + XCTAssertEqual(2, remainingMsgCount); + + // now trigger it with 'test_event' and we would expect one message to be displayed and removed + // from cache + [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"test_event"]; + // Expecting the m1 being used for display + XCTAssertEqualObjects(self.m1.renderData.messageID, display.message.campaignInfo.messageID); + + remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + + // Now only one message remaining in the queue + XCTAssertEqual(1, remainingMsgCount); +} + +// no regular message rendering if suppress message display flag is turned on +- (void)testNoRenderingIfMessageDisplayIsSuppressed { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + self.displayExecutor.suppressMessageDisplay = YES; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + // no message display has happened + XCTAssertNil(display.message); + + NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + // no message is removed from the cache + XCTAssertEqual(2, remainingMsgCount); + + // now allow message rendering again + self.displayExecutor.suppressMessageDisplay = NO; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + NSInteger remainingMsgCount2 = [self.clientMessageCache allRegularMessages].count; + // one message was rendered and removed from the cache + XCTAssertEqual(1, remainingMsgCount2); +} + +// No contextual message rendering if suppress message display flag is turned on +- (void)testNoContextualMsgRenderingIfMessageDisplayIsSuppressed { + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + + [self.clientMessageCache setMessageData:@[ self.m1, self.m3 ]]; + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + + self.displayExecutor.suppressMessageDisplay = YES; + [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"test_event"]; + + // no message display has happened + XCTAssertNil(display.message); + + NSInteger remainingMsgCount = [self.clientMessageCache allRegularMessages].count; + // No message is removed from the cache. + XCTAssertEqual(2, remainingMsgCount); + + // now re-enable message rendering again + self.displayExecutor.suppressMessageDisplay = NO; + [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:@"test_event"]; + + NSInteger remainingMsgCount2 = [self.clientMessageCache allRegularMessages].count; + // one message was rendered and removed from the cache + XCTAssertEqual(1, remainingMsgCount2); +} + +- (void)testMessageClickedCallback { + FIRInAppMessagingDisplayTestDelegate *delegate = + [[FIRInAppMessagingDisplayTestDelegate alloc] init]; + self.mockInAppMessaging.delegate = delegate; + + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionClick]; + self.displayExecutor.messageDisplayComponent = display; + [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]]; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + XCTAssertTrue(delegate.receivedMessageClickedCallback); +} + +- (void)testMessageImpressionCallback { + FIRInAppMessagingDisplayTestDelegate *delegate = + [[FIRInAppMessagingDisplayTestDelegate alloc] init]; + self.mockInAppMessaging.delegate = delegate; + + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionImpressionDetected]; + self.displayExecutor.messageDisplayComponent = display; + [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]]; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + // Verify that the message content handed to display component is expected + XCTAssertTrue(delegate.receivedMessageImpressionCallback); +} + +- (void)testMessageErrorCallback { + FIRInAppMessagingDisplayTestDelegate *delegate = + [[FIRInAppMessagingDisplayTestDelegate alloc] init]; + self.mockInAppMessaging.delegate = delegate; + + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionError]; + self.displayExecutor.messageDisplayComponent = display; + [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]]; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + // Verify that the message content handed to display component is expected + XCTAssertTrue(delegate.receivedMessageErrorCallback); +} + +- (void)testMessageDismissedCallback { + FIRInAppMessagingDisplayTestDelegate *delegate = + [[FIRInAppMessagingDisplayTestDelegate alloc] init]; + self.mockInAppMessaging.delegate = delegate; + + // This setup allows next message to be displayed from display interval perspective. + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]) + .andReturn(DISPLAY_MIN_INTERVALS * 60 + 100); + OCMStub(self.mockInAppMessaging.delegate).andReturn(delegate); + + FIRIAMMessageDisplayForTesting *display = [[FIRIAMMessageDisplayForTesting alloc] + initWithDelegateInteraction:FIRInAppMessagingDelegateInteractionDismiss]; + self.displayExecutor.messageDisplayComponent = display; + [self.clientMessageCache setMessageData:@[ self.m2, self.m4 ]]; + [self.displayExecutor checkAndDisplayNextAppForegroundMessage]; + + // Verify that the message content handed to display component is expected + XCTAssertTrue(delegate.receivedMessageDismissedCallback); +} + +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMElapsedTimeTrackerTests.m b/InAppMessaging/Example/Tests/FIRIAMElapsedTimeTrackerTests.m new file mode 100644 index 00000000000..1e69b430d09 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMElapsedTimeTrackerTests.m @@ -0,0 +1,72 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import "FIRIAMElapsedTimeTracker.h" + +@interface FIRIAMElapsedTimeTrackerTests : XCTestCase +@property id mockTimeFetcher; +@property FIRIAMElapsedTimeTracker *tracker; + +@end + +@implementation FIRIAMElapsedTimeTrackerTests + +- (void)setUp { + [super setUp]; + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testTrackingTimeWithPauses { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + + // set up the time moments to be returned + // 0 start + // 15 pause + // 20 resume + // 30 measure the total tracked time + // given the above sequence, + // at time = 30 seconds, we expect the tracked time to be 15 + (30 - 20) = 25 seconds + + NSArray *currentTimes = @[ + [NSNumber numberWithDouble:0], [NSNumber numberWithDouble:15], [NSNumber numberWithDouble:20], + [NSNumber numberWithDouble:30] + ]; + __block int nextTimeToReturn = 0; + + // start with timestamp as 0 + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andDo(^(NSInvocation *invocation) { + NSTimeInterval time = [currentTimes[nextTimeToReturn++] doubleValue]; + [invocation setReturnValue:&time]; + }); + + self.tracker = [[FIRIAMElapsedTimeTracker alloc] initWithTimeFetcher:_mockTimeFetcher]; + [self.tracker pause]; + [self.tracker resume]; + + NSTimeInterval trackedTime = [self.tracker trackedTimeSoFar]; + XCTAssertEqualWithAccuracy(25, trackedTime, 0.01); +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMFetchFlowTests.m b/InAppMessaging/Example/Tests/FIRIAMFetchFlowTests.m new file mode 100644 index 00000000000..65eaffee468 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMFetchFlowTests.m @@ -0,0 +1,331 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import "FIRIAMAnalyticsEventLogger.h" +#import "FIRIAMDisplayTriggerDefinition.h" +#import "FIRIAMFetchFlow.h" +#import "FIRIAMMessageContentDataWithImageURL.h" +#import "FIRIAMSDKModeManager.h" + +@interface FIRIAMFetchFlowTests : XCTestCase +@property(nonatomic) FIRIAMFetchSetting *fetchSetting; +@property FIRIAMMessageClientCache *clientMessageCache; +@property id mockMessageFetcher; +@property id mockBookkeeper; +@property id mockTimeFetcher; +@property FIRIAMFetchFlow *flow; +@property FIRIAMActivityLogger *activityLogger; +@property FIRIAMSDKModeManager *mockSDKModeManager; + +@property id mockAnaltycisEventLogger; + +// three pre-defined messages +@property FIRIAMMessageDefinition *m1, *m2, *m3; +@end + +CGFloat FETCH_MIN_INTERVALS = 1; + +@implementation FIRIAMFetchFlowTests +- (void)setupMessageTexture { + // startTime, endTime here ensures messages with them are active + NSTimeInterval activeStartTime = 0; + NSTimeInterval activeEndTime = [[NSDate date] timeIntervalSince1970] + 10000; + + FIRIAMDisplayTriggerDefinition *triggerDefinition = + [[FIRIAMDisplayTriggerDefinition alloc] initWithFirebaseAnalyticEvent:@"test_event"]; + + FIRIAMMessageContentDataWithImageURL *m1ContentData = + [[FIRIAMMessageContentDataWithImageURL alloc] + initWithMessageTitle:@"m1 title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://unsplash.it/300/300"] + usingURLSession:nil]; + + FIRIAMRenderingEffectSetting *renderSetting1 = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting1.viewMode = FIRIAMRenderAsBannerView; + + FIRIAMMessageRenderData *renderData1 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m1" + messageName:@"name" + contentData:m1ContentData + renderingEffect:renderSetting1]; + + self.m1 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData1 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ triggerDefinition ]]; + + FIRIAMMessageContentDataWithImageURL *m2ContentData = + [[FIRIAMMessageContentDataWithImageURL alloc] + initWithMessageTitle:@"m2 title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://unsplash.it/300/400"] + usingURLSession:nil]; + + FIRIAMRenderingEffectSetting *renderSetting2 = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting2.viewMode = FIRIAMRenderAsModalView; + + FIRIAMMessageRenderData *renderData2 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m2" + messageName:@"name" + contentData:m2ContentData + renderingEffect:renderSetting2]; + + self.m2 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData2 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ triggerDefinition ]]; + + FIRIAMMessageContentDataWithImageURL *m3ContentData = + [[FIRIAMMessageContentDataWithImageURL alloc] + initWithMessageTitle:@"m3 title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://unsplash.it/400/300"] + usingURLSession:nil]; + + FIRIAMRenderingEffectSetting *renderSetting3 = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting3.viewMode = FIRIAMRenderAsImageOnlyView; + + FIRIAMMessageRenderData *renderData3 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m3" + messageName:@"name" + contentData:m3ContentData + renderingEffect:renderSetting3]; + + self.m3 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData3 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ triggerDefinition ]]; +} + +- (void)setUp { + [super setUp]; + [self setupMessageTexture]; + + self.fetchSetting = [[FIRIAMFetchSetting alloc] init]; + self.fetchSetting.fetchMinIntervalInMinutes = FETCH_MIN_INTERVALS; + self.mockMessageFetcher = OCMProtocolMock(@protocol(FIRIAMMessageFetcher)); + + FIRIAMFetchResponseParser *parser = + [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:[[FIRIAMTimerWithNSDate alloc] init]]; + + self.clientMessageCache = [[FIRIAMMessageClientCache alloc] initWithBookkeeper:self.mockBookkeeper + usingResponseParser:parser]; + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + self.mockBookkeeper = OCMProtocolMock(@protocol(FIRIAMBookKeeper)); + self.activityLogger = OCMClassMock([FIRIAMActivityLogger class]); + self.mockAnaltycisEventLogger = OCMProtocolMock(@protocol(FIRIAMAnalyticsEventLogger)); + + self.mockSDKModeManager = OCMClassMock([FIRIAMSDKModeManager class]); + + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + + self.flow = [[FIRIAMFetchFlow alloc] initWithSetting:self.fetchSetting + messageCache:self.clientMessageCache + messageFetcher:self.mockMessageFetcher + timeFetcher:self.mockTimeFetcher + bookKeeper:self.mockBookkeeper + activityLogger:self.activityLogger + analyticsEventLogger:self.mockAnaltycisEventLogger + FIRIAMSDKModeManager:self.mockSDKModeManager]; +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +// In happy path, the fetch is allowed and we are able to fetch two messages back +- (void)testHappyPath { + OCMStub([self.mockBookkeeper lastFetchTime]).andReturn(0); + + // Set it up so that we already have impressions for m1 and m3 + FIRIAMImpressionRecord *impression1 = + [[FIRIAMImpressionRecord alloc] initWithMessageID:self.m1.renderData.messageID + impressionTimeInSeconds:1233]; + + FIRIAMImpressionRecord *impression2 = [[FIRIAMImpressionRecord alloc] initWithMessageID:@"m3" + impressionTimeInSeconds:5678]; + + NSArray *impressions = @[ impression1, impression2 ]; + OCMStub([self.mockBookkeeper getImpressions]).andReturn(impressions); + + NSArray *fetchedMessages = @[ self.m1, self.m2 ]; + + // 200 seconds is larger than fetch wait time which is 100 in this setup + OCMStub([self.mockBookkeeper nextFetchWaitTime]).andReturn(100); + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(200); + + OCMStub([self.mockSDKModeManager currentMode]).andReturn(FIRIAMSDKModeRegular); + + NSNumber *fetchWaitTimeFromResponse = [NSNumber numberWithInt:2000]; + + OCMStub([self.mockMessageFetcher + fetchMessagesWithImpressionList:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:fetchedMessages, + fetchWaitTimeFromResponse, + [NSNull null], [NSNull null], + nil])]); + [self.flow checkAndFetch]; + + // We expect m1 and m2 to be dumped into clientMessageCache. + NSArray *foundMessages = [self.clientMessageCache allRegularMessages]; + XCTAssertEqual(2, foundMessages.count); + XCTAssertEqualObjects(foundMessages[0].renderData.messageID, self.m1.renderData.messageID); + XCTAssertEqualObjects(foundMessages[1].renderData.messageID, self.m2.renderData.messageID); + + // Verify that we record the new fetch with bookkeeper + OCMVerify([self.mockBookkeeper recordNewFetchWithFetchCount:2 + withTimestampInSeconds:200 + nextFetchWaitTime:fetchWaitTimeFromResponse]); + + // So we are sending the request with impression for m1 and m3 and getting back messages for m1 + // and m2. In here m1 is a recurring message and after the fetch, we should call + // book keeper's clearImpressionsWithMessageList: method with m1 which is an intersection + // between the request impression list and the response message id list. We are skipping + // m2 since it's not included in the impression records sent along with the request. + OCMVerify( + [self.mockBookkeeper clearImpressionsWithMessageList:@[ self.m1.renderData.messageID ]]); +} + +// No fetch is to be performed if the required fetch interval is not met +- (void)testNoFetchDueToIntervalConstraint { + OCMStub([self.mockSDKModeManager currentMode]).andReturn(FIRIAMSDKModeRegular); + + // We need to wait at least 300 seconds before making another fetch + OCMStub([self.mockBookkeeper nextFetchWaitTime]).andReturn(300); + + // And it's only been 200 seconds since last fetch, so no fetch should happen + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(200); + OCMStub([self.mockBookkeeper lastFetchTime]).andReturn(0); + + // We don't expect fetchMessages: for self.mockMessageFetcher to be triggred + OCMReject([self.mockMessageFetcher fetchMessagesWithImpressionList:[OCMArg any] + withCompletion:[OCMArg any]]); + [self.flow checkAndFetch]; + + NSArray *foundMessages = [self.clientMessageCache allRegularMessages]; + XCTAssertEqual(0, foundMessages.count); +} + +// Fetch always in newly installed mode +- (void)testAlwaysFetchForNewlyInstalledMode { + OCMStub([self.mockBookkeeper lastFetchTime]).andReturn(0); + OCMStub([self.mockSDKModeManager currentMode]).andReturn(FIRIAMSDKModeNewlyInstalled); + OCMStub([self.mockMessageFetcher + fetchMessagesWithImpressionList:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@[ self.m1, self.m2 ], + [NSNull null], [NSNull null], + [NSNull null], nil])]); + + // 100 seconds is less than fetch wait time which is 1000 in this setup, + // but since we are in newly installed mode, fetch would still happen + OCMStub([self.mockBookkeeper nextFetchWaitTime]).andReturn(1000); + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(100); + + [self.flow checkAndFetch]; + + // we expect m1 and m2 to be dumped into clientMessageCache + NSArray *foundMessages = [self.clientMessageCache allRegularMessages]; + XCTAssertEqual(2, foundMessages.count); + XCTAssertEqualObjects(foundMessages[0].renderData.messageID, self.m1.renderData.messageID); + XCTAssertEqualObjects(foundMessages[1].renderData.messageID, self.m2.renderData.messageID); + + // we expect to register a fetch with sdk manager + OCMVerify([self.mockSDKModeManager registerOneMoreFetch]); +} + +// Fetch always in testing app instance mode +- (void)testAlwaysFetchForTestingAppInstanceMode { + OCMStub([self.mockBookkeeper lastFetchTime]).andReturn(0); + OCMStub([self.mockSDKModeManager currentMode]).andReturn(FIRIAMSDKModeTesting); + OCMStub([self.mockMessageFetcher + fetchMessagesWithImpressionList:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@[ self.m1, self.m2 ], + [NSNull null], [NSNull null], + [NSNull null], nil])]); + // 100 seconds is less than fetch wait time which is 1000 in this setup, + // but since we are in testing app instance mode, fetch would still happen + OCMStub([self.mockBookkeeper nextFetchWaitTime]).andReturn(1000); + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(100); + + [self.flow checkAndFetch]; + + // we expect m1 and m2 to be dumped into clientMessageCache + NSArray *foundMessages = [self.clientMessageCache allRegularMessages]; + XCTAssertEqual(2, foundMessages.count); + XCTAssertEqualObjects(foundMessages[0].renderData.messageID, self.m1.renderData.messageID); + XCTAssertEqualObjects(foundMessages[1].renderData.messageID, self.m2.renderData.messageID); + + // we expect to register a fetch with sdk manager + OCMVerify([self.mockSDKModeManager registerOneMoreFetch]); +} + +- (void)testTurnIntoTestigModeOnSeeingTestMessage { + OCMStub([self.mockBookkeeper lastFetchTime]).andReturn(0); + OCMStub([self.mockSDKModeManager currentMode]).andReturn(FIRIAMSDKModeNewlyInstalled); + + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m2.renderData]; + + OCMStub([self.mockMessageFetcher + fetchMessagesWithImpressionList:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@[ self.m1, testMessage ], + [NSNull null], [NSNull null], + [NSNull null], nil])]); + self.fetchSetting.fetchMinIntervalInMinutes = 10; // at least 600 seconds between fetches + // 100 seconds is larger than FETCH_MIN_INTERVALS minutes + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(100); + + [self.flow checkAndFetch]; + + // Expecting turning sdk mode into a testing instance + OCMVerify([self.mockSDKModeManager becomeTestingInstance]); +} + +- (void)testNotTurningIntoTestingModeIfAlreadyInTestingMode { + OCMStub([self.mockBookkeeper lastFetchTime]).andReturn(0); + OCMStub([self.mockSDKModeManager currentMode]).andReturn(FIRIAMSDKModeTesting); + + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:self.m2.renderData]; + + OCMStub([self.mockMessageFetcher + fetchMessagesWithImpressionList:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:@[ self.m1, testMessage ], + [NSNull null], [NSNull null], + [NSNull null], nil])]); + self.fetchSetting.fetchMinIntervalInMinutes = 10; // at least 600 seconds between fetches + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(1000); + OCMReject([self.mockSDKModeManager becomeTestingInstance]); + + [self.flow checkAndFetch]; +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMFetchResponseParserTests.m b/InAppMessaging/Example/Tests/FIRIAMFetchResponseParserTests.m new file mode 100644 index 00000000000..0365d072d86 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMFetchResponseParserTests.m @@ -0,0 +1,180 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import "FIRIAMDisplayTriggerDefinition.h" +#import "FIRIAMFetchResponseParser.h" +#import "FIRIAMMessageContentDataWithImageURL.h" +#import "FIRIAMMessageDefinition.h" +#import "FIRIAMTimeFetcher.h" +#import "UIColor+FIRIAMHexString.h" + +@interface FIRIAMFetchResponseParserTests : XCTestCase +@property(nonatomic, copy) NSString *jsonResposne; +@property(nonatomic) FIRIAMFetchResponseParser *parser; +@property(nonatomic) id mockTimeFetcher; +@end + +@implementation FIRIAMFetchResponseParserTests + +- (void)setUp { + [super setUp]; + self.mockTimeFetcher = OCMProtocolMock(@protocol(FIRIAMTimeFetcher)); + self.parser = [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:self.mockTimeFetcher]; +} +- (void)tearDown { + [super tearDown]; +} + +- (void)testRegularConversion { + NSString *testJsonDataFilePath = + [[NSBundle bundleForClass:[self class]] pathForResource:@"TestJsonDataFromFetch" + ofType:@"txt"]; + + NSTimeInterval currentMoment = 100000000; + OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); + + self.jsonResposne = [[NSString alloc] initWithContentsOfFile:testJsonDataFilePath + encoding:NSUTF8StringEncoding + error:nil]; + + NSData *data = [self.jsonResposne dataUsingEncoding:NSUTF8StringEncoding]; + NSError *errorJson = nil; + NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data + options:kNilOptions + error:&errorJson]; + + NSInteger discardCount; + NSNumber *fetchWaitTime; + NSArray *results = + [self.parser parseAPIResponseDictionary:responseDict + discardedMsgCount:&discardCount + fetchWaitTimeInSeconds:&fetchWaitTime]; + + double nextFetchEpochTimeInResponse = + [responseDict[@"expirationEpochTimestampMillis"] doubleValue]; + + // fetch wait time should be (next fetch epoch time - curret moment) + XCTAssertEqualWithAccuracy([fetchWaitTime doubleValue], + nextFetchEpochTimeInResponse / 1000 - currentMoment, 0.1); + + XCTAssertEqual(4, [results count]); + XCTAssertEqual(0, discardCount); + + FIRIAMMessageDefinition *first = results[0]; + XCTAssertEqualObjects(@"13313766398414028800", first.renderData.messageID); + XCTAssertEqualObjects(@"first campaign", first.renderData.name); + XCTAssertEqualObjects(@"I heard you like In-App Messages", + first.renderData.contentData.titleText); + XCTAssertEqualObjects(@"This is message body", first.renderData.contentData.bodyText); + XCTAssertEqual(FIRIAMRenderAsModalView, first.renderData.renderingEffectSettings.viewMode); + XCTAssertEqualWithAccuracy(1523986039, first.startTime, 0.1); + XCTAssertEqualWithAccuracy(1526986039, first.endTime, 0.1); + XCTAssertNotNil(first.renderData.renderingEffectSettings.textColor); + XCTAssertEqualObjects(first.renderData.renderingEffectSettings.displayBGColor, + [UIColor firiam_colorWithHexString:@"#fffff8"]); + XCTAssertEqualObjects(first.renderData.renderingEffectSettings.btnBGColor, + [UIColor firiam_colorWithHexString:@"#000000"]); + XCTAssertEqualObjects(first.renderData.contentData.actionURL.absoluteString, + @"https://www.google.com"); + XCTAssertEqual(FIRIAMRenderTriggerOnAppForeground, first.renderTriggers[0].triggerType); + + FIRIAMMessageDefinition *second = results[1]; + XCTAssertEqualObjects(@"9350598726327992320", second.renderData.messageID); + XCTAssertEqualObjects(@"Inception1", second.renderData.name); + XCTAssertEqualObjects(@"Test 2", second.renderData.contentData.titleText); + XCTAssertNil(second.renderData.contentData.bodyText); + XCTAssertEqual(FIRIAMRenderAsModalView, second.renderData.renderingEffectSettings.viewMode); + XCTAssertEqual(2, second.renderTriggers.count); + + XCTAssertEqualObjects(second.renderData.renderingEffectSettings.displayBGColor, + [UIColor firiam_colorWithHexString:@"#ffffff"]); + + // Third message is a banner view message based on a analytics event trigger. + FIRIAMMessageDefinition *third = results[2]; + XCTAssertEqualObjects(@"14819094573862617088", third.renderData.messageID); + XCTAssertEqual(FIRIAMRenderAsBannerView, third.renderData.renderingEffectSettings.viewMode); + XCTAssertEqual(1, third.renderTriggers.count); + XCTAssertEqualObjects(@"jackpot", third.renderTriggers[0].firebaseEventName); +} + +- (void)testParsingTestMessage { + NSString *testJsonDataFilePath = [[NSBundle bundleForClass:[self class]] + pathForResource:@"TestJsonDataWithTestMessageFromFetch" + ofType:@"txt"]; + + self.jsonResposne = [[NSString alloc] initWithContentsOfFile:testJsonDataFilePath + encoding:NSUTF8StringEncoding + error:nil]; + + NSData *data = [self.jsonResposne dataUsingEncoding:NSUTF8StringEncoding]; + NSError *errorJson = nil; + NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data + options:kNilOptions + error:&errorJson]; + + NSInteger discardCount; + NSNumber *fetchWaitTime; + NSArray *results = + [self.parser parseAPIResponseDictionary:responseDict + discardedMsgCount:&discardCount + fetchWaitTimeInSeconds:&fetchWaitTime]; + + // In our fixture file used in this test, there is no fetch expiration time + XCTAssertNil(fetchWaitTime); + + XCTAssertEqual(2, [results count]); + XCTAssertEqual(0, discardCount); + + // First is a test message and the second one is not. + XCTAssertTrue(results[0].isTestMessage); + XCTAssertTrue(results[0].renderData.renderingEffectSettings.isTestMessage); + + XCTAssertFalse(results[1].isTestMessage); + XCTAssertFalse(results[1].renderData.renderingEffectSettings.isTestMessage); +} + +- (void)testParsingInvalidTestMessageNodes { + NSString *testJsonDataFilePath = [[NSBundle bundleForClass:[self class]] + pathForResource:@"JsonDataWithInvalidMessagesFromFetch" + ofType:@"txt"]; + + self.jsonResposne = [[NSString alloc] initWithContentsOfFile:testJsonDataFilePath + encoding:NSUTF8StringEncoding + error:nil]; + + NSData *data = [self.jsonResposne dataUsingEncoding:NSUTF8StringEncoding]; + NSError *errorJson = nil; + NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data + options:kNilOptions + error:&errorJson]; + + NSInteger discardCount; + NSNumber *fetchWaitTime; + NSArray *results = + [self.parser parseAPIResponseDictionary:responseDict + discardedMsgCount:&discardCount + fetchWaitTimeInSeconds:&fetchWaitTime]; + + XCTAssertEqual(0, [results count]); + + // First node missing title, second one missig triggering conditions and the third one + // contains invalid type node. + XCTAssertEqual(3, discardCount); +} + +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMMessageClientCacheTests.m b/InAppMessaging/Example/Tests/FIRIAMMessageClientCacheTests.m new file mode 100644 index 00000000000..c0b95c539ac --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMMessageClientCacheTests.m @@ -0,0 +1,378 @@ +/* + * Copyright 2017 Google + * + * 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 + +#import "FIRIAMDisplayCheckOnAnalyticEventsFlow.h" +#import "FIRIAMDisplayTriggerDefinition.h" +#import "FIRIAMMessageClientCache.h" +#import "FIRIAMMessageContentDataWithImageURL.h" +#import "FIRIAMMessageDefinition.h" +#import "FIRIAMTimeFetcher.h" + +@interface FIRIAMMessageClientCacheTests : XCTestCase +@property id mockBookkeeper; +@property(nonatomic) FIRIAMMessageClientCache *clientCache; +@end + +@interface FIRIAMMessageClientCache () +// for the purpose of unit testing validations +@property(nonatomic) NSMutableSet *firebaseAnalyticEventsToWatch; +@end + +@implementation FIRIAMMessageClientCacheTests { + // some predefined message definitions that are handy for certain test cases + FIRIAMMessageDefinition *m1, *m2, *m3, *m4, *m5; +} + +- (void)setUp { + [super setUp]; + self.mockBookkeeper = OCMProtocolMock(@protocol(FIRIAMBookKeeper)); + + FIRIAMFetchResponseParser *parser = + [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:[[FIRIAMTimerWithNSDate alloc] init]]; + self.clientCache = [[FIRIAMMessageClientCache alloc] initWithBookkeeper:self.mockBookkeeper + usingResponseParser:parser]; + + // startTime, endTime here ensures messages with them are active + NSTimeInterval activeStartTime = 0; + NSTimeInterval activeEndTime = [[NSDate date] timeIntervalSince1970] + 10000; + // m2 & m 4 will be of contextual trigger + FIRIAMDisplayTriggerDefinition *contextualTriggerDefinition = + [[FIRIAMDisplayTriggerDefinition alloc] initWithFirebaseAnalyticEvent:@"test_event"]; + + FIRIAMDisplayTriggerDefinition *contextualTriggerDefinition2 = + [[FIRIAMDisplayTriggerDefinition alloc] initWithFirebaseAnalyticEvent:@"second_event"]; + + // m1 and m3 will be of app open trigger + FIRIAMDisplayTriggerDefinition *appOpentriggerDefinition = + [[FIRIAMDisplayTriggerDefinition alloc] initForAppForegroundTrigger]; + + FIRIAMMessageContentDataWithImageURL *msgContentData = + [[FIRIAMMessageContentDataWithImageURL alloc] + initWithMessageTitle:@"title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://unsplash.it/300/300"] + usingURLSession:nil]; + + FIRIAMRenderingEffectSetting *renderSetting = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting.viewMode = FIRIAMRenderAsBannerView; + + FIRIAMMessageRenderData *renderData1 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m1" + messageName:@"name" + contentData:msgContentData + renderingEffect:renderSetting]; + + m1 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData1 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ appOpentriggerDefinition ]]; + + FIRIAMMessageRenderData *renderData2 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m2" + messageName:@"name" + contentData:msgContentData + renderingEffect:renderSetting]; + + m2 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData2 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ contextualTriggerDefinition ]]; + + FIRIAMMessageRenderData *renderData3 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m3" + messageName:@"name" + contentData:msgContentData + renderingEffect:renderSetting]; + + m3 = [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData3 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ appOpentriggerDefinition ]]; + + FIRIAMMessageRenderData *renderData4 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m4" + messageName:@"name" + contentData:msgContentData + renderingEffect:renderSetting]; + + FIRIAMMessageRenderData *renderData5 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m5" + messageName:@"name" + contentData:msgContentData + renderingEffect:renderSetting]; + + m4 = [[FIRIAMMessageDefinition alloc] + initWithRenderData:renderData4 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ contextualTriggerDefinition, contextualTriggerDefinition2 ]]; + + // m5 is of mixture of both app-foreground and contextual triggers + m5 = [[FIRIAMMessageDefinition alloc] + initWithRenderData:renderData5 + startTime:activeStartTime + endTime:activeEndTime + triggerDefinition:@[ contextualTriggerDefinition, appOpentriggerDefinition ]]; +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testResetMessages { + // test setting a mixture of display-on-app open messages and Firebase Analytics based messages + // to see if the cache will keep them correctly + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + [self.clientCache setMessageData:@[ m1, m2, m3, m4 ]]; + + NSArray *messages = [self.clientCache allRegularMessages]; + XCTAssertEqual(4, [messages count]); + + // m4 have two contextual events defined as triggers + XCTAssertEqual(2, [self.clientCache.firebaseAnalyticEventsToWatch count]); + XCTAssert([self.clientCache.firebaseAnalyticEventsToWatch containsObject:@"test_event"]); + XCTAssert([self.clientCache.firebaseAnalyticEventsToWatch containsObject:@"second_event"]); +} + +- (void)testResetMessagesWithImpressionsData { + // test setting a mixture of display-on-app open messages and Firebase Analytics based messages + // to see if the cache will keep them correctly + + NSArray *impressionList = @[ @"m1", @"m2" ]; + OCMStub([self.mockBookkeeper getMessageIDsFromImpressions]).andReturn(impressionList); + [self.clientCache setMessageData:@[ m1, m2, m3, m4 ]]; + + // m1 and m2 should have been filtered out + NSArray *messages = [self.clientCache allRegularMessages]; + XCTAssertEqual(2, messages.count); + + // m4 have two contextual events defined as triggers + XCTAssertEqual(2, self.clientCache.firebaseAnalyticEventsToWatch.count); + XCTAssert([self.clientCache.firebaseAnalyticEventsToWatch containsObject:@"test_event"]); + XCTAssert([self.clientCache.firebaseAnalyticEventsToWatch containsObject:@"second_event"]); +} + +- (void)testNextOnAppOpenDisplayMsg_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + // m1 and m3 are messages rendered on app open + [self.clientCache setMessageData:@[ m1, m2, m3, m4 ]]; + + FIRIAMMessageDefinition *nextMsgOnAppOpen = [self.clientCache nextOnAppOpenDisplayMsg]; + XCTAssertEqual(@"m1", nextMsgOnAppOpen.renderData.messageID); + // remove m1 + [self.clientCache removeMessageWithId:@"m1"]; + + // read m2 and remove it + nextMsgOnAppOpen = [self.clientCache nextOnAppOpenDisplayMsg]; + XCTAssertEqual(@"m3", nextMsgOnAppOpen.renderData.messageID); + [self.clientCache removeMessageWithId:@"m3"]; + + // no more message for display on app open + nextMsgOnAppOpen = [self.clientCache nextOnAppOpenDisplayMsg]; + XCTAssertNil(nextMsgOnAppOpen); +} + +- (void)testNextOnFirebaseAnalyticsEventDisplayMsg_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + // m2 and m4 are messages rendered on 'app open'test_event' Firebase Analytics event + [self.clientCache setMessageData:@[ m1, m2, m3, m4 ]]; + + FIRIAMMessageDefinition *nextMsgOnFIREvent = + [self.clientCache nextOnFirebaseAnalyticEventDisplayMsg:@"test_event"]; + XCTAssertEqual(@"m2", nextMsgOnFIREvent.renderData.messageID); + // remove m2 + [self.clientCache removeMessageWithId:@"m2"]; + // verify that the watch set is empty after draining all the messages + XCTAssertTrue([self.clientCache.firebaseAnalyticEventsToWatch containsObject:@"test_event"]); + + // read m4 and remove it + nextMsgOnFIREvent = [self.clientCache nextOnFirebaseAnalyticEventDisplayMsg:@"test_event"]; + XCTAssertEqual(@"m4", nextMsgOnFIREvent.renderData.messageID); + // remove m4 + [self.clientCache removeMessageWithId:@"m4"]; + + // no more message for display on Firebase Analytics event + nextMsgOnFIREvent = [self.clientCache nextOnFirebaseAnalyticEventDisplayMsg:@"test_event"]; + XCTAssertNil(nextMsgOnFIREvent); + + // verify that the watch set is empty after draining all the messages + XCTAssertEqual(0, self.clientCache.firebaseAnalyticEventsToWatch.count); +} + +- (void)testNextOnFirebaseAnalyticsEventDisplayMsgEventNameMustMatch_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + // m2 and m4 are messages rendered on 'app open'test_event' Firebase Analytics event + [self.clientCache setMessageData:@[ m1, m2, m3, m4 ]]; + + FIRIAMMessageDefinition *nextMsgOnFIREvent = + [self.clientCache nextOnFirebaseAnalyticEventDisplayMsg:@"different_event"]; + XCTAssertNil(nextMsgOnFIREvent); +} + +- (void)testNextOnFirebaseAnalyticsEventDisplayMsgEventNameCanMatchAny_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + // m4 are messages of multiple contextual triggers, one of which is for event + // 'second_event' + [self.clientCache setMessageData:@[ m1, m2, m3, m4 ]]; + + FIRIAMMessageDefinition *nextMsgOnFIREvent = + [self.clientCache nextOnFirebaseAnalyticEventDisplayMsg:@"second_event"]; + XCTAssertNotNil(nextMsgOnFIREvent); +} + +- (void)testMessageCanHaveMixedTypeOfTriggers_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + [self.clientCache setMessageData:@[ m5 ]]; + + FIRIAMMessageDefinition *nextMsgOnFIREvent = + [self.clientCache nextOnFirebaseAnalyticEventDisplayMsg:@"test_event"]; + XCTAssertNotNil(nextMsgOnFIREvent); + + // in the meanwhile, retrieving an app-foreground message should be successful + FIRIAMMessageDefinition *nextMsgOnAppForeground = [self.clientCache nextOnAppOpenDisplayMsg]; + XCTAssertNotNil(nextMsgOnAppForeground); +} + +- (void)testNextOnFirebaseAnalyticsEventDisplayMsg_handleStartEndTimeCorrectly { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + + FIRIAMDisplayTriggerDefinition *appOpentriggerDefinition = + [[FIRIAMDisplayTriggerDefinition alloc] initForAppForegroundTrigger]; + + FIRIAMMessageContentDataWithImageURL *msgContentData = + [[FIRIAMMessageContentDataWithImageURL alloc] + initWithMessageTitle:@"title" + messageBody:@"message body" + actionButtonText:nil + actionURL:[NSURL URLWithString:@"http://google.com"] + imageURL:[NSURL URLWithString:@"https://unsplash.it/300/300"] + usingURLSession:nil]; + + FIRIAMRenderingEffectSetting *renderSetting = + [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting]; + renderSetting.viewMode = FIRIAMRenderAsBannerView; + + FIRIAMMessageRenderData *renderData1 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m1" + messageName:@"name" + contentData:msgContentData + renderingEffect:renderSetting]; + + // m1 has not started yet + FIRIAMMessageDefinition *unstartedMessage = [[FIRIAMMessageDefinition alloc] + initWithRenderData:renderData1 + startTime:[[NSDate date] timeIntervalSince1970] + 10000 + endTime:[[NSDate date] timeIntervalSince1970] + 20000 + triggerDefinition:@[ appOpentriggerDefinition ]]; + + FIRIAMMessageRenderData *renderData2 = + [[FIRIAMMessageRenderData alloc] initWithMessageID:@"m2" + messageName:@"name" + contentData:msgContentData + renderingEffect:renderSetting]; + + // m2 has ended + FIRIAMMessageDefinition *endedMessage = [[FIRIAMMessageDefinition alloc] + initWithRenderData:renderData2 + startTime:[[NSDate date] timeIntervalSince1970] - 20000 + endTime:[[NSDate date] timeIntervalSince1970] - 10000 + triggerDefinition:@[ appOpentriggerDefinition ]]; + + // m3, m4 are campaigns with good start/end time + [self.clientCache setMessageData:@[ unstartedMessage, endedMessage, m3, m4 ]]; + + FIRIAMMessageDefinition *nextMsgOnAppOpen = [self.clientCache nextOnAppOpenDisplayMsg]; + FIRIAMMessageDefinition *nextMsgOnFIREvent = + [self.clientCache nextOnFirebaseAnalyticEventDisplayMsg:@"test_event"]; + XCTAssertEqual(nextMsgOnAppOpen.renderData.messageID, @"m3"); + XCTAssertEqual(nextMsgOnFIREvent.renderData.messageID, @"m4"); + + // no more on app open display message + [self.clientCache removeMessageWithId:@"m3"]; + nextMsgOnAppOpen = [self.clientCache nextOnAppOpenDisplayMsg]; + XCTAssertNil(nextMsgOnAppOpen); +} + +- (void)testCallingStartAnalyticsEventListenFlow_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + + FIRIAMDisplayCheckOnAnalyticEventsFlow *mockAnalyticsEventFlow = + OCMClassMock(FIRIAMDisplayCheckOnAnalyticEventsFlow.class); + self.clientCache.analycisEventDislayCheckFlow = mockAnalyticsEventFlow; + + // m2 and m4 are messages rendered on 'test_event' Firebase Analytics event + // so we espect the analytics event listening flow to be started + OCMExpect([mockAnalyticsEventFlow start]); + [self.clientCache setMessageData:@[ m1, m2, m3, m4 ]]; + OCMVerifyAll((id)mockAnalyticsEventFlow); +} + +- (void)testCallingStopAnalyticsEventListenFlow_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + + FIRIAMDisplayCheckOnAnalyticEventsFlow *mockAnalyticsEventFlow = + OCMClassMock(FIRIAMDisplayCheckOnAnalyticEventsFlow.class); + self.clientCache.analycisEventDislayCheckFlow = mockAnalyticsEventFlow; + + // m1 and m3 are messages rendered on app foreground triggers + OCMExpect([mockAnalyticsEventFlow stop]); + [self.clientCache setMessageData:@[ m1, m3 ]]; + OCMVerifyAll((id)mockAnalyticsEventFlow); +} + +- (void)testCallingStartAndThenStopAnalyticsEventListenFlow_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + + FIRIAMDisplayCheckOnAnalyticEventsFlow *mockAnalyticsEventFlow = + OCMClassMock(FIRIAMDisplayCheckOnAnalyticEventsFlow.class); + self.clientCache.analycisEventDislayCheckFlow = mockAnalyticsEventFlow; + + // start is triggered on the setMessageData: call + OCMExpect([mockAnalyticsEventFlow start]); + // stop is triggered on removeMessageWithId: call since m2 is the only message + // using contextual triggers + OCMExpect([mockAnalyticsEventFlow stop]); + + [self.clientCache setMessageData:@[ m1, m2, m3 ]]; + [self.clientCache removeMessageWithId:m2.renderData.messageID]; + OCMVerifyAll((id)mockAnalyticsEventFlow); +} + +- (void)testFetchTestMessageFirstOnNextOnAppOpenDisplayMsg_ok { + OCMStub([self.mockBookkeeper getImpressions]).andReturn(@[]); + + FIRIAMMessageDefinition *testMessage = + [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:m2.renderData]; + + // m1 and m3 are messages rendered on app open + [self.clientCache setMessageData:@[ m1, m2, testMessage, m3, m4 ]]; + + // we are fetching test message back + FIRIAMMessageDefinition *nextMsgOnAppOpen = [self.clientCache nextOnAppOpenDisplayMsg]; + XCTAssertEqual(testMessage.renderData.messageID, nextMsgOnAppOpen.renderData.messageID); + + // we still have 4 regular messages after the first fetch + XCTAssertEqual(4, self.clientCache.allRegularMessages.count); +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMMessageContentDataWithImageURLTests.m b/InAppMessaging/Example/Tests/FIRIAMMessageContentDataWithImageURLTests.m new file mode 100644 index 00000000000..0a71613dd70 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMMessageContentDataWithImageURLTests.m @@ -0,0 +1,241 @@ +/* + * Copyright 2017 Google + * + * 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 +#import "FIRIAMMessageContentDataWithImageURL.h" + +static NSString *defaultTitle = @"Message Title"; +static NSString *defaultBody = @"Message Body"; +static NSString *defaultActionButtonText = @"Take action"; +static NSString *defaultActionURL = @"https://foo.com/bar"; +static NSString *defaultImageURL = @"http://firebase.com/iam/test.png"; + +@interface FIRIAMMessageContentDataWithImageURLTests : XCTestCase +@property NSURLSession *mockedNSURLSession; + +@property FIRIAMMessageContentDataWithImageURL *defaultContentDataWithImageURL; +@end + +@implementation FIRIAMMessageContentDataWithImageURLTests + +- (void)setUp { + [super setUp]; + + _mockedNSURLSession = OCMClassMock([NSURLSession class]); + _defaultContentDataWithImageURL = [[FIRIAMMessageContentDataWithImageURL alloc] + initWithMessageTitle:defaultTitle + messageBody:defaultBody + actionButtonText:defaultActionButtonText + actionURL:[NSURL URLWithString:defaultActionURL] + imageURL:[NSURL URLWithString:defaultImageURL] + usingURLSession:_mockedNSURLSession]; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testReadingTitleAndBodyBackCorrectly { + XCTAssertEqualObjects(defaultTitle, self.defaultContentDataWithImageURL.titleText); + XCTAssertEqualObjects(defaultBody, self.defaultContentDataWithImageURL.bodyText); +} + +- (void)testReadingActionButtonTextCorrectly { + XCTAssertEqualObjects(defaultActionButtonText, + self.defaultContentDataWithImageURL.actionButtonText); +} + +- (void)testURLRequestUsingCorrectImageURL { + __block NSURLRequest *capturedNSURLRequest; + OCMStub([self.mockedNSURLSession + dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(NSURLRequest *request) { + capturedNSURLRequest = request; + return YES; + }] + completionHandler:[OCMArg any] // second parameter is the callback which we don't care in + // this unit testing + ]); + + [_defaultContentDataWithImageURL + loadImageDataWithBlock:^(NSData *_Nullable imageData, NSError *_Nullable error){ + }]; + + // verify that the dataTaskWithRequest:completionHandler: is triggered for NSURLSession object + OCMVerify([self.mockedNSURLSession dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg any]]); + + XCTAssertEqualObjects([capturedNSURLRequest URL].absoluteString, defaultImageURL); +} + +- (void)testReportErrorOnNonSuccessHTTPStatusCode { + // NSURLSessionDataTask * mockedDataTask = OCMClassMock([NSURLSessionDataTask class]); + __block void (^capturedCompletionHandler)(NSData *data, NSURLResponse *response, NSError *error); + + OCMStub([self.mockedNSURLSession + dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg checkWithBlock:^BOOL(id completionHandler) { + capturedCompletionHandler = completionHandler; + return YES; + }] // second parameter is the callback which we don't care in this unit testing + ]); + + XCTestExpectation *expectation = + [self expectationWithDescription:@"image load callback triggered."]; + [_defaultContentDataWithImageURL + loadImageDataWithBlock:^(NSData *_Nullable imageData, NSError *_Nullable error) { + XCTAssertNil(imageData); + XCTAssertNotNil(error); // we should report error due to the unsuccessful http status code + [expectation fulfill]; + }]; + + // verify that the dataTaskWithRequest:completionHandler: is triggered for NSURLSession object + OCMVerify([self.mockedNSURLSession dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg any]]); + + // by this time we should have capturedCompletionHandler being the callback block for the + // NSURLSessionDataTask, now supply it with invalid http status code to see how the block from + // loadImageDataWithBlock: would react to it. + + NSURL *url = [[NSURL alloc] initWithString:defaultImageURL]; + + NSHTTPURLResponse *unsuccessfulHTTPResponse = [[NSHTTPURLResponse alloc] initWithURL:url + statusCode:404 + HTTPVersion:nil + headerFields:nil]; + capturedCompletionHandler(nil, unsuccessfulHTTPResponse, nil); + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testReportErrorOnGeneralNSErrorFromNSURLSession { + NSError *customError = [[NSError alloc] initWithDomain:@"Error Domain" code:100 userInfo:nil]; + __block void (^capturedCompletionHandler)(NSData *data, NSURLResponse *response, NSError *error); + + OCMStub([self.mockedNSURLSession + dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg checkWithBlock:^BOOL(id completionHandler) { + capturedCompletionHandler = completionHandler; + return YES; + }] // second parameter is the callback which we don't care in this unit testing + ]); + + XCTestExpectation *expectation = + [self expectationWithDescription:@"image load callback triggered."]; + [_defaultContentDataWithImageURL + loadImageDataWithBlock:^(NSData *_Nullable imageData, NSError *_Nullable error) { + XCTAssertNil(imageData); + XCTAssertNotNil(error); // we should report error due to the unsuccessful http status code + XCTAssertEqualObjects(error, customError); + [expectation fulfill]; + }]; + + // verify that the dataTaskWithRequest:completionHandler: is triggered for NSURLSession object + OCMVerify([self.mockedNSURLSession dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg any]]); + + // by this time we should have capturedCompletionHandler being the callback block for the + // NSURLSessionDataTask, now feed it with an NSError see how the block from + // loadImageDataWithBlock: would react to it. + capturedCompletionHandler(nil, nil, customError); + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testReportErrorOnNonImageContentTypeResponse { + __block void (^capturedCompletionHandler)(NSData *data, NSURLResponse *response, NSError *error); + + OCMStub([self.mockedNSURLSession + dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg checkWithBlock:^BOOL(id completionHandler) { + capturedCompletionHandler = completionHandler; + return YES; + }] // second parameter is the callback which we don't care in this unit testing + ]); + + XCTestExpectation *expectation = + [self expectationWithDescription:@"image load callback triggered."]; + [_defaultContentDataWithImageURL + loadImageDataWithBlock:^(NSData *_Nullable imageData, NSError *_Nullable error) { + XCTAssertNil(imageData); + XCTAssertNotNil(error); // we should report error due to the http response + // content type being invalid + [expectation fulfill]; + }]; + + // verify that the dataTaskWithRequest:completionHandler: is triggered for NSURLSession object + OCMVerify([self.mockedNSURLSession dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg any]]); + + // by this time we should have capturedCompletionHandler being the callback block for the + // NSURLSessionDataTask, now feed it with a non-image http response to see how the block from + // loadImageDataWithBlock: would react to it. + + NSURL *url = [[NSURL alloc] initWithString:defaultImageURL]; + NSHTTPURLResponse *nonImageContentTypeHTTPResponse = + [[NSHTTPURLResponse alloc] initWithURL:url + statusCode:200 + HTTPVersion:nil + headerFields:@{@"Content-Type" : @"non-image/jpeg"}]; + capturedCompletionHandler(nil, nonImageContentTypeHTTPResponse, nil); + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testGettingImageDataSuccessfully { + NSString *imageDataString = @"test image data"; + NSData *imageData = [imageDataString dataUsingEncoding:NSUTF8StringEncoding]; + + __block void (^capturedCompletionHandler)(NSData *data, NSURLResponse *response, NSError *error); + + OCMStub([self.mockedNSURLSession + dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg checkWithBlock:^BOOL(id completionHandler) { + capturedCompletionHandler = completionHandler; + return YES; + }] // second parameter is the callback which we don't care in this unit testing + ]); + + XCTestExpectation *expectation = + [self expectationWithDescription:@"image load callback triggered."]; + [_defaultContentDataWithImageURL + loadImageDataWithBlock:^(NSData *_Nullable imageData, NSError *_Nullable error) { + XCTAssertNil(error); // no error is reported + NSString *fetchedImageDataString = [[NSString alloc] initWithData:imageData + encoding:NSUTF8StringEncoding]; + + XCTAssertEqualObjects(imageDataString, fetchedImageDataString); + + [expectation fulfill]; + }]; + + // verify that the dataTaskWithRequest:completionHandler: is triggered for NSURLSession object + OCMVerify([self.mockedNSURLSession dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg any]]); + + NSURL *url = [[NSURL alloc] initWithString:defaultImageURL]; + NSHTTPURLResponse *successfulHTTPResponse = + [[NSHTTPURLResponse alloc] initWithURL:url + statusCode:200 + HTTPVersion:nil + headerFields:@{@"Content-Type" : @"image/jpeg"}]; + // by this time we should have capturedCompletionHandler being the callback block for the + // NSURLSessionDataTask, now feed it with image data to see how the block from + // loadImageDataWithBlock: would react to it. + capturedCompletionHandler(imageData, successfulHTTPResponse, nil); + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} +@end diff --git a/InAppMessaging/Example/Tests/FIRIAMMsgFetcherUsingRestfulTests.m b/InAppMessaging/Example/Tests/FIRIAMMsgFetcherUsingRestfulTests.m new file mode 100644 index 00000000000..efdc327fb06 --- /dev/null +++ b/InAppMessaging/Example/Tests/FIRIAMMsgFetcherUsingRestfulTests.m @@ -0,0 +1,233 @@ +/* + * Copyright 2017 Google + * + * 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 +#import "FIRIAMFetchFlow.h" +#import "FIRIAMMessageDefinition.h" +#import "FIRIAMMsgFetcherUsingRestful.h" + +static NSString *serverHost = @"myhost"; +static NSString *projectNumber = @"My-project-number"; +static NSString *appId = @"My-app-id"; +static NSString *apiKey = @"Api-key"; + +@interface FIRIAMMsgFetcherUsingRestfulTests : XCTestCase +@property NSURLSession *mockedNSURLSession; +@property FIRIAMClientInfoFetcher *mockclientInfoFetcher; +@property FIRIAMMsgFetcherUsingRestful *fetcher; +@end + +@implementation FIRIAMMsgFetcherUsingRestfulTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the + // class. + self.mockedNSURLSession = OCMClassMock([NSURLSession class]); + self.mockclientInfoFetcher = OCMClassMock([FIRIAMClientInfoFetcher class]); + + FIRIAMFetchResponseParser *parser = + [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:[[FIRIAMTimerWithNSDate alloc] init]]; + + self.fetcher = + [[FIRIAMMsgFetcherUsingRestful alloc] initWithHost:serverHost + HTTPProtocol:@"https" + project:projectNumber + firebaseApp:appId + APIKey:apiKey + fetchStorage:[[FIRIAMServerMsgFetchStorage alloc] init] + instanceIDFetcher:_mockclientInfoFetcher + usingURLSession:_mockedNSURLSession + responseParser:parser]; +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testRequestConstructionWithoutImpressionData { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + + __block NSURLRequest *capturedNSURLRequest; + OCMStub([self.mockedNSURLSession + dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(NSURLRequest *request) { + capturedNSURLRequest = request; + return YES; + }] + completionHandler:[OCMArg any] // second parameter is the callback which we don't care in + // this unit testing + ]); + + NSString *iidValue = @"my iid"; + NSString *iidToken = @"my iid token"; + OCMStub([self.mockclientInfoFetcher + fetchFirebaseIIDDataWithProjectNumber:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:iidValue, iidToken, + [NSNull null], nil])]); + + NSString *osVersion = @"OS Version"; + OCMStub([self.mockclientInfoFetcher getOSVersion]).andReturn(osVersion); + NSString *appVersion = @"App Version"; + OCMStub([self.mockclientInfoFetcher getAppVersion]).andReturn(appVersion); + NSString *deviceLanguage = @"Language"; + OCMStub([self.mockclientInfoFetcher getDeviceLanguageCode]).andReturn(deviceLanguage); + NSString *timezone = @"time zone"; + OCMStub([self.mockclientInfoFetcher getTimezone]).andReturn(timezone); + + [self.fetcher + fetchMessagesWithImpressionList:@[] + withCompletion:^(NSArray *_Nullable messages, + NSNumber *nextFetchWaitTime, NSInteger discardCount, + NSError *_Nullable error){ + // blank on purpose: it won't get triggered + }]; + + // verify that the dataTaskWithRequest:completionHandler: is triggered for NSURLSession object + OCMVerify([self.mockedNSURLSession dataTaskWithRequest:[OCMArg any] + completionHandler:[OCMArg any]]); + + XCTAssertEqualObjects(@"POST", capturedNSURLRequest.HTTPMethod); + + NSDictionary *requestHeaders = capturedNSURLRequest.allHTTPHeaderFields; + + // verifying some request header fields + XCTAssertEqualObjects([NSBundle mainBundle].bundleIdentifier, + requestHeaders[@"X-Ios-Bundle-Identifier"]); + + XCTAssertEqualObjects(@"application/json", requestHeaders[@"Content-Type"]); + XCTAssertEqualObjects(@"application/json", requestHeaders[@"Accept"]); + + // verify that the request contains the desired api key + NSString *s = [NSString stringWithFormat:@"key=%@", apiKey]; + XCTAssertTrue([capturedNSURLRequest.URL.absoluteString containsString:s]); + XCTAssertTrue([capturedNSURLRequest.URL.absoluteString containsString:projectNumber]); + + // verify that we the request body contains desired iid data + NSError *errorJson = nil; + NSDictionary *requestBodyDict = + [NSJSONSerialization JSONObjectWithData:capturedNSURLRequest.HTTPBody + options:kNilOptions + error:&errorJson]; + XCTAssertEqualObjects(appId, requestBodyDict[@"requesting_client_app"][@"gmp_app_id"]); + XCTAssertEqualObjects(iidValue, requestBodyDict[@"requesting_client_app"][@"app_instance_id"]); + XCTAssertEqualObjects(iidToken, + requestBodyDict[@"requesting_client_app"][@"app_instance_id_token"]); + + XCTAssertEqualObjects(osVersion, requestBodyDict[@"client_signals"][@"platform_version"]); + XCTAssertEqualObjects(appVersion, requestBodyDict[@"client_signals"][@"app_version"]); + XCTAssertEqualObjects(deviceLanguage, requestBodyDict[@"client_signals"][@"language_code"]); + XCTAssertEqualObjects(timezone, requestBodyDict[@"client_signals"][@"time_zone"]); +} + +- (void)testRequestConstructionWithImpressionData { + __block NSURLRequest *capturedNSURLRequest; + OCMStub([self.mockedNSURLSession + dataTaskWithRequest:[OCMArg checkWithBlock:^BOOL(NSURLRequest *request) { + capturedNSURLRequest = request; + return YES; + }] + completionHandler:[OCMArg any] // second parameter is the callback which we don't care in + // this unit testing + ]); + + NSString *iidValue = @"my iid"; + NSString *iidToken = @"my iid token"; + OCMStub([self.mockclientInfoFetcher + fetchFirebaseIIDDataWithProjectNumber:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:iidValue, iidToken, + [NSNull null], nil])]); + + // this is to test the case that only partial client signal fields are available + NSString *osVersion = @"OS Version"; + OCMStub([self.mockclientInfoFetcher getOSVersion]).andReturn(osVersion); + NSString *appVersion = @"App Version"; + OCMStub([self.mockclientInfoFetcher getAppVersion]).andReturn(appVersion); + + long impression1Timestamp = 12345; + FIRIAMImpressionRecord *impression1 = + [[FIRIAMImpressionRecord alloc] initWithMessageID:@"impression 1" + impressionTimeInSeconds:impression1Timestamp]; + long impression2Timestamp = 45678; + FIRIAMImpressionRecord *impression2 = + [[FIRIAMImpressionRecord alloc] initWithMessageID:@"impression 2" + impressionTimeInSeconds:impression2Timestamp]; + + [self.fetcher + fetchMessagesWithImpressionList:@[ impression1, impression2 ] + withCompletion:^(NSArray *_Nullable messages, + NSNumber *_Nullable nextFetchWaitTime, + NSInteger discardCount, NSError *_Nullable error){ + // blank on purpose: it won't get triggered + }]; + + // verify that the captured nsurl request has expected body + NSError *errorJson = nil; + NSDictionary *requestBodyDict = + [NSJSONSerialization JSONObjectWithData:capturedNSURLRequest.HTTPBody + options:kNilOptions + error:&errorJson]; + + XCTAssertEqualObjects(impression1.messageID, + requestBodyDict[@"already_seen_campaigns"][0][@"campaign_id"]); + XCTAssertEqualWithAccuracy( + impression1Timestamp * 1000, + ((NSNumber *)requestBodyDict[@"already_seen_campaigns"][0][@"impression_timestamp_millis"]) + .longValue, + 0.1); + XCTAssertEqualObjects(impression2.messageID, + requestBodyDict[@"already_seen_campaigns"][1][@"campaign_id"]); + XCTAssertEqualWithAccuracy( + impression2Timestamp * 1000, + ((NSNumber *)requestBodyDict[@"already_seen_campaigns"][1][@"impression_timestamp_millis"]) + .longValue, + 0.1); + + XCTAssertEqualObjects(osVersion, requestBodyDict[@"client_signals"][@"platform_version"]); + XCTAssertEqualObjects(appVersion, requestBodyDict[@"client_signals"][@"app_version"]); + // not expexting language siganl since it's not mocked on mockclientInfoFetcher + XCTAssertNil(requestBodyDict[@"client_signals"][@"language_code"]); +} + +- (void)testBailoutOnIIDError { + // in this test, the attempt to fetch iid data failed and as a result, we expect the whole + // fetch operation attempt to fail with that error + NSError *iidError = [[NSError alloc] initWithDomain:@"Error Domain" code:100 userInfo:nil]; + OCMStub([self.mockclientInfoFetcher + fetchFirebaseIIDDataWithProjectNumber:[OCMArg any] + withCompletion:([OCMArg invokeBlockWithArgs:[NSNull null], + [NSNull null], iidError, + nil])]); + + XCTestExpectation *expectation = + [self expectationWithDescription:@"fetch callback block triggered."]; + [self.fetcher + fetchMessagesWithImpressionList:@[] + withCompletion:^(NSArray *_Nullable messages, + NSNumber *_Nullable nextFetchWaitTime, + NSInteger discardCount, NSError *_Nullable error) { + // expecting triggering the completion callback with error + XCTAssertNil(messages); + XCTAssertEqualObjects(iidError, error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} +@end diff --git a/InAppMessaging/Example/Tests/Info.plist b/InAppMessaging/Example/Tests/Info.plist new file mode 100644 index 00000000000..6c6c23c43ad --- /dev/null +++ b/InAppMessaging/Example/Tests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/InAppMessaging/Example/Tests/JsonDataWithInvalidMessagesFromFetch.txt b/InAppMessaging/Example/Tests/JsonDataWithInvalidMessagesFromFetch.txt new file mode 100644 index 00000000000..33f7492b1ec --- /dev/null +++ b/InAppMessaging/Example/Tests/JsonDataWithInvalidMessagesFromFetch.txt @@ -0,0 +1,64 @@ +{ + "messages": [ + { + "vanillaPayload": { + "campaignId": "2108810525516234752" + }, + "content": { + "modal": { + "body": { + "hexColor": "#000000" + }, + "backgroundHexColor": "#ffffff" + } + }, + "triggeringConditions": [ + { + "fiamTrigger": "ON_FOREGROUND" + } + ], + "isTestCampaign": true + }, + + { + "vanillaPayload": { + "campaignId": "2108810525516234752" + }, + "content": { + "modal": { + "title": { + "text": "FAST", + "hexColor": "#000000" + }, + "body": { + "hexColor": "#000000" + }, + "backgroundHexColor": "#ffffff" + } + }, + }, + { + "vanillaPayload": { + "campaignId": "2108810525516234752" + }, + "content": { + "unknown-type": { + "title": { + "text": "FAST", + "hexColor": "#000000" + }, + "body": { + "hexColor": "#000000" + }, + "backgroundHexColor": "#ffffff" + } + }, + "triggeringConditions": [ + { + "fiamTrigger": "ON_FOREGROUND" + } + ], + "isTestCampaign": true + }, + ] +} diff --git a/InAppMessaging/Example/Tests/NSString+InterlaceStringsTests.m b/InAppMessaging/Example/Tests/NSString+InterlaceStringsTests.m new file mode 100644 index 00000000000..727effbc350 --- /dev/null +++ b/InAppMessaging/Example/Tests/NSString+InterlaceStringsTests.m @@ -0,0 +1,60 @@ +/* + * Copyright 2019 Google + * + * 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 "NSString+FIRInterlaceStrings.h" + +@interface NSString_InterlaceStringsTests : XCTestCase + +@end + +@implementation NSString_InterlaceStringsTests + +- (void)testEmptyStrings { + NSString *stringOne = @""; + NSString *stringTwo = @""; + XCTAssertEqualObjects(@"", [NSString fir_interlaceString:stringOne withString:stringTwo]); +} + +- (void)testSimpleExample { + NSString *stringOne = @"fe"; + NSString *stringTwo = @"rd"; + XCTAssertEqualObjects(@"fred", [NSString fir_interlaceString:stringOne withString:stringTwo]); +} + +- (void)testLongerExample { + NSString *stringOne = @"fefittn"; + NSString *stringTwo = @"rdlnsoe"; + XCTAssertEqualObjects(@"fredflintstone", [NSString fir_interlaceString:stringOne + withString:stringTwo]); +} + +- (void)testLongerFirstString { + NSString *stringOne = @"fe'lastnameisflintstone"; + NSString *stringTwo = @"rds"; + XCTAssertEqualObjects(@"fred'slastnameisflintstone", [NSString fir_interlaceString:stringOne + withString:stringTwo]); +} + +- (void)testLongerSecondString { + NSString *stringOne = @"fe'"; + NSString *stringTwo = @"rdslastnameisflintstone"; + XCTAssertEqualObjects(@"fred'slastnameisflintstone", [NSString fir_interlaceString:stringOne + withString:stringTwo]); +} + +@end diff --git a/InAppMessaging/Example/Tests/UIColor+FIRIAMHexStringTests.m b/InAppMessaging/Example/Tests/UIColor+FIRIAMHexStringTests.m new file mode 100644 index 00000000000..483d61b88b2 --- /dev/null +++ b/InAppMessaging/Example/Tests/UIColor+FIRIAMHexStringTests.m @@ -0,0 +1,59 @@ +/* + * Copyright 2017 Google + * + * 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 "UIColor+FIRIAMHexString.h" + +@interface UIColor_FIRIAMHexStringTests : XCTestCase + +@end + +@implementation UIColor_FIRIAMHexStringTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the + // class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. + [super tearDown]; +} + +- (void)testNilHexString { + UIColor *color = [UIColor firiam_colorWithHexString:nil]; + XCTAssertNil(color); +} + +- (void)testEmptyHexString { + UIColor *color = [UIColor firiam_colorWithHexString:@""]; + XCTAssertNil(color); +} + +- (void)testInvalidHexString { + UIColor *color = [UIColor firiam_colorWithHexString:@"#sssfsss"]; + XCTAssertNil(color); +} + +- (void)testValidHexString { + UIColor *color = [UIColor firiam_colorWithHexString:@"#00FFEE"]; + XCTAssertNotNil(color); +} + +@end diff --git a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/BannerMessageViewController.swift b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/BannerMessageViewController.swift index d219ab042d5..4ed74bada15 100644 --- a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/BannerMessageViewController.swift +++ b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/BannerMessageViewController.swift @@ -16,20 +16,19 @@ import UIKit -import FirebaseInAppMessaging - class BannerMessageViewController: CommonMessageTestVC { let displayImpl = InAppMessagingDefaultDisplayImpl() @IBOutlet var verifyLabel: UILabel! - override func messageClicked() { - super.messageClicked() + override func messageClicked(_ inAppMessage: InAppMessagingDisplayMessage) { + super.messageClicked(inAppMessage) verifyLabel.text = "message clicked!" } - override func messageDismissed(dismissType dimissType: FIRInAppMessagingDismissType) { - super.messageClicked() + override func messageDismissed(_ inAppMessage: InAppMessagingDisplayMessage, + dismissType: FIRInAppMessagingDismissType) { + super.messageClicked(inAppMessage) verifyLabel.text = "message dismissed!" } @@ -38,26 +37,32 @@ class BannerMessageViewController: CommonMessageTestVC { let imageRawData = produceImageOfSize(size: CGSize(width: 200, height: 200)) let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) - let modalMessage = InAppMessagingBannerDisplay(messageID: "messageId", - renderAsTestMessage: false, - titleText: normalMessageTitle, - bodyText: normalMessageBody, - textColor: UIColor.black, - backgroundColor: UIColor.blue, - imageData: fiamImageData) - - displayImpl.displayMessage(modalMessage, displayDelegate: self) + let bannerMessage = InAppMessagingBannerDisplay(messageID: "messageId", + campaignName: "testCampaign", + renderAsTestMessage: false, + triggerType: .onAnalyticsEvent, + titleText: normalMessageTitle, + bodyText: normalMessageBody, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: fiamImageData, + actionURL: URL(string: "http://firebase.com")) + + displayImpl.displayMessage(bannerMessage, displayDelegate: self) } @IBAction func showBannerViewWithoutImageTapped(_ sender: Any) { verifyLabel.text = "Verification Label" let modalMessage = InAppMessagingBannerDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, + triggerType: .onAnalyticsEvent, titleText: normalMessageTitle, bodyText: normalMessageBody, textColor: UIColor.black, backgroundColor: UIColor.blue, - imageData: nil) + imageData: nil, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -68,12 +73,15 @@ class BannerMessageViewController: CommonMessageTestVC { let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) let modalMessage = InAppMessagingBannerDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, + triggerType: .onAnalyticsEvent, titleText: normalMessageTitle, bodyText: normalMessageBody, textColor: UIColor.black, backgroundColor: UIColor.blue, - imageData: fiamImageData) + imageData: fiamImageData, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -84,12 +92,15 @@ class BannerMessageViewController: CommonMessageTestVC { let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) let modalMessage = InAppMessagingBannerDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, + triggerType: .onAnalyticsEvent, titleText: normalMessageTitle, bodyText: normalMessageBody, textColor: UIColor.black, backgroundColor: UIColor.blue, - imageData: fiamImageData) + imageData: fiamImageData, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -100,12 +111,15 @@ class BannerMessageViewController: CommonMessageTestVC { let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) let modalMessage = InAppMessagingBannerDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, + triggerType: .onAnalyticsEvent, titleText: normalMessageTitle, bodyText: longBodyText, textColor: UIColor.black, backgroundColor: UIColor.blue, - imageData: fiamImageData) + imageData: fiamImageData, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -116,12 +130,15 @@ class BannerMessageViewController: CommonMessageTestVC { let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) let modalMessage = InAppMessagingBannerDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, + triggerType: .onAnalyticsEvent, titleText: longTitleText, bodyText: normalMessageBody, textColor: UIColor.black, backgroundColor: UIColor.blue, - imageData: fiamImageData) + imageData: fiamImageData, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } diff --git a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/CommonMessageTestVC.swift b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/CommonMessageTestVC.swift index 81ca8567622..12292dd445f 100644 --- a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/CommonMessageTestVC.swift +++ b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/CommonMessageTestVC.swift @@ -16,22 +16,27 @@ import Foundation -import FirebaseInAppMessaging - class CommonMessageTestVC: UIViewController, InAppMessagingDisplayDelegate { var messageClosedWithClick = false var messageClosedDismiss = false // start of InAppMessagingDisplayDelegate functions - func messageClicked() { + func messageClicked(_ inAppMessage: InAppMessagingDisplayMessage) { print("message clicked to follow action url") messageClosedWithClick = true } - func impressionDetected() { print("valid impression detected") } - func displayErrorEncountered(_ error: Error) { print("error encountered \(error)") } - func messageDismissed(dismissType: FIRInAppMessagingDismissType) { + func impressionDetected(for inAppMessage: InAppMessagingDisplayMessage) { + print("valid impression detected") + } + + func displayError(for inAppMessage: InAppMessagingDisplayMessage, error: Error) { + print("error encountered \(error)") + } + + func messageDismissed(_ inAppMessage: InAppMessagingDisplayMessage, + dismissType: FIRInAppMessagingDismissType) { print("message dimissed with type \(dismissType)") messageClosedDismiss = true } diff --git a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/ImageOnlyMessageViewController.swift b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/ImageOnlyMessageViewController.swift index dc43c24c836..1d6a8cfc42e 100644 --- a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/ImageOnlyMessageViewController.swift +++ b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/ImageOnlyMessageViewController.swift @@ -14,20 +14,20 @@ * limitations under the License. */ import UIKit -import FirebaseInAppMessaging class ImageOnlyMessageViewController: CommonMessageTestVC { let displayImpl = InAppMessagingDefaultDisplayImpl() @IBOutlet var verifyLabel: UILabel! - override func messageClicked() { - super.messageClicked() + override func messageClicked(_ inAppMessage: InAppMessagingDisplayMessage) { + super.messageClicked(inAppMessage) verifyLabel.text = "message clicked!" } - override func messageDismissed(dismissType dimissType: FIRInAppMessagingDismissType) { - super.messageClicked() + override func messageDismissed(_ inAppMessage: InAppMessagingDisplayMessage, + dismissType: FIRInAppMessagingDismissType) { + super.messageClicked(inAppMessage) verifyLabel.text = "message dismissed!" } @@ -37,9 +37,11 @@ class ImageOnlyMessageViewController: CommonMessageTestVC { let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) let modalMessage = InAppMessagingImageOnlyDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, - imageData: fiamImageData) - + triggerType: .onAnalyticsEvent, + imageData: fiamImageData, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -49,9 +51,11 @@ class ImageOnlyMessageViewController: CommonMessageTestVC { let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) let modalMessage = InAppMessagingImageOnlyDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, - imageData: fiamImageData) - + triggerType: .onAnalyticsEvent, + imageData: fiamImageData, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -61,9 +65,11 @@ class ImageOnlyMessageViewController: CommonMessageTestVC { let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) let modalMessage = InAppMessagingImageOnlyDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, - imageData: fiamImageData) - + triggerType: .onAnalyticsEvent, + imageData: fiamImageData, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -73,9 +79,11 @@ class ImageOnlyMessageViewController: CommonMessageTestVC { let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) let modalMessage = InAppMessagingImageOnlyDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, - imageData: fiamImageData) - + triggerType: .onAnalyticsEvent, + imageData: fiamImageData, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -84,9 +92,11 @@ class ImageOnlyMessageViewController: CommonMessageTestVC { let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) let modalMessage = InAppMessagingImageOnlyDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, - imageData: fiamImageData) - + triggerType: .onAnalyticsEvent, + imageData: fiamImageData, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } } diff --git a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/ModalMessageViewController.swift b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/ModalMessageViewController.swift index 3a0bc46daae..6ba528acc76 100644 --- a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/ModalMessageViewController.swift +++ b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/ModalMessageViewController.swift @@ -15,20 +15,20 @@ */ import UIKit -import FirebaseInAppMessaging class ModalMessageViewController: CommonMessageTestVC { let displayImpl = InAppMessagingDefaultDisplayImpl() @IBOutlet var verifyLabel: UILabel! - override func messageClicked() { - super.messageClicked() + override func messageClicked(_ inAppMessage: InAppMessagingDisplayMessage) { + super.messageClicked(inAppMessage) verifyLabel.text = "message clicked!" } - override func messageDismissed(dismissType dimissType: FIRInAppMessagingDismissType) { - super.messageClicked() + override func messageDismissed(_ inAppMessage: InAppMessagingDisplayMessage, + dismissType: FIRInAppMessagingDismissType) { + super.messageClicked(inAppMessage) verifyLabel.text = "message dismissed!" } @@ -38,13 +38,16 @@ class ModalMessageViewController: CommonMessageTestVC { let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, + triggerType: .onAnalyticsEvent, titleText: normalMessageTitle, bodyText: normalMessageBody, textColor: UIColor.black, backgroundColor: UIColor.blue, imageData: fiamImageData, - actionButton: defaultActionButton) + actionButton: defaultActionButton, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -52,13 +55,16 @@ class ModalMessageViewController: CommonMessageTestVC { @IBAction func showWithoutImage(_ sender: Any) { verifyLabel.text = "Verification Label" let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, + triggerType: .onAnalyticsEvent, titleText: normalMessageTitle, bodyText: normalMessageBody, textColor: UIColor.black, backgroundColor: UIColor.blue, imageData: nil, - actionButton: defaultActionButton) + actionButton: defaultActionButton, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -69,13 +75,16 @@ class ModalMessageViewController: CommonMessageTestVC { let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, + triggerType: .onAnalyticsEvent, titleText: normalMessageTitle, bodyText: normalMessageBody, textColor: UIColor.black, backgroundColor: UIColor.blue, imageData: fiamImageData, - actionButton: nil) + actionButton: nil, + actionURL: nil) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -83,13 +92,16 @@ class ModalMessageViewController: CommonMessageTestVC { @IBAction func showWithoutImageAndButton(_ sender: Any) { verifyLabel.text = "Verification Label" let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, + triggerType: .onAnalyticsEvent, titleText: normalMessageTitle, bodyText: normalMessageBody, textColor: UIColor.black, backgroundColor: UIColor.blue, imageData: nil, - actionButton: nil) + actionButton: nil, + actionURL: nil) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -97,13 +109,16 @@ class ModalMessageViewController: CommonMessageTestVC { @IBAction func showWithLargeBody(_ sender: Any) { verifyLabel.text = "Verification Label" let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, + triggerType: .onAnalyticsEvent, titleText: normalMessageTitle, bodyText: longBodyText, textColor: UIColor.black, backgroundColor: UIColor.blue, imageData: nil, - actionButton: defaultActionButton) + actionButton: defaultActionButton, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -114,13 +129,16 @@ class ModalMessageViewController: CommonMessageTestVC { let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, - titleText: longBodyText, + triggerType: .onAnalyticsEvent, + titleText: longTitleText, bodyText: longBodyText, textColor: UIColor.black, backgroundColor: UIColor.blue, imageData: fiamImageData, - actionButton: defaultActionButton) + actionButton: defaultActionButton, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -128,13 +146,16 @@ class ModalMessageViewController: CommonMessageTestVC { @IBAction func showWithLargeTitle(_ sender: Any) { verifyLabel.text = "Verification Label" let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, + triggerType: .onAnalyticsEvent, titleText: longBodyText, bodyText: normalMessageBody, textColor: UIColor.black, backgroundColor: UIColor.blue, imageData: nil, - actionButton: defaultActionButton) + actionButton: defaultActionButton, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -142,13 +163,16 @@ class ModalMessageViewController: CommonMessageTestVC { @IBAction func showWithLargeTitleAndBodyWithoutImage(_ sender: Any) { verifyLabel.text = "Verification Label" let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, - titleText: longBodyText, + triggerType: .onAnalyticsEvent, + titleText: longTitleText, bodyText: longBodyText, textColor: UIColor.black, backgroundColor: UIColor.blue, imageData: nil, - actionButton: defaultActionButton) + actionButton: defaultActionButton, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -156,13 +180,16 @@ class ModalMessageViewController: CommonMessageTestVC { @IBAction func showWithLargeTitleWithoutBodyWithoutImageWithoutButton(_ sender: Any) { verifyLabel.text = "Verification Label" let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, + triggerType: .onAnalyticsEvent, titleText: longBodyText, bodyText: "", textColor: UIColor.black, backgroundColor: UIColor.blue, imageData: nil, - actionButton: nil) + actionButton: nil, + actionURL: nil) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -173,13 +200,16 @@ class ModalMessageViewController: CommonMessageTestVC { let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, + triggerType: .onAnalyticsEvent, titleText: normalMessageTitle, bodyText: normalMessageBody, textColor: UIColor.black, backgroundColor: UIColor.blue, imageData: fiamImageData, - actionButton: defaultActionButton) + actionButton: defaultActionButton, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } @@ -190,13 +220,16 @@ class ModalMessageViewController: CommonMessageTestVC { let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + campaignName: "testCampaign", renderAsTestMessage: false, + triggerType: .onAnalyticsEvent, titleText: normalMessageTitle, bodyText: normalMessageBody, textColor: UIColor.black, backgroundColor: UIColor.blue, imageData: fiamImageData, - actionButton: defaultActionButton) + actionButton: defaultActionButton, + actionURL: URL(string: "http://firebase.com")) displayImpl.displayMessage(modalMessage, displayDelegate: self) } diff --git a/InAppMessagingDisplay/Example/InAppMessagingDisplay-Sample.xcodeproj/project.pbxproj b/InAppMessagingDisplay/Example/InAppMessagingDisplay-Sample.xcodeproj/project.pbxproj index 8698f0fa84e..50976b1f195 100644 --- a/InAppMessagingDisplay/Example/InAppMessagingDisplay-Sample.xcodeproj/project.pbxproj +++ b/InAppMessagingDisplay/Example/InAppMessagingDisplay-Sample.xcodeproj/project.pbxproj @@ -3,11 +3,10 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ - 4265DE16D92C99B9A6E56539 /* Pods_FiamDisplaySwiftExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C72088F73776B875CB106D8 /* Pods_FiamDisplaySwiftExample.framework */; }; AD7200B52124D19200AFD5F3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7200B42124D19200AFD5F3 /* AppDelegate.swift */; }; AD7200BA2124D19200AFD5F3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD7200B82124D19200AFD5F3 /* Main.storyboard */; }; AD7200BC2124D19400AFD5F3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AD7200BB2124D19400AFD5F3 /* Assets.xcassets */; }; @@ -33,10 +32,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 0F66149C0079E8409F390CBE /* Pods-InAppMessagingDisplay-Sample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessagingDisplay-Sample.release.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessagingDisplay-Sample/Pods-InAppMessagingDisplay-Sample.release.xcconfig"; sourceTree = ""; }; - 46449F37C45EA76C97C32634 /* Pods-FiamDisplaySwiftExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FiamDisplaySwiftExample.release.xcconfig"; path = "Pods/Target Support Files/Pods-FiamDisplaySwiftExample/Pods-FiamDisplaySwiftExample.release.xcconfig"; sourceTree = ""; }; - 5C72088F73776B875CB106D8 /* Pods_FiamDisplaySwiftExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FiamDisplaySwiftExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 8FF302EB136148B923534F2E /* Pods-FiamDisplaySwiftExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FiamDisplaySwiftExample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-FiamDisplaySwiftExample/Pods-FiamDisplaySwiftExample.debug.xcconfig"; sourceTree = ""; }; AD7200B22124D19200AFD5F3 /* FiamDisplaySwiftExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FiamDisplaySwiftExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; AD7200B42124D19200AFD5F3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; AD7200B92124D19200AFD5F3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -53,8 +48,6 @@ AD7200DB2126136D00AFD5F3 /* InAppMessagingDisplayUITestsBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppMessagingDisplayUITestsBase.swift; sourceTree = ""; }; AD7200DD2126147100AFD5F3 /* InAppMessagingDisplayBannerViewUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppMessagingDisplayBannerViewUITests.swift; sourceTree = ""; }; AD7200DF2126150600AFD5F3 /* InAppMessagingDisplayImageOnlyViewUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppMessagingDisplayImageOnlyViewUITests.swift; sourceTree = ""; }; - C265D6A970BC7B345291115C /* Pods_InAppMessagingDisplay_Sample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InAppMessagingDisplay_Sample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F9F84709E9638EACD6BB35C4 /* Pods-InAppMessagingDisplay-Sample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessagingDisplay-Sample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessagingDisplay-Sample/Pods-InAppMessagingDisplay-Sample.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -62,7 +55,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4265DE16D92C99B9A6E56539 /* Pods_FiamDisplaySwiftExample.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,26 +68,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 9355239275ABBF1A3B074E54 /* Pods */ = { - isa = PBXGroup; - children = ( - F9F84709E9638EACD6BB35C4 /* Pods-InAppMessagingDisplay-Sample.debug.xcconfig */, - 0F66149C0079E8409F390CBE /* Pods-InAppMessagingDisplay-Sample.release.xcconfig */, - 8FF302EB136148B923534F2E /* Pods-FiamDisplaySwiftExample.debug.xcconfig */, - 46449F37C45EA76C97C32634 /* Pods-FiamDisplaySwiftExample.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9619953F9C0FE918881BCDED /* Frameworks */ = { - isa = PBXGroup; - children = ( - C265D6A970BC7B345291115C /* Pods_InAppMessagingDisplay_Sample.framework */, - 5C72088F73776B875CB106D8 /* Pods_FiamDisplaySwiftExample.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; AD7200B32124D19200AFD5F3 /* FiamDisplaySwiftExample */ = { isa = PBXGroup; children = ( @@ -130,8 +102,6 @@ AD7200B32124D19200AFD5F3 /* FiamDisplaySwiftExample */, AD7200D22125F92100AFD5F3 /* InAppMessagingDisplay-UITests */, ADA7B5B921223CED00B1C614 /* Products */, - 9355239275ABBF1A3B074E54 /* Pods */, - 9619953F9C0FE918881BCDED /* Frameworks */, ); sourceTree = ""; }; @@ -151,12 +121,9 @@ isa = PBXNativeTarget; buildConfigurationList = AD7200C32124D19400AFD5F3 /* Build configuration list for PBXNativeTarget "FiamDisplaySwiftExample" */; buildPhases = ( - 08D42C2738A5B40BC9075B65 /* [CP] Check Pods Manifest.lock */, AD7200AE2124D19200AFD5F3 /* Sources */, AD7200AF2124D19200AFD5F3 /* Frameworks */, AD7200B02124D19200AFD5F3 /* Resources */, - 0B0EF24B15B4BCBCC06B5C35 /* [CP] Copy Pods Resources */, - 4F123C68EFE3A41A52219FAA /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -243,65 +210,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 08D42C2738A5B40BC9075B65 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-FiamDisplaySwiftExample-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 0B0EF24B15B4BCBCC06B5C35 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-FiamDisplaySwiftExample/Pods-FiamDisplaySwiftExample-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInAppMessagingDisplay/InAppMessagingDisplayResources.bundle", - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/InAppMessagingDisplayResources.bundle", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FiamDisplaySwiftExample/Pods-FiamDisplaySwiftExample-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - 4F123C68EFE3A41A52219FAA /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-FiamDisplaySwiftExample/Pods-FiamDisplaySwiftExample-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", - "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FiamDisplaySwiftExample/Pods-FiamDisplaySwiftExample-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ AD7200AE2124D19200AFD5F3 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -358,14 +266,9 @@ /* Begin XCBuildConfiguration section */ AD7200C12124D19400AFD5F3 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8FF302EB136148B923534F2E /* Pods-FiamDisplaySwiftExample.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - "HEADER_SEARCH_PATHS[arch=*]" = ( - "\"${PODS_ROOT}/../../../Firebase/InAppMessagingDisplay/\"/**", - "\"${PODS_ROOT}/../../../Firebase/InAppMessaging/\"/**", - ); INFOPLIST_FILE = FiamDisplaySwiftExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -383,7 +286,6 @@ }; AD7200C22124D19400AFD5F3 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 46449F37C45EA76C97C32634 /* Pods-FiamDisplaySwiftExample.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; diff --git a/InAppMessagingDisplay/Example/Podfile b/InAppMessagingDisplay/Example/Podfile index d1b566b6a77..239b3e94570 100644 --- a/InAppMessagingDisplay/Example/Podfile +++ b/InAppMessagingDisplay/Example/Podfile @@ -4,5 +4,7 @@ use_frameworks! target 'FiamDisplaySwiftExample' do platform :ios, '8.0' pod 'FirebaseInAppMessagingDisplay', :path => '../..' + pod 'FirebaseInAppMessaging', :path => '../..' + pod 'FirebaseAnalyticsInterop', :path => '../..' end diff --git a/Interop/Analytics/Public/FIRAnalyticsInterop.h b/Interop/Analytics/Public/FIRAnalyticsInterop.h index 5e30168e4f1..a5143c5e1d1 100644 --- a/Interop/Analytics/Public/FIRAnalyticsInterop.h +++ b/Interop/Analytics/Public/FIRAnalyticsInterop.h @@ -17,6 +17,7 @@ #import @class FIRAConditionalUserProperty; +@protocol FIRAnalyticsInteropListener; NS_ASSUME_NONNULL_BEGIN @@ -47,6 +48,13 @@ NS_ASSUME_NONNULL_BEGIN /// Sets user property. - (void)setUserPropertyWithOrigin:(NSString *)origin name:(NSString *)name value:(id)value; +/// Registers an Analytics listener for the given origin. +- (void)registerAnalyticsListener:(id)listener + withOrigin:(NSString *)origin; + +/// Unregisters an Analytics listener for the given origin. +- (void)unregisterAnalyticsListenerWithOrigin:(NSString *)origin; + @end NS_ASSUME_NONNULL_END diff --git a/Interop/Analytics/Public/FIRAnalyticsInteropListener.h b/Interop/Analytics/Public/FIRAnalyticsInteropListener.h new file mode 100644 index 00000000000..45cde55061d --- /dev/null +++ b/Interop/Analytics/Public/FIRAnalyticsInteropListener.h @@ -0,0 +1,24 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +/// Handles events and messages from Analytics. +@protocol FIRAnalyticsInteropListener + +/// Triggers when an Analytics event happens for the registered origin with +/// `FIRAnalyticsInterop`s `registerAnalyticsListener:withOrigin:`. +- (void)messageTriggered:(NSString *)name parameters:(NSDictionary *)parameters; + +@end \ No newline at end of file diff --git a/README.md b/README.md index bfaceebc6c4..ad8de0fa481 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ This repository contains a subset of the Firebase iOS SDK source. It currently includes FirebaseCore, FirebaseAuth, FirebaseDatabase, FirebaseFirestore, -FirebaseFunctions, FirebaseInAppMessagingDisplay, FirebaseMessaging and -FirebaseStorage. +FirebaseFunctions, FirebaseInstanceID, FirebaseInAppMessaging, +FirebaseInAppMessagingDisplay, FirebaseMessaging and FirebaseStorage. The repository also includes GoogleUtilities source. The [GoogleUtilities](GoogleUtilities/README.md) pod is @@ -86,6 +86,10 @@ Firestore and Functions have self contained Xcode projects. See ### Code Formatting +To ensure that the code is formatted consistently, run the script +[./scripts/style.sh](https://github.com/firebase/firebase-ios-sdk/blob/master/scripts/style.sh) +before creating a PR. + Travis will verify that any code changes are done in a style compliant way. Install `clang-format` and `swiftformat`. This command will get the right `clang-format` version: @@ -96,6 +100,14 @@ This command will get the right `clang-format` version: Select a scheme and press Command-u to build a component and run its unit tests. +#### Viewing Code Coverage + +First, make sure that [xcov](https://github.com/nakiostudio/xcov) is installed with `gem install xcov`. + +After running the `AllUnitTests_iOS` scheme in Xcode, execute +`xcov --workspace Firebase.xcworkspace --scheme AllUnitTests_iOS --output_directory xcov_output` +at Example/ in the terminal. This will aggregate the coverage, and you can run `open xcov_output/index.html` to see the results. + ### Running Sample Apps In order to run the sample apps and integration tests, you'll need valid `GoogleService-Info.plist` files for those samples. The Firebase Xcode project contains dummy plist @@ -157,10 +169,9 @@ very grateful! We'd like to empower as many developers as we can to be able to participate in the Firebase community. ### macOS and tvOS -FirebaseAuth, FirebaseCore, FirebaseDatabase and FirebaseStorage now compile, run unit tests, and -work on macOS and tvOS, thanks to contributions from the community. There are a few tweaks needed, -like ensuring iOS-only, macOS-only, or tvOS-only code is correctly guarded with checks for -`TARGET_OS_IOS`, `TARGET_OS_OSX` and `TARGET_OS_TV`. +Thanks to contributions from the community, FirebaseAuth, FirebaseCore, FirebaseDatabase, +FirebaseFunctions and FirebaseStorage now compile, run unit tests, and work on macOS and tvOS. +FirebaseFirestore is availiable for macOS and FirebaseMessaging for tvOS. For tvOS, checkout the [Sample](Example/tvOSSample). @@ -169,10 +180,19 @@ actively developed primarily for iOS. While we can catch basic unit test issues may be some changes where the SDK no longer works as expected on macOS or tvOS. If you encounter this, please [file an issue](https://github.com/firebase/firebase-ios-sdk/issues). -For installation instructions, see [above](README.md#accessing-firebase-source-snapshots). +Note that the Firebase pod is not available for macOS and tvOS. -Note that the Firebase pod is not available for macOS and tvOS. Install a selection of the -`FirebaseAuth`, `FirebaseCore`, `FirebaseDatabase` and `FirebaseStorage` CocoaPods. +To install, add a subset of the following to the Podfile: + +``` +pod 'FirebaseAuth' +pod 'FirebaseCore' +pod 'FirebaseDatabase' +pod 'FirebaseFirestore' # Only iOS and macOS +pod 'FirebaseFunctions' +pod 'FirebaseMessaging' # Only iOS and tvOS +pod 'FirebaseStorage' +``` ## Roadmap diff --git a/ROADMAP.md b/ROADMAP.md index 1b6601a4bc8..07acdf00bf7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,21 +4,6 @@ The Firebase team plans to open source more of Firebase components. -## Continuous Integration - -* [Stabilize Travis](https://github.com/firebase/firebase-ios-sdk/issues/102) -* [Verify Objective-C style guide compliance](https://github.com/firebase/firebase-ios-sdk/issues/103) - -## Samples and Integration Tests - -Add more samples to better demonstrate the capabilities of Firebase and help -developers onboard. - -## Xcode 9 Workflow - -[Ensure Firebase open source development works well with Xcode 9's git and -GitHub features](https://github.com/firebase/firebase-ios-sdk/issues/101). - ## Other Check out the [issue list](https://github.com/firebase/firebase-ios-sdk/issues) diff --git a/Releases/Manifests/5.17.0.json b/Releases/Manifests/5.17.0.json new file mode 100644 index 00000000000..c2a4da04395 --- /dev/null +++ b/Releases/Manifests/5.17.0.json @@ -0,0 +1,8 @@ +{ + "FirebaseAnalyticsInterop":"1.2.0", + "FirebaseAuth":"5.3.1", + "FirebaseCore":"5.3.0", + "FirebaseFirestore":"1.0.1", + "FirebaseFunctions":"2.2.1", + "FirebaseMessaging":"3.3.1" +} diff --git a/Releases/Manifests/5.18.0.json b/Releases/Manifests/5.18.0.json new file mode 100644 index 00000000000..34a052ca92e --- /dev/null +++ b/Releases/Manifests/5.18.0.json @@ -0,0 +1,10 @@ +{ + "FirebaseAuth":"5.4.0", + "FirebaseCore":"5.3.1", + "FirebaseDynamicLinks":"3.4.1", + "FirebaseFirestore":"1.0.2", + "FirebaseFunctions":"2.3.0", + "FirebaseInAppMessaging":"0.13.0", + "FirebaseInAppMessagingDisplay":"0.13.0", + "FirebaseMessaging":"3.3.2" +} diff --git a/Releases/Manifests/5.19.0.json b/Releases/Manifests/5.19.0.json new file mode 100644 index 00000000000..b8d333d612e --- /dev/null +++ b/Releases/Manifests/5.19.0.json @@ -0,0 +1,12 @@ +{ + "FirebaseAuth":"5.4.1", + "FirebaseCore":"5.4.0", + "FirebaseDatabase":"5.1.1", + "FirebaseDynamicLinks":"3.4.2", + "FirebaseFirestore":"1.1.0", + "FirebaseFunctions":"2.4.0", + "FirebaseInAppMessagingDisplay":"0.13.1", + "FirebaseInstanceID":"3.8.0", + "FirebaseMessaging":"3.4.0", + "FirebaseStorage":"3.1.1" +} diff --git a/Releases/update-versions.py b/Releases/update-versions.py index 980f539ecd5..097828f3798 100755 --- a/Releases/update-versions.py +++ b/Releases/update-versions.py @@ -188,7 +188,7 @@ def UpdateTags(version_data, firebase_version, first=False): LogOrRun("git push --delete origin '{}'".format(tag)) LogOrRun("git tag --delete '{}'".format(tag)) LogOrRun("git tag '{}'".format(tag)) - LogOrRun('git push origin --tags') + LogOrRun("git push origin '{}'".format(tag)) def GetCpdcInternal(): diff --git a/SymbolCollisionTest/Podfile b/SymbolCollisionTest/Podfile index 725ea1eb9ed..d2ae9ceaa29 100644 --- a/SymbolCollisionTest/Podfile +++ b/SymbolCollisionTest/Podfile @@ -6,7 +6,7 @@ target 'SymbolCollisionTest' do # use_frameworks! # Firebase Pods - pod 'Firebase', '5.16.0' + pod 'Firebase', '5.19.0' pod 'FirebaseAnalytics' pod 'FirebaseAuth' pod 'FirebaseCore' diff --git a/ZipBuilder/.gitignore b/ZipBuilder/.gitignore new file mode 100644 index 00000000000..02c087533d1 --- /dev/null +++ b/ZipBuilder/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj diff --git a/ZipBuilder/FirebaseSDKs.proto b/ZipBuilder/FirebaseSDKs.proto new file mode 100644 index 00000000000..b06d0a38877 --- /dev/null +++ b/ZipBuilder/FirebaseSDKs.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package ZipBuilder; + +// A list of all Firebase SDKs. +message FirebaseSDKs { + repeated SDK sdk = 1; +} + +// Represents a single SDK that should be released. +message SDK { + // SDK name + string name = 1; + + // MPM name for the blueprint. For internal use only. + string mpm_name = 2; + + // Public version + string public_version = 3; + + // List of MPM patterns to build + repeated string mpm_pattern = 4; + + // An optional list of additional build flags. For internal use only. + BuildFlag build_flags = 5; + + // List of MPM patterns to build (optional nightly override). For internal use only. + repeated string nightly_mpm_pattern = 6; + + // Whether or not the SDK is built from open-source. For internal use only. + bool open_source = 7; + + // Whether or not to strip the i386 architecture from the build. + bool strip_i386 = 8; +} + +// Any extra build flags needed to build the SDK. For internal use only. +message BuildFlag { + // An additional build flag needed to build the SDK + repeated string flag = 1; +} diff --git a/ZipBuilder/Package.swift b/ZipBuilder/Package.swift new file mode 100644 index 00000000000..1c4d2d60672 --- /dev/null +++ b/ZipBuilder/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version:4.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +/* + * Copyright 2019 Google + * + * 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 PackageDescription + +let package = Package( + name: "ZipBuilder", + dependencies: [ + .package(url: "https://github.com/apple/swift-protobuf.git", .exact("1.2.0")), + ], + targets: [ + .target( + name: "ZipBuilder", + dependencies: ["SwiftProtobuf"] + ), + ] +) diff --git a/ZipBuilder/README.md b/ZipBuilder/README.md new file mode 100644 index 00000000000..8759b101857 --- /dev/null +++ b/ZipBuilder/README.md @@ -0,0 +1,50 @@ +# Firebase Zip File Builder + +This project builds the Firebase iOS Zip file for distribution. +More instructions to come. + +## Priorities + +The following section describes the priorities taken while building this tool and should be followed +for any modifications. + +### Readable and Maintainable +This code will rarely be modified outside of bug fixes, but read very frequently. There should be no +"magic lines" that do multiple things. Verbosity is preferred over making the code shorter and +performing multiple actions at once. All functions should be well documented. + +### Avoid Calling bash Commands Where Possible +Instead of using `cat`, `find`, `grep`, or `perl` use `String` APIs to read the contents of a file, +`FileManager` to properly list contents of a directory, `RegularExpression` for pattern matching, +etc. If there's a `Foundation` API available, it should be used. + +### Understandable Output +The output of the script should make it immediately obvious if there were any issues and exactly +what those issues were, without looking at the code. It should also be very clear if the Zip file +was properly built and output the file location. + +### Show Xcode and API Output on Failures +In the event that there's an Xcode build failure, the logs should be surfaced immediately to aid +debugging. Release engineers should not have to find the Xcode project manually. That being said, a +link to the Xcode project file should be logged as well in case it's necessary. Same goes for errors +logged by exceptions (ex: `FileManager`). + +### Testable and Debuggable +Components and functions should be split up in a way that make them easy to test and easy to debug. +Prefer small functions that have proper failure conditions and input validated with `guard` +statements, throwing `fatalError` with a well written error message if it's a critical issue that +prevents the Zip file from being built properly. + +### Works from the Command Line or Xcode (Environment Agnostic) +The script should be able to run from the command line to allow for easier automation and Xcode for +simpler debugging and maintenance. + +### Any Failure Exits Immediately +The script should not continue if anything necessary for a successful Zip file fails. This includes +things like compiling storyboards, moving resources, missing files, etc. This is to ensure the +integrity of the zip file and that any issues during testing are SDK bugs and not related to the +files and folders. + +### Prefer File `URL`s over Strings +Instead of relying on `String`s to represent file paths, use `URL`s as soon as possible to avoid any +missed or double slashes along with other issues. diff --git a/ZipBuilder/Sources/ZipBuilder/CocoaPodUtils.swift b/ZipBuilder/Sources/ZipBuilder/CocoaPodUtils.swift new file mode 100644 index 00000000000..67fdc56f2e0 --- /dev/null +++ b/ZipBuilder/Sources/ZipBuilder/CocoaPodUtils.swift @@ -0,0 +1,406 @@ +/* + * Copyright 2019 Google + * + * 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 + +/// CocoaPod related utility functions. The enum type is used as a namespace here instead of having +/// root functions, and no cases should be added to it. +public enum CocoaPodUtils { + // MARK: - Public API + + /// Errors associated with generating or installing subspecs. + public enum PodfileError: Error { + /// No subspecs were specified to install. + case noSubspecs + } + + /// Information associated with an installed pod. + public struct PodInfo { + /// Public name of the pod. + var name: String + + /// The version of the pod. + var version: String + + /// The location of the pod on disk. + var installedLocation: URL + + /// A key that can be generated and used for identifying pods due to binary differences. + var cacheKey: String? + + /// Default initializer. Explicitly declared to take advantage of default arguments. + init(name: String, version: String, installedLocation: URL, cacheKey: String? = nil) { + self.name = name + self.version = version + self.installedLocation = installedLocation + self.cacheKey = cacheKey + } + } + + /// Executes the `pod cache clean --all` command to remove any cached CocoaPods. + public static func cleanPodCache() { + let result = Shell.executeCommandFromScript("pod cache clean --all", outputToConsole: false) + switch result { + case let .error(code): + fatalError("Could not clean the pod cache, the command exited with \(code). Try running the" + + "command in Terminal to see what's wrong.") + case .success: + // No need to do anything else, continue on. + print("Successfully cleaned pod cache.") + return + } + } + + /// Executes the `pod cache list` command to get the Pods curerntly cached on your machine. + /// + /// - Parameter dir: The directory containing all installed pods. + /// - Returns: A dictionary keyed by the pod name, then by version number. + public static func listPodCache(inDir dir: URL) -> [String: [String: PodInfo]] { + let result = Shell.executeCommandFromScript("pod cache list", outputToConsole: false) + switch result { + case let .error(code): + fatalError("Could not list the pod cache in \(dir), the command exited with \(code). Try " + + "running in Terminal to see what's wrong.") + case let .success(output): + return parsePodsCache(output: output.components(separatedBy: "\n")) + } + } + + /// Gets metadata from installed Pods. Reads the `Podfile.lock` file and parses it. + public static func installedPodsInfo(inProjectDir projectDir: URL) -> [PodInfo] { + // Read from the Podfile.lock to get the installed versions and names. + let podfileLock: String + do { + podfileLock = try String(contentsOf: projectDir.appendingPathComponent("Podfile.lock")) + } catch { + fatalError("Could not read contents of `Podfile.lock` to get installed Pod info in " + + "\(projectDir): \(error)") + } + + // Get the versions in the format of [PodName: VersionString]. + let versions = loadVersionsFromPodfileLock(contents: podfileLock) + + // Generate an InstalledPod for each Pod found. + let podsDir = projectDir.appendingPathComponent("Pods") + var installedPods: [PodInfo] = [] + for (podName, version) in versions { + let podDir = podsDir.appendingPathComponent(podName) + guard FileManager.default.directoryExists(at: podDir) else { + fatalError("Directory for \(podName) doesn't exist at \(podDir) - failed while getting " + + "information for installed Pods.") + } + + // Generate the cache key for this framework. We will use the list of subspecs used in the Pod + // to generate this, since a Pod like GoogleUtilities could build different sources based on + // what subspecs are included. + let cacheKey = self.cacheKey(forPod: podName, fromPodfileLock: podfileLock) + let podInfo = PodInfo(name: podName, + version: version, + installedLocation: podDir, + cacheKey: cacheKey) + installedPods.append(podInfo) + } + + return installedPods + } + + /// Install an array of subspecs from the Firebase pod in a specific directory, returning an array + /// of PodInfo for each pod that was installed. + @discardableResult + public static func installSubspecs(_ subspecs: [Subspec], + inDir directory: URL, + customSpecRepos: [URL]? = nil) -> [PodInfo] { + let fileManager = FileManager.default + // Ensure the directory exists, otherwise we can't install all subspecs. + guard fileManager.directoryExists(at: directory) else { + fatalError("Attempted to install subpecs (\(subspecs)) in a directory that doesn't exist: \(directory)") + } + + // Ensure there are actual podspecs to install. + guard !subspecs.isEmpty else { + fatalError("Attempted to install an empty array of subspecs") + } + + // Attempt to write the Podfile to disk. + do { + try writePodfile(for: subspecs, toDirectory: directory, customSpecRepos: customSpecRepos) + } catch let FileManager.FileError.directoryNotFound(path) { + fatalError("Failed to write Podfile with subspecs \(subspecs) at path \(path)") + } catch let FileManager.FileError.writeToFileFailed(path, error) { + fatalError("Failed to write Podfile for all subspecs at path: \(path), error: \(error)") + } catch { + fatalError("Unspecified error writing Podfile for all subspecs to disk: \(error)") + } + + // Run pod install on the directory that contains the Podfile and blank Xcode project. + let result = Shell.executeCommandFromScript("pod _1.5.3_ install", workingDir: directory) + switch result { + case let .error(code, output): + fatalError(""" + `pod install` failed with exit code \(code) while trying to install subspecs: + \(subspecs) + + Output from `pod install`: + \(output) + """) + case let .success(output): + // Print the output to the console and return the information for all installed pods. + print(output) + return installedPodsInfo(inProjectDir: directory) + } + } + + /// Load versions of installed Pods from the contents of a `Podfile.lock` file. + /// + /// - Parameter contents: The contents of a `Podfile.lock` file. + /// - Returns: A dictionary with names of the pod for keys and a string representation of the + /// version for values. + public static func loadVersionsFromPodfileLock(contents: String) -> [String: String] { + // This pattern matches a framework name with its version (two to three components) + // Examples: + // - FirebaseUI/Google (4.1.1): + // - GoogleSignIn (4.0.2): + + // Force unwrap the regular expression since we know it will work, it's a constant being passed + // in. If any changes are made, be sure to run this script to ensure it works. + let regex = try! NSRegularExpression(pattern: " - (.+) \\((\\d+\\.\\d+\\.?\\d*)\\)", + options: []) + let quotes = CharacterSet(charactersIn: "\"") + var frameworks: [String: String] = [:] + contents.components(separatedBy: .newlines).forEach { line in + if let (framework, version) = detectVersion(fromLine: line, matching: regex) { + let coreFramework = framework.components(separatedBy: "/")[0] + let key = coreFramework.trimmingCharacters(in: quotes) + frameworks[key] = version + } + } + return frameworks + } + + public static func updateRepos() { + let result = Shell.executeCommandFromScript("pod repo update") + switch result { + case let .error(_, output): + fatalError("Command `pod repo update` failed: \(output)") + case .success: + return + } + } + + // MARK: - Private Helpers + + // Tests the input to see if it matches a CocoaPod framework and its version. + // Returns the framework and version or nil if match failed. + // Used to process entries from Podfile.lock + + /// Tests the input and sees if it matches a CocoaPod framework and its version. This is used to + /// process entries from Podfile.lock. + /// + /// - Parameters: + /// - input: A line entry from Podfile.lock. + /// - regex: The regex to match compared to the input. + /// - Returns: A tuple of the framework and version, if it can be parsed. + private static func detectVersion(fromLine input: String, + matching regex: NSRegularExpression) -> (framework: String, version: String)? { + let matches = regex.matches(in: input, range: NSRange(location: 0, length: input.utf8.count)) + let nsString = input as NSString + + guard let match = matches.first else { + return nil + } + + guard match.numberOfRanges == 3 else { + print("Version number regex matches: expected 3, but found \(match.numberOfRanges).") + return nil + } + + let framework = nsString.substring(with: match.range(at: 1)) as String + let version = nsString.substring(with: match.range(at: 2)) as String + + return (framework, version) + } + + /// Generates a key representing the unique combination of all subspecs used for that Pod. This is + /// necessary for Pods like GoogleUtilities, where we will need to include all subspecs as part of + /// a build. Otherwise we could accidentally use a cached framework that doesn't include all the + /// code necessary to function. + /// + /// - Parameters: + /// - framework: The framework being built. + /// - podfileLock: The contents of the Podfile.lock for the project. + /// - Returns: A key to describe the full set of subspecs used to build the framework, or an empty + /// String if there were no specific subspecs used. + private static func cacheKey(forPod podName: String, + fromPodfileLock podfileLock: String) -> String? { + // Ignore the umbrella Firebase pod, cacheing doesn't make sense. + guard podName != "Firebase" else { return nil } + + // Get the first section of the Podfile containing only Pods installed, the only thing we care + // about. + guard let podsInstalled = podfileLock.components(separatedBy: "DEPENDENCIES:").first else { + fatalError(""" + Could not generate cache key for \(podName) from Podfile.lock contents - is this a valid + Podfile.lock? + ---------- Podfile.lock contents ---------- + \(podfileLock) + ------------------------------------------- + """) + } + + // Only get the lines that start with " - ", and have the framework we're looking for since + // they are the top level pods that are installed. + // Example result of a single line: `- GoogleUtilities/Environment (~> 5.2)`. + let lines = podsInstalled.components(separatedBy: .newlines).filter { + $0.hasPrefix(" - ") && $0.contains(podName) + } + + // Get a list of all the subspecs used to build this framework, and use that to generate the + // cache key. + var uniqueSubspecs = Set() + for line in lines.sorted() { + // Separate the line into readable chunks, using a space and quote as a separator. + // Example result: `["-", "GoogleUtilities/Environment", "(~>", "5.2)"]`. + let components = line.components(separatedBy: CharacterSet(charactersIn: " \"")) + + // The Pod and subspec will be the only variables we care about, filter out the rest. + // Example result: 'GoogleUtilities/Environment' or `FirebaseCore`. Only Pods with a subspec + // should be included here, which are always in the format of `PodName/SubspecName`. + guard let fullPodName = components.filter({ $0.contains("\(podName)/") }).first else { + continue + } + + // The fullPodName will be something like `GoogleUtilities/UserDefaults`, get the subspec + // name. + let subspec = fullPodName.replacingOccurrences(of: "\(podName)/", with: "") + if !subspec.isEmpty { + uniqueSubspecs.insert(subspec) + } + } + + // Return nil if there are no subpsecs used, since no cache key is necessary. + guard !uniqueSubspecs.isEmpty else { + return nil + } + + // Assemble the cache key based on the framework name, and all subspecs (sorted alphabetically + // for repeatability) separated by a `+` (as was previously used). + return podName + "+" + uniqueSubspecs.sorted().joined(separator: "+") + } + + /// Create the contents of a Podfile for an array of subspecs. This assumes the array of subspecs + /// is not empty. + private static func generatePodfile(for subspecs: [Subspec], + customSpecsRepos: [URL]? = nil) -> String { + // Get the largest minimum supported iOS version from the array of subspecs. + let minVersions = subspecs.map { $0.minSupportedIOSVersion() } + + // Get the maximum version out of all the minimum versions supported. + guard let largestMinVersion = minVersions.max() else { + // This shouldn't happen, but in the interest of completeness quit the script and describe + // how this could be fixed. + fatalError(""" + Could not retrieve the largest minimum iOS version for the Podfile - array of subspecs + to install is likely empty. This is likely a programmer error - no function should be + calling \(#function) before validating that the subspecs array is not empty. + """) + } + + // Start assembling the Podfile. + var podfile: String = "" + + // If custom Specs repos were passed in, prefix the Podfile with the custom repos followed by + // the CocoaPods master Specs repo. + if let customSpecsRepos = customSpecsRepos { + let reposText = customSpecsRepos.map { "source '\($0)'" } + podfile += """ + \(reposText.joined(separator: "\n")) + source 'https://github.com/CocoaPods/Specs.git' + + """ // Explicit newline above to ensure it's included in the String. + } + + // Include the calculated minimum iOS version. + podfile += """ + platform :ios, '\(largestMinVersion.podVersion())' + target 'FrameworkMaker' do\n + """ + + // Loop through the subspecs passed in and use the rawValue (actual Pod name). + for subspec in subspecs { + podfile += " pod 'Firebase/\(subspec.rawValue)'\n" + } + + podfile += "end" + return podfile + } + + /// Parse the output from Pods Cache + private static func parsePodsCache(output: [String]) -> [String: [String: PodInfo]] { + var podName: String? + var podVersion: String? + + var podsCache: [String: [String: PodInfo]] = [:] + + for line in output { + let trimmedLine = line.trimmingCharacters(in: .whitespaces) + let parts = trimmedLine.components(separatedBy: ":") + if trimmedLine.hasSuffix(":") { + podName = parts[0] + } else { + guard parts.count == 2 else { continue } + let key = parts[0].trimmingCharacters(in: .whitespaces) + let value = parts[1].trimmingCharacters(in: .whitespaces) + + switch key { + case "- Version": + podVersion = value + case "Pod": + let podLocation = URL(fileURLWithPath: value) + let podInfo = PodInfo(name: podName!, version: podVersion!, installedLocation: podLocation) + if podsCache[podName!] == nil { + podsCache[podName!] = [:] + } + podsCache[podName!]![podVersion!] = podInfo + + default: + break + } + } + } + + return podsCache + } + + /// Write a podfile that contains all the subspecs passed in to the directory passed in with a + /// name "Podfile". + private static func writePodfile(for subspecs: [Subspec], + toDirectory directory: URL, + customSpecRepos: [URL]?) throws { + guard FileManager.default.directoryExists(at: directory) else { + // Throw an error so the caller can provide a better error message. + throw FileManager.FileError.directoryNotFound(path: directory.path) + } + + // Generate the full path of the Podfile and attempt to write it to disk. + let path = directory.appendingPathComponent("Podfile") + let podfile = generatePodfile(for: subspecs, customSpecsRepos: customSpecRepos) + do { + try podfile.write(toFile: path.path, atomically: true, encoding: .utf8) + } catch { + throw FileManager.FileError.writeToFileFailed(file: path.path, error: error) + } + } +} diff --git a/ZipBuilder/Sources/ZipBuilder/FileManager+Utils.swift b/ZipBuilder/Sources/ZipBuilder/FileManager+Utils.swift new file mode 100644 index 00000000000..085c75e670c --- /dev/null +++ b/ZipBuilder/Sources/ZipBuilder/FileManager+Utils.swift @@ -0,0 +1,183 @@ +/* + * Copyright 2019 Google + * + * 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 + +// Extensions to FileManager that make scripting easier or cleaner for error reporting. +public extension FileManager { + // MARK: - Helper Enum Declarations + + /// Describes a type of file to be searched for. + enum SearchFileType { + /// All folders with a `.bundle` extension. + case bundles + + /// A directory with an optional name. If name is `nil`, all directories will be matched. + case directories(name: String?) + + /// All folders with a `.framework` extension. + case frameworks + + /// All headers with a `.h` extension. + case headers + + /// All files with the `.storyboard` extension. + case storyboards + } + + // MARK: - Error Declarations + + /// Errors that can be used to propagate up through the script related to files. + enum FileError: Error { + case directoryNotFound(path: String) + case failedToCreateDirectory(path: String, error: Error) + case writeToFileFailed(file: String, error: Error) + } + + /// Errors that can occur during a recursive search operation. + enum RecursiveSearchError: Error { + case failedToCreateEnumerator(forDirectory: URL) + } + + // MARK: - Directory Management + + /// Convenience function to determine if there's a directory at the given file URL using existing + /// FileManager calls. + func directoryExists(at url: URL) -> Bool { + var isDir: ObjCBool = false + let exists = fileExists(atPath: url.path, isDirectory: &isDir) + return exists && isDir.boolValue + } + + /// Convenience function to determine if a given file URL is a directory. + func isDirectory(at url: URL) -> Bool { + return directoryExists(at: url) + } + + /// Returns the URL to the Firebase cache directory, and creates it if it doesn't exist. + func firebaseCacheDirectory() throws -> URL { + // Get the URL for the cache directory. + let cacheDir: URL = try url(for: .cachesDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true) + + // Get the cache root path, and if it already exists just return the URL. + let cacheRoot = cacheDir.appendingPathComponent("firebase_oss_framework_cache") + if directoryExists(at: cacheRoot) { + return cacheRoot + } + + // The cache root folder doesn't exist yet, create it. + try createDirectory(at: cacheRoot, withIntermediateDirectories: false, attributes: nil) + + return cacheRoot + } + + /// Removes a directory if it exists. This is helpful to clean up error handling for checks that + /// shouldn't fail. The only situation this could potentially fail is permission errors or if a + /// folder is open in Finder, and in either state the user needs to close the window or fix the + /// permissions. A fatal error will be thrown in those situations. + func removeDirectoryIfExists(at url: URL) { + guard directoryExists(at: url) else { return } + + do { + try removeItem(at: url) + } catch { + fatalError(""" + Tried to remove directory \(url) but it failed - close any Finder windows and try again. + Error: \(error) + """) + } + } + + /// Returns a deterministic path of a temporary directory for the given name. Note: This does + /// *not* create the directory if it doesn't exist, merely generates the name for creation. + func temporaryDirectory(withName name: String) -> URL { + // Get access to the temporary directory. + let tempDir: URL + if #available(OSX 10.12, *) { + tempDir = temporaryDirectory + } else { + tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) + } + + // Organize all temporary directories into a "FirebaseZipRelease" directory. + let firebaseDir = tempDir.appendingPathComponent("FirebaseZipRelease", isDirectory: true) + return firebaseDir.appendingPathComponent(name, isDirectory: true) + } + + // MARK: Searching + + /// Recursively search for a set of items in a particular directory. + func recursivelySearch(for type: SearchFileType, in dir: URL) throws -> [URL] { + // Throw an error so an appropriate error can be logged from the caller. + + guard directoryExists(at: dir) else { + throw FileError.directoryNotFound(path: dir.path) + } + + // We have a directory, create an enumerator to do a recursive search. + let keys: [URLResourceKey] = [.nameKey, .isDirectoryKey] + guard let dirEnumerator = enumerator(at: dir, includingPropertiesForKeys: keys) else { + // Throw an error so an appropriate error can be logged from the caller. + throw RecursiveSearchError.failedToCreateEnumerator(forDirectory: dir) + } + + // Recursively search using the enumerator, adding any matches to the array. + var matches: [URL] = [] + while let fileURL = dirEnumerator.nextObject() as? URL { + switch type { + case let .directories(name): + // Skip any non-directories. + guard directoryExists(at: fileURL) else { continue } + + // Get the name of the directory we're searching for. If there's not a specific name + // being searched for, add it as a match and move on. + guard let name = name else { + matches.append(fileURL) + continue + } + + // If the last path component is a match, it's a directory we're looking for! + if fileURL.lastPathComponent == name { + matches.append(fileURL) + } + case .bundles: + // The only thing of interest is the path extension being ".bundle". + if fileURL.pathExtension == "bundle" { + matches.append(fileURL) + } + case .headers: + if fileURL.pathExtension == "h" { + matches.append(fileURL) + } + case .storyboards: + // The only thing of interest is the path extension being ".storyboard". + if fileURL.pathExtension == "storyboard" { + matches.append(fileURL) + } + case .frameworks: + // We care if it's a directory and has a .framework extension. + if directoryExists(at: fileURL), fileURL.pathExtension == "framework" { + matches.append(fileURL) + } + } + } + + return matches + } +} diff --git a/ZipBuilder/Sources/ZipBuilder/FirebaseSDKs.pb.swift b/ZipBuilder/Sources/ZipBuilder/FirebaseSDKs.pb.swift new file mode 100644 index 00000000000..c31bde14206 --- /dev/null +++ b/ZipBuilder/Sources/ZipBuilder/FirebaseSDKs.pb.swift @@ -0,0 +1,307 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +// DO NOT EDIT. +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: FirebaseSDKs.proto +// +// For information on using the generated types, please see the documenation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that your are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// A list of all Firebase SDKs. +struct ZipBuilder_FirebaseSDKs { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var sdk: [ZipBuilder_SDK] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +/// Represents a single SDK that should be released. +struct ZipBuilder_SDK { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// SDK name + var name: String { + get {return _storage._name} + set {_uniqueStorage()._name = newValue} + } + + /// MPM name for the blueprint. For internal use only. + var mpmName: String { + get {return _storage._mpmName} + set {_uniqueStorage()._mpmName = newValue} + } + + /// Public version + var publicVersion: String { + get {return _storage._publicVersion} + set {_uniqueStorage()._publicVersion = newValue} + } + + /// List of MPM patterns to build + var mpmPattern: [String] { + get {return _storage._mpmPattern} + set {_uniqueStorage()._mpmPattern = newValue} + } + + /// An optional list of additional build flags. For internal use only. + var buildFlags: ZipBuilder_BuildFlag { + get {return _storage._buildFlags ?? ZipBuilder_BuildFlag()} + set {_uniqueStorage()._buildFlags = newValue} + } + /// Returns true if `buildFlags` has been explicitly set. + var hasBuildFlags: Bool {return _storage._buildFlags != nil} + /// Clears the value of `buildFlags`. Subsequent reads from it will return its default value. + mutating func clearBuildFlags() {_uniqueStorage()._buildFlags = nil} + + /// List of MPM patterns to build (optional nightly override). For internal use only. + var nightlyMpmPattern: [String] { + get {return _storage._nightlyMpmPattern} + set {_uniqueStorage()._nightlyMpmPattern = newValue} + } + + /// Whether or not the SDK is built from open-source. For internal use only. + var openSource: Bool { + get {return _storage._openSource} + set {_uniqueStorage()._openSource = newValue} + } + + /// Whether or not to strip the i386 architecture from the build. + var stripI386: Bool { + get {return _storage._stripI386} + set {_uniqueStorage()._stripI386 = newValue} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _storage = _StorageClass.defaultInstance +} + +/// Any extra build flags needed to build the SDK. For internal use only. +struct ZipBuilder_BuildFlag { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// An additional build flag needed to build the SDK + var flag: [String] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "ZipBuilder" + +extension ZipBuilder_FirebaseSDKs: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".FirebaseSDKs" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "sdk"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeRepeatedMessageField(value: &self.sdk) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.sdk.isEmpty { + try visitor.visitRepeatedMessageField(value: self.sdk, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ZipBuilder_FirebaseSDKs, rhs: ZipBuilder_FirebaseSDKs) -> Bool { + if lhs.sdk != rhs.sdk {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension ZipBuilder_SDK: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".SDK" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "name"), + 2: .standard(proto: "mpm_name"), + 3: .standard(proto: "public_version"), + 4: .standard(proto: "mpm_pattern"), + 5: .standard(proto: "build_flags"), + 6: .standard(proto: "nightly_mpm_pattern"), + 7: .standard(proto: "open_source"), + 8: .standard(proto: "strip_i386"), + ] + + fileprivate class _StorageClass { + var _name: String = String() + var _mpmName: String = String() + var _publicVersion: String = String() + var _mpmPattern: [String] = [] + var _buildFlags: ZipBuilder_BuildFlag? = nil + var _nightlyMpmPattern: [String] = [] + var _openSource: Bool = false + var _stripI386: Bool = false + + static let defaultInstance = _StorageClass() + + private init() {} + + init(copying source: _StorageClass) { + _name = source._name + _mpmName = source._mpmName + _publicVersion = source._publicVersion + _mpmPattern = source._mpmPattern + _buildFlags = source._buildFlags + _nightlyMpmPattern = source._nightlyMpmPattern + _openSource = source._openSource + _stripI386 = source._stripI386 + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + + mutating func decodeMessage(decoder: inout D) throws { + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &_storage._name) + case 2: try decoder.decodeSingularStringField(value: &_storage._mpmName) + case 3: try decoder.decodeSingularStringField(value: &_storage._publicVersion) + case 4: try decoder.decodeRepeatedStringField(value: &_storage._mpmPattern) + case 5: try decoder.decodeSingularMessageField(value: &_storage._buildFlags) + case 6: try decoder.decodeRepeatedStringField(value: &_storage._nightlyMpmPattern) + case 7: try decoder.decodeSingularBoolField(value: &_storage._openSource) + case 8: try decoder.decodeSingularBoolField(value: &_storage._stripI386) + default: break + } + } + } + } + + func traverse(visitor: inout V) throws { + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + if !_storage._name.isEmpty { + try visitor.visitSingularStringField(value: _storage._name, fieldNumber: 1) + } + if !_storage._mpmName.isEmpty { + try visitor.visitSingularStringField(value: _storage._mpmName, fieldNumber: 2) + } + if !_storage._publicVersion.isEmpty { + try visitor.visitSingularStringField(value: _storage._publicVersion, fieldNumber: 3) + } + if !_storage._mpmPattern.isEmpty { + try visitor.visitRepeatedStringField(value: _storage._mpmPattern, fieldNumber: 4) + } + if let v = _storage._buildFlags { + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + } + if !_storage._nightlyMpmPattern.isEmpty { + try visitor.visitRepeatedStringField(value: _storage._nightlyMpmPattern, fieldNumber: 6) + } + if _storage._openSource != false { + try visitor.visitSingularBoolField(value: _storage._openSource, fieldNumber: 7) + } + if _storage._stripI386 != false { + try visitor.visitSingularBoolField(value: _storage._stripI386, fieldNumber: 8) + } + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ZipBuilder_SDK, rhs: ZipBuilder_SDK) -> Bool { + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._name != rhs_storage._name {return false} + if _storage._mpmName != rhs_storage._mpmName {return false} + if _storage._publicVersion != rhs_storage._publicVersion {return false} + if _storage._mpmPattern != rhs_storage._mpmPattern {return false} + if _storage._buildFlags != rhs_storage._buildFlags {return false} + if _storage._nightlyMpmPattern != rhs_storage._nightlyMpmPattern {return false} + if _storage._openSource != rhs_storage._openSource {return false} + if _storage._stripI386 != rhs_storage._stripI386 {return false} + return true + } + if !storagesAreEqual {return false} + } + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension ZipBuilder_BuildFlag: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".BuildFlag" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "flag"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeRepeatedStringField(value: &self.flag) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.flag.isEmpty { + try visitor.visitRepeatedStringField(value: self.flag, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ZipBuilder_BuildFlag, rhs: ZipBuilder_BuildFlag) -> Bool { + if lhs.flag != rhs.flag {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/ZipBuilder/Sources/ZipBuilder/FrameworkBuilder.swift b/ZipBuilder/Sources/ZipBuilder/FrameworkBuilder.swift new file mode 100755 index 00000000000..c57349522e5 --- /dev/null +++ b/ZipBuilder/Sources/ZipBuilder/FrameworkBuilder.swift @@ -0,0 +1,495 @@ +/* + * Copyright 2019 Google + * + * 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 + +/// Different architectures to build frameworks for. +private enum Architecture: String, CaseIterable { + /// The target platform that the framework is built for. + enum TargetPlatform: String { + case device = "iphoneos" + case simulator = "iphonesimulator" + + /// Arguments that should be included as part of the build process for each target platform. + func extraArguments() -> [String] { + switch self { + case .device: + // For device, we want to enable bitcode. + return ["OTHER_CFLAGS=$(value) " + "-fembed-bitcode"] + case .simulator: + // No extra arguments are required for simulator builds. + return [] + } + } + } + + case arm64 + case armv7 + case i386 + case x86_64 + + /// The platform associated with the architecture. + var platform: TargetPlatform { + switch self { + case .arm64, .armv7: return .device + case .i386, .x86_64: return .simulator + } + } +} + +/// A structure to build a .framework in a given project directory. +struct FrameworkBuilder { + /// The directory containing the Xcode project and Pods folder. + private let projectDir: URL + + /// The Pods directory for building the framework. + private var podsDir: URL { + return projectDir.appendingPathComponent("Pods", isDirectory: true) + } + + /// Default initializer. + init(projectDir: URL) { + self.projectDir = projectDir + } + + // MARK: - Public Functions + + /// Build a fat library framework file for a given framework name. + /// + /// - Parameters: + /// - framework: The name of the Framework being built. + /// - version: String representation of the version. + /// - cacheKey: The key used for caching this framework build. If nil, the framework name will + /// be used. + /// - cacheEnabled: Flag for enabling the cache. Defaults to false. + /// - Returns: A URL to the framework that was built (or pulled from the cache) and a URL to the + /// Resources directory containing all required bundles. + public func buildFramework(withName podName: String, + version: String, + cacheKey: String?, + cacheEnabled: Bool = false) -> (framework: URL, resources: URL) { + print("Building \(podName)") + +// Cache is temporarily disabled due to pod cache list issues. + // Get the CocoaPods cache to see if we can pull from any frameworks already built. +// let podsCache = CocoaPodUtils.listPodCache(inDir: projectDir) +// +// guard let cachedVersions = podsCache[podName] else { +// fatalError("Cannot find a pod cache for framework \(podName).") +// } +// +// guard let podInfo = cachedVersions[version] else { +// fatalError(""" +// Cannot find a pod cache for framework \(podName) at version \(version). +// Something could be wrong with your CocoaPods cache - try running the following: +// +// pod cache clean '\(podName)' --all +// """) +// } +// +// // TODO: Figure out if we need the MD5 at all. + let md5 = podName +// let md5 = Shell.calculateMD5(for: podInfo.installedLocation) + + // Get (or create) the cache directory for storing built frameworks. + let fileManager = FileManager.default + var cachedFrameworkRoot: URL + do { + let cacheDir = try fileManager.firebaseCacheDirectory() + cachedFrameworkRoot = cacheDir.appendingPathComponents([podName, version, md5]) + if let cacheKey = cacheKey { + cachedFrameworkRoot.appendPathComponent(cacheKey) + } + } catch { + fatalError("Could not create caches directory for building frameworks: \(error)") + } + + // Build the full cached framework path. + let cachedFrameworkDir = cachedFrameworkRoot.appendingPathComponent("\(podName).framework") + let cachedFrameworkExists = fileManager.directoryExists(at: cachedFrameworkDir) + let cachedResourcesDir = cachedFrameworkRoot.appendingPathComponent("Resources") + if cachedFrameworkExists, cacheEnabled { + print("Framework \(podName) version \(version) has already been built and cached at " + + "\(cachedFrameworkDir)") + return (cachedFrameworkDir, cachedResourcesDir) + } else { + let (frameworkDir, bundles) = compileFrameworkAndResources(withName: podName) + do { + // Remove the previously cached framework, if it exists, otherwise the `moveItem` call will + // fail. + if cachedFrameworkExists { + try fileManager.removeItem(at: cachedFrameworkDir) + } else if !fileManager.directoryExists(at: cachedFrameworkRoot) { + // If the root directory doesn't exist, create it so the `moveItem` will succeed. + try fileManager.createDirectory(at: cachedFrameworkRoot, + withIntermediateDirectories: true, + attributes: nil) + } + + // Move any Resource bundles into the Resources folder. Remove the existing Resources folder + // and create a new one. + if fileManager.directoryExists(at: cachedResourcesDir) { + try fileManager.removeItem(at: cachedResourcesDir) + } + + // Create the directory where all the bundles will be kept and copy each one. + try fileManager.createDirectory(at: cachedResourcesDir, + withIntermediateDirectories: true, + attributes: nil) + for bundle in bundles { + let destination = cachedResourcesDir.appendingPathComponent(bundle.lastPathComponent) + try fileManager.moveItem(at: bundle, to: destination) + } + + // Move the newly built framework to the cache directory. NOTE: This needs to happen after + // the Resources are moved since the Resources are contained in the frameworkDir. + try fileManager.moveItem(at: frameworkDir, to: cachedFrameworkDir) + + return (cachedFrameworkDir, cachedResourcesDir) + } catch { + fatalError("Could not move built frameworks into the cached frameworks directory: \(error)") + } + } + } + + // MARK: - Private Helpers + + /// This runs a command and immediately returns a Shell result. + /// NOTE: This exists in conjunction with the `Shell.execute...` due to issues with different + /// `.bash_profile` environment variables. This should be consolidated in the future. + private func syncExec(command: String, args: [String] = []) -> Shell.Result { + let task = Process() + task.launchPath = command + task.arguments = args + task.launch() + task.waitUntilExit() +// +// var pipe = Pipe() +// task.standardOutput = pipe +// let handle = pipe.fileHandleForReading + + // Normally we'd use a pipe to retrieve the output, but for whatever reason it slows things down + // tremendously for xcodebuild. + let output = "The task completed." + guard task.terminationStatus == 0 else { + return .error(code: task.terminationStatus, output: output) + } + + return .success(output: output) + } + + /// Uses `xcodebuild` to build a framework for a specific architecture slice. + /// + /// - Parameters: + /// - framework: Name of the framework being built. + /// - arch: Architecture slice to build. + /// - buildDir: Location where the project should be built. + /// - logRoot: Root directory where all logs should be written. + /// - Returns: A URL to the thin library that was built. + private func buildThin(framework: String, + arch: Architecture, + buildDir: URL, + logRoot: URL) -> URL { + let platform = arch.platform + let workspacePath = projectDir.appendingPathComponent("FrameworkMaker.xcworkspace").path + let standardOptions = ["build", + "-configuration", "release", + "-workspace", workspacePath, + "-scheme", framework, + "GCC_GENERATE_DEBUGGING_SYMBOLS=No", + "ARCHS=\(arch.rawValue)", + "BUILD_DIR=\(buildDir.path)", + "-sdk", platform.rawValue] + let args = standardOptions + platform.extraArguments() + print(""" + Compiling \(framework) for \(arch.rawValue) with command: + /usr/bin/xcodebuild \(args.joined(separator: " ")) + """) + + // Regardless if it succeeds or not, we want to write the log to file in case we need to inspect + // things further. + let logFileName = "\(framework)-\(arch.rawValue)-\(platform.rawValue).txt" + let logFile = logRoot.appendingPathComponent(logFileName) + + let result = syncExec(command: "/usr/bin/xcodebuild", args: args) + switch result { + case let .error(code, output): + // Write output to disk and print the location of it. Force unwrapping here since it's going + // to crash anyways, and at this point the root log directory exists, we know it's UTF8, so it + // should pass every time. Revisit if that's not the case. + try! output.write(to: logFile, atomically: true, encoding: .utf8) + fatalError("Error building \(framework) for \(arch.rawValue). Code: \(code). See the build " + + "log at \(logFile)") + + case let .success(output): + // Try to write the output to the log file but if it fails it's not a huge deal since it was + // a successful build. + try? output.write(to: logFile, atomically: true, encoding: .utf8) + + // Use the Xcode-generated path to return the path to the compiled library. + let libPath = buildDir.appendingPathComponents(["Release-\(platform.rawValue)", + framework, + "lib\(framework).a"]) + return libPath + } + } + + // Extract the framework and library dependencies for a framework from + // Pods/Target Support Files/{framework}/{framework}.xcconfig. + private func getModuleDependencies(forFramework framework: String) -> + (frameworks: [String], libraries: [String]) { + let xcconfigFile = podsDir.appendingPathComponents(["Target Support Files", + framework, + "\(framework).xcconfig"]) + do { + let text = try String(contentsOf: xcconfigFile) + let lines = text.components(separatedBy: .newlines) + for line in lines { + if line.hasPrefix("OTHER_LDFLAGS =") { + var dependencyFrameworks: [String] = [] + var dependencyLibraries: [String] = [] + let tokens = line.components(separatedBy: " ") + var addNext = false + for token in tokens { + if addNext { + dependencyFrameworks.append(token) + addNext = false + } else if token == "-framework" { + addNext = true + } else if token.hasPrefix("-l") { + let index = token.index(token.startIndex, offsetBy: 2) + dependencyLibraries.append(String(token[index...])) + } + } + + return (dependencyFrameworks, dependencyLibraries) + } + } + } catch { + fatalError("Failed to open \(xcconfigFile): \(error)") + } + return ([], []) + } + + private func makeModuleMap(baseDir: URL, framework: String, dir: URL) { + let dependencies = getModuleDependencies(forFramework: framework) + let moduleDir = dir.appendingPathComponent("Modules") + do { + try FileManager.default.createDirectory(at: moduleDir, + withIntermediateDirectories: true, + attributes: nil) + } catch { + fatalError("Could not create Modules directory for framework: \(framework). \(error)") + } + + let modulemap = moduleDir.appendingPathComponent("module.modulemap") + // The base of the module map. The empty line at the end is intentional, do not remove it. + var content = """ + framework module \(framework) { + umbrella header "\(framework).h" + export * + module * { export * } + + """ + for framework in dependencies.frameworks { + content += " link framework " + framework + "\n" + } + for library in dependencies.libraries { + content += " link " + library + "\n" + } + content += "}\n" + + do { + try content.write(to: modulemap, atomically: true, encoding: .utf8) + } catch { + fatalError("Could not write modulemap to disk for \(framework): \(error)") + } + } + + /// Compiles the specified framework in a temporary directory and writes the build logs to file. + /// This will compile all architectures and use the lipo command to create a "fat" archive. + /// + /// - Parameter framework: The name of the framework to be built. + /// - Returns: A path to the newly compiled framework and Resource bundles. + private func compileFrameworkAndResources(withName framework: String) -> + (framework: URL, resourceBundles: [URL]) { + let fileManager = FileManager.default + let outputDir = fileManager.temporaryDirectory(withName: "frameworkBeingBuilt") + let logsDir = fileManager.temporaryDirectory(withName: "buildLogs") + do { + // Remove the compiled frameworks directory, this isn't the cache we're using. + if fileManager.directoryExists(at: outputDir) { + try fileManager.removeItem(at: outputDir) + } + + try fileManager.createDirectory(at: outputDir, + withIntermediateDirectories: true, + attributes: nil) + + // Create our logs directory if it doesn't exist. + if !fileManager.directoryExists(at: logsDir) { + try fileManager.createDirectory(at: logsDir, + withIntermediateDirectories: true, + attributes: nil) + } + } catch { + fatalError("Failure creating temporary directory while building \(framework): \(error)") + } + + // Build every architecture and save the locations in an array to be assembled. + // TODO: Pass in supported architectures here, for those that don't support individual + // architectures (MLKit). + var thinArchives = [URL]() + for arch in Architecture.allCases { + let buildDir = projectDir.appendingPathComponent(arch.rawValue) + let thinArchive = buildThin(framework: framework, + arch: arch, + buildDir: buildDir, + logRoot: logsDir) + thinArchives.append(thinArchive) + } + + // Create the framework directory in the filesystem for the thin archives to go. + let frameworkDir = outputDir.appendingPathComponent("\(framework).framework") + do { + try fileManager.createDirectory(at: frameworkDir, + withIntermediateDirectories: true, + attributes: nil) + } catch { + fatalError("Could not create framework directory while building framework \(framework). " + + "\(error)") + } + + // Build the fat archive using the `lipo` command. We need the full archive path and the list of + // thin paths (as Strings, not URLs). + let thinPaths = thinArchives.map { $0.path } + let fatArchive = frameworkDir.appendingPathComponent(framework) + let result = syncExec(command: "/usr/bin/lipo", args: ["-create", "-output", fatArchive.path] + thinPaths) + switch result { + case let .error(code, output): + fatalError(""" + lipo command exited with \(code) when trying to build \(framework). Output: + \(output) + """) + case .success: + print("lipo command for \(framework) succeeded.") + } + + // Remove the temporary thin archives. + for thinArchive in thinArchives { + do { + try fileManager.removeItem(at: thinArchive) + } catch { + // Just log a warning instead of failing, since this doesn't actually affect the build + // itself. This should only be shown to help users clean up their disk afterwards. + print(""" + WARNING: Failed to remove temporary thin archive at \(thinArchive.path). This should be + removed from your system to save disk space. \(error). You should be able to remove the + archive from Terminal with: + rm \(thinArchive.path) + """) + } + } + + // Verify Firebase headers include an explicit umbrella header for Firebase.h. + let headersDir = podsDir.appendingPathComponents(["Headers", "Public", framework]) + if framework.hasPrefix("Firebase") { + let frameworkHeader = headersDir.appendingPathComponent("\(framework).h") + guard fileManager.fileExists(atPath: frameworkHeader.path) else { + fatalError("Missing explicit umbrella header for \(framework).") + } + } + + // Copy the Headers over. Pass in the prefix to remove in order to generate the relative paths + // for some frameworks that have nested folders in their public headers. + let headersDestination = frameworkDir.appendingPathComponent("Headers") + do { + try recursivelyCopyHeaders(from: headersDir, to: headersDestination) + } catch { + fatalError("Could not copy headers from \(headersDir) to Headers directory in " + + "\(headersDestination): \(error)") + } + + // Move all the Resources into .bundle directories in the destination Resources dir. The + // Resources live are contained within the folder structure: + // `projectDir/arch/Release-platform/FrameworkName` + let arch = Architecture.arm64 + let contentsDir = projectDir.appendingPathComponents([arch.rawValue, + "Release-\(arch.platform.rawValue)", + framework]) + let resourceDir = frameworkDir.appendingPathComponent("Resources") + let bundles: [URL] + do { + bundles = try ResourcesManager.moveAllBundles(inDirectory: contentsDir, to: resourceDir) + } catch { + fatalError("Could not move bundles into Resources directory while building \(framework): " + + "\(error)") + } + + makeModuleMap(baseDir: outputDir, framework: framework, dir: frameworkDir) + return (frameworkDir, bundles) + } + + /// Recrusively copies headers from the given directory to the destination directory. This does a + /// deep copy and resolves and symlinks (which CocoaPods uses in the Public headers folder). + /// Throws FileManager errors if something goes wrong during the operations. + /// Note: This is only needed now because the `cp` command has a flag that did this for us, but + /// FileManager does not. + private func recursivelyCopyHeaders(from headersDir: URL, + to destinationDir: URL, + fileManager: FileManager = FileManager.default) throws { + // Copy the public headers into the new framework. Unfortunately we can't just copy the + // `Headers` directory since it uses aliases, so we'll recursively search the public Headers + // directory from CocoaPods and resolve all the aliases manually. + let fileManager = FileManager.default + + // Create the Headers directory if it doesn't exist. + try fileManager.createDirectory(at: destinationDir, + withIntermediateDirectories: true, + attributes: nil) + + // Get all the header aliases from the CocoaPods directory and get their real path as well as + // their relative path to the Headers directory they are in. This is needed to preserve proper + // imports for nested folders. + let aliasedHeaders = try fileManager.recursivelySearch(for: .headers, in: headersDir) + let mappedHeaders: [(relativePath: String, resolvedLocation: URL)] = aliasedHeaders.map { + // Standardize the URL because the aliasedHeaders could be at `/private/var` or `/var` which + // are symlinked to each other on macOS. This will let us remove the `headersDir` prefix and + // be left with just the relative path we need. + let standardized = $0.standardizedFileURL + let relativePath = standardized.path.replacingOccurrences(of: "\(headersDir.path)/", with: "") + let resolvedLocation = standardized.resolvingSymlinksInPath() + return (relativePath, resolvedLocation) + } + + // Copy all the headers into the Headers directory created above. + for (relativePath, location) in mappedHeaders { + // Append the proper filename to our Headers directory, then try copying it over. + let finalPath = destinationDir.appendingPathComponent(relativePath) + + // Create the destination folder if it doesn't exist. + let parentDir = finalPath.deletingLastPathComponent() + if !fileManager.directoryExists(at: parentDir) { + try fileManager.createDirectory(at: parentDir, + withIntermediateDirectories: true, + attributes: nil) + } + + print("Attempting to copy \(location) to \(finalPath)") + try fileManager.copyItem(at: location, to: finalPath) + } + } +} diff --git a/ZipBuilder/Sources/ZipBuilder/LaunchArgs.swift b/ZipBuilder/Sources/ZipBuilder/LaunchArgs.swift new file mode 100644 index 00000000000..85350906101 --- /dev/null +++ b/ZipBuilder/Sources/ZipBuilder/LaunchArgs.swift @@ -0,0 +1,229 @@ +/* + * Copyright 2019 Google + * + * 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 + +/// Describes an object that can check if a file eists in the filesystem. Used to allow for better +/// testing with FileManager. +protocol FileChecker { + /// Returns a Boolean value that indicates whether a file or directory exists at a specified path. + /// This matches the `FileManager` API. + func fileExists(atPath: String) -> Bool + + /// Returns a Boolean value that indicates whether a directory exists at a specified path. + func directoryExists(at url: URL) -> Bool +} + +// Make FileManager a FileChecker. This is empty since FileManager already provides this +// functionality (natively and through our extensions). +extension FileManager: FileChecker {} + +// TODO: Evaluate if we should switch to Swift Package Manager's internal `Utility` module that +// contains `ArgumentParser`. No immediate need, but provides some nice benefits. +/// LaunchArgs reads from UserDefaults to assemble all launch arguments coming from the command line +/// or the Xcode scheme. UserDefaults contains all launch arguments that are in the format of: +/// `-myKey myValue`. +struct LaunchArgs { + /// Keys associated with the launch args. See `Usage` for descriptions of each flag. + private enum Key: String, CaseIterable { + case cacheEnabled + case customSpecRepos + case coreDiagnosticsDir + case deleteCache + case existingVersions + case outputDir + case releasingSDKs + case templateDir + case updatePodRepo + + /// Usage description for the key. + var usage: String { + switch self { + case .cacheEnabled: + return "A flag to control using the cache for frameworks." + case .coreDiagnosticsDir: + return "The path to the `CoreDiagnostics.framework` file built with the Zip flag enabled." + case .customSpecRepos: + return "A comma separated list of custom CocoaPod Spec repos." + case .deleteCache: + return "A flag to empty the cache. Note: if this flag and the `cacheEnabled` flag is " + + "set, it will fail since that's probably unintended." + case .existingVersions: + return "The file path to a textproto file containing the existing released SDK versions, " + + "of type `ZipBuilder_FirebaseSDKs`." + case .outputDir: + return "The directory to copy the built Zip file to." + case .releasingSDKs: + return "The file path to a textproto file containing all the releasing SDKs, of type " + + "`ZipBuilder_Release`." + case .templateDir: + return "The path to the directory containing the blank xcodeproj and Info.plist for " + + "building source based frameworks" + case .updatePodRepo: + return "A flag to run `pod repo update` before building the zip file." + } + } + } + + /// A file URL to a textproto with the contents of a `ZipBuilder_FirebaseSDKs` object. Used to + /// verify expected version numbers. + let allSDKsPath: URL? + + /// The path to the `CoreDiagnostics.framework` file built with the Zip flag enabled. + let coreDiagnosticsDir: URL + + /// A file URL to a textproto with the contents of a `ZipBuilder_Release` object. Used to verify + /// expected version numbers. + let currentReleasePath: URL? + + /// Custom CocoaPods spec repos to be used. If not provided, the tool will only use the CocoaPods + /// master repo. + let customSpecRepos: [URL]? + + /// The directory to copy the built Zip file to. If this is not set, the path to the Zip file will + /// just be logged to the console. + let outputDir: URL? + + /// The path to the directory containing the blank xcodeproj and Info.plist for building source + /// based frameworks. + let templateDir: URL + + /// A flag to control using the cache for frameworks. + let cacheEnabled: Bool + + /// A flag to delete the cache from the cache directory. + let deleteCache: Bool + + /// A flag to update the Pod Repo or not. + let updatePodRepo: Bool + + /// Initializes with values pulled from the instance of UserDefaults passed in. + /// + /// - Parameters: + /// - defaults: User defaults containing launch arguments. Defaults to `standard`. + /// - fileChecker: An object that can check if a file exists or not. Defaults to + /// `FileManager.default`. + init(userDefaults defaults: UserDefaults = UserDefaults.standard, + fileChecker: FileChecker = FileManager.default) { + // Override default values for specific keys. + // - Always run `pod repo update` unless explicitly set to false. + defaults.register(defaults: [Key.updatePodRepo.rawValue: true]) + + // Get the project template directory, and fail if it doesn't exist. + guard let templatePath = defaults.string(forKey: Key.templateDir.rawValue) else { + LaunchArgs.exitWithUsageAndLog("Missing required key: `\(Key.templateDir)` for the folder " + + "containing all required files to build frameworks.") + } + + templateDir = URL(fileURLWithPath: templatePath) + + // Parse the path to CoreDiagnostics.framework. + guard let diagnosticsPath = defaults.string(forKey: Key.coreDiagnosticsDir.rawValue) else { + LaunchArgs.exitWithUsageAndLog("Missing required key: `\(Key.coreDiagnosticsDir)` for the " + + "path to the CoreDiagnostics framework.") + } + + coreDiagnosticsDir = URL(fileURLWithPath: diagnosticsPath) + + // Parse the existing versions key. + if let existingVersions = defaults.string(forKey: Key.existingVersions.rawValue) { + let url = URL(fileURLWithPath: existingVersions) + guard fileChecker.fileExists(atPath: url.path) else { + LaunchArgs.exitWithUsageAndLog("Could not parse \(Key.existingVersions) key: value " + + "passed in is not a file URL or the file does not exist. Value: \(existingVersions)") + } + + allSDKsPath = url.standardizedFileURL + } else { + // No argument was passed in. + allSDKsPath = nil + } + + // Parse the current releases key. + if let currentRelease = defaults.string(forKey: Key.releasingSDKs.rawValue) { + let url = URL(fileURLWithPath: currentRelease) + guard fileChecker.fileExists(atPath: url.path) else { + LaunchArgs.exitWithUsageAndLog("Could not parse \(Key.releasingSDKs) key: value passed " + + "in is not a file URL or the file does not exist. Value: \(currentRelease)") + } + + currentReleasePath = url.standardizedFileURL + } else { + // No argument was passed in. + currentReleasePath = nil + } + + // Parse the output directory key. + if let outputPath = defaults.string(forKey: Key.outputDir.rawValue) { + let url = URL(fileURLWithPath: outputPath) + guard fileChecker.directoryExists(at: url) else { + LaunchArgs.exitWithUsageAndLog("Could not parse \(Key.outputDir) key: value " + + "passed in is not a file URL or the directory does not exist. Value: \(outputPath)") + } + + outputDir = url.standardizedFileURL + } else { + // No argument was passed in. + outputDir = nil + } + + // Parse the custom specs key. + if let customSpecs = defaults.string(forKey: Key.customSpecRepos.rawValue) { + // Custom specs are passed in as a comma separated list of URLs. Split the String by each + // comma and map it to URLs. If any URL is invalid, fail immediately. + let specs = customSpecs.split(separator: ",").map { (specStr: Substring) -> URL in + guard let spec = URL(string: String(specStr)) else { + LaunchArgs.exitWithUsageAndLog("Error parsing specs: \(specStr) is not a valid URL.") + } + + return spec + } + + customSpecRepos = specs + } else { + // No argument was passed in. + customSpecRepos = nil + } + + updatePodRepo = defaults.bool(forKey: Key.updatePodRepo.rawValue) + + // Parse the cache keys. If no value is provided for each, it defaults to `false`. + cacheEnabled = defaults.bool(forKey: Key.cacheEnabled.rawValue) + deleteCache = defaults.bool(forKey: Key.deleteCache.rawValue) + + if deleteCache, cacheEnabled { + LaunchArgs.exitWithUsageAndLog("Invalid pair - attempted to delete the cache and enable " + + "it at the same time. Please remove on of the keys and try " + + "again.") + } + } + + /// Prints an error that occurred, the proper usage String, and quits the application. + private static func exitWithUsageAndLog(_ errorText: String) -> Never { + print(errorText) + + // Loop over all the possible keys and print their description. + print("Usage: `swift run ZipBuilder [ARGS]` where args are:") + for option in Key.allCases { + print(""" + -\(option.rawValue) + \(option.usage) + """) + } + + fatalError("Invalid arguments. See output above for specific error and usage instructions.") + } +} diff --git a/ZipBuilder/Sources/ZipBuilder/ManifestReader.swift b/ZipBuilder/Sources/ZipBuilder/ManifestReader.swift new file mode 100644 index 00000000000..ac33748e3c3 --- /dev/null +++ b/ZipBuilder/Sources/ZipBuilder/ManifestReader.swift @@ -0,0 +1,56 @@ +/* + * Copyright 2019 Google + * + * 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 + +/// Common functions for Firebase iOS SDK Release manifests. Intentionally empty, this enum is used +/// as a namespace. +enum ManifestReader {} + +extension ManifestReader { + /// Load all the publicly released SDKs and their versions. Will cause a fatal error if the file + /// cannot be read or proto cannot be generated. + static func loadAllReleasedSDKs(fromTextproto textproto: URL) -> ZipBuilder_FirebaseSDKs { + // Read the textproto and create it from the proto's generated API. Fail if anything fails in + // the process. + do { + let protoText = try String(contentsOf: textproto, encoding: .utf8) + // Internally the `build_flags` field is named `blaze_flags`. Replace it. + let blazelessText = protoText.replacingOccurrences(of: "blaze_flags", with: "build_flags") + let proto = try ZipBuilder_FirebaseSDKs(textFormatString: blazelessText) + return proto + } catch { + fatalError("Could not create proto from file containing all released SDKs: \(error)") + } + } + + /// Load the current release manifest for the SDKs that are slated for release. Will cause a fatal + /// error if the file cannot be read or proto cannot be generated. + /// + /// - Parameter textproto: The path to the textproto file describing the current release. + /// - Returns: An instance of ZipBuilder_Release describing specific versions to build. + static func loadCurrentRelease(fromTextproto textproto: URL) -> ZipBuilder_Release { + // Read the textproto and create it from the proto's generated API. Fail if anything fails in + // the process. + do { + let protoText = try String(contentsOf: textproto, encoding: .utf8) + let proto = try ZipBuilder_Release(textFormatString: protoText) + return proto + } catch { + fatalError("Could not create proto from current release manifest: \(error)") + } + } +} diff --git a/ZipBuilder/Sources/ZipBuilder/Release.pb.swift b/ZipBuilder/Sources/ZipBuilder/Release.pb.swift new file mode 100644 index 00000000000..9cac799471f --- /dev/null +++ b/ZipBuilder/Sources/ZipBuilder/Release.pb.swift @@ -0,0 +1,205 @@ +/* + * Copyright 2019 Google + * + * 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. + */ + +// DO NOT EDIT. +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: Release.proto +// +// For information on using the generated types, please see the documenation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that your are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +struct ZipBuilder_Release { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// Targeted release date + var targetDate: String = String() + + /// Release name (e.g. M15 - Beryllium) + var name: String = String() + + /// Release code (e.g. M15) + var code: String = String() + + /// Whether or not the release went out + var released: Bool = false + + /// List of SDKs that are part of the release + var sdk: [ZipBuilder_ReleasingSDK] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct ZipBuilder_ReleasingSDK { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// SDK name as from all_firebase_ios_sdks.textproto + var sdkName: String = String() + + /// The version of SDK to release + var sdkVersion: String = String() + + /// Whether or not a launchal is required for this release + var launchcalRequired: Bool = false + + /// Ariane link + var launchcalLink: String = String() + + /// An link to the change log + var changelogLink: String = String() + + /// A link to the release hotlist + var hotlistLink: String = String() + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +fileprivate let _protobuf_package = "ZipBuilder" + +extension ZipBuilder_Release: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Release" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "target_date"), + 2: .same(proto: "name"), + 3: .same(proto: "code"), + 4: .same(proto: "released"), + 5: .same(proto: "sdk"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self.targetDate) + case 2: try decoder.decodeSingularStringField(value: &self.name) + case 3: try decoder.decodeSingularStringField(value: &self.code) + case 4: try decoder.decodeSingularBoolField(value: &self.released) + case 5: try decoder.decodeRepeatedMessageField(value: &self.sdk) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.targetDate.isEmpty { + try visitor.visitSingularStringField(value: self.targetDate, fieldNumber: 1) + } + if !self.name.isEmpty { + try visitor.visitSingularStringField(value: self.name, fieldNumber: 2) + } + if !self.code.isEmpty { + try visitor.visitSingularStringField(value: self.code, fieldNumber: 3) + } + if self.released != false { + try visitor.visitSingularBoolField(value: self.released, fieldNumber: 4) + } + if !self.sdk.isEmpty { + try visitor.visitRepeatedMessageField(value: self.sdk, fieldNumber: 5) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ZipBuilder_Release, rhs: ZipBuilder_Release) -> Bool { + if lhs.targetDate != rhs.targetDate {return false} + if lhs.name != rhs.name {return false} + if lhs.code != rhs.code {return false} + if lhs.released != rhs.released {return false} + if lhs.sdk != rhs.sdk {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension ZipBuilder_ReleasingSDK: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".ReleasingSDK" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "sdk_name"), + 2: .standard(proto: "sdk_version"), + 3: .standard(proto: "launchcal_required"), + 4: .standard(proto: "launchcal_link"), + 5: .standard(proto: "changelog_link"), + 6: .standard(proto: "hotlist_link"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + switch fieldNumber { + case 1: try decoder.decodeSingularStringField(value: &self.sdkName) + case 2: try decoder.decodeSingularStringField(value: &self.sdkVersion) + case 3: try decoder.decodeSingularBoolField(value: &self.launchcalRequired) + case 4: try decoder.decodeSingularStringField(value: &self.launchcalLink) + case 5: try decoder.decodeSingularStringField(value: &self.changelogLink) + case 6: try decoder.decodeSingularStringField(value: &self.hotlistLink) + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.sdkName.isEmpty { + try visitor.visitSingularStringField(value: self.sdkName, fieldNumber: 1) + } + if !self.sdkVersion.isEmpty { + try visitor.visitSingularStringField(value: self.sdkVersion, fieldNumber: 2) + } + if self.launchcalRequired != false { + try visitor.visitSingularBoolField(value: self.launchcalRequired, fieldNumber: 3) + } + if !self.launchcalLink.isEmpty { + try visitor.visitSingularStringField(value: self.launchcalLink, fieldNumber: 4) + } + if !self.changelogLink.isEmpty { + try visitor.visitSingularStringField(value: self.changelogLink, fieldNumber: 5) + } + if !self.hotlistLink.isEmpty { + try visitor.visitSingularStringField(value: self.hotlistLink, fieldNumber: 6) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ZipBuilder_ReleasingSDK, rhs: ZipBuilder_ReleasingSDK) -> Bool { + if lhs.sdkName != rhs.sdkName {return false} + if lhs.sdkVersion != rhs.sdkVersion {return false} + if lhs.launchcalRequired != rhs.launchcalRequired {return false} + if lhs.launchcalLink != rhs.launchcalLink {return false} + if lhs.changelogLink != rhs.changelogLink {return false} + if lhs.hotlistLink != rhs.hotlistLink {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/ZipBuilder/Sources/ZipBuilder/Release.proto b/ZipBuilder/Sources/ZipBuilder/Release.proto new file mode 100644 index 00000000000..e3a18efdeb8 --- /dev/null +++ b/ZipBuilder/Sources/ZipBuilder/Release.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package ZipBuilder; + +message Release { + // Targeted release date + string target_date = 1; + // Release name (e.g. M15 - Beryllium) + string name = 2; + // Release code (e.g. M15) + string code = 3; + // Whether or not the release went out + bool released = 4; + // List of SDKs that are part of the release + repeated ReleasingSDK sdk = 5; +} + +message ReleasingSDK { + // SDK name as from all_firebase_ios_sdks.textproto + string sdk_name = 1; + + // The version of SDK to release + string sdk_version = 2; + + // Whether or not a launchal is required for this release + bool launchcal_required = 3; + + // Ariane link + string launchcal_link = 4; + + // An link to the change log + string changelog_link = 5; + + // A link to the release hotlist + string hotlist_link = 6; +} diff --git a/ZipBuilder/Sources/ZipBuilder/ResourcesManager.swift b/ZipBuilder/Sources/ZipBuilder/ResourcesManager.swift new file mode 100644 index 00000000000..dd918966baa --- /dev/null +++ b/ZipBuilder/Sources/ZipBuilder/ResourcesManager.swift @@ -0,0 +1,172 @@ +/* + * Copyright 2019 Google + * + * 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 + +/// Functions related to managing resources. Intentionally empty, this enum is used as a namespace. +enum ResourcesManager {} + +extension ResourcesManager { + /// Recursively searches for Resources directories in `dir`, creates a `.bundle` based on each + /// folder contained there, and moves them to the Resources directory `resourceDir`. + /// + /// - Parameters: + /// - dir: The directory to search for Resource directories. + /// - destinationDir: The destination Resources directory. This function will create the Resources + /// directory if it doesn't exist. + public static func createBundleForFoldersInResourcesDirs(containedIn dir: URL, + destinationDir: URL) throws -> [URL] { + let fileManager = FileManager.default + let existingResources = try fileManager.recursivelySearch(for: .directories(name: "Resources"), + in: dir) + + // Only continue if there are Resources to bundle. + guard !existingResources.isEmpty else { return [] } + + // Create the umbrella Resources folder if it doesn't exist. + if !fileManager.directoryExists(at: destinationDir) { + // Create a Resources directory if there is at least one bundle and the directory doesn't + // already exist. + try fileManager.createDirectory(at: destinationDir, + withIntermediateDirectories: true, + attributes: nil) + } + + // For each "Resources" directory found, turn each folder into a `.bundle`. + var bundles: [URL] = [] + for resourceDir in existingResources { + // Get all the folders in the "Resources" directory and loop through them. + let containedFolders = try fileManager.contentsOfDirectory(atPath: resourceDir.path) + for folderToBundle in containedFolders { + let folder = resourceDir.appendingPathComponent(folderToBundle) + guard fileManager.isDirectory(at: folder) else { continue } + + // Generate the name and location based on the folder name. + let name = folder.lastPathComponent + ".bundle" + let location = destinationDir.appendingPathComponent(name) + + // Copy the existing Resources folder to the new bundle location. + try fileManager.copyItem(at: folder, to: location) + + // Compile any storyboards that exist in the new bundle. + compileStoryboards(inDir: location) + + bundles.append(location) + } + } + + return bundles + } + + /// Recursively searches for bundles in `dir` and moves them to the Resources directory + /// `resourceDir`. + /// + /// - Parameters: + /// - dir: The directory to search for Resource bundles. + /// - resourceDir: The destination Resources directory. This function will create the Resources + /// directory if it doesn't exist. + /// - Returns: An array of URLs pointing to the newly located bundles. + /// - Throws: Any file system errors that occur. + public static func moveAllBundles(inDirectory dir: URL, to resourceDir: URL) throws -> [URL] { + let fileManager = FileManager.default + let allBundles = try fileManager.recursivelySearch(for: .bundles, in: dir) + + // Find the bundle directories and move them into a Resources directory. + if !allBundles.isEmpty, !fileManager.directoryExists(at: resourceDir) { + // Create a Resources directory if there is at least one bundle and the directory doesn't + // already exist. + try fileManager.createDirectory(at: resourceDir, + withIntermediateDirectories: true, + attributes: nil) + } + + // Move each bundle to the Resources/ directory. + var movedBundles: [URL] = [] + for bundle in allBundles { + let newLocation = resourceDir.appendingPathComponent(bundle.lastPathComponent) + try fileManager.moveItem(at: bundle, to: newLocation) + movedBundles.append(newLocation) + } + + return movedBundles + } + + /// Searches for and attempts to remove all empty "Resources" directories in a given directory. + /// This is a recrusive search. + /// + /// - Parameter dir: The directory to recursively search for Resources directories in. + public static func removeEmptyResourcesDirectories(in dir: URL) { + // Find all the Resources directories to begin with. + let fileManager = FileManager.default + guard let resourceDirs = try? fileManager.recursivelySearch(for: .directories(name: "Resources"), in: dir) else { + print("Attempted to remove empty resource directories, but it failed. This shouldn't be " + + "classified as an error, but something to look out for.") + return + } + + // Get the contents of each directory and if it's empty, remove it. + for resourceDir in resourceDirs { + guard let contents = try? fileManager.contentsOfDirectory(atPath: resourceDir.path) else { + print("WARNING: Failed to get contents of apparent Resources directory at \(resourceDir)") + continue + } + + // Remove the directory if it's empty. Only warn if it's not successful, since it's not a + // requirement but a nice to have. + if contents.isEmpty { + do { + try fileManager.removeItem(at: resourceDir) + } catch { + print("WARNING: Failed to remove empty Resources directory while cleaning up folder " + + "heirarchy: \(error)") + } + } + } + } + + /// Finds and compiles all `.storyboard` files in a directory, removing the original file. + private static func compileStoryboards(inDir dir: URL) { + let fileManager = FileManager.default + let storyboards: [URL] + do { + storyboards = try fileManager.recursivelySearch(for: .storyboards, in: dir) + } catch { + fatalError("Failed to search for storyboards in directory: \(error)") + } + + // Compile each storyboard, then remove it. + for storyboard in storyboards { + // Compiled storyboards have the extension `storyboardc`. + let compiledPath = storyboard.deletingPathExtension().appendingPathExtension("storyboardc") + + // Run the command and throw an error if it fails. + let command = "ibtool --compile \(compiledPath.path) \(storyboard.path)" + let result = Shell.executeCommandFromScript(command) + switch result { + case .success: + // Remove the original storyboard file and continue. + do { + try fileManager.removeItem(at: storyboard) + } catch { + fatalError("Could not remove storyboard file \(storyboard) from bundle after " + + "compilation: \(error)") + } + case let .error(code, output): + fatalError("Failed to compile storyboard \(storyboard): error \(code) \(output)") + } + } + } +} diff --git a/ZipBuilder/Sources/ZipBuilder/ShellUtils.swift b/ZipBuilder/Sources/ZipBuilder/ShellUtils.swift new file mode 100644 index 00000000000..59b6a256170 --- /dev/null +++ b/ZipBuilder/Sources/ZipBuilder/ShellUtils.swift @@ -0,0 +1,172 @@ +/* + * Copyright 2019 Google + * + * 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 + +/// Convenience function for calling functions in the Shell. This should be used sparingly and only +/// when interacting with tools that can't be accessed directly in Swift (i.e. CocoaPods, +/// xcodebuild, etc). Intentionally empty, this enum is used as a namespace. +internal enum Shell {} + +extension Shell { + /// A type to represent the result of running a shell command. + enum Result { + /// The command was successfully run (based on the output code), with the output string as the + /// associated value. + case success(output: String) + + /// The command failed with a given exit code and output. + case error(code: Int32, output: String) + } + + /// Execute a command in the user's shell. This creates a temporary shell script and runs the + /// command from there, instead of calling via `Process()` directly in order to include the + /// appropriate environment variables. This is mostly for CocoaPods commands, but doesn't hurt + /// other commands. + /// + /// - Parameters: + /// - command: The command to run in the shell. + /// - outputToConsole: A flag if the command output should be written to the console as well. + /// - workingDir: An optional working directory to run the shell command in. + /// - Returns: A Result containing output information from the command. + static func executeCommandFromScript(_ command: String, + outputToConsole: Bool = true, + workingDir: URL? = nil) -> Result { + let scriptPath: URL + do { + let tempScriptsDir = FileManager.default.temporaryDirectory(withName: "temp_scripts") + try FileManager.default.createDirectory(at: tempScriptsDir, + withIntermediateDirectories: true, + attributes: nil) + scriptPath = tempScriptsDir.appendingPathComponent("wrapper.sh") + + // Write the temporary script contents to the script's path. CocoaPods complains when LANG + // isn't set in the environment, so explicitly set it here. The `/usr/local/git/current/bin` + // is to allow the `sso` protocol if it's there. + let contents = """ + export PATH="/usr/local/bin:/usr/local/git/current/bin:$PATH" + export LANG="en_US.UTF-8" + source ~/.bash_profile + \(command) + """ + try contents.write(to: scriptPath, atomically: true, encoding: .utf8) + } catch let FileManager.FileError.failedToCreateDirectory(path, error) { + fatalError("Could not execute shell command: \(command) - could not create temporary " + + "script directory at \(path). \(error)") + } catch { + fatalError("Could not execute shell command: \(command) - unexpected error. \(error)") + } + + // Remove the temporary script at the end of this function. If it fails, it's not a big deal + // since it will be over-written next time and won't affect the Zip file, so we can ignore + // any failure. + defer { try? FileManager.default.removeItem(at: scriptPath) } + + // Let the process call directly into the temporary shell script we created. + let task = Process() + task.arguments = [scriptPath.path] + + if #available(OSX 10.13, *) { + if let workingDir = workingDir { + task.currentDirectoryURL = workingDir + } + + // Explicitly use `/bin/bash`. Investigate whether or not we can use `/usr/local/env` + task.executableURL = URL(fileURLWithPath: "/bin/bash") + } else { + // Assign the old `currentDirectoryPath` property if `currentDirectoryURL` isn't available. + if let workingDir = workingDir { + task.currentDirectoryPath = workingDir.path + } + task.launchPath = "/bin/bash" + } + + // Assign a pipe to read as soon as data is available, log it to the console if requested, but + // also keep an array of the output in memory so we can pass it back to functions. + // Assign a pipe to grab the output, and handle it differently if we want to stream the results + // to the console or not. + let pipe = Pipe() + task.standardOutput = pipe + let outHandle = pipe.fileHandleForReading + var output: [String] = [] + + // If we want to output to the console, create a readabilityHandler and save each line along the + // way. Otherwise, we can just read the pipe at the end. By disabling outputToConsole, some + // commands (such as any xcodebuild) can run much, much faster. + if outputToConsole { + outHandle.readabilityHandler = { pipe in + // This will be run any time data is sent to the pipe. We want to print it and store it for + // later. Ignore any non-valid Strings. + guard let line = String(data: pipe.availableData, encoding: .utf8) else { + print("Could not get data from pipe for command \(command): \(pipe.availableData)") + return + } + output.append(line) + print(line) + } + // Also set the termination handler on the task in order to stop the readabilityHandler from + // parsing any more data from the task. + task.terminationHandler = { t in + guard let stdOut = t.standardOutput as? Pipe else { return } + + stdOut.fileHandleForReading.readabilityHandler = nil + } + } + + // Launch the task and wait for it to exit. This will trigger the above readabilityHandler + // method and will redirect the command output back to the console for quick feedback. + if outputToConsole { + print("Running command: \(command).") + print("----------------- COMMAND OUTPUT -----------------") + } + task.launch() + task.waitUntilExit() + if outputToConsole { print("----------------- END COMMAND OUTPUT -----------------") } + + let fullOutput: String + if outputToConsole { + fullOutput = output.joined(separator: "\n") + } else { + let outData = outHandle.readDataToEndOfFile() + // Force unwrapping since we know it's UTF8 coming from the console. + fullOutput = String(data: outData, encoding: .utf8)! + } + + // Check if the task succeeded or not, and return the failure code if it didn't. + guard task.terminationStatus == 0 else { + return Result.error(code: task.terminationStatus, output: fullOutput) + } + + // The command was successful, return the output. + return Result.success(output: fullOutput) + } + + /// Calculates the MD5 for a given file or folder URL. + static func calculateMD5(for url: URL) -> String { + // TODO: Replace this with something more straightforward, and use CommonCrypto when we switch + // to Xcode 10. + let command = "find \(url.path) -type f -exec md5 '{}' + | sort -k 2 | md5" + let result = executeCommandFromScript(command, outputToConsole: false) + switch result { + case let .error(_, output): + fatalError("Failed to compute an MD5 hash for \(url): \(output)") + case let .success(output): + // The output has newlines and a few spaces, only use alphanumerics. + let excludedChars = CharacterSet.alphanumerics.inverted + return output.trimmingCharacters(in: excludedChars) + } + } +} diff --git a/ZipBuilder/Sources/ZipBuilder/Subspec.swift b/ZipBuilder/Sources/ZipBuilder/Subspec.swift new file mode 100644 index 00000000000..8f63c0177fb --- /dev/null +++ b/ZipBuilder/Sources/ZipBuilder/Subspec.swift @@ -0,0 +1,182 @@ +/* + * Copyright 2019 Google + * + * 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 + +// TODO: Auto generate this list from the Firebase.podspec, probably with a script. +/// All the subspecs available in the Firebase pod. +public enum Subspec: String, CaseIterable { + case abTesting = "ABTesting" + case adMob = "AdMob" + case analytics = "Analytics" + case auth = "Auth" + case core = "Core" + case crash = "Crash" + case database = "Database" + case dynamicLinks = "DynamicLinks" + case firestore = "Firestore" + case functions = "Functions" + case inAppMessaging = "InAppMessaging" + case inAppMessagingDisplay = "InAppMessagingDisplay" + case invites = "Invites" + case messaging = "Messaging" + case mlModelInterpreter = "MLModelInterpreter" + case mlNaturalLanguage = "MLNaturalLanguage" + case mlNLLanguageID = "MLNLLanguageID" + case mlNLSmartReply = "MLNLSmartReply" + case mlVision = "MLVision" + case mlVisionBarcodeModel = "MLVisionBarcodeModel" + case mlVisionFaceModel = "MLVisionFaceModel" + case mlVisionLabelModel = "MLVisionLabelModel" + case mlVisionTextModel = "MLVisionTextModel" + case performance = "Performance" + case remoteConfig = "RemoteConfig" + case storage = "Storage" + + /// Flag to explicitly exclude any Resources from being copied. + public var excludeResources: Bool { + switch self { + case .mlVision, .mlVisionBarcodeModel, .mlVisionLabelModel: + return true + default: + return false + } + } + + /// The minimum supported iOS version. + public func minSupportedIOSVersion() -> OperatingSystemVersion { + // All ML pods have a minimum iOS version of 9.0. + if rawValue.hasPrefix("ML") { + return OperatingSystemVersion(majorVersion: 9, minorVersion: 0, patchVersion: 0) + } else { + return OperatingSystemVersion(majorVersion: 8, minorVersion: 0, patchVersion: 0) + } + } + + /// Describes the dependency on other frameworks for the README file. + public func readmeHeader() -> String { + var header = "## \(rawValue)" + if self != .analytics { + header += " (~> Analytics)" + } + header += "\n" + return header + } + + // TODO: Evaluate if there's a way to do this that doesn't require the hardcoded values to be + // maintained. + /// Returns folders to remove from the Zip file from a specific subspec for de-duplication. This + /// is necessary for the MLKit frameworks because of their unique structure, an unnecessary amount + /// of frameworks get pulled in. + public func duplicateFrameworksToRemove() -> [String] { + switch self { + case .mlVision: + return ["BarcodeDetector.framework", + "FaceDetector.framework", + "LabelDetector.framework", + "TextDetector.framework"] + case .mlVisionBarcodeModel: + return ["FaceDetector.framework", + "GTMSessionFetcher.framework", + "GoogleMobileVision.framework", + "LabelDetector.framework", + "Protobuf.framework", + "TextDetector.framework"] + case .mlVisionFaceModel: + return ["BarcodeDetector.framework", + "GTMSessionFetcher.framework", + "GoogleMobileVision.framework", + "LabelDetector.framework", + "Protobuf.framework", + "TextDetector.framework"] + case .mlVisionLabelModel: + return ["BarcodeDetector.framework", + "FaceDetector.framework", + "GTMSessionFetcher.framework", + "GoogleMobileVision.framework", + "Protobuf.framework", + "TextDetector.framework"] + case .mlVisionTextModel: + return ["BarcodeDetector.framework", + "FaceDetector.framework", + "GTMSessionFetcher.framework", + "GoogleMobileVision.framework", + "LabelDetector.framework", + "Protobuf.framework"] + default: + // By default, no folders need to be removed. + return [] + } + } + + /// Returns a group of duplicate Resources that should be removed, if any. + public func duplicateResourcesToRemove() -> [String] { + switch self { + case .mlVisionFaceModel: + return ["GoogleMVTextDetectorResources.bundle"] + case .mlVisionTextModel: + return ["GoogleMVFaceDetectorResources.bundle"] + default: + // By default, no resources should be removed. + return [] + } + } +} + +/// Add comparitor for OperatingSystemVersion. We only need the `>` since we don't care about equals +/// or anything else, we just need to find the largest value between two structs. +extension OperatingSystemVersion: Comparable, Equatable { + public static func < (lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool { + // The priority is major.minor.patch, only keep searching to lower importance if the higher + // levels are equal. + if lhs.majorVersion < rhs.majorVersion { return true } + if lhs.majorVersion > rhs.majorVersion { return false } + + // Major version must be equal, continue to minor. + if lhs.minorVersion < rhs.minorVersion { return true } + if lhs.minorVersion > rhs.minorVersion { return false } + + // Down to patch, just return the comparison since there are no more levels. + return lhs.minorVersion < rhs.minorVersion + } + + public static func > (lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool { + // The priority is major.minor.patch, only keep searching to lower importance if the higher + // levels are equal. + if lhs.majorVersion > rhs.majorVersion { return true } + if lhs.majorVersion < rhs.majorVersion { return false } + + // Major version must be equal, continue to minor. + if lhs.minorVersion > rhs.minorVersion { return true } + if lhs.minorVersion < rhs.minorVersion { return false } + + // Down to patch, just return the comparison since there are no more levels. + return lhs.minorVersion > rhs.minorVersion + } + + public static func == (lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool { + return lhs.majorVersion == rhs.majorVersion && + lhs.minorVersion == rhs.minorVersion && + lhs.patchVersion == rhs.patchVersion + } +} + +extension OperatingSystemVersion { + /// The string to define this operating system in a Podfile. In the form "MAJOR.MINOR" + public func podVersion() -> String { + return "\(majorVersion).\(minorVersion)" + } +} diff --git a/ZipBuilder/Sources/ZipBuilder/URL+Utils.swift b/ZipBuilder/Sources/ZipBuilder/URL+Utils.swift new file mode 100644 index 00000000000..f9cd99e4cbd --- /dev/null +++ b/ZipBuilder/Sources/ZipBuilder/URL+Utils.swift @@ -0,0 +1,28 @@ +/* + * Copyright 2019 Google + * + * 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 + +/// Utilities to simplify URL manipulation. +public extension URL { + /// Appends each item in the array as a component to the existing URL. + func appendingPathComponents(_ components: [String]) -> URL { + // Append multiple path components in a single call to prevent long lines of multiple calls. + var result = self + components.forEach({ result.appendPathComponent($0) }) + return result + } +} diff --git a/ZipBuilder/Sources/ZipBuilder/Zip.swift b/ZipBuilder/Sources/ZipBuilder/Zip.swift new file mode 100644 index 00000000000..4379703901f --- /dev/null +++ b/ZipBuilder/Sources/ZipBuilder/Zip.swift @@ -0,0 +1,56 @@ +/* + * Copyright 2019 Google + * + * 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 + +/// Convenience +struct Zip { + /// Compresses the contents of the directory into a Zip file that resides beside the directory + /// being compressed and has the same name as the directory with a `.zip` suffix. + /// + /// - Parameter directory: The directory to compress. + /// - Returns: A URL to the Zip file created. + static func zipContents(ofDir directory: URL) -> URL { + // Ensure the directory being compressed exists. + guard FileManager.default.directoryExists(at: directory) else { + fatalError("Attempted to compress contents of \(directory) but the directory does not exist.") + } + + // This `zip` command needs to be run in the parent directory. + let parentDir = directory.deletingLastPathComponent() + let zip = parentDir.appendingPathComponent("Firebase.zip") + + // If it exists already, try to remove it. + if FileManager.default.fileExists(atPath: zip.path) { + try? FileManager.default.removeItem(at: zip) + } + + // Run the `zip` command. This could be replaced with a proper Zip library in the future. + let command = "zip -q -r -dg \(zip.lastPathComponent) \(directory.lastPathComponent)" + let result = Shell.executeCommandFromScript(command, workingDir: parentDir) + switch result { + case .success: + print("Successfully built Zip file.") + return zip + case let .error(code, output): + fatalError("Error \(code) building zip file: \(output)") + } + } + + // Mark initialization as unavailable. + @available(*, unavailable) + init() { fatalError() } +} diff --git a/ZipBuilder/Sources/ZipBuilder/ZipBuilder.swift b/ZipBuilder/Sources/ZipBuilder/ZipBuilder.swift new file mode 100644 index 00000000000..091dc64a5e1 --- /dev/null +++ b/ZipBuilder/Sources/ZipBuilder/ZipBuilder.swift @@ -0,0 +1,796 @@ +/* + * Copyright 2019 Google + * + * 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 + +/// Misc. constants used in the script. +private struct Constants { + /// Constants related to the Xcode project template. + public struct ProjectPath { + // Required for building. + public static let infoPlist = "Info.plist" + public static let projectFile = "FrameworkMaker.xcodeproj" + + /// All required files for building the Zip file. + public static let requiredFilesForBuilding: [String] = [projectFile, infoPlist] + + // Required for distribution. + public static let firebaseHeader = "Firebase.h" + public static let readmeName = "README.md" + public static let modulemap = "module.modulemap" + public static let notices = "NOTICES" + + // Directory containing extra FirebaseCrash scripts. + public static let crashDir = "Crash" + + /// All required files for distribution. Note: the readmeTemplate is also needed for + /// distribution but is copied separately since it's modified. + public static let requiredFilesForDistribution: [String] = [firebaseHeader, modulemap, notices] + + // Make the struct un-initializable. + @available(*, unavailable) + init() { fatalError() } + } + + /// The text added to the README for a product if it contains Resources. The empty line at the end + /// is intentional. + public static let resourcesRequiredText = """ + You'll also need to add the resources in the Resources + directory into your target's main bundle. + + """ + + // Make the struct un-initializable. + @available(*, unavailable) + init() { fatalError() } +} + +/// A package of files to install for a specific Pod. Used for closed source SDKs in the framework +/// generation step to determine the files required during packaging. +private struct FilesToInstall { + /// All frameworks required for the product to function. + let frameworks: [URL] + /// Any resources required for the product. + let resourceBundles: [URL] + + /// Default initializer. + init(frameworks: [URL], resourceBundles: [URL] = []) { + self.frameworks = frameworks + self.resourceBundles = resourceBundles + } +} + +/// A zip file builder. The zip file can be built with the `build()` function. +struct ZipBuilder { + struct FilesystemPaths { + // MARK: - Required Paths + + /// The path to the CoreDiagnostics.framework directory with the Zip flag enabled. + var coreDiagnosticsDir: URL + + /// The path to the directory containing the blank xcodeproj and Info.plist for building source + /// based frameworks. + var templateDir: URL + + // MARK: - Optional Paths + + /// A file URL to a textproto with the contents of a `ZipBuilder_FirebaseSDKs` object. Used to + /// verify expected version numbers. + var allSDKsPath: URL? + + /// A file URL to a textproto with the contents of a `ZipBuilder_Release` object. Used to verify + /// expected version numbers. + var currentReleasePath: URL? + + /// Default initializer with all required paths. + init(templateDir: URL, coreDiagnosticsDir: URL) { + self.templateDir = templateDir + self.coreDiagnosticsDir = coreDiagnosticsDir + } + } + + /// Custom CocoaPods spec repos to be used. If not provided, the tool will only use the CocoaPods + /// master repo. + private let customSpecRepos: [URL]? + + /// Paths needed throughout the process of packaging the Zip file. + private let paths: FilesystemPaths + + /// Determines if the cache should be used or not. + private let useCache: Bool + + /// Default initializer. If allSDKsPath and currentReleasePath are provided, it will also verify + /// that the + /// + /// - Parameters: + /// - paths: Paths that are needed throughout the process of packaging the Zip file. + /// - customSpecRepo: A custom spec repo to be used for fetching CocoaPods from. + /// - useCache: Enables or disables the cache. + init(paths: FilesystemPaths, + customSpecRepos: [URL]? = nil, + useCache: Bool = false) { + self.paths = paths + self.customSpecRepos = customSpecRepos + self.useCache = useCache + } + + /// Try to build and package the contents of the Zip file. This will throw an error as soon as it + /// encounters an error, or will quit due to a fatal error with the appropriate log. + /// + /// - Returns: A URL to the folder that should be compressed and distributed. + /// - Throws: One of many errors that could have happened during the build phase. + func buildAndAssembleZipDir() throws -> URL { + let projectDir = FileManager.default.temporaryDirectory(withName: "project") + + // If it exists, remove it before we re-create it. This is simpler than removing all objects. + if FileManager.default.directoryExists(at: projectDir) { + try FileManager.default.removeItem(at: projectDir) + } + + do { + // Create the directory and all intermediate directories. + try FileManager.default.createDirectory(at: projectDir, + withIntermediateDirectories: true, + attributes: nil) + } catch { + // Use `do/catch` instead of `guard let tempDir = try?` so we can print the error thrown. + fatalError("Cannot create temporary directory at beginning of script: \(error)") + } + + // Copy the Xcode project needed in order to be able to install Pods there. + let templateFiles = Constants.ProjectPath.requiredFilesForBuilding.map { + paths.templateDir.appendingPathComponent($0) + } + for file in templateFiles { + // Each file should be copied to the temporary project directory with the same name. + let destination = projectDir.appendingPathComponent(file.lastPathComponent) + do { + if !FileManager.default.fileExists(atPath: destination.path) { + print("Copying template file \(file) to \(destination)...") + try FileManager.default.copyItem(at: file, to: destination) + } + } catch { + fatalError("Could not copy template project to temporary directory in order to install " + + "pods. Failed while attempting to copy \(file) to \(destination). \(error)") + } + } + + // Get the README template ready (before attempting to build everything in case this fails, + // otherwise debugging it will take a long time). + let readmePath = paths.templateDir.appendingPathComponent(Constants.ProjectPath.readmeName) + let readmeTemplate: String + do { + readmeTemplate = try String(contentsOf: readmePath) + } catch { + fatalError("Could not get contents of the README template: \(error)") + } + + // Break the `subspecsToInstall` into a variable since it's helpful when debugging non-cache + // builds to just install a subset: `[.core, .analytics, .storage, .firestore]` for example. + let subspecsToInstall = Subspec.allCases + + // Remove CocoaPods cache so the build gets updates after a version is rebuilt during the + // release process. + CocoaPodUtils.cleanPodCache() + + // We need to install all the subpsecs in order to get every single framework that we'll need + // for the zip file. We can't install each one individually since some pods depend on different + // subspecs from the same pod (ex: GoogleUtilities, GoogleToolboxForMac, etc). All of the code + // wouldn't be included so we need to install all of the subspecs to catch the superset of all + // required frameworks, then use that as the source of frameworks to pull from when including + // the folders in each product directory. + CocoaPodUtils.installSubspecs(subspecsToInstall, + inDir: projectDir, + customSpecRepos: customSpecRepos) + + // If any expected versions were passed in, we should verify that those were actually installed + // and get the list of actual versions we'll be using to build the Zip file. This method will + // throw a fatalError if any versions are mismatched. + validateExpectedVersions(inProjectDir: projectDir) + + let installedPods = CocoaPodUtils.installedPodsInfo(inProjectDir: projectDir) + let filesToInstall = generateFrameworksWithResources(fromPods: installedPods, + inProjectDir: projectDir, + useCache: useCache) + + // Create an array that has the Pod name as the key and the array of frameworks needed - this + // will be used as the source of truth for all frameworks to be copied in each product's + // directory. + var frameworks = filesToInstall.mapValues { $0.frameworks } + for (framework, paths) in frameworks { + print("Frameworks for pod: \(framework) were compiled at \(paths)") + } + + // Overwrite the `FirebaseCoreDiagnostics.framework` in the `FirebaseAnalytics` folder. This is + // needed because it was compiled specifically with the `ZIP` bit enabled, helping us understand + // the distribution of CocoaPods vs Zip file integrations. + if subspecsToInstall.contains(.analytics) { + let overriddenAnalytics: [URL] = { + guard let currentFrameworks = frameworks["FirebaseAnalytics"] else { + fatalError("Attempted to replace CoreDiagnostics framework but the FirebaseAnalytics " + + "directory does not exist. Existing directories: \(frameworks.keys)") + } + + // Filter out any CoreDiagnostics directories from the frameworks to install. There should + // only be one. + let withoutDiagnostics: [URL] = currentFrameworks.filter { url in + url.lastPathComponent != "FirebaseCoreDiagnostics.framework" + } + + return withoutDiagnostics + [paths.coreDiagnosticsDir] + }() + + // Set the newly required framework paths for Analytics. + frameworks["FirebaseAnalytics"] = overriddenAnalytics + } + + // TODO: The folder heirarchy should change in Firebase 6. + // Time to assemble the folder structure of the Zip file. In order to get the frameworks + // required, we will `pod install` only those subspecs and then fetch the information for all + // the frameworks that were installed, copying the frameworks from our list of compiled + // frameworks. The whole process is: + // 1. Copy any required files (headers, modulemap, etc) over beforehand to fail fast if anything + // is misconfigured. + // 2. Get the frameworks required for Analytics, copy them to the Analytics folder. + // 3. Go through the rest of the subspecs (excluding those included in Analytics) and copy them + // to a folder with the name of the subspec. + // 4. Assemble the `README` file based off the template and copy it to the directory. + // 5. Return the URL of the folder containing the contents of the Zip file. + + // Create the directory that will hold all the contents of the Zip file. + let zipDir = FileManager.default.temporaryDirectory(withName: "Firebase") + do { + if FileManager.default.directoryExists(at: zipDir) { + try FileManager.default.removeItem(at: zipDir) + } + + try FileManager.default.createDirectory(at: zipDir, + withIntermediateDirectories: true, + attributes: nil) + } + + // Copy all the other required files to the Zip directory. + let distributionFiles = Constants.ProjectPath.requiredFilesForDistribution.map { + paths.templateDir.appendingPathComponent($0) + } + for file in distributionFiles { + // Each file should be copied to the destination project directory with the same name. + let destination = zipDir.appendingPathComponent(file.lastPathComponent) + do { + if !FileManager.default.fileExists(atPath: destination.path) { + print("Copying final distribution file \(file) to \(destination)...") + try FileManager.default.copyItem(at: file, to: destination) + } + } catch { + fatalError("Could not copy final distribution files to temporary directory before " + + "building. Failed while attempting to copy \(file) to \(destination). \(error)") + } + } + + // Start with installing Analytics, since we'll need to exclude those frameworks from the rest + // of the folders. + let analyticsFrameworks: [String] + let analyticsDir: URL + do { + // This returns the Analytics directory and a list of framework names that Analytics reqires. + /// Example: ["FirebaseInstanceID", "GoogleAppMeasurement", "nanopb", <...>] + let (dir, frameworks) = try installAndCopyFrameworks(forSubspec: .analytics, + projectDir: projectDir, + rootZipDir: zipDir, + builtFrameworks: frameworks) + analyticsFrameworks = frameworks + analyticsDir = dir + } catch { + fatalError("Could not copy frameworks from Analytics into the zip file: \(error)") + } + + // Start the README dependencies string with the frameworks built in Analytics. + var readmeDeps = dependencyString(for: .analytics, + in: analyticsDir, + frameworks: analyticsFrameworks) + + // Loop through all the other subspecs that aren't Core and Analytics and write them to their + // final destination, including resources. + let resourceBundles = filesToInstall.mapValues({ $0.resourceBundles }).filter({ !$0.value.isEmpty }) + let remainingSubspecs = subspecsToInstall.filter { $0 != .analytics && $0 != .core } + for spec in remainingSubspecs { + do { + let (specDir, podFrameworks) = + try installAndCopyFrameworks(forSubspec: spec, + projectDir: projectDir, + rootZipDir: zipDir, + builtFrameworks: frameworks, + podsToIgnore: analyticsFrameworks, + foldersToIgnore: spec.duplicateFrameworksToRemove()) + + // Copy any Resources from closed source pods into their destination folder. Open source + // pods have already had the Resources taken care of. + for pod in podFrameworks { + guard let bundles = resourceBundles[pod] else { continue } + + // If Resources should be excluded (for example from MLKit), move on to the next one. + guard !spec.excludeResources else { continue } + + // There are bundles to copy! Create a Resources directory. + let resourceDir = specDir.appendingPathComponent("Resources", isDirectory: true) + try FileManager.default.createDirectory(at: resourceDir, + withIntermediateDirectories: true, + attributes: nil) + + // Copy each bundle individually, skipping duplicates. + let bundlesToSkip = spec.duplicateResourcesToRemove() + for bundle in bundles { + let name = bundle.lastPathComponent + guard !bundlesToSkip.contains(name) else { continue } + + let destination = resourceDir.appendingPathComponent(name, isDirectory: true) + try FileManager.default.copyItem(at: bundle, to: destination) + } + } + + readmeDeps += dependencyString(for: spec, in: specDir, frameworks: podFrameworks) + } catch { + fatalError("Could not copy frameworks from \(spec.rawValue) into the zip file: \(error)") + } + } + + // Assemble the README. Start with the version text, then use the template to inject the + // versions and the list of frameworks to include for each pod. + let versionsText = versionsString(for: installedPods) + let readmeText = readmeTemplate.replacingOccurrences(of: "__INTEGRATION__", with: readmeDeps) + .replacingOccurrences(of: "__VERSIONS__", with: versionsText) + do { + try readmeText.write(to: zipDir.appendingPathComponent(Constants.ProjectPath.readmeName), + atomically: true, + encoding: .utf8) + } catch { + fatalError("Could not write README to Zip directory: \(error)") + } + + // TODO: Remove this manual copy once FirebaseCrash is removed from the Zip file. + // Copy over the Crash scripts, if Crash should be installed + if subspecsToInstall.contains(.crash) { + do { + let crashDir = paths.templateDir.appendingPathComponent(Constants.ProjectPath.crashDir) + let crashFiles = try FileManager.default.contentsOfDirectory(at: crashDir, + includingPropertiesForKeys: nil, + options: []) + let crashZipDir = zipDir.appendingPathComponent("Crash") + for file in crashFiles { + let destination = crashZipDir.appendingPathComponent(file.lastPathComponent) + try FileManager.default.copyItem(at: file, to: destination) + } + } catch { + fatalError("Could not copy extra Crash tools: \(error)") + } + } + + print("Contents of the Zip file were assembled at: \(zipDir)") + return zipDir + } + + // MARK: - Private + + /// Copies all frameworks from the `InstalledPod` (pulling from the `frameworkLocations`) and copy + /// them to the destination directory. + /// + /// - Parameters: + /// - installedPods: All the Pods installed for a given set of subspecs, which will be used as a + /// list to find out what frameworks to copy to the destination. + /// - dir: Destination directory for all the frameworks. + /// - frameworkLocations: A dictionary containing the pod name as the key and a location to + /// the compiled frameworks. + /// - ignoreFrameworks: A list of Pod + /// - Throws: Various FileManager errors in case the copying fails, or an error if the framework + // doesn't exist in `frameworkLocations`. + private func copyFrameworks(fromPods installedPods: [CocoaPodUtils.PodInfo], + toDirectory dir: URL, + frameworkLocations: [String: [URL]], + podsToIgnore: [String], + foldersToIgnore: [String]) throws { + let fileManager = FileManager.default + if !fileManager.directoryExists(at: dir) { + try fileManager.createDirectory(at: dir, withIntermediateDirectories: false, attributes: nil) + } + + // Loop through each InstalledPod item and get the name so we can fetch the framework and copy + // it to the destination directory. + for pod in installedPods { + // Skip the Firebase pod, any Interop pods, and specifically ignored frameworks. + guard pod.name != "Firebase", + !pod.name.contains("Interop"), + !podsToIgnore.contains(pod.name) else { + continue + } + + guard let frameworks = frameworkLocations[pod.name] else { + let reason = "Unable to find frameworks for \(pod.name) in cache of frameworks built to " + + "include in the Zip file for that framework's folder." + let error = NSError(domain: "com.firebase.zipbuilder", + code: 1, + userInfo: [NSLocalizedDescriptionKey: reason]) + throw error + } + + // Copy each of the frameworks over, unless it's explicitly ignored. + for framework in frameworks { + let frameworkName = framework.lastPathComponent + if foldersToIgnore.contains(frameworkName) { + continue + } + + let destination = dir.appendingPathComponent(frameworkName) + try fileManager.copyItem(at: framework, to: destination) + } + } + } + + /// Creates the String required for this subspec to be added to the README. Creates a header and + /// lists each framework in alphabetical order with the appropriate indentation, as well as a + /// message about resources if they exist. + /// + /// - Parameters: + /// - subspec: The subspec that requires documentation. + /// - dir: The directory where everything lives. Used to check if the spec has resources. + /// - frameworks: All the frameworks required by the subspec. + /// - Returns: A string with a header for the subspec name, and a list of frameworks required to + /// integrate for the product to work. Formatted and ready for insertion into the + /// README. + private func dependencyString(for subspec: Subspec, in dir: URL, frameworks: [String]) -> String { + var result = subspec.readmeHeader() + for framework in frameworks.sorted() { + result += "- \(framework).framework\n" + } + + result += "\n" + + // Check if there is a Resources directory, and if so, add the disclaimer to the dependency + // string. + do { + let fileManager = FileManager.default + let resourceDirs = try fileManager.recursivelySearch(for: .directories(name: "Resources"), + in: dir) + if !resourceDirs.isEmpty { + result += Constants.resourcesRequiredText + } + } catch { + fatalError(""" + Tried to find Resources directory for \(subspec) in order to build the README, but an error + occurred: \(error). + """) + } + + return result + } + + /// Assembles the expected versions based on the release manifests passed in, if they were. + /// Returns an array with the SDK name as the key and version as the value, + private func expectedVersions() -> [String: String] { + // Merge the versions from the current release and the known public versions. + var releasingVersions: [String: String] = [:] + + // Check the existing expected versions and build a dictionary out of the expected versions. + if let sdksPath = paths.allSDKsPath { + let allSDKs = ManifestReader.loadAllReleasedSDKs(fromTextproto: sdksPath) + print("Parsed the following SDKs from the public release manifest:") + + for sdk in allSDKs.sdk { + releasingVersions[sdk.name] = sdk.publicVersion + print("\(sdk.name): \(sdk.publicVersion)") + } + } + + // Override any of the expected versions with the current release manifest, if it exists. + if let releasePath = paths.currentReleasePath { + let currentRelease = ManifestReader.loadCurrentRelease(fromTextproto: releasePath) + print("Overriding the following SDKs, taken from the current release manifest:") + for sdk in currentRelease.sdk { + releasingVersions[sdk.sdkName] = sdk.sdkVersion + print("\(sdk.sdkName): \(sdk.sdkVersion)") + } + } + + if !releasingVersions.isEmpty { + print("Final expected versions for the Zip file: \(releasingVersions)") + } + + return releasingVersions + } + + /// Installs a subspec and attempts to copy all the frameworks required for it from + /// `buildFramework` and puts them into a new directory in the `rootZipDir` matching the + /// subspec's name. This also will move any Resources directory outside of the frameworks and + /// place them in the same directory as the rest of the frameworks. + /// + /// - Parameters: + /// - subspec: The subspec to install and get the dependencies list. + /// - projectDir: Root of the project containing the Podfile. + /// - rootZipDir: The root directory to be turned into the Zip file. + /// - builtFrameworks: All frameworks that have been built, with the framework name as the key + /// and the framework's location as the value. + /// - podsToIgnore: Pods to avoid copying, if any. + /// - foldersToIgnore: Specific folders to avoid copying, if any. + /// - Throws: Throws various errors from copying frameworks. + /// - Returns: The directory containing all the frameworks and the names of the frameworks that + /// were copied for this subspec. + @discardableResult + func installAndCopyFrameworks( + forSubspec subspec: Subspec, + projectDir: URL, + rootZipDir: URL, + builtFrameworks: [String: [URL]], + podsToIgnore: [String] = [], + foldersToIgnore: [String] = [] + ) throws -> (output: URL, frameworks: [String]) { + let installedPods = CocoaPodUtils.installSubspecs([subspec], inDir: projectDir, customSpecRepos: customSpecRepos) + let productDir = rootZipDir.appendingPathComponent(subspec.rawValue) + try copyFrameworks(fromPods: installedPods, + toDirectory: productDir, + frameworkLocations: builtFrameworks, + podsToIgnore: podsToIgnore, + foldersToIgnore: foldersToIgnore) + + // Return the names of all the installed frameworks. + let namedFrameworks = installedPods.map { $0.name } + let copiedFrameworks = namedFrameworks.filter { + // Only return the frameworks that aren't contained in the "podsToIgnore" array, aren't an + // interop framework (since they don't compile to frameworks), or the Firebase pod itself. + !(podsToIgnore.contains($0) || $0.hasSuffix("Interop") || $0 == "Firebase") + } + + return (productDir, copiedFrameworks) + } + + /// Validates that the expected versions (based on the release manifest passed in, if there was + /// one) match the expected versions installed and listed in the Podfile.lock in a project + /// directory. + /// + /// - Parameter projectDir: The directory containing the Podfile.lock file of installed pods. + private func validateExpectedVersions(inProjectDir projectDir: URL) { + // Get the expected versions based on the release manifests, if there are any. We'll use this to + // validate the versions pulled from CocoaPods. Expected versions could be empty, in which case + // validation succeeds. + let expected = expectedVersions() + if !expected.isEmpty { + // There are some expected versions, read from the CocoaPods Podfile.lock and grab the + // installed versions. + let podfileLock: String + do { + podfileLock = try String(contentsOf: projectDir.appendingPathComponent("Podfile.lock")) + } catch { + fatalError("Could not read contents of `Podfile.lock` to validate versions in " + + "\(projectDir): \(error)") + } + + // Get the versions in the format of [PodName: VersionString]. + let actual = CocoaPodUtils.loadVersionsFromPodfileLock(contents: podfileLock) + + // Loop through the expected versions and verify the actual versions match. + for podName in expected.keys where !podName.contains("SmartReply") { + guard let actualVersion = actual[podName], + let expectedVersion = expected[podName], + actualVersion == expectedVersion else { + fatalError(""" + Version mismatch from expected versions and version installed in CocoaPods: + Pod Name: \(podName) + Expected Version: \(String(describing: expected[podName])) + Actual Version: \(String(describing: actual[podName])) + Please verify that the expected version is correct, and the Podspec dependencies are + appropriately versioned. + """) + } + + print("Successfully verified version of \(podName) is \(actualVersion)") + } + } + } + + /// Creates the String that displays all the versions of each pod, in alphabetical order. + /// + /// - Parameter pods: All pods that were installed, with their versions. + /// - Returns: A String to be added to the README. + private func versionsString(for pods: [CocoaPodUtils.PodInfo]) -> String { + // Get the longest name in order to generate padding with spaces so it looks nicer. + let maxLength: Int = { + guard let pod = pods.max(by: { $0.name.count < $1.name.count }) else { + // The longest pod as of this writing is 29 characters, if for whatever reason this fails + // just assume 30 characters long. + return 30 + } + + // Return room for a space afterwards. + return pod.name.count + 1 + }() + + let header: String = { + // Center the CocoaPods title within the spaces given. If there's an odd number of spaces, add + // the extra space after the CocoaPods title. + let cocoaPods = "CocoaPod" + let spacesToPad = maxLength - cocoaPods.count + let halfPadding = String(repeating: " ", count: spacesToPad / 2) + + // Start with the spaces padding, then add the CocoaPods title. + var result = halfPadding + cocoaPods + halfPadding + if spacesToPad % 2 != 0 { + // Add an extra space since the padding isn't even + result += " " + } + + // Add the versioning text and return. + result += "| Version\n" + + // Add a line underneath each. + result += String(repeating: "-", count: maxLength) + "|" + String(repeating: "-", count: 9) + result += "\n" + return result + }() + + // Sort the pods by name for a cleaner display. + let sortedPods = pods.sorted { $0.name < $1.name } + + // Get the name and version of each pod, padding it along the way. + var podVersions: String = "" + for pod in sortedPods { + // Insert the name and enough spaces to reach the end of the column. + let podName = pod.name + podVersions += podName + String(repeating: " ", count: maxLength - podName.count) + + // Add a pipe and the version. + podVersions += "| " + pod.version + "\n" + } + + return header + podVersions + } + + // MARK: - Framework Generation + + /// Generates all the .framework files from a Pods directory. This will go through the contents of + /// the directory, copy the .frameworks to a temporary directory and compile any source based + /// CocoaPods. Returns a dictionary with the framework name for the key and all information for + /// files to install (frameworks and resources). + private func generateFrameworksWithResources(fromPods pods: [CocoaPodUtils.PodInfo], + inProjectDir projectDir: URL, + useCache: Bool = false) -> [String: FilesToInstall] { + // Verify the Pods folder exists and we can get the contents of it. + let fileManager = FileManager.default + + // Create the temporary directory we'll be storing the build/assembled frameworks in, and remove + // the Resources directory if it already exists. + let tempDir = fileManager.temporaryDirectory(withName: "all_frameworks") + let tempResourceDir = tempDir.appendingPathComponent("Resources") + do { + try fileManager.createDirectory(at: tempDir, + withIntermediateDirectories: true, + attributes: nil) + if fileManager.directoryExists(at: tempResourceDir) { + try fileManager.removeItem(at: tempResourceDir) + } + } catch { + fatalError("Cannot create temporary directory to store frameworks and resources from the " + + "full build: \(error)") + } + + // Loop through each pod folder and check if the frameworks already exist, or they need to be + // compiled. If they exist, add them to the frameworks dictionary. + var toInstall: [String: FilesToInstall] = [:] + for pod in pods { + var frameworks: [URL] = [] + // Ignore any Interop pods or the Firebase umbrella pod. + guard !pod.name.contains("Interop"), pod.name != "Firebase" else { + continue + } + + // Get all the frameworks contained in this directory. + var foundFrameworks: [URL] + do { + foundFrameworks = try fileManager.recursivelySearch(for: .frameworks, + in: pod.installedLocation) + } catch { + fatalError("Cannot search for .framework files in Pods directory " + + "\(pod.installedLocation): \(error)") + } + + // Get the resulting folder that will contain all resources for that Pod. + let podResourceDir = tempResourceDir.appendingPathComponent(pod.name) + var resourceBundles: [URL] = [] + + // If there are no frameworks, it's an open source pod and we need to compile the source to + // get a framework. + if foundFrameworks.isEmpty { + let builder = FrameworkBuilder(projectDir: projectDir) + let (framework, resourceDir) = builder.buildFramework(withName: pod.name, + version: pod.version, + cacheKey: pod.cacheKey, + cacheEnabled: useCache) + + // Move all the Resources that are contained in the resourceDir returned. + do { + resourceBundles = try ResourcesManager.moveAllBundles(inDirectory: resourceDir, + to: podResourceDir) + } catch { + fatalError("Could not move Resource bundles for \(pod.name): \(error)") + } + + frameworks = [framework] + } else { + // Copy found frameworks to a known temporary directory, and store that location. Also move + // the resources inside the .framework to be consistent with the compiled frameworks, which + // they'll be moved out afterwards. + for framework in foundFrameworks { + // Copy it to the temporary directory and save it to our list of frameworks. + let copiedLocation = tempDir.appendingPathComponent(framework.lastPathComponent) + + // Remove the framework if it exists since it could be out of date. + fileManager.removeDirectoryIfExists(at: copiedLocation) + do { + try fileManager.copyItem(at: framework, to: copiedLocation) + } catch { + fatalError("Cannot copy framework at \(framework) to \(copiedLocation) while " + + "attempting to generate frameworks. \(error)") + } + + frameworks.append(copiedLocation) + } + + // There are two sitautions for Resources in closed source Pods depending on what they use + // in their Podspec. Pods can define either pre-built bundles or a list of files for any + // number of bundles to be created. We'll search for any pre-built bundles, and if there + // aren't any, look in all the included Pods to see if there are Resources folders + // available to build bundles from. The latter is necessary for GoogleMobileVision and + // MLKit. + + // Search for any pre-built bundles. + do { + resourceBundles = try ResourcesManager.moveAllBundles(inDirectory: pod.installedLocation, + to: podResourceDir) + } catch { + fatalError("Cannot move Resource bundles for \(pod.name): \(error)") + } + + // Smart Reply packages Resources separately from other MLKit subspecs. + if pod.name == "FirebaseMLNLSmartReply" { + do { + resourceBundles = try ResourcesManager.createBundleForFoldersInResourcesDirs( + containedIn: pod.installedLocation, destinationDir: podResourceDir + ) + } catch { + fatalError("Could not generate Resource bundles for \(pod.name): \(error)") + } + } + + // Special case for MLKit *Model subspecs, explicitly copy directories from + // GoogleMobileVision. This should be fixed in the future to pull all compiled resources + // from Xcode's build directory. + if pod.name == "FirebaseMLVisionTextModel" || pod.name == "FirebaseMLVisionFaceModel" { + do { + let podsDir = pod.installedLocation.deletingLastPathComponent() + let gmvDir = podsDir.appendingPathComponent("GoogleMobileVision") + resourceBundles = try ResourcesManager.createBundleForFoldersInResourcesDirs( + containedIn: gmvDir, destinationDir: podResourceDir + ) + } catch { + fatalError("Could not generate Resource bundles for \(pod.name): \(error)") + } + } + } + + let podFiles = FilesToInstall(frameworks: frameworks, resourceBundles: resourceBundles) + toInstall[pod.name] = podFiles + } + + return toInstall + } +} diff --git a/ZipBuilder/Sources/ZipBuilder/main.swift b/ZipBuilder/Sources/ZipBuilder/main.swift new file mode 100644 index 00000000000..9764acef772 --- /dev/null +++ b/ZipBuilder/Sources/ZipBuilder/main.swift @@ -0,0 +1,86 @@ +/* + * Copyright 2019 Google + * + * 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 + +// Get the launch arguments, parsed by user defaults. +let args = LaunchArgs() + +// Clear the cache if requested. +if args.deleteCache { + do { + let cacheDir = try FileManager.default.firebaseCacheDirectory() + try FileManager.default.removeItem(at: cacheDir) + } catch { + fatalError("Could not empty the cache before building the zip file: \(error)") + } +} + +// Keep timing for how long it takes to build the zip file for information purposes. +let buildStart = Date() +var cocoaPodsUpdateMessage: String = "" + +// Do a Pod Update if requested. +if args.updatePodRepo { + CocoaPodUtils.updateRepos() + cocoaPodsUpdateMessage = "CocoaPods took \(-buildStart.timeIntervalSinceNow) seconds to update." +} + +var paths = ZipBuilder.FilesystemPaths(templateDir: args.templateDir, + coreDiagnosticsDir: args.coreDiagnosticsDir) +paths.allSDKsPath = args.allSDKsPath +paths.currentReleasePath = args.currentReleasePath +let builder = ZipBuilder(paths: paths, + customSpecRepos: args.customSpecRepos, + useCache: args.cacheEnabled) + +do { + // Build the zip file and get the path. + let location = try builder.buildAndAssembleZipDir() + print("Location of directory to be Zipped: \(location)") + + print("Attempting to Zip the directory...") + let zipped = Zip.zipContents(ofDir: location) + + // If an output directory was specified, copy the Zip file to that directory. Otherwise just print + // the location for further use. + if let outputDir = args.outputDir { + do { + let destination = outputDir.appendingPathComponent(zipped.lastPathComponent) + try FileManager.default.copyItem(at: zipped, to: destination) + } catch { + fatalError("Could not copy Zip file to output directory: \(error)") + } + } else { + print("Success! Zip file can be found at \(zipped.path)") + } + + // Get the time since the start of the build to get the full time. + let secondsSinceStart = -Int(buildStart.timeIntervalSinceNow) + print(""" + Time profile: + It took \(secondsSinceStart) seconds (~\(secondsSinceStart / 60)m) to build the zip file. + \(cocoaPodsUpdateMessage) + """) +} catch { + let secondsSinceStart = -buildStart.timeIntervalSinceNow + print(""" + Time profile: + The build failed in \(secondsSinceStart) seconds (~\(secondsSinceStart / 60)m). + \(cocoaPodsUpdateMessage) + """) + fatalError("Could not build the zip file: \(error)") +} diff --git a/ZipBuilder/Template/Crash/batch-upload b/ZipBuilder/Template/Crash/batch-upload new file mode 100755 index 00000000000..053a3ee7fe3 --- /dev/null +++ b/ZipBuilder/Template/Crash/batch-upload @@ -0,0 +1,416 @@ +#!/bin/bash + +usage () { + echo >&2 "usage: ${0##*/} [-hv] [-p google-service] [-i info] service-account-file {mach-o file|uuid} ..." +} + +help () { + usage + cat >&2 </dev/null)"}" +fi + +var_check FCR_PROD_VERS FCR_BUNDLE_ID + +ERROR=$'environment variable empty or unset\n\nExplicitly add to environment or set GoogleService-Info.plist (-p)\nand Info.plist (-i) flags to extract values from the files.\n\nTry "'"$0"' -h" for details.' + +: "${FIREBASE_API_KEY:?"${ERROR}"}" "${FIREBASE_APP_ID:?"${ERROR}"}" +: "${FCR_PROD_VERS:?"${ERROR}"}" "${FCR_BUNDLE_ID:?"${ERROR}"}" + +# Extract key from legacy cache. + +if [[ ! "${SERVICE_ACCOUNT_FILE}" ]]; then + xcwarning "Running extract-keys on desktop." + EXTRACT_KEYS="$(script_dir)/extract-keys" + (cd "${HOME}/Desktop"; "${EXTRACT_KEYS}") || exit $? + SERVICE_ACCOUNT_FILE="${HOME}/Desktop/${FIREBASE_APP_ID}.json" + xcdebug "Using ${SERVICE_ACCOUNT_FILE} as account file. Please move this and all other extracted keys to a safe place." +fi + +if [[ ! -f "${SERVICE_ACCOUNT_FILE}" ]]; then + echo >&2 "Unable to find service account file." + echo >&2 + usage + exit 2 +fi + +# usage: extract_symbols_and_upload *dwarf-file* *arch* *exe-file* +# +# Do NOT use the dSYM bundle path. While it may work on occasion, it +# is not guaranteed to do so; the full path to the DWARF companion +# file will always work. (Discovered by Kerem Erkan.) +# +# If the executable is empty, use the DWARF companion file as a proxy +# for the executable. +extract_symbols_and_upload () { + local DWARF_COMPANION="$1" ARCH="$2" EXECUTABLE="$3" + + if [[ ! "${EXECUTABLE}" ]]; then + xcdebug "No executable; using ${DWARF_COMPANION} as symbol source." + + EXECUTABLE="${DWARF_COMPANION}" + unset DWARF_COMPANION + fi + + [[ "${EXECUTABLE}" ]] || return 1 + + if [[ -x "${SWIFT_DEMANGLE:=$(xcrun --find swift-demangle 2>/dev/null)}" ]]; + then + SWIFT_DEMANGLE_COMMAND="${SWIFT_DEMANGLE} -simplified" + else + SWIFT_DEMANGLE_COMMAND=/bin/cat + fi + fcr_mktemp SYMBOL_FILE + + "${DUMP_SYMS:="$(script_dir)/dump_syms"}" -a "${ARCH}" ${DWARF_COMPANION:+-g "${DWARF_COMPANION}"} "${EXECUTABLE}" | ${SWIFT_DEMANGLE_COMMAND} >|"${SYMBOL_FILE}" || return $? + + fcr_upload_files "${SYMBOL_FILE}" || return $? +} + +# usage: is_executable *path* +# +# Check to see if the file is an executable or a dSYM bundle +is_executable () { + [[ -f "$1" || ( -d "$1" && "${1%/}" == *.dSYM ) ]] +} + +# usage: is_uuid *string* +# +# Verify that the argument is a UUID. +is_uuid () { + [[ "$1" =~ ^[[:xdigit:]]{8}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{4}-[[:xdigit:]]{12}$ ]] +} + +# usage: set_uuids_archs *mach-o-file* +# +# side effect: appends to UUIDS, ARCHS +# +# Extract the uuid and architecture information from the given Mach-O +# file and append the information to the UUIDS and ARCHS arrays. +set_uuids_archs () { + eval "$(dwarfdump --uuid "$1" | awk '/^UUID:/ { print "UUIDS+=(" $2 "); ARCHS+=" $3 }')" +} + +# usage: mdls_to_bash +# +# Convert the output of mdls to a string consumable by bash. mdls +# outputs string arrays as quoted strings separated by commas, and +# Unicode characters as '\Uxxxx'. +# +# Note: this is sensitive to the current locale. If the locale is not +# UTF-8, then wide-character warnings will result if the strings +# contain non-ASCII characters. This is actually a desired behavior, +# because bash has issues with non-Unicode encodings for file names. +# (The macOS default is to have UTF-8 enabled, so this should not be a +# problem for the majority of use cases.) +mdls_to_bash () { + perl -C -ple 's/,$//; s/\\U(....)/chr hex $1/ge' +} + +for EXE; do + if is_executable "${EXE}"; then + xcdebug "Assuming ${EXE} is an executable or dSYM bundle." + + # Import architecture UUID information + UUIDS=() ARCHS=() + set_uuids_archs "${EXE}" + + for I in "${!UUIDS[@]}"; do + xcdebug "Found ${UUIDS[$I]} for ${ARCHS[$I]} in ${EXE}" + done + + if ((${#UUIDS[*]} == 0)); then + xcwarning "${EXE} exists, but has no architecture information." + continue + fi + + if [[ "${EXE}" = *.dSYM ]]; then + xcdebug "Removing dSYM bundle as executable target." + unset EXE + fi + + elif is_uuid "${EXE}"; then + xcdebug "${EXE} looks like a UUID to me." + UUIDS=("${EXE}"); unset EXE + + else + xcwarning "${EXE}: not an executable, bundle, or UUID." + continue + fi + + BUNDLES=() + + for UUID in "${UUIDS[@]}"; do + xcdebug "Searching for ${UUID} ..." + + QUERY_UUID="com_apple_xcode_dsym_uuids == '${UUID}'" + QUERY_TYPE="kMDItemContentType == 'com.apple.xcode.dsym' || kMDItemContentType == 'com.apple.xcode.archive'" + QUERY="(${QUERY_UUID}) && (${QUERY_TYPE})" + + if ((VERBOSE > 1)); then + xcnote "Passing query \"${QUERY}\" to mdfind." + fi + + MD_FIND_RESULT=() + + eval "$(mdfind "${QUERY}" -0 | xargs -0 perl -le 'print "MD_FIND_RESULT+=(\Q$_\E)" for @ARGV')" + + xcdebug "mdfind returned (${MD_FIND_RESULT[*]})" + + # BUNDLES should contain no duplicates. + for I in "${!MD_FIND_RESULT[@]}"; do + for BUNDLE in "${BUNDLES[@]}"; do + if [[ "${MD_FIND_RESULT[$I]}" == "$BUNDLE" ]]; then + unset "MD_FIND_RESULT[$I]" + fi + done + done + + BUNDLES+=("${MD_FIND_RESULT[@]}") + done + + if [[ ${#BUNDLES[@]} == 0 && ${#ARCHS[@]} == 0 ]]; then + xcwarning "No executable or bundle found for ${UUIDS[*]}." + xcnote "Try passing in the executable itself instead of a UUID." + continue + fi + + xcdebug "BUNDLES = (${BUNDLES[*]})" + + if [[ ${#BUNDLES[@]} == 0 ]]; then + xcdebug "No dSYM bundle found." + + # The dSYM has to be on a normal volume (not temporary). It + # can, however, be shared among multiple executables. + if [[ ! "${SCRATCH_BUNDLE}" ]]; then + SCRATCH_BUNDLE="${HOME}/com.google.BatchUploadScratchFile.dSYM" + FCR_TEMPORARY_FILES+=("${SCRATCH_BUNDLE}") + fi + + xcdebug "Creating one in ${SCRATCH_BUNDLE}" + + BUNDLES=("${SCRATCH_BUNDLE}") + + # Create the dSYM bundle. This may produce an empty dSYM + # bundle if the executable has no debugging information. + xcrun dsymutil -o "${BUNDLES[0]}" "${EXE}"; STATUS=$? + + if ((STATUS)); then + xcwarning "Command dsymutil failed with exit code ${STATUS}." + continue + fi + + # Import the dSYM bundle. There is a momentary delay between + # creating the bundle and having it indexed; explicitly + # importing guarantees the mds database is up-to-date when we + # ask it for information about UUIDs and paths. + mdimport "${SCRATCH_BUNDLE}"; STATUS=$? + + if ((STATUS)); then + xcwarning "Command mdimport failed with exit code ${STATUS}." + continue + fi + fi + + SEEN_ARCH=() SEEN_PATH=() + + for BUNDLE in "${BUNDLES[@]}"; do + typeset -a BNDL_UUIDS BNDL_PATHS # keeps ShellLint happy + + eval "BNDL_UUIDS=$(mdls -raw -name com_apple_xcode_dsym_uuids "${BUNDLE}" | mdls_to_bash)" + eval "BNDL_PATHS=$(mdls -raw -name com_apple_xcode_dsym_paths "${BUNDLE}" | mdls_to_bash)" + + # Neither of these SHOULD occur, but curious things happen out + # in the field. + if ((${#BNDL_UUIDS[@]} != ${#BNDL_PATHS[@]})); then + xcwarning "${BUNDLE}: Malformed dSYM bundle." + continue + elif ((${#BNDL_UUIDS[@]} == 0)); then + xcwarning "${BUNDLE}: No DWARF information." + continue + fi + + # If no executable was specified, then the UUIDS and ARCHS + # arrays are empty. Populate them with information from the + # bundle. + if [[ ! "${EXE}" ]]; then + # The final UUIDS setting will be the intersection of the + # discovered set and the originally specified UUIDS. This + # is to prevent uploading potentially private information. + SOUGHT_UUIDS=("${UUIDS[@]}") + + UUIDS=() ARCHS=() + for BNDL_PATH in "${BNDL_PATHS[@]}"; do + set_uuids_archs "${BUNDLE}/${BNDL_PATH}" + done + + if ((${#SOUGHT_UUIDS[@]})); then + for I in "${!UUIDS[@]}"; do + for UUID in "${SOUGHT_UUIDS[@]}"; do + if [[ "${UUIDS[$I]}" == "${UUID}" ]]; then + continue 2 + fi + done + + # This is not the DWARF you are looking for... + xcdebug "Rejecting ${UUIDS[$I]} (${ARCHS[$I]}) as candidate DWARF file." + unset "UUIDS[$I]" "ARCHS[$I]" + done + fi + + unset SOUGHT_UUIDS + fi + + for I in "${!BNDL_UUIDS[@]}"; do + # See comment on extract_symbols_and_upload for why the + # full path to the companion file is required. + + BNDL_UUID="${BNDL_UUIDS[$I]}" DWARF_COMPANION="${BUNDLE}/${BNDL_PATHS[$I]}" + + for J in "${!ARCHS[@]}"; do + # A dSYM bundle can contain multiple architectures for + # multiple applications. Make sure we get the right + # one. + if [[ "${BNDL_UUID}" == "${UUIDS[$J]}" ]]; then + ARCH="${ARCHS[$J]}" + break + fi + done + + if [[ ! "${ARCH}" ]]; then + # This is not an error: it is legal for a dSYM bundle + # to contain debugging information for multiple + # executables (such as a framework with multiple + # subframeworks). Just ignore it. + xcdebug "No matching information found in ${DWARF_COMPANION} with UUID ${BNDL_UUID}." + continue + fi + + xcdebug "Found ${UUID} for ${ARCH} in ${DWARF_COMPANION}" + + # Have we already uploaded this file? + for J in "${!SEEN_ARCH[@]}"; do + if [[ "${ARCH}" == "${SEEN_ARCH[$J]}" ]] && cmp -s "${DWARF_COMPANION}" "${SEEN_PATH[$J]}"; then + xcdebug "${DWARF_COMPANION}: copy of ${SEEN_PATH[$J]}; no need to upload." + continue 2 + fi + done + + if [[ -f "${DWARF_COMPANION}" ]]; then + extract_symbols_and_upload "${DWARF_COMPANION}" "${ARCH}" "${EXE}" || exit $? + SEEN_ARCH+=("${ARCH}") SEEN_PATH+=("${DWARF_COMPANION}") + fi + done + done +done + +# For debugging odd cases. +if "${KEEP_TEMPORARIES}"; then + FCR_TEMPORARY_FILES=() +fi + +echo "Done." diff --git a/ZipBuilder/Template/Crash/dump_syms b/ZipBuilder/Template/Crash/dump_syms new file mode 100755 index 00000000000..8d0ef781cc8 Binary files /dev/null and b/ZipBuilder/Template/Crash/dump_syms differ diff --git a/ZipBuilder/Template/Crash/extract-keys b/ZipBuilder/Template/Crash/extract-keys new file mode 100755 index 00000000000..0da57003f9a --- /dev/null +++ b/ZipBuilder/Template/Crash/extract-keys @@ -0,0 +1,12 @@ +#!/bin/bash + +PLIST="${HOME}/Library/Preferences/com.google.SymbolUpload.plist" + +[[ -f $PLIST ]] || exit + +defaults read com.google.SymbolUpload | +perl -nle '/"(app_\d+_\d+_ios_.*)"/ and print $1' | +while read KEY; do + APP_ID="${KEY#app_}"; APP_ID="${APP_ID//_/:}" + plutil -extract "${KEY}" json -o "${APP_ID}.json" "${PLIST}" +done diff --git a/ZipBuilder/Template/Crash/upload-sym b/ZipBuilder/Template/Crash/upload-sym new file mode 100755 index 00000000000..1f8327dcebf --- /dev/null +++ b/ZipBuilder/Template/Crash/upload-sym @@ -0,0 +1,273 @@ +#!/bin/bash + +usage () { + echo >&2 "usage: $0 [-h] [-v] [-w|-e] service-account-file" +} + +help () { + usage + + cat >&2 <&2 "Either -w or -e may be specified, but not both." + echo >&2 + usage + exit 2 +fi + +SERVICE_ACCOUNT_FILE="$1"; shift + +if (($#)); then + echo >&2 "Unexpected argument '$1'" + echo >&2 + usage + exit 2 +fi + +export PATH=/bin:/usr/bin # play it safe + +# Load common utility routines. + +. "$(dirname "$0")/upload-sym-util.bash" + +# Make the error output Xcode-friendly. + +# This is a bit of Bash voodoo that cries for an explanation and is +# horribly underdocumented on-line. The construct '>(...)' starts a +# subprocess with its stdin connected to a pipe. After starting the +# subprocess, the parser replaces the construct with the NAME of the +# writable end of the pipe as a named file descriptor '/dev/fd/XX', +# then reevaluates the line. So, after the subprocess is started +# (which filters stdin and outputs to stderr [not stdout]), the line +# "exec 2> /dev/fd/XX" is evaluated. This redirects the main +# process's stderr to the given file descriptor. +# +# The end result is that anything sent to stderr of the form: +# file.in: line 47: blah blah +# is replaced with +# file.in:47: error: blah blah +# which Xcode will detect and emphasize in the formatted output. + +exec 2> >(sed -e 's/: line \([0-9]*\):/:\1: error:/' >&2) + +# Be long-winded about problems. The user may not understand how this +# script works or what prerequisites it has. If the user sees this, +# it is likely that they are executing the script outside of an Xcode +# build. + +ERRMSG=$'Value missing\n\nThis script must be executed as part of an Xcode build stage to have the\nproper environment variables set.' + +# Locate Xcode-generated files. + +: "${TARGET_BUILD_DIR:?"${ERRMSG}"}" +: "${FULL_PRODUCT_NAME:?"${ERRMSG}"}" + +DSYM_BUNDLE="${DWARF_DSYM_FOLDER_PATH?"${ERRMSG}"}/${DWARF_DSYM_FILE_NAME?"${ERRMSG}"}" +[[ -e "${DSYM_BUNDLE}" ]] || unset DSYM_BUNDLE + +EXECUTABLE="${TARGET_BUILD_DIR?"${ERRMSG}"}/${EXECUTABLE_PATH?"${ERRMSG}"}" + +# Locate dump_syms utility. + +if ! [[ -f "${FCR_DUMP_SYMS:=$(script_dir)/dump_syms}" && -x "${FCR_DUMP_SYMS}" ]]; then + xcerror "Cannot find dump_syms." + xcnote "It should have been installed with the Cocoapod. The location of dump_syms can be explicitly set using the environment variable FCR_DUMP_SYMS if you are using a non-standard install." + + exit 2 +fi + +if [[ ! "${FIREBASE_API_KEY}" || ! "${FIREBASE_APP_ID}" ]]; then + : "${SERVICE_PLIST:="$(find "${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}" -name GoogleService-Info.plist | head -n1)"}" + : "${SERVICE_PLIST:?"GoogleService-Info.plist could not be located"}" + : "${FIREBASE_API_KEY:="$(property API_KEY "${SERVICE_PLIST}")"}" + : "${FIREBASE_APP_ID:="$(property GOOGLE_APP_ID "${SERVICE_PLIST}")"}" +fi + +if ! [[ "${FIREBASE_API_KEY}" ]]; then + xcerror "Unable to get API_KEY from ${SERVICE_PLIST}." + xcnote "Specify FIREBASE_API_KEY in environment." + exit 2 +fi + +if ! [[ "${FIREBASE_APP_ID}" ]]; then + xcerror "Unable to get GOOGLE_APP_ID from ${SERVICE_PLIST}." + xcnote "Specify FIREBASE_APP_ID in environment." + exit 2 +fi + +# Load Info.plist values (Bundle ID & version) + +INFOPLIST="${TARGET_BUILD_DIR}/${INFOPLIST_PATH}" + +if [[ -f "${INFOPLIST}" ]]; then + : "${FCR_PROD_VERS:="$(property CFBundleShortVersionString "${INFOPLIST}")"}" + : "${FCR_BUNDLE_ID:="$(property CFBundleIdentifier "${INFOPLIST}")"}" +fi + +if ! [[ "${FCR_PROD_VERS}" ]]; then + xcerror "Unable to get CFBundleShortVersionString from Info.plist." + xcnote "Specify FCR_PROD_VERS in environment." + exit 2 +fi + +if ! [[ "${FCR_BUNDLE_ID}" ]]; then + xcerror "Unable to get CFBundleIdentifier from Info.plist." + xcnote "Specify FCR_BUNDLE_ID in environment." + exit 2 +fi + +# Support legacy account file cache before giving up + +if [[ ! -f "${SERVICE_ACCOUNT_FILE}" ]]; then + xcwarning "Unable to find service account JSON file: ${SERVICE_ACCOUNT_FILE}" + "Please ensure you've followed the steps at:" + "https://firebase.google.com/docs/crash/ios#upload_symbol_files" + + xcdebug "Trying to extract JSON file from cache." + + CACHE_PLIST="${HOME}/Library/Preferences/com.google.SymbolUpload.plist" + + if [[ -f "${CACHE_PLIST}" ]]; then + fcr_mktemp SERVICE_ACCOUNT_FILE + /usr/bin/plutil -extract "app_${FIREBASE_APP_ID//:/_}" \ + json -o "${SERVICE_ACCOUNT_FILE}" "${CACHE_PLIST}" >/dev/null 2>&1 + if [[ ! -s "${SERVICE_ACCOUNT_FILE}" ]]; then + xcwarning "${FIREBASE_APP_ID} not found in cache." + /bin/rm -f "${SERVICE_ACCOUNT_FILE}" + else + xcnote "${FIREBASE_APP_ID} found in cache. Consider using extract-keys.pl to reduce reliance on cache." + fi + else + xcnote "No cache file found." + fi +fi + +if [[ ! -f "${SERVICE_ACCOUNT_FILE}" ]]; then + xcerror "All attempts to find the service account JSON file have failed." + xcnote "You must supply it on the command line." + echo >&2 -n "$0:1: note: "; usage + exit 2 +fi + +# Dump collected information if requested + +if ((VERBOSE >= 2)); then + xcnote "FIREBASE_API_KEY = ${FIREBASE_API_KEY}" + xcnote "FIREBASE_APP_ID = ${FIREBASE_APP_ID}" + xcnote "DSYM_BUNDLE = ${DSYM_BUNDLE:-(unset, will use symbols in executable)}" + xcnote "EXECUTABLE = ${EXECUTABLE}" + xcnote "INFOPLIST = ${INFOPLIST}" + xcnote "FCR_PROD_VERS = ${FCR_PROD_VERS}" + xcnote "FCR_BUNDLE_ID = ${FCR_BUNDLE_ID}" +fi + +# Create and upload symbol files for each architecture +if [[ -x "${SWIFT_DEMANGLE:=$(xcrun --find swift-demangle 2>/dev/null)}" ]]; then + SWIFT_DEMANGLE_COMMAND="${SWIFT_DEMANGLE} -simplified" +else + SWIFT_DEMANGLE_COMMAND=/bin/cat +fi + +for ARCH in ${ARCHS?:}; do + SYMBOL_FILE="SYMBOL_FILE_${ARCH}" + fcr_mktemp "${SYMBOL_FILE}" SCRATCH + + # Just because there is a dSYM bundle at that path does not mean + # it is the RIGHT dSYM bundle... + + if [[ -d "${DSYM_BUNDLE}" ]]; then + DSYM_UUID="$(dwarfdump --arch "${ARCH}" --uuid "${DSYM_BUNDLE}" | awk '{print $2}')" + EXE_UUID="$(dwarfdump --arch "${ARCH}" --uuid "${EXECUTABLE}" | awk '{print $2}')" + if ((VERBOSE > 1)); then + xcnote "dSYM bundle UUID: ${DSYM_UUID}" + xcnote "Executable UUID: ${EXE_UUID}" + fi + if [[ "${DSYM_UUID}" != "${EXE_UUID}" ]]; then + xcdebug "Current dSYM bundle is not valid." + unset DSYM_BUNDLE + fi + fi + + if [[ ! -d "${DSYM_BUNDLE}" ]]; then + xcdebug "Extracting dSYM from executable." + fcr_mktempdir TMP_DSYM + DSYM_BUNDLE="${TMP_DSYM}/${EXECUTABLE##*/}.dSYM" + xcrun dsymutil -o "${DSYM_BUNDLE}" "${EXECUTABLE}" + STATUS=$? + if ((STATUS)); then + xcerror "Command dsymutil failed with exit code ${STATUS}." + exit ${STATUS} + fi + fi + + "${FCR_DUMP_SYMS}" -a "${ARCH}" -g "${DSYM_BUNDLE}" "${EXECUTABLE}" >"${SCRATCH}" 2> >(sed -e 's/^/warning: dump_syms: /' | grep -v 'failed to demangle' >&2) + + STATUS=$? + if ((STATUS)); then + xcerror "Command dump_syms failed with exit code ${STATUS}." + exit ${STATUS} + fi + + ${SWIFT_DEMANGLE_COMMAND} <"${SCRATCH}" >|"${!SYMBOL_FILE}" || exit 1 + + if ((VERBOSE >= 2)); then + xcnote "${EXECUTABLE##*/} (architecture ${ARCH}) symbol dump follows (first 20 lines):" + head >&2 -n20 "${!SYMBOL_FILE}" + elif ((VERBOSE >= 1)); then + xcnote "${EXECUTABLE##*/} (architecture ${ARCH}) symbol dump follows (first line only):" + head >&2 -n1 "${!SYMBOL_FILE}" + fi + + fcr_upload_files "${!SYMBOL_FILE}" || exit 1 +done diff --git a/ZipBuilder/Template/Crash/upload-sym-util.bash b/ZipBuilder/Template/Crash/upload-sym-util.bash new file mode 100644 index 00000000000..e98b0b1b189 --- /dev/null +++ b/ZipBuilder/Template/Crash/upload-sym-util.bash @@ -0,0 +1,396 @@ +# Copyright 2019 Google +# +# 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. + +# Output a clickable message. This will not count as a warning or +# error. + +xcnote () { + echo >&2 "${BASH_SOURCE[1]}:${BASH_LINENO[0]}: note: $*" +} + +# Output a clickable message prefixed with a warning symbol (U+26A0) +# and highlighted yellow. This will increase the overall warning +# count. A non-zero value for the variable ERRORS_ONLY will force +# warnings to be treated as errors. + +if ((ERRORS_ONLY)); then + xcwarning () { + echo >&2 "${BASH_SOURCE[1]}:${BASH_LINENO[0]}: error: $*" + } +else + xcwarning () { + echo >&2 "${BASH_SOURCE[1]}:${BASH_LINENO[0]}: warning: $*" + } +fi + +# Output a clickable message prefixed with a halt symbol (U+1F6D1) and +# highlighted red. This will increase the overall error count. Xcode +# will flag the build as failed if the error count is non-zero at the +# end of the build, even if this script returns a successful exit +# code. Set WARNINGS_ONLY to non-zero to prevent this. + +if ((WARNINGS_ONLY)); then + xcerror () { + echo >&2 "${BASH_SOURCE[1]}:${BASH_LINENO[0]}: warning: $*" + } +else + xcerror () { + echo >&2 "${BASH_SOURCE[1]}:${BASH_LINENO[0]}: error: $*" + } +fi + +xcdebug () { + if ((VERBOSE)); then + echo >&2 "${BASH_SOURCE[1]}:${BASH_LINENO[0]}: note: $*" + fi +} + +# Locate the script directory. + +script_dir () { + local SCRIPT="$0" SCRIPT_DIR="$(dirname "$0")" + + while SCRIPT="$(readlink "${SCRIPT}")"; do + [[ "${SCRIPT}" != /* ]] && SCRIPT="${SCRIPT_DIR}/${SCRIPT}" + SCRIPT_DIR="$(dirname "${SCRIPT}")" + done + + ( cd "${SCRIPT_DIR}"; pwd -P ) +} + +# Timestamp needed for various operations. Does not need to be exact, +# but does need to be consistent across web service calls. + +readonly NOW="$(/bin/date +%s)" + +# All files created by fcr_mktemp will be listed in FCR_TEMPORARY_FILES. +# Delete these when the enclosing script exits. (You may manually +# add files to this array as well to have them cleaned up on exit.) + +typeset -a FCR_TEMPORARY_FILES +trap 'STATUS=$?; rm -rf "${FCR_TEMPORARY_FILES[@]}"; exit ${STATUS}' 0 1 2 15 + +# Create a temporary file and add it to the list of files to delete when the +# script finishes. +# +# usage: fcr_mktemp VARNAME... + +fcr_mktemp () { + for VAR; do + eval "${VAR}=\$(mktemp -t com.google.FIRCrash) || return 1" + FCR_TEMPORARY_FILES+=("${!VAR}") + done +} + +# Create a temporary directory and add it to the list of files to +# delete when the script finishes. +# +# usage: fcr_mktempdir VARNAME... + +fcr_mktempdir () { + for VAR; do + eval "${VAR}=\$(mktemp -d -t com.google.FIRCrash) || return 1" + FCR_TEMPORARY_FILES+=("${!VAR}") + done +} + +# The keys we care about in the JSON objects. There are others that +# we do not use. Note that 'expires_at' and 'app_id' are not part of +# the original payload, but are computed from the environment used to +# make the call. + +FCR_SVC_KEYS=(client_email private_key private_key_id token_uri type) +FCR_TOK_KEYS=(access_token expires_at token_type app_id) + +# Extract a value from the property list. +# +# usage: property *name* *file* + +property () { + [[ -f "$2" ]] || echo '{}' >|"$2" # keeps PlistBuddy quiet + /usr/libexec/PlistBuddy "$2" -c "Print :$1" 2>/dev/null +} + +# Retrieve the property from the service account property list. +# +# usage: svc_property *name* + +svc_property () { + property "$1" "${SVC_PLIST}" +} + +# Does the same as svc_property above but for the token cache +# property list. +# +# usage: tok_property *name* + +tok_property () { + property "$1" "${TOK_PLIST}" +} + +# Verify that the service account property list has values for the +# required keys. Does not check the values themselves. + +fcr_verify_svc_plist () { + for key in "${FCR_SVC_KEYS[@]}"; do + if ! svc_property "${key}" >/dev/null; then + xcdebug "${key} not found in ${SVC_PLIST}. Service account invalid." + return 1 + fi + done +} + +# Verify that the token cache property list has values for the +# required keys. If the token_type is incorrect, the expiration date +# has been passed, or the application id does not match, return +# failure. + +fcr_verify_tok_plist () { + for key in "${FCR_TOK_KEYS[@]}"; do + if ! tok_property "${key}" >/dev/null; then + xcdebug "${key} not found in ${TOK_PLIST}. Token invalid." + return 1 + fi + done + + if [[ "$(tok_property token_type)" != "Bearer" ]]; then + xcwarning "Invalid token type '$(tok_property token_type)'." + return 1 + fi + + if (($(tok_property expires_at) <= NOW)); then + xcdebug "Token well-formed but expired at $(date -jf %s "$(tok_property expires_at)")." + echo '{}' >|"${TOK_PLIST}" + return 1 + fi + + if [[ "$(tok_property app_id)" != "${FIREBASE_APP_ID}" ]]; then + xcdebug "Cached token is for a different application." + echo '{}' >|"${TOK_PLIST}" + return 1 + fi +} + +# Convert a JSON certificate file to a PList certificate file. +# +# usage: fcr_load_certificate VARNAME + +fcr_load_certificate () { + : "${SERVICE_ACCOUNT_FILE:?must be the path to the service account JSON file.}" + fcr_mktemp "$1" + + if ! /usr/bin/plutil -convert binary1 "${SERVICE_ACCOUNT_FILE}" -o "${!1}"; then + xcerror "Unable to read service account file ${SERVICE_ACCOUNT_FILE}." + return 2 + fi +} + +# BASE64URL uses a sligtly different character set than BASE64, and +# uses no padding characters. + +function base64url () { + /usr/bin/base64 | sed -e 's/=//g; s/+/-/g; s/\//_/g' +} + +# Assemble the JSON Web Token (RFC 1795) +# +# usage: fcr_create_jwt *client-email* *token-uri* + +fcr_create_jwt () { + local JWT_HEADER="$(base64url <<<'{"alg":"RS256","typ":"JWT"}')" + local JWT_CLAIM="$(base64url <<<'{'"\"iss\":\"${1:?}\",\"aud\":\"${2:?}\",\"exp\":\"$((NOW + 3600))\",\"iat\":\"${NOW}\",\"scope\":\"https://www.googleapis.com/auth/mobilecrashreporting\""'}')" + local JWT_BODY="${JWT_HEADER}.${JWT_CLAIM}" + local JWT_SIG="$(echo -n "${JWT_BODY}" | openssl dgst -sha256 -sign <(svc_property private_key) -binary | base64url)" + + echo "${JWT_BODY}.${JWT_SIG}" +} + +# Set the BEARER_TOKEN variable for authentication. +# +# usage: fcr_authenticate + +fcr_authenticate () { + : "${FIREBASE_APP_ID:?required to select authentication credentials}" + + local SVC_PLIST + + fcr_load_certificate SVC_PLIST || return 2 + + local TOK_PLIST="${HOME}/Library/Preferences/com.google.SymbolUploadToken.plist" + + if ((VERBOSE > 2)); then + CURLOPT='--trace-ascii /dev/fd/2' + elif ((VERBOSE > 1)); then + CURLOPT='--verbose' + else + CURLOPT='' + fi + + # If the token will expire in the next sixty seconds (or already + # has), reload it. + if ! fcr_verify_tok_plist; then + xcdebug "Token cannot be used. Requesting OAuth2 token using installed credentials." + + if ! fcr_verify_svc_plist; then + xcerror "Incorrect/incomplete service account file." + return 2 + else + xcdebug "Certificate information appears valid." + fi + + TOKEN_URI="$(svc_property token_uri)" + CLIENT_EMAIL="$(svc_property client_email)" + + # Assemble the JSON Web Token (RFC 1795) + local JWT="$(fcr_create_jwt "${CLIENT_EMAIL}" "${TOKEN_URI}")" + + fcr_mktemp TOKEN_JSON + + HTTP_STATUS="$(curl ${CURLOPT} -o "${TOKEN_JSON}" -s -d grant_type='urn:ietf:params:oauth:grant-type:jwt-bearer' -d assertion="${JWT}" -w '%{http_code}' "${TOKEN_URI}")" + + if [[ "${HTTP_STATUS}" == 403 ]]; then + xcerror "Invalid certificate. Unable to retrieve OAuth2 token." + return 2 + elif [[ "${HTTP_STATUS}" != 200 ]]; then + cat >&2 "${TOKEN_JSON}" + return 2 + fi + + # Store the token in the preferences directory for future use. + /usr/bin/plutil -convert binary1 "${TOKEN_JSON}" -o "${TOK_PLIST}" + + EXPIRES_IN="$(tok_property expires_in)" + EXPIRES_AT="$((EXPIRES_IN + NOW))" + + /usr/libexec/PlistBuddy \ + -c "Add :app_id string \"${FIREBASE_APP_ID}\"" \ + -c "Add :expires_at integer ${EXPIRES_AT}" \ + -c "Add :expiration_date date $(TZ=GMT date -jf %s ${EXPIRES_AT})" \ + "${TOK_PLIST}" + + if ! fcr_verify_tok_plist; then + ((VERBOSE)) && /usr/libexec/PlistBuddy -c 'Print' "${TOK_PLIST}" + + echo '{}' >|"${TOK_PLIST}" + xcwarning "Token returned is not valid." + xcnote "If this error persists, download a fresh certificate." + + return 2 + fi + else + xcdebug "Token still valid." + EXPIRES_AT="$(tok_property expires_at)" + fi + + xcdebug "Token will expire on $(date -jf %s "${EXPIRES_AT}")." + xcdebug "Using service account with key $(svc_property private_key_id)." + + BEARER_TOKEN="$(tok_property access_token)" + + if [[ ! "${BEARER_TOKEN}" ]]; then + if ((VERBOSE)); then + xcwarning "Current malformed token cache:" + tok_property | while read; do xcnote "${REPLY}"; done + fi + xcerror "Unable to retrieve authentication token from server." + return 2 + fi + + return 0 +} + +# Upload the files to the server. +# +# Arguments: Names of files to upload. + +fcr_upload_files() { + fcr_authenticate || return $? + + : "${FCR_PROD_VERS:?}" + : "${FCR_BUNDLE_ID:?}" + : "${FIREBASE_APP_ID:?}" + : "${FIREBASE_API_KEY:?}" + : "${FCR_BASE_URL:=https://mobilecrashreporting.googleapis.com}" + + fcr_mktemp FILE_UPLOAD_LOCATION_PLIST META_UPLOAD_RESULT_PLIST + + if ((VERBOSE > 2)); then + CURLOPT='--trace-ascii /dev/fd/2' + elif ((VERBOSE > 1)); then + CURLOPT='--verbose' + else + CURLOPT='' + fi + + for FILE; do + xcdebug "Get signed URL for uploading." + + URL="${FCR_BASE_URL}/v1/apps/${FIREBASE_APP_ID}" + + HTTP_STATUS="$(curl ${CURLOPT} -o "${FILE_UPLOAD_LOCATION_PLIST}" -sL -H "X-Ios-Bundle-Identifier: ${FCR_BUNDLE_ID}" -H "Authorization: Bearer ${BEARER_TOKEN}" -X POST -d '' -w '%{http_code}' "${URL}/symbolFileUploadLocation?key=${FIREBASE_API_KEY}")" + STATUS=$? + + if [[ "${STATUS}" == 22 && "${HTTP_STATUS}" == 403 ]]; then + xcerror "Unable to access resource. Token invalid." + xcnote "Please verify the service account file." + return 2 + elif [[ "${STATUS}" != 0 ]]; then + xcerror "curl exited with non-zero status ${STATUS}." + ((STATUS == 22)) && xcerror "HTTP response code is ${HTTP_STATUS}." + return 2 + fi + + /usr/bin/plutil -convert binary1 "${FILE_UPLOAD_LOCATION_PLIST}" || return 1 + + UPLOAD_KEY="$(property uploadKey "${FILE_UPLOAD_LOCATION_PLIST}")" + UPLOAD_URL="$(property uploadUrl "${FILE_UPLOAD_LOCATION_PLIST}")" + ERRMSG="$(property error:message "${FILE_UPLOAD_LOCATION_PLIST}")" + + if [[ "${ERRMSG}" ]]; then + if ((VERBOSE)); then + xcnote "Server response:" + /usr/bin/plutil -p "${FILE_UPLOAD_LOCATION_PLIST}" >&2 + fi + xcerror "symbolFileUploadLocation: ${ERRMSG}" + xcnote "symbolFileUploadLocation: Failed to get upload location." + return 1 + fi + + xcdebug "Upload symbol file." + + HTTP_STATUS=$(curl ${CURLOPT} -sfL -H 'Content-Type: text/plain' -H "Authorization: Bearer ${BEARER_TOKEN}" -w '%{http_code}' -T "${FILE}" "${UPLOAD_URL}") + STATUS=$? + + if ((STATUS == 22)); then # exit code 22 is a non-successful HTTP response + xcerror "upload: Unable to upload symbol file (HTTP Status ${HTTP_STATUS})." + return 1 + elif ((STATUS != 0)); then + xcerror "upload: Unable to upload symbol file (reason unknown)." + return 1 + fi + + xcdebug "Upload metadata information." + + curl ${CURLOPT} -sL -H 'Content-Type: application/json' -H "X-Ios-Bundle-Identifier: ${FCR_BUNDLE_ID}" -H "Authorization: Bearer ${BEARER_TOKEN}" -X POST -d '{"upload_key":"'"${UPLOAD_KEY}"'","symbol_file_mapping":{"symbol_type":2,"app_version":"'"${FCR_PROD_VERS}"'"}}' "${URL}/symbolFileMappings:upsert?key=${FIREBASE_API_KEY}" >|"${META_UPLOAD_RESULT_PLIST}" || return 1 + /usr/bin/plutil -convert binary1 "${META_UPLOAD_RESULT_PLIST}" || return 1 + + ERRMSG="$(property error:message "${META_UPLOAD_RESULT_PLIST}")" + + if [[ "${ERRMSG}" ]]; then + xcerror "symbolFileMappings:upsert: ${ERRMSG}" + xcnote "symbolFileMappings:upsert: The metadata for the symbol file failed to update." + return 1 + fi + done +} diff --git a/ZipBuilder/Template/Crash/upload-sym.sh b/ZipBuilder/Template/Crash/upload-sym.sh new file mode 100755 index 00000000000..5fc7d1a2b3e --- /dev/null +++ b/ZipBuilder/Template/Crash/upload-sym.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Copyright 2019 Google +# +# 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. + +echo "$0:0: error: $0 has been removed. Please use upload-sym instead." +exit 1 diff --git a/ZipBuilder/Template/Firebase.h b/ZipBuilder/Template/Firebase.h new file mode 100644 index 00000000000..e7b4d649be6 --- /dev/null +++ b/ZipBuilder/Template/Firebase.h @@ -0,0 +1,131 @@ +/* + * Copyright 2019 Google + * + * 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 + +#if !defined(__has_include) +#error \ + "Firebase.h won't import anything if your compiler doesn't support __has_include. Please \ + import the headers individually." +#else +#if __has_include() +#import +#else +#ifndef FIREBASE_ANALYTICS_SUPPRESS_WARNING +#warning \ + "FirebaseAnalytics.framework is not included in your target. Please add \ +`Firebase/Core` to your Podfile or add FirebaseAnalytics.framework to your project to ensure \ +Firebase services work as intended." +#endif // #ifndef FIREBASE_ANALYTICS_SUPPRESS_WARNING +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#if __has_include() +#import +#endif + +#endif // defined(__has_include) diff --git a/ZipBuilder/Template/FrameworkMaker.xcodeproj/project.pbxproj b/ZipBuilder/Template/FrameworkMaker.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..20dad19f945 --- /dev/null +++ b/ZipBuilder/Template/FrameworkMaker.xcodeproj/project.pbxproj @@ -0,0 +1,240 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXFileReference section */ + 05A46BD71CC9B2BE007BDB33 /* FrameworkMaker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FrameworkMaker.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05A46BD41CC9B2BE007BDB33 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05A46BCE1CC9B2BE007BDB33 = { + isa = PBXGroup; + children = ( + 05A46BD81CC9B2BE007BDB33 /* Products */, + ); + sourceTree = ""; + }; + 05A46BD81CC9B2BE007BDB33 /* Products */ = { + isa = PBXGroup; + children = ( + 05A46BD71CC9B2BE007BDB33 /* FrameworkMaker.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05A46BD61CC9B2BE007BDB33 /* FrameworkMaker */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05A46BEE1CC9B2BE007BDB33 /* Build configuration list for PBXNativeTarget "FrameworkMaker" */; + buildPhases = ( + 05A46BD31CC9B2BE007BDB33 /* Sources */, + 05A46BD41CC9B2BE007BDB33 /* Frameworks */, + 05A46BD51CC9B2BE007BDB33 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FrameworkMaker; + productName = FrameworkMaker; + productReference = 05A46BD71CC9B2BE007BDB33 /* FrameworkMaker.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05A46BCF1CC9B2BE007BDB33 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0730; + ORGANIZATIONNAME = "Google, Inc."; + TargetAttributes = { + 05A46BD61CC9B2BE007BDB33 = { + CreatedOnToolsVersion = 7.3; + }; + }; + }; + buildConfigurationList = 05A46BD21CC9B2BE007BDB33 /* Build configuration list for PBXProject "FrameworkMaker" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05A46BCE1CC9B2BE007BDB33; + productRefGroup = 05A46BD81CC9B2BE007BDB33 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05A46BD61CC9B2BE007BDB33 /* FrameworkMaker */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05A46BD51CC9B2BE007BDB33 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05A46BD31CC9B2BE007BDB33 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05A46BEC1CC9B2BE007BDB33 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05A46BED1CC9B2BE007BDB33 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05A46BEF1CC9B2BE007BDB33 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = google.FrameworkMaker; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 05A46BF01CC9B2BE007BDB33 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = google.FrameworkMaker; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05A46BD21CC9B2BE007BDB33 /* Build configuration list for PBXProject "FrameworkMaker" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05A46BEC1CC9B2BE007BDB33 /* Debug */, + 05A46BED1CC9B2BE007BDB33 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05A46BEE1CC9B2BE007BDB33 /* Build configuration list for PBXNativeTarget "FrameworkMaker" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05A46BEF1CC9B2BE007BDB33 /* Debug */, + 05A46BF01CC9B2BE007BDB33 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05A46BCF1CC9B2BE007BDB33 /* Project object */; +} diff --git a/ZipBuilder/Template/Info.plist b/ZipBuilder/Template/Info.plist new file mode 100644 index 00000000000..16be3b68112 --- /dev/null +++ b/ZipBuilder/Template/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ZipBuilder/Template/NOTICES b/ZipBuilder/Template/NOTICES new file mode 100644 index 00000000000..ad93dba4eae --- /dev/null +++ b/ZipBuilder/Template/NOTICES @@ -0,0 +1,375 @@ +Google LevelDB +Copyright (c) 2011 The LevelDB Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation andor other materials provided with the distribution. + +Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-- + +Square Socket Rocket +Copyright 2012 Square Inc. + +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. + +-- + +APLevelDB +Created by Adam Preble on 12312. +Copyright (c) 2012 Adam Preble. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, andor sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Portions of APLevelDB are based on LevelDB-ObjC: +https://github.com/hoisie/LevelDB-ObjC +Specifically the SliceFromString/StringFromSlice macros, and the structure of +the enumeration methods. License for those potions follows: +Copyright (c) 2011 Pave Labs +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +-- + +sqlite3 +2001 September 15 + +The author disclaims copyright to this source code. In place of +a legal notice, here is a blessing: + + May you do good and not evil. + May you find forgiveness for yourself and forgive others. + May you share freely, never taking more than you give. + + +This header file defines the interface that the SQLite library +presents to client programs. If a C-function, structure, datatype, +or constant definition does not appear in this file, then it is +not a published API of SQLite, is subject to change without +notice, and should not be referenced by programs that use SQLite. + +Some of the definitions that are in this file are marked as +"experimental". Experimental interfaces are normally new +features recently added to SQLite. We do not anticipate changes +to experimental interfaces but reserve the right to make minor changes +if experience from use "in the wild" suggest such changes are prudent. + +The official C-language API documentation for SQLite is derived +from comments in this file. This file is the authoritative source +on how SQLite interfaces are suppose to operate. + +The name of this file under configuration management is "sqlite.h.in". +The makefile makes some minor changes to this file (such as inserting +the version number) and changes its name to "sqlite3.h" as +part of the build process. + +-- + +sysutsname.h +Copyright (c) 2000 Apple Computer, Inc. All rights reserved. + +This file contains Original Code andor Modifications of Original Code +as defined in and that are subject to the Apple Public Source License +Version 2.0 (the 'License'). You may not use this file except in +compliance with the License. The rights granted to you under the License +may not be used to create, or enable the creation or redistribution of, +unlawful or unlicensed copies of an Apple operating system, or to +circumvent, violate, or enable the circumvention or violation of, any +terms of an Apple operating system software license agreement. + +Please obtain a copy of the License at +http://www.opensource.apple.com/apsl and read it before using this file. + +The Original Code and all software distributed under the License are +distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, +INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. +Please see the License for the specific language governing rights and +limitations under the License. + +Copyright 1993,1995 NeXT Computer Inc. All Rights Reserved +Copyright (c) 1994 The Regents of the University of California. All rights reserved. +This code is derived from software contributed to Berkeley by +Chuck Karish of Mindcraft, Inc. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation andor other materials provided with the distribution. +3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: +* This product includes software developed by the University of +* California, Berkeley and its contributors. +4. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +-- + +GTMNSData+zlib.h +Copyright 2007-2008 Google Inc. + +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. + +-- + +GTMDefines.h +Copyright 2008 Google Inc. + +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. + +-- + +ProtocolBuffer +Copyright 2008 Cyrus Najmabadi +Copyright 2011 Google Inc. + +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. + +-- + +GTMDefines.h +Copyright 2008 Google Inc. + +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. + +-- + +fbase64.c + +Copyright (c) 1996 by Internet Software Consortium. +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. +THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS +ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE +CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + +Portions Copyright (c) 1995 by International Business Machines, Inc. +International Business Machines, Inc. (hereinafter called IBM) grants +permission under its copyrights to use, copy, modify, and distribute this +Software with or without fee, provided that the above copyright notice and +all paragraphs of this notice appear in all copies, and that the name of IBM +not be used in connection with the marketing of any product incorporating +the Software or modifications thereof, without specific, written prior +permission. +To the extent it has a right to do so, IBM grants an immunity from suit +under its patents, if any, for the use, sale or manufacture of products to +the extent that such products are used for performing Domain Name System +dynamic updates in TCP/IP networks by means of the Software. No immunity is +granted for any product per se or for any other function of any product. +THE SOFTWARE IS PROVIDED "AS IS", AND IBM DISCLAIMS ALL WARRANTIES, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE. IN NO EVENT SHALL IBM BE LIABLE FOR ANY SPECIAL, +DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER ARISING +OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE, EVEN +IF IBM IS APPRISED OF THE POSSIBILITY OF SUCH DAMAGES. + +OPENBSD ORIGINAL: lib/libc/net/base64.c */ + +-- + +FIRAppEnvironmentUtil.m + +The following copyright from Landon J. Fuller applies to the isAppEncrypted function. +Copyright (c) 2017 Landon J. Fuller +All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Comment from iPhone Dev Wiki +Crack Prevention: +App Store binaries are signed by both their developer and Apple. This encrypts the binary so +that decryption keys are needed in order to make the binary readable. When iOS executes the +binary, the decryption keys are used to decrypt the binary into a readable state where it is +then loaded into memory and executed. iOS can tell the encryption status of a binary via the +cryptid structure member of LC_ENCRYPTION_INFO MachO load command. If cryptid is a non-zero +value then the binary is encrypted. +'Cracking' works by letting the kernel decrypt the binary then siphoning the decrypted data into +a new binary file, resigning, and repackaging. This will only work on jailbroken devices as +codesignature validation has been removed. Resigning takes place because while the codesignature +doesn't have to be valid thanks to the jailbreak, it does have to be in place unless you have +AppSync or similar to disable codesignature checks. +More information at Landon Fuller's blog + +-- + +tflite + +Copyright 2017 The TensorFlow Authors. All Rights Reserved. + +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. + +-- + +FirebaseCore + +Copyright 2017 Google. + +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. + +-- + +google_api_objectivec_client_for_rest + +Copyright 2011 Google Inc. + +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. + +-- + +ocmock + +Copyright (c) 2006-2016 Erik Doernenburg and contributors + +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. + +-- diff --git a/ZipBuilder/Template/README.md b/ZipBuilder/Template/README.md new file mode 100644 index 00000000000..9b4d889825c --- /dev/null +++ b/ZipBuilder/Template/README.md @@ -0,0 +1,91 @@ +# Firebase iOS SDKs + +This directory contains the full Firebase distribution, packaged as static +frameworks that can be integrated into your app. + +# Integration Instructions + +Each Firebase component requires several frameworks in order to function +properly. Each section below lists the frameworks you'll need to include +in your project in order to use that Firebase SDK in your application. + +To integrate a Firebase SDK with your app: + +1. Find the desired SDK in the list below. +2. Make sure you have an Xcode project open in Xcode. +3. In Xcode, hit `⌘-1` to open the Project Navigator pane. It will open on + left side of the Xcode window if it wasn't already open. +4. Drag each framework from the "Analytics" directory into the Project + Navigator pane. In the dialog box that appears, make sure the target you + want the framework to be added to has a checkmark next to it, and that + you've selected "Copy items if needed". If you already have Firebase + frameworks in your project, make sure that you replace them with the new + versions. +5. Drag each framework from the directory named after the SDK into the Project + Navigator pane. Note that there may be no additional frameworks, in which + case this directory will be empty. For instance, if you want the Database + SDK, look in the Database folder for the required frameworks. In the dialog + box that appears, make sure the target you want this framework to be added to + has a checkmark next to it, and that you've selected "Copy items if needed." + + *Do not add the Firebase frameworks to the "Embed Frameworks" Xcode build + phase. The Firebase frameworks are not embedded dynamic frameworks, but are + [static frameworks](https://www.raywenderlich.com/65964/create-a-framework-for-ios) + which cannot be embedded into your application's bundle.* + +6. If the SDK has resources, go into the Resources folders, which will be in + the SDK folder. Drag all of those resources into the Project Navigator, just + like the frameworks, again making sure that the target you want to add these + resources to has a checkmark next to it, and that you've selected "Copy items + if needed". +7. Add the -ObjC flag to "Other Linker Settings": + a. In your project settings, open the Settings panel for your target + b. Go to the Build Settings tab and find the "Other Linker Flags" setting + in the Linking section. + c. Double-click the setting, click the '+' button, and add "-ObjC" (without + quotes) +8. Drag the `Firebase.h` header in this directory into your project. This will + allow you to `#import "Firebase.h"` and start using any Firebase SDK that you + have. +9. If you're using Swift, or you want to use modules, drag module.modulemap into + your project and update your User Header Search Paths to contain the + directory that contains your module map. +10. You're done! Compile your target and start using Firebase. + +If you want to add another SDK, repeat the steps above with the frameworks for +the new SDK. You only need to add each framework once, so if you've already +added a framework for one SDK, you don't need to add it again. Note that some +frameworks are required by multiple SDKs, and so appear in multiple folders. + +The Firebase frameworks list the system libraries and frameworks they depend on +in their modulemaps. If you have disabled the "Link Frameworks Automatically" +option in your Xcode project/workspace, you will need to add the system +frameworks and libraries listed in each Firebase framework's +.framework/Modules/module.modulemap file to your target's or targets' +"Link Binary With Libraries" build phase. + +"(~> X)" below means that the SDK requires all of the frameworks from X. You +should make sure to include all of the frameworks from X when including the SDK. + +NOTE: If you are upgrading FirebaseAnalytics from before Firebase 5.5.0, + `FirebaseNanoPB` has been renamed to `MeasurementNanoPB`. After you add + `MeasurementNanoPB` to your project, please remove `FirebaseNanoPB` as it + no longer provides any functionality. + +__INTEGRATION__ +# Samples + +You can get samples for Firebase from https://github.com/firebase/quickstart-ios: + + git clone https://github.com/firebase/quickstart-ios + +Note that several of the samples depend on SDKs that are not included with +this archive; for example, FirebaseUI. For the samples that depend on SDKs not +included in this archive, you'll need to use CocoaPods. + +# Versions + +The frameworks in this directory map to these versions of the Firebase SDKs in +CocoaPods. + +__VERSIONS__ diff --git a/ZipBuilder/Template/module.modulemap b/ZipBuilder/Template/module.modulemap new file mode 100644 index 00000000000..f545307d12b --- /dev/null +++ b/ZipBuilder/Template/module.modulemap @@ -0,0 +1,4 @@ +module Firebase { + header "Firebase.h" + export * +} \ No newline at end of file diff --git a/cmake/cc_rules.cmake b/cmake/cc_rules.cmake index bf376102a96..39b56c9fcd8 100644 --- a/cmake/cc_rules.cmake +++ b/cmake/cc_rules.cmake @@ -30,7 +30,7 @@ function(cc_library name) maybe_remove_objc_sources(sources ${ccl_SOURCES}) add_library(${name} ${sources}) - add_objc_flags(${name} ccl) + add_objc_flags(${name} ${ccl_SOURCES}) target_include_directories( ${name} PUBLIC @@ -107,7 +107,7 @@ function(cc_binary name) maybe_remove_objc_sources(sources ${ccb_SOURCES}) add_executable(${name} ${sources}) - add_objc_flags(${name} ccb) + add_objc_flags(${name} ${ccb_SOURCES}) add_test(${name} ${name}) target_include_directories(${name} PUBLIC ${FIREBASE_SOURCE_DIR}) @@ -137,7 +137,7 @@ function(cc_test name) maybe_remove_objc_sources(sources ${cct_SOURCES}) add_executable(${name} ${sources}) - add_objc_flags(${name} cct) + add_objc_flags(${name} ${cct_SOURCES}) add_test(${name} ${name}) target_include_directories(${name} PUBLIC ${FIREBASE_SOURCE_DIR}) diff --git a/default.profraw b/default.profraw new file mode 100644 index 00000000000..d102825dd2d Binary files /dev/null and b/default.profraw differ diff --git a/scripts/build.sh b/scripts/build.sh index 881b72cad1f..0441ae1a057 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -27,7 +27,7 @@ USAGE: $0 product [platform] [method] product can be one of: Firebase Firestore - InAppMessagingDisplay + InAppMessaging SymbolCollision platform can be one of: @@ -69,6 +69,7 @@ fi # Runs xcodebuild with the given flags, piping output to xcpretty # If xcodebuild fails with known error codes, retries once. function RunXcodebuild() { + echo xcodebuild "$@" xcodebuild "$@" | xcpretty; result=$? if [[ $result == 65 ]]; then @@ -115,6 +116,7 @@ xcb_flags+=( ONLY_ACTIVE_ARCH=YES CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=YES + COMPILER_INDEX_STORE_ENABLE=NO ) # TODO(varconst): --warn-unused-vars - right now, it makes the log overflow on @@ -183,15 +185,15 @@ case "$product-$method-$platform" in test if [[ $platform == 'iOS' ]]; then - RunXcodebuild \ - -workspace 'Functions/Example/FirebaseFunctions.xcworkspace' \ - -scheme "FirebaseFunctions_Tests" \ + # Run integration tests (not allowed on PRs) + if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then + RunXcodebuild \ + -workspace 'Example/Firebase.xcworkspace' \ + -scheme "Auth_ApiTests" \ "${xcb_flags[@]}" \ build \ test - # Run integration tests (not allowed on PRs) - if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then RunXcodebuild \ -workspace 'Example/Firebase.xcworkspace' \ -scheme "Storage_IntegrationTests_iOS" \ @@ -218,21 +220,28 @@ case "$product-$method-$platform" in "${xcb_flags[@]}" \ build \ test - - cd Functions/Example - sed -i -e 's/use_frameworks/\#use_frameworks/' Podfile - pod update --no-repo-update - cd ../.. - RunXcodebuild \ - -workspace 'Functions/Example/FirebaseFunctions.xcworkspace' \ - -scheme "FirebaseFunctions_Tests" \ - "${xcb_flags[@]}" \ - build \ - test fi ;; - InAppMessagingDisplay-xcodebuild-iOS) + InAppMessaging-xcodebuild-iOS) + RunXcodebuild \ + -workspace 'InAppMessaging/Example/InAppMessaging-Example-iOS.xcworkspace' \ + -scheme 'InAppMessaging_Example_iOS' \ + "${xcb_flags[@]}" \ + build \ + test + + cd InAppMessaging/Example + sed -i -e 's/use_frameworks/\#use_frameworks/' Podfile + pod update --no-repo-update + cd ../.. + RunXcodebuild \ + -workspace 'InAppMessaging/Example/InAppMessaging-Example-iOS.xcworkspace' \ + -scheme 'InAppMessaging_Example_iOS' \ + "${xcb_flags[@]}" \ + build \ + test + # Run UI tests on both iPad and iPhone simulators # TODO: Running two destinations from one xcodebuild command stopped working with Xcode 10. # Consider separating static library tests to a separate job. @@ -296,16 +305,6 @@ case "$product-$method-$platform" in "${xcb_flags[@]}" \ build - # Firestore_FuzzTests_iOS require a Clang that supports -fsanitize-coverage=trace-pc-guard - # and cannot run with thread sanitizer. - if [[ "$xcode_major" -ge 9 ]] && ! [[ -n "${SANITIZERS:-}" && "$SANITIZERS" = *"tsan"* ]]; then - RunXcodebuild \ - -workspace 'Firestore/Example/Firestore.xcworkspace' \ - -scheme "Firestore_FuzzTests_iOS" \ - "${xcb_flags[@]}" \ - FUZZING_TARGET="NONE" \ - test - fi ;; Firestore-cmake-macOS) diff --git a/scripts/check.sh b/scripts/check.sh new file mode 100755 index 00000000000..ad5c18ab0eb --- /dev/null +++ b/scripts/check.sh @@ -0,0 +1,220 @@ +#!/bin/bash + +# Copyright 2019 Google +# +# 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. + +# Checks that the current state of the tree is sane and optionally auto-fixes +# errors automatically. Meant for interactive use. + +function usage() { + cat <] + +Runs auto-formatting scripts, source-tree checks, and linters on any files that +have changed since master. + +By default, any changes are left as uncommited changes in the working tree. You +can review them with git diff. Pass --commit to automatically commit any changes. + +Pass an alternate revision to use as the basis for checking changes. + +OPTIONS: + + --allow-dirty + By default, check.sh requires a clean working tree to keep any generated + changes separate from logical changes. + + --commit + Commit any auto-generated changes with a message indicating which tool made + the changes. + + --amend + Commit any auto-generated changes by amending the HEAD commit. + + --fixup + Commit any auto-generated changes with a fixup! message for the HEAD + commit. The next rebase will squash these fixup commits. + + --test-only + Run all checks without making any changes to local files. + + + Specifies a starting revision other than the default of master. + + +EXAMPLES: + + check.sh + Runs automated checks and formatters on all changed files since master. + Check for changes with git diff. + + check.sh --commit + Runs automated checks and formatters on all changed files since master and + commits the results. + + check.sh --amend HEAD + Runs automated checks and formatters on all changed files since the last + commit and amends the last commit with the difference. + + check.sh --allow-dirty HEAD + Runs automated checks and formatters on all changed files since the last + commit and intermingles the changes with any pending changes. Useful for + interactive use from an editor. + +EOF +} + +set -euo pipefail +unset CDPATH + +# Change to the top-directory of the working tree +top_dir=$(git rev-parse --show-toplevel) +cd "${top_dir}" + +ALLOW_DIRTY=false +COMMIT_METHOD="none" +START_REVISION="master" +TEST_ONLY=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --) + # Do nothing: explicitly allow this, but ignore it + ;; + + -h | --help) + usage + exit 1 + ;; + + --allow-dirty) + ALLOW_DIRTY=true + ;; + + --amend) + COMMIT_METHOD=amend + ;; + + --fixup) + COMMIT_METHOD=fixup + ;; + + --commit) + COMMIT_METHOD=message + ;; + + --test-only) + # In test-only mode, no changes are made, so there's no reason to + # require a clean source tree. + ALLOW_DIRTY=true + TEST_ONLY=true + ;; + + *) + if git rev-parse "$1" >& /dev/null; then + START_REVISION="$1" + break + fi + ;; + esac + shift +done + +if [[ "${TEST_ONLY}" == true && "${COMMIT_METHOD}" != "none" ]]; then + echo "--test-only cannot be combined with --amend, --fixup, or --commit" + exit 1 +fi + +if [[ "${ALLOW_DIRTY}" == true && "${COMMIT_METHOD}" == "message" ]]; then + echo "--allow-dirty and --commit are mutually exclusive" + exit 1 +fi + +if ! git diff-index --quiet HEAD --; then + if [[ "${ALLOW_DIRTY}" != true ]]; then + echo "You have local changes that could be overwritten by this script." + echo "Please commit your changes first or pass --allow-dirty." + exit 2 + fi +fi + +# Record actual start, but only if the revision is specified as a single +# commit. Ranges specified with .. or ... are left alone. +if [[ "${START_REVISION}" == *..* ]]; then + START_SHA="${START_REVISION}" +else + START_SHA=$(git rev-parse "${START_REVISION}") +fi + +# If committing --fixup, avoid messages with fixup! fixup! that might come from +# multiple fixup commits. +HEAD_SHA=$(git rev-parse HEAD) + +function maybe_commit() { + local message="$1" + + if [[ "${COMMIT_METHOD}" == "none" ]]; then + return + fi + + echo "${message}" + case "${COMMIT_METHOD}" in + amend) + git commit -a --amend -C "${HEAD_SHA}" + ;; + + fixup) + git commit -a --fixup="${HEAD_SHA}" + ;; + + message) + git commit -a -m "${message}" + ;; + + *) + echo "Unknown commit method ${COMMIT_METHOD}" 1>&2 + exit 2 + ;; + esac +} + +style_cmd=("${top_dir}/scripts/style.sh") +if [[ "${TEST_ONLY}" == true ]]; then + style_cmd+=(test-only) +fi +style_cmd+=("${START_SHA}") + +# Restyle and commit any changes +"${style_cmd[@]}" +if ! git diff --quiet; then + maybe_commit "style.sh generated changes" +fi + +# If there are changes to the Firestore project, ensure they're ordered +# correctly to minimize conflicts. +if ! git diff --quiet "${START_SHA}" -- Firestore; then + "${top_dir}/scripts/sync_project.rb" + if ! git diff --quiet; then + maybe_commit "sync_project.rb generated changes" + fi +fi + +# Check lint errors. +"${top_dir}/scripts/check_whitespace.sh" +"${top_dir}/scripts/check_copyright.sh" +"${top_dir}/scripts/check_no_module_imports.sh" +"${top_dir}/scripts/check_test_inclusion.py" + +# Google C++ style +"${top_dir}/scripts/lint.sh" "${START_SHA}" diff --git a/scripts/cpplint.py b/scripts/cpplint.py index 51fbe246ae0..4431fa5cb9f 100644 --- a/scripts/cpplint.py +++ b/scripts/cpplint.py @@ -553,6 +553,9 @@ # This is set by --headers flag. _hpp_headers = set(['h']) +# Source filename extensions +_cpp_extensions = set(['cc', 'mm']) + # {str, bool}: a map from error categories to booleans which indicate if the # category should be suppressed for every line. _global_error_suppressions = {} @@ -569,6 +572,15 @@ def ProcessHppHeadersOption(val): def IsHeaderExtension(file_extension): return file_extension in _hpp_headers +def IsSourceExtension(file_extension): + return file_extension in _cpp_extensions + +def IsSourceFilename(filename): + global _cpp_extensions + ext = os.path.splitext(filename)[-1].lower() + ext = ext[1:] # leading dot + return IsSourceExtension(ext) + def ParseNolintSuppressions(filename, raw_line, linenum, error): """Updates the global list of line error-suppressions. @@ -4579,7 +4591,7 @@ def CheckIncludeLine(filename, clean_lines, linenum, include_state, error): error(filename, linenum, 'build/include', 4, '"%s" already included at %s:%s' % (include, filename, duplicate_line)) - elif (include.endswith('.cc') and + elif (IsSourceFilename(include) and os.path.dirname(fileinfo.RepositoryName()) != os.path.dirname(include)): error(filename, linenum, 'build/include', 4, 'Do not include .cc files from other packages') @@ -5390,6 +5402,7 @@ def ExpectingFunctionArgs(clean_lines, linenum): ('', ('map', 'multimap',)), ('', ('allocator', 'make_shared', 'make_unique', 'shared_ptr', 'unique_ptr', 'weak_ptr')), + ('', ('ostream',)), ('', ('queue', 'priority_queue',)), ('', ('set', 'multiset',)), ('', ('stack',)), @@ -5415,6 +5428,7 @@ def ExpectingFunctionArgs(clean_lines, linenum): ) _RE_PATTERN_STRING = re.compile(r'\bstring\b') +_RE_PATTERN_OSTREAM = re.compile(r'\bostream\b') _re_pattern_headers_maybe_templates = [] for _header, _templates in _HEADERS_MAYBE_TEMPLATES: @@ -5536,8 +5550,17 @@ def CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error, io: The IO factory to use to read the header file. Provided for unittest injection. """ - required = {} # A map of header name to linenumber and the template entity. - # Example of required: { '': (1219, 'less<>') } + # A map of entity to a tuple of line number and tuple of headers. + # Example: { 'less<>': (1219, ('',)) } + # Example: { 'ostream': (1234, ('', '', '')) } + required = {} + + def Require(entity, linenum, *headers): + """Adds an entity at the given line, along with a list of possible headers + in which to find it. The first header is treated as the preferred header. + """ + required[entity] = (linenum, headers) + for linenum in xrange(clean_lines.NumLines()): line = clean_lines.elided[linenum] @@ -5551,11 +5574,19 @@ def CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error, # (We check only the first match per line; good enough.) prefix = line[:matched.start()] if prefix.endswith('std::') or not prefix.endswith('::'): - required[''] = (linenum, 'string') + Require('string', linenum, '') + + # Ostream is special too -- also non-templatized + matched = _RE_PATTERN_OSTREAM.search(line) + if matched: + if IsSourceFilename(filename): + Require('ostream', linenum, '', '') + else: + Require('ostream', linenum, '', '', '') for pattern, template, header in _re_pattern_headers_maybe_templates: if pattern.search(line): - required[header] = (linenum, template) + Require(template, linenum, header) # The following function is just a speed up, no semantics are changed. if not '<' in line: # Reduces the cpu time usage by skipping lines. @@ -5568,7 +5599,7 @@ def CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error, # (We check only the first match per line; good enough.) prefix = line[:matched.start()] if prefix.endswith('std::') or not prefix.endswith('::'): - required[header] = (linenum, template) + Require(template, linenum, header) # The policy is that if you #include something in foo.h you don't need to # include it again in foo.cc. Here, we will look at possible includes. @@ -5605,16 +5636,37 @@ def CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error, # didn't include it in the .h file. # TODO(unknown): Do a better job of finding .h files so we are confident that # not having the .h file means there isn't one. - if filename.endswith('.cc') and not header_found: + if IsSourceFilename(filename) and not header_found: return + # Keep track of which headers have been reported already + reported = set() + # All the lines have been processed, report the errors found. - for required_header_unstripped in required: - template = required[required_header_unstripped][1] - if required_header_unstripped.strip('<>"') not in include_dict: - error(filename, required[required_header_unstripped][0], + for template in required: + line_and_headers = required[template] + headers = line_and_headers[1] + found = False + for required_header_unstripped in headers: + if required_header_unstripped in reported: + found = True + break + + if required_header_unstripped.strip('<>"') in include_dict: + found = True + break + + if not found: + preferred_header = headers[0] + reported.add(preferred_header) + if len(headers) < 2: + alternatives = '' + else: + alternatives = ' (or ' + ', '.join(headers[1:]) + ')' + error(filename, line_and_headers[0], 'build/include_what_you_use', 4, - 'Add #include ' + required_header_unstripped + ' for ' + template) + 'Add #include ' + preferred_header + ' for ' + template + + alternatives) _RE_PATTERN_EXPLICIT_MAKEPAIR = re.compile(r'\bmake_pair\s*<') diff --git a/scripts/if_changed.sh b/scripts/if_changed.sh index 40a865372bf..6c4f6f5031e 100755 --- a/scripts/if_changed.sh +++ b/scripts/if_changed.sh @@ -45,25 +45,42 @@ elif [[ -z "$TRAVIS_COMMIT_RANGE" ]]; then else case "$PROJECT-$METHOD" in + Firebase-pod-lib-lint) # Combines Firebase-* and InAppMessaging* + check_changes '^(Firebase/Auth|Firebase/Core|Firebase/Database|Firebase/DynamicLinks|'\ +'Firebase/Messaging|Firebase/Storage|GoogleUtilities|Interop|Example|'\ +'FirebaseAnalyticsIntop.podspec|FirebaseAuth.podspec|FirebaseAuthInterop.podspec|'\ +'FirebaseCore.podspec|FirebaseDatabase.podspec|FirebaseDynamicLinks.podspec|'\ +'FirebaseMessaging.podspec|FirebaseStorage.podspec|'\ +'FirebaseStorage.podspec|Firebase/InAppMessagingDisplay|InAppMessagingDisplay|'\ +'InAppMessaging|Firebase/InAppMessaging|'\ +'FirebaseInAppMessaging.podspec|FirebaseInAppMessagingDisplay.podspec)' + ;; + Firebase-*) check_changes '^(Firebase/Auth|Firebase/Core|Firebase/Database|Firebase/DynamicLinks|'\ -'Firebase/Messaging|Firebase/Storage|Functions|GoogleUtilities|Interop|Example|'\ +'Firebase/Messaging|Firebase/Storage|GoogleUtilities|Interop|Example|'\ 'FirebaseAnalyticsIntop.podspec|FirebaseAuth.podspec|FirebaseAuthInterop.podspec|'\ 'FirebaseCore.podspec|FirebaseDatabase.podspec|FirebaseDynamicLinks.podspec|'\ -'FirebaseFunctions.podspec|FirebaseMessaging.podspec|FirebaseStorage.podspec|'\ +'FirebaseMessaging.podspec|FirebaseStorage.podspec|'\ 'FirebaseStorage.podspec)' ;; - InAppMessagingDisplay-*) - check_changes '^(Firebase/InAppMessagingDisplay|InAppMessagingDisplay)' + Functions-*) + check_changes '^(Firebase/Core|Functions|GoogleUtilities|FirebaseFunctions.podspec)' + ;; + + InAppMessaging-*) + check_changes '^(Firebase/InAppMessagingDisplay|InAppMessagingDisplay|InAppMessaging|'\ +'Firebase/InAppMessaging)' ;; Firestore-xcodebuild|Firestore-pod-lib-lint) - check_changes '^(Firestore|FirebaseFirestore.podspec|FirebaseFirestoreSwift.podspec)' + check_changes '^(Firestore|FirebaseFirestore.podspec|FirebaseFirestoreSwift.podspec|'\ +'GoogleUtilities)' ;; Firestore-cmake) - check_changes '^(Firestore/(core|third_party)|cmake)' + check_changes '^(Firestore/(core|third_party)|cmake|GoogleUtilities)' ;; *) diff --git a/scripts/install_prereqs.sh b/scripts/install_prereqs.sh index a0dc1b96c2f..7707c581d92 100755 --- a/scripts/install_prereqs.sh +++ b/scripts/install_prereqs.sh @@ -27,15 +27,25 @@ case "$PROJECT-$PLATFORM-$METHOD" in bundle exec pod install --project-directory=Example --repo-update bundle exec pod install --project-directory=Functions/Example bundle exec pod install --project-directory=GoogleUtilities/Example - bundle exec pod install --project-directory=InAppMessagingDisplay/Example # Set up GoogleService-Info.plist for Storage and Database integration tests. The decrypting # is not supported for pull requests. See https://docs.travis-ci.com/user/encrypting-files/ if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then - openssl aes-256-cbc -K $encrypted_2c8d10c8cc1d_key -iv $encrypted_2c8d10c8cc1d_iv \ - -in scripts/travis-encrypted/database-storage/GoogleService-Info.plist.enc \ - -out Example/Storage/App/GoogleService-Info.plist -d - cp Example/Storage/App/GoogleService-Info.plist Example/Database/App/GoogleService-Info.plist + openssl aes-256-cbc -K $encrypted_824e27188cd5_key -iv $encrypted_824e27188cd5_iv \ + -in scripts/travis-encrypted/Secrets.tar.enc \ + -out scripts/travis-encrypted/Secrets.tar -d + + tar xvf scripts/travis-encrypted/Secrets.tar + + cp Secrets/Auth/Sample/Application.plist Example/Auth/Sample/Application.plist + cp Secrets/Auth/Sample/AuthCredentials.h Example/Auth/Sample/AuthCredentials.h + cp Secrets/Auth/Sample/GoogleService-Info_multi.plist Example/Auth/Sample/GoogleService-Info_multi.plist + cp Secrets/Auth/Sample/GoogleService-Info.plist Example/Auth/Sample/GoogleService-Info.plist + cp Secrets/Auth/Sample/Sample.entitlements Example/Auth/Sample/Sample.entitlements + cp Secrets/Auth/ApiTests/AuthCredentials.h Example/Auth/ApiTests/AuthCredentials.h + + cp Secrets/Storage/App/GoogleService-Info.plist Example/Storage/App/GoogleService-Info.plist + cp Secrets/Storage/App/GoogleService-Info.plist Example/Database/App/GoogleService-Info.plist fi ;; @@ -45,9 +55,16 @@ case "$PROJECT-$PLATFORM-$METHOD" in bundle exec pod install --project-directory=GoogleUtilities/Example ;; - InAppMessagingDisplay-iOS-xcodebuild) + Functions-*) + bundle exec pod repo update + # Start server for Functions integration tests. + ./Functions/Backend/start.sh synchronous + ;; + + InAppMessaging-iOS-xcodebuild) gem install xcpretty bundle exec pod install --project-directory=InAppMessagingDisplay/Example --repo-update + bundle exec pod install --project-directory=InAppMessaging/Example --repo-update ;; Firestore-*-xcodebuild | Firestore-*-fuzz) diff --git a/scripts/pod_lib_lint.sh b/scripts/pod_lib_lint.sh index 8472a8f902a..a9432c323a4 100755 --- a/scripts/pod_lib_lint.sh +++ b/scripts/pod_lib_lint.sh @@ -18,6 +18,8 @@ # # Runs pod lib lint for the given podspec +set -euo pipefail + if [[ $# -lt 1 ]]; then cat 1>&2 <